diff --git a/src/__tests__/diff2html-tests.ts b/src/__tests__/diff2html-tests.ts index 23f89ac1..752cbd54 100644 --- a/src/__tests__/diff2html-tests.ts +++ b/src/__tests__/diff2html-tests.ts @@ -224,7 +224,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -281,7 +281,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -358,7 +358,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -416,7 +416,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -440,7 +440,7 @@ describe('Diff2Html', () => { - +
 
@@ -487,7 +487,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -511,7 +511,7 @@ describe('Diff2Html', () => { - +
 
@@ -578,7 +578,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -602,7 +602,7 @@ describe('Diff2Html', () => { - +
 
@@ -671,7 +671,7 @@ describe('Diff2Html', () => { - +
@@ -1,7 +1,6 @@
@@ -754,7 +754,7 @@ describe('Diff2Html', () => { - +
@@ -11,7 +10,7 @@ $a="<table><tr><td>- 1.1.9: Fix around ubuntu's inability to cache promises. [#8
@@ -917,7 +917,7 @@ describe('Diff2Html', () => { - +
@@ -1,2 +1,2 @@
@@ -1024,7 +1024,7 @@ describe('Diff2Html', () => { - +
@@ -1,5 +1,5 @@
@@ -1107,7 +1107,7 @@ describe('Diff2Html', () => { - +
@@ -24,12 +24,7 @@ $(function() {
@@ -1290,7 +1290,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -1370,7 +1370,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -1432,7 +1432,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
@@ -1512,7 +1512,7 @@ describe('Diff2Html', () => { - +
@@ -1 +1 @@
diff --git a/src/__tests__/hogan-cache-tests.ts b/src/__tests__/hogan-cache-tests.ts index dc26cd8a..5fb94d70 100644 --- a/src/__tests__/hogan-cache-tests.ts +++ b/src/__tests__/hogan-cache-tests.ts @@ -11,7 +11,7 @@ describe('HoganJsUtils', () => { }); expect(result).toMatchInlineSnapshot(` " - +
File without changes
diff --git a/src/__tests__/line-by-line-tests.ts b/src/__tests__/line-by-line-tests.ts index e8f5bff4..fdfb53a6 100644 --- a/src/__tests__/line-by-line-tests.ts +++ b/src/__tests__/line-by-line-tests.ts @@ -11,7 +11,7 @@ describe('LineByLineRenderer', () => { const fileHtml = lineByLineRenderer.generateEmptyDiff(); expect(fileHtml).toMatchInlineSnapshot(` " - +
File without changes
@@ -468,7 +468,7 @@ describe('LineByLineRenderer', () => { - +
@@ -1 +1 @@
@@ -541,7 +541,7 @@ describe('LineByLineRenderer', () => { - - @@ -671,7 +671,7 @@ describe('LineByLineRenderer', () => { expect(html).toMatchInlineSnapshot(` " - diff --git a/src/__tests__/side-by-side-printer-tests.ts b/src/__tests__/side-by-side-printer-tests.ts index ab886aba..52c07f62 100644 --- a/src/__tests__/side-by-side-printer-tests.ts +++ b/src/__tests__/side-by-side-printer-tests.ts @@ -12,7 +12,7 @@ describe('SideBySideRenderer', () => { expect(fileHtml).toMatchInlineSnapshot(` { "left": " - - @@ -117,7 +117,7 @@ describe('SideBySideRenderer', () => { ", "right": " - @@ -298,7 +298,7 @@ describe('SideBySideRenderer', () => { - @@ -322,7 +322,7 @@ describe('SideBySideRenderer', () => { - @@ -382,7 +382,7 @@ describe('SideBySideRenderer', () => {
+
File without changes
@@ -602,7 +602,7 @@ describe('LineByLineRenderer', () => {
+
+
@@ -1 +1 @@
+
File without changes
@@ -81,7 +81,7 @@ describe('SideBySideRenderer', () => { { "left": "
+
@@ -19,7 +19,7 @@
+
 
+
@@ -1 +1 @@
+
 
- - @@ -468,7 +468,7 @@ describe('SideBySideRenderer', () => { - diff --git a/src/__tests__/wrapped-side-by-side-printer-tests.ts b/src/__tests__/wrapped-side-by-side-printer-tests.ts new file mode 100644 index 00000000..cbbc4607 --- /dev/null +++ b/src/__tests__/wrapped-side-by-side-printer-tests.ts @@ -0,0 +1,518 @@ +import WrappedSideBySideRenderer from '../wrapped-side-by-side-renderer'; +import HoganJsUtils from '../hoganjs-utils'; +import { LineType, DiffLine, DiffFile, LineMatchingType } from '../types'; +import { CSSLineClass } from '../render-utils'; + +describe('WrappedSideBySideRenderer', () => { + describe('generateEmptyDiff', () => { + it('should return an empty diff', () => { + const hoganUtils = new HoganJsUtils({}); + const wrappedSideBySideRenderer = new WrappedSideBySideRenderer(hoganUtils, {}); + const fileHtml = wrappedSideBySideRenderer.generateEmptyDiff(); + expect(fileHtml).toMatchInlineSnapshot(` + " + + " + `); + }); + }); + + describe('generateWrappedSideBySideFileHtml', () => { + it('should generate lines with the right prefixes', () => { + const hoganUtils = new HoganJsUtils({}); + const wrappedSideBySideRenderer = new WrappedSideBySideRenderer(hoganUtils, {}); + + const file: DiffFile = { + isGitDiff: true, + blocks: [ + { + lines: [ + { + content: ' context', + type: LineType.CONTEXT, + oldNumber: 19, + newNumber: 19, + }, + { + content: '-removed', + type: LineType.DELETE, + oldNumber: 20, + newNumber: undefined, + }, + { + content: '+added', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 20, + }, + { + content: '+another added', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 21, + }, + ], + oldStartLine: 19, + newStartLine: 19, + header: '@@ -19,7 +19,7 @@', + }, + ], + deletedLines: 1, + addedLines: 2, + checksumBefore: 'fc56817', + checksumAfter: 'e8e7e49', + mode: '100644', + oldName: 'coverage.init', + language: 'init', + newName: 'coverage.init', + isCombined: false, + }; + + const fileHtml = wrappedSideBySideRenderer.generateFileHtml(file); + + expect(fileHtml).toMatchInlineSnapshot(` + " + + + + + + + + + + + + + + + + + + " + `); + }); + }); + + describe('generateSingleLineHtml', () => { + it('should work for insertions', () => { + const file = { + addedLines: 12, + deletedLines: 41, + language: 'js', + oldName: 'my/file/name.js', + newName: 'my/file/name.js', + isCombined: false, + isGitDiff: false, + blocks: [], + }; + const hoganUtils = new HoganJsUtils({}); + const wrappedSideBySideRenderer = new WrappedSideBySideRenderer(hoganUtils, {}); + const fileHtml = wrappedSideBySideRenderer.generateLineHtml(file, { + type: CSSLineClass.INSERTS, + prefix: '+', + content: 'test', + number: 30, + }); + + expect(fileHtml).toMatchInlineSnapshot(` + " + + + + + " + `); + }); + it('should work for deletions', () => { + const file = { + addedLines: 12, + deletedLines: 41, + language: 'js', + oldName: 'my/file/name.js', + newName: 'my/file/name.js', + isCombined: false, + isGitDiff: false, + blocks: [], + }; + const hoganUtils = new HoganJsUtils({}); + const wrappedSideBySideRenderer = new WrappedSideBySideRenderer(hoganUtils, {}); + const fileHtml = wrappedSideBySideRenderer.generateLineHtml( + file, + { + type: CSSLineClass.DELETES, + prefix: '-', + content: 'test', + number: 30, + }, + undefined, + ); + + expect(fileHtml).toMatchInlineSnapshot(` + " + + + + + " + `); + }); + }); + + describe('generateWrappedSideBySideJsonHtml', () => { + it('should work for list of files', () => { + const exampleJson: DiffFile[] = [ + { + blocks: [ + { + lines: [ + { + content: '-test', + type: LineType.DELETE, + oldNumber: 1, + newNumber: undefined, + }, + { + content: '+test1r', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 1, + }, + ], + oldStartLine: 1, + oldStartLine2: undefined, + newStartLine: 1, + header: '@@ -1 +1 @@', + }, + ], + deletedLines: 1, + addedLines: 1, + checksumBefore: '0000001', + checksumAfter: '0ddf2ba', + oldName: 'sample', + language: 'txt', + newName: 'sample', + isCombined: false, + isGitDiff: true, + }, + ]; + + const hoganUtils = new HoganJsUtils({}); + const wrappedSideBySideRenderer = new WrappedSideBySideRenderer(hoganUtils, { matching: LineMatchingType.LINES }); + const html = wrappedSideBySideRenderer.render(exampleJson); + expect(html).toMatchInlineSnapshot(` + "
+
+
+ + sample + CHANGED + +
+
+
+
+
File without changes
@@ -454,7 +454,7 @@ describe('SideBySideRenderer', () => {
+
+
 
+
+ File without changes +
+
+
@@ -19,7 +19,7 @@
+
+ 19 + +
+   + context +
+
+ 19 + +
+   + context +
+
+ 20 + +
+ - + removed +
+
+ 20 + +
+ + + added +
+
+ + +
+   +
+
+
+ 21 + +
+ + + another added +
+
+ 30 + +
+ + + test +
+
+ + +
+   +
+
+
+ 30 + +
+ - + test +
+
+ + +
+   +
+
+
+ + + + + + + + + + + + + + + + + +
+
@@ -1 +1 @@
+
+ 1 + +
+ - + test +
+
+ 1 + +
+ + + test1r +
+
+ + + + " + `); + }); + it('should work for files without blocks', () => { + const exampleJson: DiffFile[] = [ + { + blocks: [], + oldName: 'sample', + language: 'js', + newName: 'sample', + isCombined: false, + addedLines: 0, + deletedLines: 0, + isGitDiff: false, + }, + ]; + + const hoganUtils = new HoganJsUtils({}); + const wrappedSideBySideRenderer = new WrappedSideBySideRenderer(hoganUtils, {}); + const html = wrappedSideBySideRenderer.render(exampleJson); + expect(html).toMatchInlineSnapshot(` + "
+
+
+ + sample + CHANGED + +
+
+
+ + + + + + + + + + + + +
+
+ File without changes +
+
+
+
+
+
" + `); + }); + + it('should work for too big file diff', () => { + const exampleJson = [ + { + blocks: [ + { + header: 'Custom link to render', + lines: [], + newStartLine: 0, + oldStartLine: 0, + oldStartLine2: undefined, + }, + ], + deletedLines: 0, + addedLines: 0, + oldName: 'sample', + language: 'js', + newName: 'sample', + isCombined: false, + isGitDiff: false, + isTooBig: true, + }, + ]; + + const hoganUtils = new HoganJsUtils({}); + const wrappedSideBySideRenderer = new WrappedSideBySideRenderer(hoganUtils); + const html = wrappedSideBySideRenderer.render(exampleJson); + expect(html).toMatchInlineSnapshot(` + "
+
+
+ + sample + CHANGED + +
+
+
+ + + + + + + + + + + + + +
+ +
+
+
+
+
" + `); + }); + }); + + describe('processLines', () => { + it('should process file lines', () => { + const file = { + addedLines: 12, + deletedLines: 41, + language: 'js', + oldName: 'my/file/name.js', + newName: 'my/file/name.js', + isCombined: false, + isGitDiff: false, + blocks: [], + }; + + const oldLines: DiffLine[] = [ + { + content: '-test', + type: LineType.DELETE, + oldNumber: 1, + newNumber: undefined, + }, + ]; + + const newLines: DiffLine[] = [ + { + content: '+test1r', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 1, + }, + ]; + + const hoganUtils = new HoganJsUtils({}); + const wrappedSideBySideRenderer = new WrappedSideBySideRenderer(hoganUtils, { matching: LineMatchingType.LINES }); + const html = wrappedSideBySideRenderer.processChangedLines(file, false, oldLines, newLines); + + expect(html).toMatchInlineSnapshot(` + " + + 1 + + +
+ - + test +
+ + + 1 + + +
+ + + test1r +
+ + " + `); + }); + }); +}); diff --git a/src/diff2html.ts b/src/diff2html.ts index 06f00f73..9632fbbc 100644 --- a/src/diff2html.ts +++ b/src/diff2html.ts @@ -2,6 +2,10 @@ import * as DiffParser from './diff-parser'; import { FileListRenderer } from './file-list-renderer'; import LineByLineRenderer, { LineByLineRendererConfig, defaultLineByLineRendererConfig } from './line-by-line-renderer'; import SideBySideRenderer, { SideBySideRendererConfig, defaultSideBySideRendererConfig } from './side-by-side-renderer'; +import WrappedSideBySideRenderer, { + WrappedSideBySideRendererConfig, + defaultWrappedSideBySideRendererConfig, +} from './wrapped-side-by-side-renderer'; import { DiffFile, OutputFormatType } from './types'; import HoganJsUtils, { HoganJsUtilsConfig } from './hoganjs-utils'; @@ -9,6 +13,7 @@ export interface Diff2HtmlConfig extends DiffParser.DiffParserConfig, LineByLineRendererConfig, SideBySideRendererConfig, + WrappedSideBySideRendererConfig, HoganJsUtilsConfig { outputFormat?: OutputFormatType; drawFileList?: boolean; @@ -17,6 +22,7 @@ export interface Diff2HtmlConfig export const defaultDiff2HtmlConfig = { ...defaultLineByLineRendererConfig, ...defaultSideBySideRendererConfig, + ...defaultWrappedSideBySideRendererConfig, outputFormat: OutputFormatType.LINE_BY_LINE, drawFileList: true, }; @@ -38,9 +44,11 @@ export function html(diffInput: string | DiffFile[], configuration: Diff2HtmlCon const fileList = config.drawFileList ? new FileListRenderer(hoganUtils, fileListRendererConfig).render(diffJson) : ''; const diffOutput = - config.outputFormat === 'side-by-side' - ? new SideBySideRenderer(hoganUtils, config).render(diffJson) - : new LineByLineRenderer(hoganUtils, config).render(diffJson); + config.outputFormat === 'line-by-line' + ? new LineByLineRenderer(hoganUtils, config).render(diffJson) + : config.outputFormat === 'side-by-side' + ? new SideBySideRenderer(hoganUtils, config).render(diffJson) + : new WrappedSideBySideRenderer(hoganUtils, config).render(diffJson); return fileList + diffOutput; } diff --git a/src/templates/generic-block-header.mustache b/src/templates/generic-block-header.mustache index efbbc424..240f4678 100644 --- a/src/templates/generic-block-header.mustache +++ b/src/templates/generic-block-header.mustache @@ -1,6 +1,6 @@ - +
{{#blockHeader}}{{{blockHeader}}}{{/blockHeader}}{{^blockHeader}} {{/blockHeader}}
diff --git a/src/templates/generic-empty-diff.mustache b/src/templates/generic-empty-diff.mustache index 8cf14439..c67e9411 100644 --- a/src/templates/generic-empty-diff.mustache +++ b/src/templates/generic-empty-diff.mustache @@ -1,5 +1,5 @@ - +
File without changes
diff --git a/src/templates/wrapped-side-by-side-file-diff.mustache b/src/templates/wrapped-side-by-side-file-diff.mustache new file mode 100644 index 00000000..e862e985 --- /dev/null +++ b/src/templates/wrapped-side-by-side-file-diff.mustache @@ -0,0 +1,20 @@ +
+
+ {{{filePath}}} +
+
+
+ + + + + + + + + {{{diffs}}} + +
+
+
+
diff --git a/src/templates/wrapped-side-by-side-half-line.mustache b/src/templates/wrapped-side-by-side-half-line.mustache new file mode 100644 index 00000000..e9441f81 --- /dev/null +++ b/src/templates/wrapped-side-by-side-half-line.mustache @@ -0,0 +1,19 @@ + + {{{lineNumber}}} + + +
+ {{#prefix}} + {{{prefix}}} + {{/prefix}} + {{^prefix}} +   + {{/prefix}} + {{#content}} + {{{content}}} + {{/content}} + {{^content}} +
+ {{/content}} +
+ diff --git a/src/templates/wrapped-side-by-side-line.mustache b/src/templates/wrapped-side-by-side-line.mustache new file mode 100644 index 00000000..481f1d0c --- /dev/null +++ b/src/templates/wrapped-side-by-side-line.mustache @@ -0,0 +1,4 @@ + + {{{left}}} + {{{right}}} + diff --git a/src/types.ts b/src/types.ts index d226286b..6b426163 100644 --- a/src/types.ts +++ b/src/types.ts @@ -70,11 +70,12 @@ export interface DiffFile extends DiffFileName { mode?: string; } -export type OutputFormatType = 'line-by-line' | 'side-by-side'; +export type OutputFormatType = 'line-by-line' | 'side-by-side' | 'wrapped-side-by-side'; export const OutputFormatType: { [_: string]: OutputFormatType } = { LINE_BY_LINE: 'line-by-line', SIDE_BY_SIDE: 'side-by-side', + WRAPPED_SIDE_BY_SIDE: 'wrapped-side-by-side', }; export type LineMatchingType = 'lines' | 'words' | 'none'; diff --git a/src/ui/css/diff2html.css b/src/ui/css/diff2html.css index f133b013..c60c02fc 100644 --- a/src/ui/css/diff2html.css +++ b/src/ui/css/diff2html.css @@ -176,6 +176,22 @@ font-size: 13px; } +.d2h-diff-wrapped-table { + width: 100%; + border-collapse: collapse; + font-family: 'Menlo', 'Consolas', monospace; + font-size: 13px; + table-layout: fixed; +} + +.d2h-diff-wrapped-col-linenumber { + width: 4em; +} + +.d2h-diff-wrapped-col-contents { + width: calc(50% - 4em); +} + .d2h-files-diff { display: flex; width: 100%; @@ -215,6 +231,15 @@ padding: 0 4.5em; } +.d2h-code-wrapped-side-line { + display: inline-block; + white-space: nowrap; + user-select: none; + width: calc(100% - 1em); + /* No compensate for the line numbers */ + padding-left: 0.5em; +} + .d2h-code-line-ctn { display: inline-block; background: none; @@ -226,8 +251,21 @@ vertical-align: middle; } +.d2h-code-wrapped-line-ctn { + display: inline-block; + background: none; + padding: 0; + word-wrap: normal; + white-space: pre-wrap; + user-select: text; + width: 100%; + vertical-align: middle; + word-break: break-word; +} + .d2h-code-line del, -.d2h-code-side-line del { +.d2h-code-side-line del, +.d2h-code-wrapped-side-line del { display: inline-block; margin-top: -1px; text-decoration: none; @@ -236,7 +274,8 @@ } .d2h-code-line ins, -.d2h-code-side-line ins { +.d2h-code-side-line ins, +.d2h-code-wrapped-side-line ins { display: inline-block; margin-top: -1px; text-decoration: none; @@ -253,6 +292,15 @@ white-space: pre; } +.d2h-code-wrapped-line-prefix { + display: inline; + background: none; + padding: 0; + word-wrap: normal; + white-space: pre; + vertical-align: top; +} + .line-num1 { box-sizing: border-box; float: left; @@ -310,6 +358,25 @@ content: '\200b'; } +.d2h-code-wrapped-side-linenumber { + display: table-cell; + box-sizing: border-box; + width: 4em; + background-color: var(--d2h-bg-color); + color: var(--d2h-dim-color); + text-align: right; + border: solid var(--d2h-line-border-color); + border-width: 0 1px 0 1px; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 0.5em 0 0.5em; +} + +.d2h-code-wrapped-side-linenumber:after { + content: '\200b'; +} + .d2h-code-side-emptyplaceholder, .d2h-emptyplaceholder { background-color: var(--d2h-empty-placeholder-bg-color); @@ -318,13 +385,16 @@ .d2h-code-linenumber, .d2h-code-side-linenumber, +.d2h-code-wrapped-side-linenumber, .d2h-code-line-prefix, +.d2h-code-wrapped-line-prefix, .d2h-emptyplaceholder { user-select: none; } .d2h-code-linenumber, -.d2h-code-side-linenumber { +.d2h-code-side-linenumber, +.d2h-code-wrapped-side-linenumber { direction: rtl; } @@ -484,12 +554,14 @@ } .d2h-dark-color-scheme .d2h-code-line del, -.d2h-dark-color-scheme .d2h-code-side-line del { +.d2h-dark-color-scheme .d2h-code-side-line del, +.d2h-dark-color-scheme .d2h-code-wrapped-side-line del { background-color: var(--d2h-dark-del-highlight-bg-color); } .d2h-dark-color-scheme .d2h-code-line ins, -.d2h-dark-color-scheme .d2h-code-side-line ins { +.d2h-dark-color-scheme .d2h-code-side-line ins, +.d2h-dark-color-scheme .d2h-code-wrapped-side-line ins { background-color: var(--d2h-dark-ins-highlight-bg-color); } @@ -503,6 +575,12 @@ border-color: var(--d2h-dark-line-border-color); } +.d2h-dark-color-scheme .d2h-code-wrapped-side-linenumber { + background-color: var(--d2h-dark-bg-color); + color: var(--d2h-dark-dim-color); + border-color: var(--d2h-dark-line-border-color); +} + .d2h-dark-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder, .d2h-dark-color-scheme .d2h-files-diff .d2h-emptyplaceholder { background-color: var(--d2h-dark-empty-placeholder-bg-color); @@ -624,12 +702,14 @@ } .d2h-auto-color-scheme .d2h-code-line del, - .d2h-auto-color-scheme .d2h-code-side-line del { + .d2h-auto-color-scheme .d2h-code-side-line del, + .d2h-dark-color-scheme .d2h-code-wrapped-side-line del { background-color: var(--d2h-dark-del-highlight-bg-color); } .d2h-auto-color-scheme .d2h-code-line ins, - .d2h-auto-color-scheme .d2h-code-side-line ins { + .d2h-auto-color-scheme .d2h-code-side-line ins, + .d2h-dark-color-scheme .d2h-code-wrapped-side-line ins { background-color: var(--d2h-dark-ins-highlight-bg-color); } @@ -643,6 +723,12 @@ border-color: var(--d2h-dark-line-border-color); } + .d2h-auto-color-scheme .d2h-code-wrapped-side-linenumber { + background-color: var(--d2h-dark-bg-color); + color: var(--d2h-dark-dim-color); + border-color: var(--d2h-dark-line-border-color); + } + .d2h-auto-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder, .d2h-auto-color-scheme .d2h-files-diff .d2h-emptyplaceholder { background-color: var(--d2h-dark-empty-placeholder-bg-color); diff --git a/src/ui/js/diff2html-ui-base.ts b/src/ui/js/diff2html-ui-base.ts index f2379501..f1f7fb79 100644 --- a/src/ui/js/diff2html-ui-base.ts +++ b/src/ui/js/diff2html-ui-base.ts @@ -164,7 +164,7 @@ export class Diff2HtmlUI { } // Collect all the code lines and execute the highlight on them - const codeLines = file.querySelectorAll('.d2h-code-line-ctn'); + const codeLines = file.querySelectorAll('.d2h-code-line-ctn, .d2h-code-wrapped-line-ctn'); codeLines.forEach(line => { const text = line.textContent; const lineParent = line.parentNode; diff --git a/src/wrapped-side-by-side-renderer.ts b/src/wrapped-side-by-side-renderer.ts new file mode 100644 index 00000000..a68118fd --- /dev/null +++ b/src/wrapped-side-by-side-renderer.ts @@ -0,0 +1,286 @@ +import HoganJsUtils from './hoganjs-utils'; +import * as Rematch from './rematch'; +import * as renderUtils from './render-utils'; +import { + DiffFile, + DiffLine, + LineType, + DiffBlock, + DiffLineDeleted, + DiffLineContent, + DiffLineContext, + DiffLineInserted, +} from './types'; +import { max } from './utils'; + +export interface WrappedSideBySideRendererConfig extends renderUtils.RenderConfig { + renderNothingWhenEmpty?: boolean; + matchingMaxComparisons?: number; + maxLineSizeInBlockForComparison?: number; +} + +export const defaultWrappedSideBySideRendererConfig = { + ...renderUtils.defaultRenderConfig, + renderNothingWhenEmpty: false, + matchingMaxComparisons: 2500, + maxLineSizeInBlockForComparison: 200, +}; + +const genericTemplatesPath = 'generic'; +const baseTemplatesPath = 'wrapped-side-by-side'; +const iconsBaseTemplatesPath = 'icon'; +const tagsBaseTemplatesPath = 'tag'; + +export default class WrappedSideBySideRenderer { + private readonly hoganUtils: HoganJsUtils; + private readonly config: typeof defaultWrappedSideBySideRendererConfig; + + constructor(hoganUtils: HoganJsUtils, config: WrappedSideBySideRendererConfig = {}) { + this.hoganUtils = hoganUtils; + this.config = { ...defaultWrappedSideBySideRendererConfig, ...config }; + } + + 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'); + + return this.hoganUtils.render(genericTemplatesPath, 'wrapper', { + colorScheme: renderUtils.colorSchemeToCss(this.config.colorScheme), + content: diffsHtml, + }); + } + + makeFileDiffHtml(file: DiffFile, diffs: string): 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)); + + return fileDiffTemplate.render({ + file: file, + fileHtmlId: renderUtils.getHtmlId(file), + diffs: diffs, + filePath: filePathTemplate.render( + { + fileDiffName: renderUtils.filenameDiff(file), + }, + { + fileIcon: fileIconTemplate, + fileTag: fileTagTemplate, + }, + ), + }); + } + + generateEmptyDiff(): string { + return this.hoganUtils.render(genericTemplatesPath, 'empty-diff', { + emptyHeaderSpan: 4, + contentClass: 'd2h-code-wrapped-side-line', + CSSLineClass: renderUtils.CSSLineClass, + }); + } + + generateFileHtml(file: DiffFile): string { + const matcher = Rematch.newMatcherFn( + Rematch.newDistanceFn((e: DiffLine) => renderUtils.deconstructLine(e.content, file.isCombined).content), + ); + + return file.blocks + .map(block => { + let lines = this.hoganUtils.render(genericTemplatesPath, 'block-header', { + CSSLineClass: renderUtils.CSSLineClass, + blockHeader: file.isTooBig ? block.header : renderUtils.escapeForHtml(block.header), + blockHeaderSpan: 3, + lineClass: 'd2h-code-wrapped-side-linenumber', + contentClass: 'd2h-code-wrapped-side-line', + }); + + this.applyLineGroupping(block).forEach(([contextLines, oldLines, newLines]) => { + if (oldLines.length && newLines.length && !contextLines.length) { + this.applyRematchMatching(oldLines, newLines, matcher).map(([oldLines, newLines]) => { + lines += this.processChangedLines(file, file.isCombined, oldLines, newLines); + }); + } else if (contextLines.length) { + contextLines.forEach(line => { + const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined); + lines += this.generateLineHtml( + file, + { + type: renderUtils.CSSLineClass.CONTEXT, + prefix: prefix, + content: content, + number: line.oldNumber, + }, + { + type: renderUtils.CSSLineClass.CONTEXT, + prefix: prefix, + content: content, + number: line.newNumber, + }, + ); + }); + } else if (oldLines.length || newLines.length) { + lines += this.processChangedLines(file, file.isCombined, oldLines, newLines); + } else { + console.error('Unknown state reached while processing groups of lines', contextLines, oldLines, newLines); + } + }); + + return lines; + }) + .join('\n'); + } + + applyLineGroupping(block: DiffBlock): DiffLineGroups { + const blockLinesGroups: DiffLineGroups = []; + + let oldLines: (DiffLineDeleted & DiffLineContent)[] = []; + let newLines: (DiffLineInserted & DiffLineContent)[] = []; + + for (let i = 0; i < block.lines.length; i++) { + const diffLine = block.lines[i]; + + if ( + (diffLine.type !== LineType.INSERT && newLines.length) || + (diffLine.type === LineType.CONTEXT && oldLines.length > 0) + ) { + blockLinesGroups.push([[], oldLines, newLines]); + oldLines = []; + newLines = []; + } + + if (diffLine.type === LineType.CONTEXT) { + blockLinesGroups.push([[diffLine], [], []]); + } else if (diffLine.type === LineType.INSERT && oldLines.length === 0) { + blockLinesGroups.push([[], [], [diffLine]]); + } else if (diffLine.type === LineType.INSERT && oldLines.length > 0) { + newLines.push(diffLine); + } else if (diffLine.type === LineType.DELETE) { + oldLines.push(diffLine); + } + } + + if (oldLines.length || newLines.length) { + blockLinesGroups.push([[], oldLines, newLines]); + oldLines = []; + newLines = []; + } + + return blockLinesGroups; + } + + applyRematchMatching( + oldLines: DiffLine[], + newLines: DiffLine[], + matcher: Rematch.MatcherFn, + ): DiffLine[][][] { + const comparisons = oldLines.length * newLines.length; + const maxLineSizeInBlock = max(oldLines.concat(newLines).map(elem => elem.content.length)); + const doMatching = + comparisons < this.config.matchingMaxComparisons && + maxLineSizeInBlock < this.config.maxLineSizeInBlockForComparison && + (this.config.matching === 'lines' || this.config.matching === 'words'); + + return doMatching ? matcher(oldLines, newLines) : [[oldLines, newLines]]; + } + + processChangedLines(file: DiffFile, isCombined: boolean, oldLines: DiffLine[], newLines: DiffLine[]): string { + let lines = ''; + + const maxLinesNumber = Math.max(oldLines.length, newLines.length); + for (let i = 0; i < maxLinesNumber; i++) { + const oldLine = oldLines[i]; + const newLine = newLines[i]; + + const diff = + oldLine !== undefined && newLine !== undefined + ? renderUtils.diffHighlight(oldLine.content, newLine.content, isCombined, this.config) + : undefined; + + const preparedOldLine = + oldLine !== undefined && oldLine.oldNumber !== undefined + ? { + ...(diff !== undefined + ? { + prefix: diff.oldLine.prefix, + content: diff.oldLine.content, + type: renderUtils.CSSLineClass.DELETE_CHANGES, + } + : { + ...renderUtils.deconstructLine(oldLine.content, isCombined), + type: renderUtils.toCSSClass(oldLine.type), + }), + number: oldLine.oldNumber, + } + : undefined; + + const preparedNewLine = + newLine !== undefined && newLine.newNumber !== undefined + ? { + ...(diff !== undefined + ? { + prefix: diff.newLine.prefix, + content: diff.newLine.content, + type: renderUtils.CSSLineClass.INSERT_CHANGES, + } + : { + ...renderUtils.deconstructLine(newLine.content, isCombined), + type: renderUtils.toCSSClass(newLine.type), + }), + number: newLine.newNumber, + } + : undefined; + + lines += this.generateLineHtml(file, preparedOldLine, preparedNewLine); + } + + return lines; + } + + generateLineHtml(file: DiffFile, oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): string { + return this.hoganUtils.render(baseTemplatesPath, 'line', { + left: this.generateSingleHtml(file, oldLine), + right: this.generateSingleHtml(file, newLine), + }); + } + + generateSingleHtml(file: DiffFile, line?: DiffPreparedLine): string { + const lineClass = 'd2h-code-wrapped-side-linenumber'; + const contentClass = 'd2h-code-wrapped-side-line'; + + return this.hoganUtils.render(baseTemplatesPath, 'half-line', { + type: line?.type || `${renderUtils.CSSLineClass.CONTEXT} d2h-emptyplaceholder`, + lineClass: line !== undefined ? lineClass : `${lineClass} d2h-code-side-emptyplaceholder`, + contentClass: line !== undefined ? contentClass : `${contentClass} d2h-code-side-emptyplaceholder`, + prefix: line?.prefix === ' ' ? ' ' : line?.prefix, + content: line?.content, + lineNumber: line?.number, + line, + file, + }); + } +} + +type DiffLineGroups = [ + (DiffLineContext & DiffLineContent)[], + (DiffLineDeleted & DiffLineContent)[], + (DiffLineInserted & DiffLineContent)[], +][]; + +type DiffPreparedLine = { + type: renderUtils.CSSLineClass; + prefix: string; + content: string; + number: number; +}; diff --git a/website/templates/pages/demo/content.handlebars b/website/templates/pages/demo/content.handlebars index 0db7b0a7..90fc70fc 100644 --- a/website/templates/pages/demo/content.handlebars +++ b/website/templates/pages/demo/content.handlebars @@ -39,6 +39,7 @@