Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy loading files support #410

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/diff2html.ts
Expand Up @@ -12,13 +12,15 @@ export interface Diff2HtmlConfig
HoganJsUtilsConfig {
outputFormat?: OutputFormatType;
drawFileList?: boolean;
lazy?: boolean;
}

export const defaultDiff2HtmlConfig = {
...defaultLineByLineRendererConfig,
...defaultSideBySideRendererConfig,
outputFormat: OutputFormatType.LINE_BY_LINE,
drawFileList: true,
lazy: false,
};

export function parse(diffInput: string, configuration: Diff2HtmlConfig = {}): DiffFile[] {
Expand All @@ -36,8 +38,18 @@ export function html(diffInput: string | DiffFile[], configuration: Diff2HtmlCon

const diffOutput =
config.outputFormat === 'side-by-side'
? new SideBySideRenderer(hoganUtils, config).render(diffJson)
: new LineByLineRenderer(hoganUtils, config).render(diffJson);
? new SideBySideRenderer(hoganUtils, config).render(config.lazy ? [] : diffJson)
: new LineByLineRenderer(hoganUtils, config).render(config.lazy ? [] : diffJson);

return fileList + diffOutput;
}

export function htmlFile(diffFile: DiffFile, configuration: Diff2HtmlConfig = {}): string {
const config = { ...defaultDiff2HtmlConfig, ...configuration };

const hoganUtils = new HoganJsUtils(config);

return config.outputFormat === 'side-by-side'
? new SideBySideRenderer(hoganUtils, config).renderFile(diffFile)
: new LineByLineRenderer(hoganUtils, config).renderFile(diffFile);
}
5 changes: 5 additions & 0 deletions src/line-by-line-renderer.ts
Expand Up @@ -55,6 +55,11 @@ export default class LineByLineRenderer {
return this.hoganUtils.render(genericTemplatesPath, 'wrapper', { content: diffsHtml });
}

renderFile(diffFile: DiffFile): string {
const diffs = diffFile.blocks.length ? this.generateFileHtml(diffFile) : this.generateEmptyDiff();
return this.makeFileDiffHtml(diffFile, diffs);
}

makeFileDiffHtml(file: DiffFile, diffs: string): string {
if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return '';

Expand Down
17 changes: 6 additions & 11 deletions src/side-by-side-renderer.ts
Expand Up @@ -40,21 +40,16 @@ export default class SideBySideRenderer {
}

render(diffFiles: DiffFile[]): string {
const diffsHtml = diffFiles
.map(file => {
let diffs;
if (file.blocks.length) {
diffs = this.generateFileHtml(file);
} else {
diffs = this.generateEmptyDiff();
}
return this.makeFileDiffHtml(file, diffs);
})
.join('\n');
const diffsHtml = diffFiles.map(this.renderFile).join('\n');

return this.hoganUtils.render(genericTemplatesPath, 'wrapper', { content: diffsHtml });
}

renderFile(diffFile: DiffFile): string {
const diffs = diffFile.blocks.length ? this.generateFileHtml(diffFile) : this.generateEmptyDiff();
return this.makeFileDiffHtml(diffFile, diffs);
}

makeFileDiffHtml(file: DiffFile, diffs: FileHtml): string {
if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return '';

Expand Down
109 changes: 73 additions & 36 deletions src/ui/js/diff2html-ui-base.ts
@@ -1,6 +1,6 @@
import { closeTags, nodeStream, mergeStreams, getLanguage } from './highlight.js-helpers';

import { html, Diff2HtmlConfig, defaultDiff2HtmlConfig } from '../../diff2html';
import { html, parse, Diff2HtmlConfig, defaultDiff2HtmlConfig, htmlFile } from '../../diff2html';
import { DiffFile } from '../../types';
import { HighlightResult, HLJSApi } from 'highlight.js';

Expand All @@ -15,6 +15,7 @@ export interface Diff2HtmlUIConfig extends Diff2HtmlConfig {
*/
smartSelection?: boolean;
fileContentToggle?: boolean;
lazy?: boolean;
}

export const defaultDiff2HtmlUIConfig = {
Expand All @@ -29,10 +30,12 @@ export const defaultDiff2HtmlUIConfig = {
*/
smartSelection: true,
fileContentToggle: true,
lazy: true,
};

export class Diff2HtmlUI {
readonly config: typeof defaultDiff2HtmlUIConfig;
readonly diffFiles: DiffFile[];
readonly diffHtml: string;
readonly targetElement: HTMLElement;
readonly hljs: HLJSApi | null = null;
Expand All @@ -41,13 +44,20 @@ export class Diff2HtmlUI {

constructor(target: HTMLElement, diffInput?: string | DiffFile[], config: Diff2HtmlUIConfig = {}, hljs?: HLJSApi) {
this.config = { ...defaultDiff2HtmlUIConfig, ...config };
this.diffHtml = diffInput !== undefined ? html(diffInput, this.config) : target.innerHTML;

if (config.lazy && (config.fileListStartVisible ?? true)) {
this.config.fileListStartVisible = true;
}

this.diffFiles = typeof diffInput === 'string' ? parse(diffInput, this.config) : diffInput ?? [];
this.diffHtml = diffInput !== undefined ? html(this.diffFiles, this.config) : target.innerHTML;
this.targetElement = target;
if (hljs !== undefined) this.hljs = hljs;
}

draw(): void {
this.targetElement.innerHTML = this.diffHtml;
if (this.config.lazy) this.bindDrawFiles();
if (this.config.synchronisedScroll) this.synchronisedScroll();
if (this.config.highlight) this.highlightCode();
if (this.config.fileListToggle) this.fileListToggle(this.config.fileListStartVisible);
Expand Down Expand Up @@ -76,6 +86,27 @@ export class Diff2HtmlUI {
});
}

bindDrawFiles(): void {
const fileListItems: NodeListOf<HTMLElement> = this.targetElement.querySelectorAll('.d2h-file-name');
fileListItems.forEach((i, idx) =>
i.addEventListener('click', () => {
const fileId = i.getAttribute('href');
if (fileId && this.targetElement.querySelector(fileId)) {
return;
}

const tmpDiv = document.createElement('div');
tmpDiv.innerHTML = htmlFile(this.diffFiles[idx], this.config);
const fileElem = tmpDiv.querySelector('.d2h-file-wrapper');

if (fileElem) {
this.targetElement.querySelector('.d2h-wrapper')?.appendChild(fileElem);
this.highlightFile(fileElem);
}
}),
);
}

fileListToggle(startVisible: boolean): void {
const showBtn: HTMLElement | null = this.targetElement.querySelector('.d2h-show');
const hideBtn: HTMLElement | null = this.targetElement.querySelector('.d2h-hide');
Expand Down Expand Up @@ -138,43 +169,49 @@ export class Diff2HtmlUI {

// Collect all the diff files and execute the highlight on their lines
const files = this.targetElement.querySelectorAll('.d2h-file-wrapper');
files.forEach(file => {
files.forEach(this.highlightFile);
}

highlightFile(file: Element): void {
if (this.hljs === null) {
throw new Error('Missing a `highlight.js` implementation. Please provide one when instantiating Diff2HtmlUI.');
}

// HACK: help Typescript know that `this.hljs` is defined since we already checked it
if (this.hljs === null) return;
const language = file.getAttribute('data-lang');
const hljsLanguage = language ? getLanguage(language) : 'plaintext';

// Collect all the code lines and execute the highlight on them
const codeLines = file.querySelectorAll('.d2h-code-line-ctn');
codeLines.forEach(line => {
// HACK: help Typescript know that `this.hljs` is defined since we already checked it
if (this.hljs === null) return;
const language = file.getAttribute('data-lang');
const hljsLanguage = language ? getLanguage(language) : 'plaintext';

// Collect all the code lines and execute the highlight on them
const codeLines = file.querySelectorAll('.d2h-code-line-ctn');
codeLines.forEach(line => {
// HACK: help Typescript know that `this.hljs` is defined since we already checked it
if (this.hljs === null) return;

const text = line.textContent;
const lineParent = line.parentNode;

if (text === null || lineParent === null || !this.isElement(lineParent)) return;

const result: HighlightResult = closeTags(
this.hljs.highlight(text, {
language: hljsLanguage,
ignoreIllegals: true,
}),
);

const originalStream = nodeStream(line);
if (originalStream.length) {
const resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
resultNode.innerHTML = result.value;
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
}

line.classList.add('hljs');
if (result.language) {
line.classList.add(result.language);
}
line.innerHTML = result.value;
});
const text = line.textContent;
const lineParent = line.parentNode;

if (text === null || lineParent === null || !this.isElement(lineParent)) return;

const result: HighlightResult = closeTags(
this.hljs.highlight(text, {
language: hljsLanguage,
ignoreIllegals: true,
}),
);

const originalStream = nodeStream(line);
if (originalStream.length) {
const resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
resultNode.innerHTML = result.value;
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
}

line.classList.add('hljs');
if (result.language) {
line.classList.add(result.language);
}
line.innerHTML = result.value;
});
}

Expand Down