Revert to original overflow behaviour by default; add overflow: 'scroll' as an option

Fixed issues with line-by-line rendering and HTML escaping
This commit is contained in:
Lordfirespeed 2023-03-15 04:23:16 +00:00
parent 591334d942
commit 9790e447c6
No known key found for this signature in database
GPG key ID: 9C01965EC9DE6690
10 changed files with 306 additions and 67 deletions

View file

@ -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 (
'<tr' +
this.hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-line',
colspan: '2',
CSSLineClass: renderUtils.CSSLineClass,
}) +
'</tr>'
);
}
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[];

View file

@ -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 = '/';

View file

@ -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(
`<tr>${block_html.left[block_line_index]} ${block_html.right[block_line_index]}</tr>`,
);
}
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(
`<tr>${fileHtml.left[block_line_index]} ${fileHtml.right[block_line_index]}</tr>\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(`<tr>${fileHtml.left[block_line_index]}</tr>\n`);
joined_strings.right = joined_strings.right.concat(`<tr>${fileHtml.right[block_line_index]}</tr>\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[];

View file

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

View file

@ -2,7 +2,7 @@
<div class="d2h-file-header">
{{{filePath}}}
</div>
<div class="d2h-file-diff">
<div class="d2h-file-diff {{overflowClass}}">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<thead hidden>

View file

@ -3,27 +3,44 @@
{{{filePath}}}
</div>
<div class="d2h-files-diff">
<div class="d2h-file-side-diff">
<div class="d2h-file-side-diff scroll-overflow">
<div class="d2h-code-wrapper">
<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>
</tr>
</thead>
<colgroup>
<col style="width: 40px;">
<col>
</colgroup>
<tbody class="d2h-diff-tbody">
{{{diffs.left}}}
</tbody>
</table>
</div>
</div>
<div class="d2h-file-side-diff scroll-overflow">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table d2h-split-diff">
<thead hidden>
<tr>
<th scope="col">Diff line number</th>
<th scope="col">Diff content</th>
</tr>
</thead>
<colgroup>
<col width="40">
<col>
<col width="40">
<col style="width: 40px;">
<col>
</colgroup>
<tbody class="d2h-diff-tbody">
{{{diffs}}}
{{{diffs.right}}}
</tbody>
</table>
</div>

View file

@ -0,0 +1,32 @@
<div id="{{fileHtmlId}}" class="d2h-file-wrapper" data-lang="{{file.language}}">
<div class="d2h-file-header">
{{{filePath}}}
</div>
<div class="d2h-files-diff">
<div class="d2h-file-side-diff wrap-overflow">
<div class="d2h-code-wrapper">
<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 style="width: 40px;">
<col>
<col style="width: 40px;">
<col>
</colgroup>
<tbody class="d2h-diff-tbody">
{{{diffs}}}
</tbody>
</table>
</div>
</div>
</div>
</div>

View file

@ -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',
};

View file

@ -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;

View file

@ -63,6 +63,8 @@ export class Diff2HtmlUI {
this.targetElement.querySelectorAll('.d2h-file-wrapper').forEach(wrapper => {
const [left, right] = Array<Element>().slice.call(wrapper.querySelectorAll('.d2h-file-side-diff'));
console.log(left, right);
if (left === undefined || right === undefined) return;
const onScroll = (event: Event): void => {