use modern methods to render HTML diffs

This commit is contained in:
Lordfirespeed 2023-03-05 22:48:40 +00:00
parent 551a0b407f
commit ab5ff8d148
No known key found for this signature in database
GPG key ID: 9C01965EC9DE6690
10 changed files with 202 additions and 195 deletions

View file

@ -82,6 +82,7 @@ export default class LineByLineRenderer {
generateEmptyDiff(): string {
return this.hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-line',
colspan: '2',
CSSLineClass: renderUtils.CSSLineClass,
});
}
@ -93,35 +94,43 @@ export default class LineByLineRenderer {
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),
lineClass: 'd2h-code-linenumber',
contentClass: 'd2h-code-line',
});
const lines: string[] = [];
this.applyLineGroupping(block).forEach(([contextLines, oldLines, newLines]) => {
lines.push(
this.hoganUtils.render(genericTemplatesPath, 'block-header', {
CSSLineClass: renderUtils.CSSLineClass,
margin_colspan: '2',
colspan: '1',
blockHeader: file.isTooBig ? block.header : renderUtils.escapeForHtml(block.header),
lineClass: 'd2h-code-linenumber',
contentClass: 'd2h-code-line',
}),
);
this.applyLineGrouping(block).forEach(([contextLines, oldLines, newLines]) => {
if (oldLines.length && newLines.length && !contextLines.length) {
this.applyRematchMatching(oldLines, newLines, matcher).map(([oldLines, newLines]) => {
const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines);
lines += left;
lines += right;
lines.push(...left);
lines.push(...right);
});
} else if (contextLines.length) {
contextLines.forEach(line => {
const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined);
lines += this.generateSingleLineHtml({
type: renderUtils.CSSLineClass.CONTEXT,
prefix: prefix,
content: content,
oldNumber: line.oldNumber,
newNumber: line.newNumber,
});
lines.push(
this.generateSingleLineHtml({
type: renderUtils.CSSLineClass.CONTEXT,
prefix: prefix,
content: content,
oldNumber: line.oldNumber,
newNumber: line.newNumber,
}),
);
});
} else if (oldLines.length || newLines.length) {
const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines);
lines += left;
lines += right;
lines.push(...left);
lines.push(...right);
} else {
console.error('Unknown state reached while processing groups of lines', contextLines, oldLines, newLines);
}
@ -129,10 +138,13 @@ export default class LineByLineRenderer {
return lines;
})
.map((block_html: string[]) => {
return block_html.map(line => `<tr>${line}</tr>`).join('\n');
})
.join('\n');
}
applyLineGroupping(block: DiffBlock): DiffLineGroups {
applyLineGrouping(block: DiffBlock): DiffLineGroups {
const blockLinesGroups: DiffLineGroups = [];
let oldLines: (DiffLineDeleted & DiffLineContent)[] = [];
@ -189,9 +201,9 @@ export default class LineByLineRenderer {
}
processChangedLines(isCombined: boolean, oldLines: DiffLine[], newLines: DiffLine[]): FileHtml {
const fileHtml = {
right: '',
left: '',
const fileHtml: FileHtml = {
left: [],
right: [],
};
const maxLinesNumber = Math.max(oldLines.length, newLines.length);
@ -241,8 +253,8 @@ export default class LineByLineRenderer {
: undefined;
const { left, right } = this.generateLineHtml(preparedOldLine, preparedNewLine);
fileHtml.left += left;
fileHtml.right += right;
fileHtml.left.push(...left);
fileHtml.right.push(...right);
}
return fileHtml;
@ -250,27 +262,34 @@ export default class LineByLineRenderer {
generateLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): FileHtml {
return {
left: this.generateSingleLineHtml(oldLine),
right: this.generateSingleLineHtml(newLine),
left: [this.generateSingleLineHtml(oldLine)],
right: [this.generateSingleLineHtml(newLine)],
};
}
generateSingleLineHtml(line?: DiffPreparedLine): string {
if (line === undefined) return '';
const lineNumberHtml = this.hoganUtils.render(baseTemplatesPath, 'numbers', {
oldNumber: line.oldNumber || '',
newNumber: line.newNumber || '',
const oldLineNumberHtml = this.hoganUtils.render(genericTemplatesPath, 'line-number', {
type: line.type,
lineClass: line.oldNumber ? 'd2h-code-linenumber' : 'd2h-code-emptyplaceholder',
lineNumber: line.oldNumber,
});
return this.hoganUtils.render(genericTemplatesPath, 'line', {
const newLineNumberHtml = this.hoganUtils.render(genericTemplatesPath, 'line-number', {
type: line.type,
lineClass: line.newNumber ? 'd2h-code-linenumber' : 'd2h-code-emptyplaceholder',
lineNumber: line.newNumber,
});
const newLineContentHtml = this.hoganUtils.render(genericTemplatesPath, 'line', {
type: line.type,
lineClass: 'd2h-code-linenumber',
contentClass: 'd2h-code-line',
prefix: line.prefix === ' ' ? '&nbsp;' : line.prefix,
content: line.content,
lineNumber: lineNumberHtml,
});
return oldLineNumberHtml.concat(newLineNumberHtml, newLineContentHtml);
}
}
@ -289,6 +308,6 @@ type DiffPreparedLine = {
};
type FileHtml = {
left: string;
right: string;
left: string[];
right: string[];
};

View file

@ -55,7 +55,7 @@ export default class SideBySideRenderer {
return this.hoganUtils.render(genericTemplatesPath, 'wrapper', { content: diffsHtml });
}
makeFileDiffHtml(file: DiffFile, diffs: FileHtml): string {
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');
@ -79,34 +79,32 @@ export default class SideBySideRenderer {
});
}
generateEmptyDiff(): FileHtml {
return {
right: '',
left: this.hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-side-line',
CSSLineClass: renderUtils.CSSLineClass,
}),
};
generateEmptyDiff(): string {
return this.hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-side-line',
colspan: '4',
CSSLineClass: renderUtils.CSSLineClass,
});
}
generateFileHtml(file: DiffFile): FileHtml {
generateFileHtml(file: DiffFile): string {
const matcher = Rematch.newMatcherFn(
Rematch.newDistanceFn((e: DiffLine) => renderUtils.deconstructLine(e.content, file.isCombined).content),
);
return file.blocks
.map(block => {
const fileHtml = {
left: this.makeHeaderHtml(block.header, file),
right: this.makeHeaderHtml(''),
const fileHtml: FileHtml = {
left: [this.makeHeaderHtml(block.header, file)],
right: [''],
};
this.applyLineGroupping(block).forEach(([contextLines, oldLines, newLines]) => {
this.applyLineGrouping(block).forEach(([contextLines, oldLines, newLines]) => {
if (oldLines.length && newLines.length && !contextLines.length) {
this.applyRematchMatching(oldLines, newLines, matcher).map(([oldLines, newLines]) => {
const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines);
fileHtml.left += left;
fileHtml.right += right;
fileHtml.left.push(...left);
fileHtml.right.push(...right);
});
} else if (contextLines.length) {
contextLines.forEach(line => {
@ -125,13 +123,13 @@ export default class SideBySideRenderer {
number: line.newNumber,
},
);
fileHtml.left += left;
fileHtml.right += right;
fileHtml.left.push(...left);
fileHtml.right.push(...right);
});
} else if (oldLines.length || newLines.length) {
const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines);
fileHtml.left += left;
fileHtml.right += right;
fileHtml.left.push(...left);
fileHtml.right.push(...right);
} else {
console.error('Unknown state reached while processing groups of lines', contextLines, oldLines, newLines);
}
@ -139,15 +137,19 @@ export default class SideBySideRenderer {
return fileHtml;
})
.reduce(
(accomulated, html) => {
return { left: accomulated.left + html.left, right: accomulated.right + html.right };
},
{ left: '', right: '' },
);
.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(
`<tr>${block_html.left[block_line_index]} ${block_html.right[block_line_index]}</tr>`,
);
}
return block_html_string;
})
.join('\n');
}
applyLineGroupping(block: DiffBlock): DiffLineGroups {
applyLineGrouping(block: DiffBlock): DiffLineGroups {
const blockLinesGroups: DiffLineGroups = [];
let oldLines: (DiffLineDeleted & DiffLineContent)[] = [];
@ -206,6 +208,8 @@ export default class SideBySideRenderer {
makeHeaderHtml(blockHeader: string, file?: DiffFile): string {
return this.hoganUtils.render(genericTemplatesPath, 'block-header', {
CSSLineClass: renderUtils.CSSLineClass,
margin_colspan: '1',
colspan: '3',
blockHeader: file?.isTooBig ? blockHeader : renderUtils.escapeForHtml(blockHeader),
lineClass: 'd2h-code-side-linenumber',
contentClass: 'd2h-code-side-line',
@ -213,9 +217,9 @@ export default class SideBySideRenderer {
}
processChangedLines(isCombined: boolean, oldLines: DiffLine[], newLines: DiffLine[]): FileHtml {
const fileHtml = {
right: '',
left: '',
const fileHtml: FileHtml = {
left: [],
right: [],
};
const maxLinesNumber = Math.max(oldLines.length, newLines.length);
@ -263,8 +267,8 @@ export default class SideBySideRenderer {
: undefined;
const { left, right } = this.generateLineHtml(preparedOldLine, preparedNewLine);
fileHtml.left += left;
fileHtml.right += right;
fileHtml.left.push(...left);
fileHtml.right.push(...right);
}
return fileHtml;
@ -272,8 +276,8 @@ export default class SideBySideRenderer {
generateLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): FileHtml {
return {
left: this.generateSingleHtml(oldLine),
right: this.generateSingleHtml(newLine),
left: [this.generateSingleHtml(oldLine)],
right: [this.generateSingleHtml(newLine)],
};
}
@ -281,14 +285,22 @@ export default class SideBySideRenderer {
const lineClass = 'd2h-code-side-linenumber';
const contentClass = 'd2h-code-side-line';
return this.hoganUtils.render(genericTemplatesPath, 'line', {
type: line?.type || `${renderUtils.CSSLineClass.CONTEXT} d2h-emptyplaceholder`,
const type = line?.type || `${renderUtils.CSSLineClass.CONTEXT} d2h-emptyplaceholder`;
const line_number_cell = this.hoganUtils.render(genericTemplatesPath, 'line-number', {
type: type,
lineClass: line !== undefined ? lineClass : `${lineClass} d2h-code-side-emptyplaceholder`,
lineNumber: line?.number,
});
const line_content_cell = this.hoganUtils.render(genericTemplatesPath, 'line', {
type: type,
contentClass: line !== undefined ? contentClass : `${contentClass} d2h-code-side-emptyplaceholder`,
prefix: line?.prefix === ' ' ? '&nbsp;' : line?.prefix,
content: line?.content,
lineNumber: line?.number,
});
return line_number_cell.concat(line_content_cell);
}
}
@ -306,6 +318,6 @@ type DiffPreparedLine = {
};
type FileHtml = {
left: string;
right: string;
left: string[];
right: string[];
};

View file

@ -1,6 +1,4 @@
<tr>
<td class="{{lineClass}} {{CSSLineClass.INFO}}"></td>
<td class="{{CSSLineClass.INFO}}">
<div class="{{contentClass}}">{{#blockHeader}}{{{blockHeader}}}{{/blockHeader}}{{^blockHeader}}&nbsp;{{/blockHeader}}</div>
</td>
</tr>
<td class="{{lineClass}} {{CSSLineClass.INFO}}" colspan="{{margin_colspan}}"></td>
<td class="{{CSSLineClass.INFO}}" colspan="{{colspan}}">
<div class="{{contentClass}}">{{#blockHeader}}{{{blockHeader}}}{{/blockHeader}}{{^blockHeader}}&nbsp;{{/blockHeader}}</div>
</td>

View file

@ -1,7 +1,7 @@
<tr>
<td class="{{CSSLineClass.INFO}}">
<div class="{{contentClass}}">
File without changes
</div>
</td>
<td class="{{CSSLineClass.INFO}}" colspan="{{#colspan}}{{{.}}}{{/colspan}}{{^colspan}}2{{/colspan}}">
<div class="{{contentClass}}">
File without changes
</div>
</td>
</tr>

View file

@ -0,0 +1 @@
<td class="{{lineClass}} {{type}}" {{#lineNumber}}data-line-number="{{{lineNumber}}}"{{/lineNumber}}></td>

View file

@ -1,21 +1,5 @@
<tr>
<td class="{{lineClass}} {{type}}">
{{{lineNumber}}}
</td>
<td class="{{type}}">
<div class="{{contentClass}}">
{{#prefix}}
<span class="d2h-code-line-prefix">{{{prefix}}}</span>
{{/prefix}}
{{^prefix}}
<span class="d2h-code-line-prefix">&nbsp;</span>
{{/prefix}}
{{#content}}
<span class="d2h-code-line-ctn">{{{content}}}</span>
{{/content}}
{{^content}}
<span class="d2h-code-line-ctn"><br></span>
{{/content}}
</div>
</td>
</tr>
<td class="{{type}}">
<div class="{{contentClass}}">
{{#prefix}}<span class="d2h-code-line-ctn d2h-code-marker" data-code-marker="{{{prefix}}}">{{/prefix}}{{^prefix}}<span class="d2h-code-line-ctn d2h-code-marker" data-code-marker=&nsbp;>{{/prefix}}{{#content}}{{content}}</span>{{/content}}{{^content}}<br></span>{{/content}}
</div>
</td>

View file

@ -5,6 +5,14 @@
<div class="d2h-file-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<thead hidden>
<tr>
<th scope="col">Original file line number</th>
<th scope="col">Diff line number</th>
<th scope="col">Content</th>
</tr>
</thead>
<tbody class="d2h-diff-tbody">
{{{diffs}}}
</tbody>

View file

@ -1,2 +0,0 @@
<div class="line-num1">{{oldNumber}}</div>
<div class="line-num2">{{newNumber}}</div>

View file

@ -5,18 +5,25 @@
<div class="d2h-files-diff">
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<table class="d2h-diff-table d2h-split-diff">
<thead hidden>
<tr>
<th scope="col">Original file line number</th>
<th scope="col">Original file content</th>
<th scope="col">Diff line number</th>
<th scope="col">Diff content</th>
</tr>
</thead>
<colgroup>
<col width="40">
<col>
<col width="40">
<col>
</colgroup>
<tbody class="d2h-diff-tbody">
{{{diffs.left}}}
</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">
{{{diffs.right}}}
{{{diffs}}}
</tbody>
</table>
</div>

View file

@ -7,6 +7,17 @@
.d2h-wrapper {
text-align: left;
word-wrap: break-word;
}
.d2h-wrapper * {
box-sizing: border-box;
}
.d2h-wrapper table {
border-spacing: 0;
border-collapse: collapse;
table-layout: fixed;
}
.d2h-file-header {
@ -94,9 +105,14 @@
.d2h-diff-table {
width: 100%;
border-collapse: collapse;
border-collapse: separate;
font-family: 'Menlo', 'Consolas', monospace;
font-size: 13px;
word-wrap: break-word;
}
.d2h-diff-table.d2h-split-diff {
table-layout: fixed;
}
.d2h-files-diff {
@ -104,46 +120,40 @@
width: 100%;
}
.d2h-file-diff {
overflow-y: hidden;
}
.d2h-files-diff.d2h-d-none,
.d2h-file-diff.d2h-d-none {
display: none;
}
.d2h-file-side-diff {
display: inline-block;
overflow-x: scroll;
overflow-y: hidden;
width: 50%;
}
.d2h-code-line {
display: inline-block;
white-space: nowrap;
user-select: none;
width: calc(100% - 16em);
/* Compensate for the absolute positioning of the line numbers */
padding: 0 8em;
width: 100%;
}
.d2h-code-line,
.d2h-code-side-line {
display: inline-block;
white-space: nowrap;
user-select: none;
width: calc(100% - 9em);
/* Compensate for the absolute positioning of the line numbers */
padding: 0 4.5em;
padding-left: 22px;
position: relative;
padding-right: 10px;
line-height: 20px;
vertical-align: top;
}
.d2h-code-marker::before {
position: absolute;
top: 1px;
left: 8px;
padding-right: 8px;
content: attr(data-code-marker);
}
.d2h-code-line-ctn {
display: inline-block;
display: table-cell;
background: none;
padding: 0;
word-wrap: normal;
white-space: pre;
word-wrap: anywhere;
white-space: pre-wrap;
overflow: visible;
user-select: text;
width: 100%;
vertical-align: middle;
@ -178,63 +188,33 @@
white-space: pre;
}
.line-num1 {
box-sizing: border-box;
float: left;
width: 3.5em;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 0.5em 0 0.5em;
}
.line-num2 {
box-sizing: border-box;
float: right;
width: 3.5em;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 0.5em 0 0.5em;
}
.d2h-code-linenumber {
box-sizing: border-box;
width: 7.5em;
/* Keep the numbers fixed on line contents scroll */
position: absolute;
display: inline-block;
background-color: #fff;
color: rgba(0, 0, 0, 0.3);
text-align: right;
border: solid #eeeeee;
border-width: 0 1px 0 1px;
cursor: pointer;
}
.d2h-code-linenumber:after {
content: '\200b';
}
.d2h-code-side-linenumber {
/* Keep the numbers fixed on line contents scroll */
position: absolute;
display: inline-block;
box-sizing: border-box;
width: 4em;
background-color: #fff;
color: rgba(0, 0, 0, 0.3);
text-align: right;
border: solid #eeeeee;
border-width: 0 1px 0 1px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 0.5em 0 0.5em;
.d2h-code-linenumber::before,
.d2h-code-side-linenumber::before {
content: attr(data-line-number);
}
.d2h-code-linenumber:after,
.d2h-code-side-linenumber:after {
content: '\200b';
}
.d2h-code-side-linenumber,
.d2h-code-linenumber {
/* Keep the numbers fixed on line contents scroll */
position: relative;
background-color: #fff;
color: rgba(0, 0, 0, 0.3);
text-align: right;
border: solid #eeeeee;
border-width: 0 1px 0 1px;
line-height: 20px;
font-size: 12px;
width: 1%;
min-width: 50px;
padding-right: 10px;
padding-left: 10px;
}
.d2h-code-side-emptyplaceholder,
.d2h-emptyplaceholder {
background-color: #f1f1f1;