diff --git a/src/__tests__/diff2html-tests.ts b/src/__tests__/diff2html-tests.ts index 6528b404..eeeae5c0 100644 --- a/src/__tests__/diff2html-tests.ts +++ b/src/__tests__/diff2html-tests.ts @@ -157,26 +157,32 @@ describe('Diff2Html', () => { "type": "delete", }, { - "content": "+ TokenRevoked, MissingToken,", + "content": "\\ No newline at end of file", "newNumber": 53, + "oldNumber": 55, + "type": "context", + }, + { + "content": "+ TokenRevoked, MissingToken,", + "newNumber": 54, "oldNumber": undefined, "type": "insert", }, { "content": "+ IndexLock, RepositoryError, NotValidRepo, PullRequestNotMergeable, BranchError,", - "newNumber": 54, + "newNumber": 55, "oldNumber": undefined, "type": "insert", }, { "content": "+ PluginError, CodeParserError, EngineError = Value", - "newNumber": 55, + "newNumber": 56, "oldNumber": undefined, "type": "insert", }, { "content": "+}", - "newNumber": 56, + "newNumber": 57, "oldNumber": undefined, "type": "insert", }, diff --git a/src/__tests__/side-by-side-printer-tests.ts b/src/__tests__/side-by-side-printer-tests.ts index ab886aba..3ea0cb7f 100644 --- a/src/__tests__/side-by-side-printer-tests.ts +++ b/src/__tests__/side-by-side-printer-tests.ts @@ -407,6 +407,357 @@ describe('SideBySideRenderer', () => { `); }); + it('should handle files without newlines at the end', () => { + const exampleJson: DiffFile[] = [ + { + blocks: [ + // Scenario 1: Old file missing newline, new file has newline + { + lines: [ + { + content: '-oldLine1', + type: LineType.DELETE, + oldNumber: 1, + newNumber: undefined, + }, + { + content: '\\ No newline at end of file', + type: LineType.CONTEXT, + oldNumber: 1, + newNumber: 1, + }, + { + content: '+newLine1', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 1, + }, + ], + oldStartLine: 1, + newStartLine: 1, + header: '@@ -1 +1 @@', + }, + // Scenario 2: Old file has newline, new file missing newline + { + lines: [ + { + content: '-oldLine2', + type: LineType.DELETE, + oldNumber: 2, + newNumber: undefined, + }, + { + content: '+newLine2', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 2, + }, + { + content: '\\ No newline at end of file', + type: LineType.CONTEXT, + oldNumber: 2, + newNumber: 2, + }, + ], + oldStartLine: 2, + newStartLine: 2, + header: '@@ -2 +2 @@', + }, + // Scenario 3: Both files missing newline + { + lines: [ + { + content: '-oldLine3', + type: LineType.DELETE, + oldNumber: 3, + newNumber: undefined, + }, + { + content: '\\ No newline at end of file', + type: LineType.CONTEXT, + oldNumber: 3, + newNumber: 3, + }, + { + content: '+newLine3', + type: LineType.INSERT, + oldNumber: undefined, + newNumber: 3, + }, + { + content: '\\ No newline at end of file', + type: LineType.CONTEXT, + oldNumber: 3, + newNumber: 3, + }, + ], + oldStartLine: 3, + newStartLine: 3, + header: '@@ -3 +3 @@', + }, + ], + deletedLines: 3, + addedLines: 3, + oldName: 'sample', + language: 'txt', + newName: 'sample', + isCombined: false, + isGitDiff: true, + }, + ]; + + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); + const html = sideBySideRenderer.render(exampleJson); + expect(html).toMatchInlineSnapshot(` + "<div class="d2h-wrapper d2h-light-color-scheme"> + <div id="d2h-675094" class="d2h-file-wrapper" data-lang="txt"> + <div class="d2h-file-header"> + <span class="d2h-file-name-wrapper"> + <svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12"> + <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path> + </svg> <span class="d2h-file-name">sample</span> + <span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span> + <label class="d2h-file-collapse"> + <input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed"> + Viewed + </label> + </div> + <div class="d2h-files-diff"> + <div class="d2h-file-side-diff"> + <div class="d2h-code-wrapper"> + <table class="d2h-diff-table"> + <tbody class="d2h-diff-tbody"> + <tr> + <td class="d2h-code-side-linenumber d2h-info"></td> + <td class="d2h-info"> + <div class="d2h-code-side-line">@@ -1 +1 @@</div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-del"> + 1 + </td> + <td class="d2h-del"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">-</span> + <span class="d2h-code-line-ctn">oldLine1</span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-cntx"> + 1 + </td> + <td class="d2h-cntx"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">\\</span> + <span class="d2h-code-line-ctn"> No newline at end of file</span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder"> + + </td> + <td class="d2h-cntx d2h-emptyplaceholder"> + <div class="d2h-code-side-line d2h-code-side-emptyplaceholder"> + <span class="d2h-code-line-prefix"> </span> + <span class="d2h-code-line-ctn"><br></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-info"></td> + <td class="d2h-info"> + <div class="d2h-code-side-line">@@ -2 +2 @@</div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-del d2h-change"> + 2 + </td> + <td class="d2h-del d2h-change"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">-</span> + <span class="d2h-code-line-ctn"><del>oldLine2</del></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder"> + + </td> + <td class="d2h-cntx d2h-emptyplaceholder"> + <div class="d2h-code-side-line d2h-code-side-emptyplaceholder"> + <span class="d2h-code-line-prefix"> </span> + <span class="d2h-code-line-ctn"><br></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-info"></td> + <td class="d2h-info"> + <div class="d2h-code-side-line">@@ -3 +3 @@</div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-del"> + 3 + </td> + <td class="d2h-del"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">-</span> + <span class="d2h-code-line-ctn">oldLine3</span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-cntx"> + 3 + </td> + <td class="d2h-cntx"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">\\</span> + <span class="d2h-code-line-ctn"> No newline at end of file</span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder"> + + </td> + <td class="d2h-cntx d2h-emptyplaceholder"> + <div class="d2h-code-side-line d2h-code-side-emptyplaceholder"> + <span class="d2h-code-line-prefix"> </span> + <span class="d2h-code-line-ctn"><br></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder"> + + </td> + <td class="d2h-cntx d2h-emptyplaceholder"> + <div class="d2h-code-side-line d2h-code-side-emptyplaceholder"> + <span class="d2h-code-line-prefix"> </span> + <span class="d2h-code-line-ctn"><br></span> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + <div class="d2h-file-side-diff"> + <div class="d2h-code-wrapper"> + <table class="d2h-diff-table"> + <tbody class="d2h-diff-tbody"> + <tr> + <td class="d2h-code-side-linenumber d2h-info"></td> + <td class="d2h-info"> + <div class="d2h-code-side-line"> </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder"> + + </td> + <td class="d2h-cntx d2h-emptyplaceholder"> + <div class="d2h-code-side-line d2h-code-side-emptyplaceholder"> + <span class="d2h-code-line-prefix"> </span> + <span class="d2h-code-line-ctn"><br></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder"> + + </td> + <td class="d2h-cntx d2h-emptyplaceholder"> + <div class="d2h-code-side-line d2h-code-side-emptyplaceholder"> + <span class="d2h-code-line-prefix"> </span> + <span class="d2h-code-line-ctn"><br></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-ins"> + 1 + </td> + <td class="d2h-ins"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">+</span> + <span class="d2h-code-line-ctn">newLine1</span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-info"></td> + <td class="d2h-info"> + <div class="d2h-code-side-line"> </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-ins d2h-change"> + 2 + </td> + <td class="d2h-ins d2h-change"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">+</span> + <span class="d2h-code-line-ctn"><ins>newLine2</ins></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-cntx"> + 2 + </td> + <td class="d2h-cntx"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">\\</span> + <span class="d2h-code-line-ctn"> No newline at end of file</span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-info"></td> + <td class="d2h-info"> + <div class="d2h-code-side-line"> </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder"> + + </td> + <td class="d2h-cntx d2h-emptyplaceholder"> + <div class="d2h-code-side-line d2h-code-side-emptyplaceholder"> + <span class="d2h-code-line-prefix"> </span> + <span class="d2h-code-line-ctn"><br></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder"> + + </td> + <td class="d2h-cntx d2h-emptyplaceholder"> + <div class="d2h-code-side-line d2h-code-side-emptyplaceholder"> + <span class="d2h-code-line-prefix"> </span> + <span class="d2h-code-line-ctn"><br></span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-ins"> + 3 + </td> + <td class="d2h-ins"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">+</span> + <span class="d2h-code-line-ctn">newLine3</span> + </div> + </td> + </tr><tr> + <td class="d2h-code-side-linenumber d2h-cntx"> + 3 + </td> + <td class="d2h-cntx"> + <div class="d2h-code-side-line"> + <span class="d2h-code-line-prefix">\\</span> + <span class="d2h-code-line-ctn"> No newline at end of file</span> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + </div>" + `); + }); + it('should work for too big file diff', () => { const exampleJson = [ { diff --git a/src/diff-parser.ts b/src/diff-parser.ts index 83d9c630..2c654202 100644 --- a/src/diff-parser.ts +++ b/src/diff-parser.ts @@ -87,16 +87,15 @@ export function parse(diffInput: string, config: DiffParserConfig = {}): DiffFil const binaryFiles = /^Binary files (.*) and (.*) differ/; const binaryDiff = /^GIT binary patch/; + const noNewlineAtEndOfFile = /^\\ No newline at end of file/; + /* Combined Diff */ const combinedIndex = /^index ([\da-z]+),([\da-z]+)\.\.([\da-z]+)/; const combinedMode = /^mode (\d{6}),(\d{6})\.\.(\d{6})/; const combinedNewFile = /^new file mode (\d{6})/; const combinedDeletedFile = /^deleted file mode (\d{6}),(\d{6})/; - const diffLines = diffInput - .replace(/\\ No newline at end of file/g, '') - .replace(/\r\n?/g, '\n') - .split('\n'); + const diffLines = diffInput.replace(/\r\n?/g, '\n').split('\n'); /* Add previous block(if exists) before start a new file */ function saveBlock(): void { @@ -471,6 +470,8 @@ export function parse(diffInput: string, config: DiffParserConfig = {}): DiffFil } else if ((values = combinedDeletedFile.exec(line))) { currentFile.deletedFileMode = values[1]; currentFile.isDeleted = true; + } else if (line.match(noNewlineAtEndOfFile)) { + createLine(line); } }); diff --git a/src/render-utils.ts b/src/render-utils.ts index f557796e..71e1916a 100644 --- a/src/render-utils.ts +++ b/src/render-utils.ts @@ -83,6 +83,8 @@ export function toCSSClass(lineType: LineType): CSSLineClass { return CSSLineClass.INSERTS; case LineType.DELETE: return CSSLineClass.DELETES; + case LineType.NO_NEW_LINE: + return CSSLineClass.CONTEXT; } } diff --git a/src/side-by-side-renderer.ts b/src/side-by-side-renderer.ts index e63893ae..43b9c901 100644 --- a/src/side-by-side-renderer.ts +++ b/src/side-by-side-renderer.ts @@ -10,6 +10,7 @@ import { DiffLineDeleted, DiffLineInserted, DiffLineContent, + DiffLineNoNewline, } from './types'; export interface SideBySideRendererConfig extends renderUtils.RenderConfig { @@ -114,20 +115,39 @@ export default class SideBySideRenderer { } else if (contextLines.length) { contextLines.forEach(line => { const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined); - const { left, right } = this.generateLineHtml( - { + let leftContext = undefined; + let rightContext = undefined; + if (line.type !== LineType.NO_NEW_LINE) { + leftContext = { type: renderUtils.CSSLineClass.CONTEXT, prefix: prefix, content: content, number: line.oldNumber, - }, - { + }; + rightContext = { type: renderUtils.CSSLineClass.CONTEXT, prefix: prefix, content: content, number: line.newNumber, - }, - ); + }; + } else if (line.type === LineType.NO_NEW_LINE) { + if (line.isLeft) { + leftContext = { + type: renderUtils.CSSLineClass.CONTEXT, + prefix: prefix, + content: content, + number: line.oldNumber, + }; + } else { + rightContext = { + type: renderUtils.CSSLineClass.CONTEXT, + prefix: prefix, + content: content, + number: line.newNumber, + }; + } + } + const { left, right } = this.generateLineHtml(leftContext, rightContext); fileHtml.left += left; fileHtml.right += right; }); @@ -155,6 +175,7 @@ export default class SideBySideRenderer { let oldLines: (DiffLineDeleted & DiffLineContent)[] = []; let newLines: (DiffLineInserted & DiffLineContent)[] = []; + let lastLineType: LineType = LineType.DELETE; for (let i = 0; i < block.lines.length; i++) { const diffLine = block.lines[i]; @@ -169,7 +190,16 @@ export default class SideBySideRenderer { } if (diffLine.type === LineType.CONTEXT) { - blockLinesGroups.push([[diffLine], [], []]); + if (diffLine.content.trim() === '\\ No newline at end of file') { + const noNewLine: DiffLineNoNewline & DiffLineContent = { + ...diffLine, + isLeft: lastLineType === LineType.DELETE, + type: LineType.NO_NEW_LINE, + }; + blockLinesGroups.push([[noNewLine], [], []]); + } else { + blockLinesGroups.push([[diffLine], [], []]); + } } else if (diffLine.type === LineType.INSERT && oldLines.length === 0) { blockLinesGroups.push([[], [], [diffLine]]); } else if (diffLine.type === LineType.INSERT && oldLines.length > 0) { @@ -177,6 +207,11 @@ export default class SideBySideRenderer { } else if (diffLine.type === LineType.DELETE) { oldLines.push(diffLine); } + + // Track the last non-context line type to determine where "No newline" belongs + if (diffLine.type !== LineType.CONTEXT) { + lastLineType = diffLine.type; + } } if (oldLines.length || newLines.length) { @@ -296,7 +331,7 @@ export default class SideBySideRenderer { } type DiffLineGroups = [ - (DiffLineContext & DiffLineContent)[], + ((DiffLineContext & DiffLineContent) | (DiffLineNoNewline & DiffLineContent))[], (DiffLineDeleted & DiffLineContent)[], (DiffLineInserted & DiffLineContent)[], ][]; diff --git a/src/types.ts b/src/types.ts index d226286b..2573e8cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export enum LineType { INSERT = 'insert', DELETE = 'delete', CONTEXT = 'context', + NO_NEW_LINE = 'noNewLine', } export interface DiffLineDeleted { @@ -27,6 +28,13 @@ export interface DiffLineContext { newNumber: number; } +export interface DiffLineNoNewline { + type: LineType.NO_NEW_LINE; + oldNumber: number; + newNumber: number; + isLeft: boolean; +} + export type DiffLineContent = { content: string; };