Skip to content

Lazy loading files support #410

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

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/diff2html.ts
Original file line number Diff line number Diff line change
@@ -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[] {
@@ -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
Original file line number Diff line number Diff line change
@@ -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 '';

17 changes: 6 additions & 11 deletions src/side-by-side-renderer.ts
Original file line number Diff line number Diff line change
@@ -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 '';

109 changes: 73 additions & 36 deletions src/ui/js/diff2html-ui-base.ts
Original file line number Diff line number Diff line change
@@ -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';

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

export const defaultDiff2HtmlUIConfig = {
@@ -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;
@@ -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);
@@ -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');
@@ -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;
});
}