diff --git a/src/line-by-line-renderer.ts b/src/line-by-line-renderer.ts index 2dd310f..77b5f18 100644 --- a/src/line-by-line-renderer.ts +++ b/src/line-by-line-renderer.ts @@ -76,15 +76,20 @@ export default class LineByLineRenderer { fileTag: fileTagTemplate, }, ), + overflowClass: this.config.diffOverflow.concat('-overflow'), }); } generateEmptyDiff(): string { - return this.hoganUtils.render(genericTemplatesPath, 'empty-diff', { - contentClass: 'd2h-code-line', - colspan: '2', - CSSLineClass: renderUtils.CSSLineClass, - }); + return ( + '' + ); } generateFileHtml(file: DiffFile): string { @@ -253,17 +258,17 @@ export default class LineByLineRenderer { : undefined; const { left, right } = this.generateLineHtml(preparedOldLine, preparedNewLine); - fileHtml.left.push(...left); - fileHtml.right.push(...right); + fileHtml.left.push(left); + fileHtml.right.push(right); } return fileHtml; } - generateLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): FileHtml { + generateLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): LineHtml { return { - left: [this.generateSingleLineHtml(oldLine)], - right: [this.generateSingleLineHtml(newLine)], + left: this.generateSingleLineHtml(oldLine), + right: this.generateSingleLineHtml(newLine), }; } @@ -307,6 +312,11 @@ type DiffPreparedLine = { newNumber?: number; }; +type LineHtml = { + left: string; + right: string; +}; + type FileHtml = { left: string[]; right: string[]; diff --git a/src/render-utils.ts b/src/render-utils.ts index 830628e..440eeb9 100644 --- a/src/render-utils.ts +++ b/src/render-utils.ts @@ -2,7 +2,15 @@ import * as jsDiff from 'diff'; import { unifyPath, hashCode } from './utils'; import * as rematch from './rematch'; -import { LineMatchingType, DiffStyleType, LineType, DiffLineParts, DiffFile, DiffFileName } from './types'; +import { + LineMatchingType, + DiffStyleType, + DiffOverflowType, + LineType, + DiffLineParts, + DiffFile, + DiffFileName, +} from './types'; export type CSSLineClass = | 'd2h-ins' @@ -37,6 +45,7 @@ export interface RenderConfig { matchWordsThreshold?: number; maxLineLengthHighlight?: number; diffStyle?: DiffStyleType; + diffOverflow?: DiffOverflowType; } export const defaultRenderConfig = { @@ -44,6 +53,7 @@ export const defaultRenderConfig = { matchWordsThreshold: 0.25, maxLineLengthHighlight: 10000, diffStyle: DiffStyleType.WORD, + diffOverflow: DiffOverflowType.SCROLL, }; const separator = '/'; diff --git a/src/side-by-side-renderer.ts b/src/side-by-side-renderer.ts index 1216632..a0ed1ed 100644 --- a/src/side-by-side-renderer.ts +++ b/src/side-by-side-renderer.ts @@ -4,6 +4,7 @@ import * as renderUtils from './render-utils'; import { DiffLine, LineType, + DiffOverflowType, DiffFile, DiffBlock, DiffLineContext, @@ -42,27 +43,43 @@ export default class SideBySideRenderer { render(diffFiles: DiffFile[]): string { const diffsHtml = diffFiles .map(file => { - let diffs; + let diffsFileHtml; if (file.blocks.length) { - diffs = this.generateFileHtml(file); + diffsFileHtml = this.generateFileHtml(file); } else { - diffs = this.generateEmptyDiff(); + diffsFileHtml = this.generateEmptyDiff(); } - return this.makeFileDiffHtml(file, diffs); + return this.makeFileDiffHtml(file, diffsFileHtml); }) .join('\n'); return this.hoganUtils.render(genericTemplatesPath, 'wrapper', { content: diffsHtml }); } - makeFileDiffHtml(file: DiffFile, diffs: string): string { + makeFileDiffHtml(file: DiffFile, diffsFileHtml: FileHtml): string { if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return ''; - const fileDiffTemplate = this.hoganUtils.template(baseTemplatesPath, 'file-diff'); + if (this.config.diffOverflow === DiffOverflowType.SCROLL) { + return this.makeScrollFileDiffHtml(file, diffsFileHtml); + } + + if (this.config.diffOverflow === DiffOverflowType.WRAP) { + return this.makeWrappedFileDiffHtml(file, diffsFileHtml); + } + + throw new Error('Unrecognised DiffOverflow setting'); + } + + makeWrappedFileDiffHtml(file: DiffFile, diffsFileHtml: FileHtml): string { + if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return ''; + + const fileDiffTemplate = this.hoganUtils.template(baseTemplatesPath, 'text-wrapped-file-diff'); const filePathTemplate = this.hoganUtils.template(genericTemplatesPath, 'file-path'); const fileIconTemplate = this.hoganUtils.template(iconsBaseTemplatesPath, 'file'); const fileTagTemplate = this.hoganUtils.template(tagsBaseTemplatesPath, renderUtils.getFileIcon(file)); + const diffs = this.joinFileHtmlForWrappedDisplay(diffsFileHtml); + return fileDiffTemplate.render({ file: file, fileHtmlId: renderUtils.getHtmlId(file), @@ -79,15 +96,50 @@ export default class SideBySideRenderer { }); } - generateEmptyDiff(): string { - return this.hoganUtils.render(genericTemplatesPath, 'empty-diff', { - contentClass: 'd2h-code-side-line', - colspan: '4', - CSSLineClass: renderUtils.CSSLineClass, + makeScrollFileDiffHtml(file: DiffFile, diffsFileHtml: FileHtml): string { + if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return ''; + + const fileDiffTemplate = this.hoganUtils.template(baseTemplatesPath, 'file-diff'); + const filePathTemplate = this.hoganUtils.template(genericTemplatesPath, 'file-path'); + const fileIconTemplate = this.hoganUtils.template(iconsBaseTemplatesPath, 'file'); + const fileTagTemplate = this.hoganUtils.template(tagsBaseTemplatesPath, renderUtils.getFileIcon(file)); + + const diffs = this.joinFileHtmlForScrollDisplay(diffsFileHtml); + + return fileDiffTemplate.render({ + file: file, + fileHtmlId: renderUtils.getHtmlId(file), + diffs: diffs, + filePath: filePathTemplate.render( + { + fileDiffName: renderUtils.filenameDiff(file), + }, + { + fileIcon: fileIconTemplate, + fileTag: fileTagTemplate, + }, + ), }); } - generateFileHtml(file: DiffFile): string { + generateEmptyDiff(): FileHtml { + return { + left: [ + this.hoganUtils.render(genericTemplatesPath, 'empty-diff', { + contentClass: 'd2h-code-side-line', + CSSLineClass: renderUtils.CSSLineClass, + }), + ], + right: [ + this.hoganUtils.render(genericTemplatesPath, 'empty-diff', { + contentClass: 'd2h-code-side-line', + CSSLineClass: renderUtils.CSSLineClass, + }), + ], + }; + } + + generateFileHtml(file: DiffFile): FileHtml { const matcher = Rematch.newMatcherFn( Rematch.newDistanceFn((e: DiffLine) => renderUtils.deconstructLine(e.content, file.isCombined).content), ); @@ -95,9 +147,12 @@ export default class SideBySideRenderer { return file.blocks .map(block => { const fileHtml: FileHtml = { - left: [this.makeHeaderHtml(block.header, file)], - right: [''], + left: [], + right: [], }; + const header = this.makeHeaderHtml(block.header, file); + fileHtml.left.push(header.left); + fileHtml.right.push(header.right); this.applyLineGrouping(block).forEach(([contextLines, oldLines, newLines]) => { if (oldLines.length && newLines.length && !contextLines.length) { @@ -123,8 +178,8 @@ export default class SideBySideRenderer { number: line.newNumber, }, ); - fileHtml.left.push(...left); - fileHtml.right.push(...right); + fileHtml.left.push(left); + fileHtml.right.push(right); }); } else if (oldLines.length || newLines.length) { const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines); @@ -137,16 +192,36 @@ export default class SideBySideRenderer { return fileHtml; }) - .map((block_html: FileHtml) => { - let block_html_string = ''; - for (let block_line_index = 0; block_line_index < block_html.left.length; block_line_index++) { - block_html_string = block_html_string.concat( - `${block_html.left[block_line_index]} ${block_html.right[block_line_index]}`, - ); - } - return block_html_string; - }) - .join('\n'); + .reduce( + (accumulator: FileHtml, { left, right }) => { + accumulator.left.push(...left); + accumulator.right.push(...right); + return accumulator; + }, + { left: [], right: [] }, + ); + } + + joinFileHtmlForWrappedDisplay(fileHtml: FileHtml): string { + let joined_string = ''; + for (let block_line_index = 0; block_line_index < fileHtml.left.length; block_line_index++) { + joined_string = joined_string.concat( + `${fileHtml.left[block_line_index]} ${fileHtml.right[block_line_index]}\n`, + ); + } + return joined_string; + } + + joinFileHtmlForScrollDisplay(fileHtml: FileHtml): { left: string; right: string } { + const joined_strings = { + left: '', + right: '', + }; + for (let block_line_index = 0; block_line_index < fileHtml.left.length; block_line_index++) { + joined_strings.left = joined_strings.left.concat(`${fileHtml.left[block_line_index]}\n`); + joined_strings.right = joined_strings.right.concat(`${fileHtml.right[block_line_index]}\n`); + } + return joined_strings; } applyLineGrouping(block: DiffBlock): DiffLineGroups { @@ -205,8 +280,40 @@ export default class SideBySideRenderer { return doMatching ? matcher(oldLines, newLines) : [[oldLines, newLines]]; } - makeHeaderHtml(blockHeader: string, file?: DiffFile): string { - return this.hoganUtils.render(genericTemplatesPath, 'block-header', { + makeHeaderHtml(blockHeader: string, file?: DiffFile): LineHtml { + if (this.config.diffOverflow === DiffOverflowType.WRAP) { + return this.makeWrappedHeaderHtml(blockHeader, file); + } + + if (this.config.diffOverflow === DiffOverflowType.SCROLL) { + return this.makeScrollHeaderHtml(blockHeader, file); + } + + throw new Error('Unrecognised DiffOverflow setting'); + } + + makeScrollHeaderHtml(blockHeader: string, file?: DiffFile): LineHtml { + const left = this.hoganUtils.render(genericTemplatesPath, 'block-header', { + CSSLineClass: renderUtils.CSSLineClass, + margin_colspan: '1', + colspan: '1', + blockHeader: file?.isTooBig ? blockHeader : renderUtils.escapeForHtml(blockHeader), + lineClass: 'd2h-code-side-linenumber', + contentClass: 'd2h-code-side-line', + }); + + const right = this.hoganUtils.render(genericTemplatesPath, 'block-header', { + CSSLineClass: renderUtils.CSSLineClass, + margin_colspan: '1', + colspan: '1', + lineClass: 'd2h-code-side-linenumber', + contentClass: 'd2h-code-side-line', + }); + return { left, right }; + } + + makeWrappedHeaderHtml(blockHeader: string, file?: DiffFile): LineHtml { + const table_element = this.hoganUtils.render(genericTemplatesPath, 'block-header', { CSSLineClass: renderUtils.CSSLineClass, margin_colspan: '1', colspan: '3', @@ -214,6 +321,10 @@ export default class SideBySideRenderer { lineClass: 'd2h-code-side-linenumber', contentClass: 'd2h-code-side-line', }); + return { + left: table_element, + right: '', + }; } processChangedLines(isCombined: boolean, oldLines: DiffLine[], newLines: DiffLine[]): FileHtml { @@ -267,17 +378,17 @@ export default class SideBySideRenderer { : undefined; const { left, right } = this.generateLineHtml(preparedOldLine, preparedNewLine); - fileHtml.left.push(...left); - fileHtml.right.push(...right); + fileHtml.left.push(left); + fileHtml.right.push(right); } return fileHtml; } - generateLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): FileHtml { + generateLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): LineHtml { return { - left: [this.generateSingleHtml(oldLine)], - right: [this.generateSingleHtml(newLine)], + left: this.generateSingleHtml(oldLine), + right: this.generateSingleHtml(newLine), }; } @@ -317,6 +428,11 @@ type DiffPreparedLine = { number: number; }; +type LineHtml = { + left: string; + right: string; +}; + type FileHtml = { left: string[]; right: string[]; diff --git a/src/templates/generic-empty-diff.mustache b/src/templates/generic-empty-diff.mustache index 9107aa3..2d025e0 100644 --- a/src/templates/generic-empty-diff.mustache +++ b/src/templates/generic-empty-diff.mustache @@ -1,7 +1,5 @@ - - -
- File without changes -
- - + +
+ File without changes +
+ diff --git a/src/templates/line-by-line-file-diff.mustache b/src/templates/line-by-line-file-diff.mustache index f5ce559..c58463f 100644 --- a/src/templates/line-by-line-file-diff.mustache +++ b/src/templates/line-by-line-file-diff.mustache @@ -2,7 +2,7 @@
{{{filePath}}}
-
+
diff --git a/src/templates/side-by-side-file-diff.mustache b/src/templates/side-by-side-file-diff.mustache index e0e9d00..8ecce25 100644 --- a/src/templates/side-by-side-file-diff.mustache +++ b/src/templates/side-by-side-file-diff.mustache @@ -3,27 +3,44 @@ {{{filePath}}}
-
+
+ + + + + + + + + + {{{diffs.left}}} + +
+
+
+
+
+ + + - - - + - {{{diffs}}} + {{{diffs.right}}}
diff --git a/src/templates/side-by-side-text-wrapped-file-diff.mustache b/src/templates/side-by-side-text-wrapped-file-diff.mustache new file mode 100644 index 0000000..236142a --- /dev/null +++ b/src/templates/side-by-side-text-wrapped-file-diff.mustache @@ -0,0 +1,32 @@ +
+
+ {{{filePath}}} +
+
+
+
+ + + + + + + + + + + + + + + + + + + {{{diffs}}} + +
+
+
+
+
diff --git a/src/types.ts b/src/types.ts index ea28b49..89b7b3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -91,3 +91,10 @@ export const DiffStyleType: { [_: string]: DiffStyleType } = { WORD: 'word', CHAR: 'char', }; + +export type DiffOverflowType = 'scroll' | 'wrap'; + +export const DiffOverflowType: { [_: string]: DiffOverflowType } = { + SCROLL: 'scroll', + WRAP: 'wrap', +}; diff --git a/src/ui/css/diff2html.css b/src/ui/css/diff2html.css index 45c2433..1c2b688 100644 --- a/src/ui/css/diff2html.css +++ b/src/ui/css/diff2html.css @@ -30,7 +30,7 @@ .d2h-file-header.d2h-sticky-header { position: sticky; top: 0; - z-index: 1; + z-index: 10; } .d2h-file-stats { @@ -104,13 +104,20 @@ .d2h-diff-table { width: 100%; - border-collapse: separate; font-family: 'Menlo', 'Consolas', monospace; font-size: 13px; word-wrap: break-word; } -.d2h-diff-table.d2h-split-diff { +.scroll-overflow .d2h-diff-table { + border-collapse: collapse; +} + +.wrap-overflow .d2h-diff-table { + border-collapse: separate; +} + +.wrap-overflow .d2h-diff-table.d2h-split-diff { table-layout: fixed; } @@ -124,8 +131,10 @@ display: none; } -.d2h-file-side-diff { - width: 100%; +.d2h-file-diff.scroll-overflow, +.d2h-file-side-diff.scroll-overflow { + overflow-x: scroll; + overflow-y: hidden; } .d2h-code-line, @@ -138,6 +147,11 @@ vertical-align: top; } +.scroll-overflow .d2h-code-line, +.scroll-overflow .d2h-code-side-line { + white-space: nowrap; +} + .d2h-code-marker::before { position: absolute; top: 1px; @@ -146,13 +160,22 @@ content: attr(data-code-marker); } +.wrap-overflow .d2h-code-line-ctn { + word-wrap: anywhere; + overflow: visible; + white-space: pre-wrap; +} + +.scroll-overflow .d2h-code-line-ctn { + overflow: scroll; +} + .d2h-code-line-ctn { display: table-cell; background: none; padding: 0; - word-wrap: anywhere; - white-space: pre-wrap; - overflow: visible; + word-wrap: normal; + white-space: pre; user-select: text; width: 100%; vertical-align: middle; @@ -197,14 +220,38 @@ content: '\200b'; } +.wrap-overflow .d2h-code-side-linenumber, +.wrap-overflow .d2h-code-linenumber, +.wrap-overflow .d2h-code-emptyplaceholder { + position: relative; +} + +.scroll-overflow .d2h-code-side-linenumber, +.scroll-overflow .d2h-code-linenumber, +.scroll-overflow .d2h-code-emptyplaceholder { + position: sticky; + z-index: 1; +} + +.scroll-overflow .d2h-code-side-linenumber { + left: 0; +} + +.scroll-overflow.d2h-file-diff tr td:first-child { + left: 0; +} + +.scroll-overflow.d2h-file-diff tr td:nth-child(2) { + left: 50px; +} + .d2h-code-side-linenumber, .d2h-code-linenumber { - /* Keep the numbers fixed on line contents scroll */ white-space: nowrap; - position: relative; background-color: #fff; color: rgba(0, 0, 0, 0.3); text-align: right; + vertical-align: top; border: solid #eeeeee; border-width: 0 1px 0 1px; line-height: 20px; diff --git a/src/ui/js/diff2html-ui-base.ts b/src/ui/js/diff2html-ui-base.ts index 9b2317f..c229993 100644 --- a/src/ui/js/diff2html-ui-base.ts +++ b/src/ui/js/diff2html-ui-base.ts @@ -63,6 +63,8 @@ export class Diff2HtmlUI { this.targetElement.querySelectorAll('.d2h-file-wrapper').forEach(wrapper => { const [left, right] = Array().slice.call(wrapper.querySelectorAll('.d2h-file-side-diff')); + console.log(left, right); + if (left === undefined || right === undefined) return; const onScroll = (event: Event): void => {