diff --git a/src/diff2html.ts b/src/diff2html.ts index 5daed0e..a57a957 100644 --- a/src/diff2html.ts +++ b/src/diff2html.ts @@ -12,6 +12,7 @@ export interface Diff2HtmlConfig HoganJsUtilsConfig { outputFormat?: OutputFormatType; drawFileList?: boolean; + lazy?: boolean; } export const defaultDiff2HtmlConfig = { @@ -19,6 +20,7 @@ export const defaultDiff2HtmlConfig = { ...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); +} diff --git a/src/line-by-line-renderer.ts b/src/line-by-line-renderer.ts index 4258095..59693bb 100644 --- a/src/line-by-line-renderer.ts +++ b/src/line-by-line-renderer.ts @@ -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 ''; diff --git a/src/side-by-side-renderer.ts b/src/side-by-side-renderer.ts index 9a0fb6b..7233fc7 100644 --- a/src/side-by-side-renderer.ts +++ b/src/side-by-side-renderer.ts @@ -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 ''; diff --git a/src/ui/js/diff2html-ui-base.ts b/src/ui/js/diff2html-ui-base.ts index 7aaaa5f..2de5ff4 100644 --- a/src/ui/js/diff2html-ui-base.ts +++ b/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'; @@ -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 = 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; }); }