2019-12-29 22:31:32 +00:00
|
|
|
import HoganJsUtils from './hoganjs-utils';
|
|
|
|
|
import * as Rematch from './rematch';
|
|
|
|
|
import * as renderUtils from './render-utils';
|
2019-11-26 23:57:47 +00:00
|
|
|
import {
|
|
|
|
|
DiffFile,
|
|
|
|
|
DiffLine,
|
|
|
|
|
LineType,
|
|
|
|
|
DiffBlock,
|
|
|
|
|
DiffLineDeleted,
|
|
|
|
|
DiffLineContent,
|
|
|
|
|
DiffLineContext,
|
2019-12-29 22:31:32 +00:00
|
|
|
DiffLineInserted,
|
|
|
|
|
} from './types';
|
2019-10-12 21:45:49 +00:00
|
|
|
|
|
|
|
|
export interface LineByLineRendererConfig extends renderUtils.RenderConfig {
|
|
|
|
|
renderNothingWhenEmpty?: boolean;
|
|
|
|
|
matchingMaxComparisons?: number;
|
|
|
|
|
maxLineSizeInBlockForComparison?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const defaultLineByLineRendererConfig = {
|
2019-10-13 18:21:19 +00:00
|
|
|
...renderUtils.defaultRenderConfig,
|
2019-10-12 21:45:49 +00:00
|
|
|
renderNothingWhenEmpty: false,
|
|
|
|
|
matchingMaxComparisons: 2500,
|
2019-12-29 22:31:32 +00:00
|
|
|
maxLineSizeInBlockForComparison: 200,
|
2019-10-12 21:45:49 +00:00
|
|
|
};
|
|
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
const genericTemplatesPath = 'generic';
|
|
|
|
|
const baseTemplatesPath = 'line-by-line';
|
|
|
|
|
const iconsBaseTemplatesPath = 'icon';
|
|
|
|
|
const tagsBaseTemplatesPath = 'tag';
|
2019-10-12 21:45:49 +00:00
|
|
|
|
|
|
|
|
export default class LineByLineRenderer {
|
|
|
|
|
private readonly hoganUtils: HoganJsUtils;
|
|
|
|
|
private readonly config: typeof defaultLineByLineRendererConfig;
|
|
|
|
|
|
2019-10-21 22:37:42 +00:00
|
|
|
constructor(hoganUtils: HoganJsUtils, config: LineByLineRendererConfig = {}) {
|
2019-10-12 21:45:49 +00:00
|
|
|
this.hoganUtils = hoganUtils;
|
|
|
|
|
this.config = { ...defaultLineByLineRendererConfig, ...config };
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-22 18:47:20 +00:00
|
|
|
render(diffFiles: DiffFile[]): string {
|
2019-11-26 09:09:59 +00:00
|
|
|
const diffsHtml = diffFiles
|
|
|
|
|
.map(file => {
|
|
|
|
|
let diffs;
|
|
|
|
|
if (file.blocks.length) {
|
|
|
|
|
diffs = this.generateFileHtml(file);
|
|
|
|
|
} else {
|
|
|
|
|
diffs = this.generateEmptyDiff();
|
|
|
|
|
}
|
|
|
|
|
return this.makeFileDiffHtml(file, diffs);
|
|
|
|
|
})
|
2019-12-29 22:31:32 +00:00
|
|
|
.join('\n');
|
2019-10-12 21:45:49 +00:00
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
return this.hoganUtils.render(genericTemplatesPath, 'wrapper', { content: diffsHtml });
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
2019-10-21 22:37:42 +00:00
|
|
|
makeFileDiffHtml(file: DiffFile, diffs: string): string {
|
2019-12-29 22:31:32 +00:00
|
|
|
if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return '';
|
2019-10-12 21:45:49 +00:00
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
const fileDiffTemplate = this.hoganUtils.template(baseTemplatesPath, 'file-diff');
|
|
|
|
|
const filePathTemplate = this.hoganUtils.template(genericTemplatesPath, 'file-path');
|
|
|
|
|
const fileIconTemplate = this.hoganUtils.template(iconsBaseTemplatesPath, 'file');
|
2019-10-12 21:45:49 +00:00
|
|
|
const fileTagTemplate = this.hoganUtils.template(tagsBaseTemplatesPath, renderUtils.getFileIcon(file));
|
|
|
|
|
|
|
|
|
|
return fileDiffTemplate.render({
|
|
|
|
|
file: file,
|
|
|
|
|
fileHtmlId: renderUtils.getHtmlId(file),
|
|
|
|
|
diffs: diffs,
|
|
|
|
|
filePath: filePathTemplate.render(
|
|
|
|
|
{
|
2019-12-29 22:31:32 +00:00
|
|
|
fileDiffName: renderUtils.filenameDiff(file),
|
2019-10-12 21:45:49 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
fileIcon: fileIconTemplate,
|
2019-12-29 22:31:32 +00:00
|
|
|
fileTag: fileTagTemplate,
|
|
|
|
|
},
|
|
|
|
|
),
|
2019-10-12 21:45:49 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
generateEmptyDiff(): string {
|
2019-12-29 22:31:32 +00:00
|
|
|
return this.hoganUtils.render(genericTemplatesPath, 'empty-diff', {
|
|
|
|
|
contentClass: 'd2h-code-line',
|
|
|
|
|
CSSLineClass: renderUtils.CSSLineClass,
|
2019-10-12 21:45:49 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-21 22:37:42 +00:00
|
|
|
generateFileHtml(file: DiffFile): string {
|
2019-11-26 09:44:09 +00:00
|
|
|
const matcher = Rematch.newMatcherFn(
|
2019-12-29 22:31:32 +00:00
|
|
|
Rematch.newDistanceFn((e: DiffLine) => renderUtils.deconstructLine(e.content, file.isCombined).content),
|
2019-10-21 22:37:42 +00:00
|
|
|
);
|
2019-10-12 21:45:49 +00:00
|
|
|
|
|
|
|
|
return file.blocks
|
|
|
|
|
.map(block => {
|
2019-12-29 22:31:32 +00:00
|
|
|
let lines = this.hoganUtils.render(genericTemplatesPath, 'block-header', {
|
2019-11-26 09:44:09 +00:00
|
|
|
CSSLineClass: renderUtils.CSSLineClass,
|
|
|
|
|
blockHeader: block.header,
|
2019-12-29 22:31:32 +00:00
|
|
|
lineClass: 'd2h-code-linenumber',
|
|
|
|
|
contentClass: 'd2h-code-line',
|
2019-11-26 09:44:09 +00:00
|
|
|
});
|
2019-10-12 21:45:49 +00:00
|
|
|
|
2019-11-26 16:07:53 +00:00
|
|
|
this.applyLineGroupping(block).forEach(([contextLines, oldLines, newLines]) => {
|
|
|
|
|
if (oldLines.length && newLines.length && !contextLines.length) {
|
2019-11-26 23:57:47 +00:00
|
|
|
this.applyRematchMatching(oldLines, newLines, matcher).map(([oldLines, newLines]) => {
|
|
|
|
|
const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines);
|
|
|
|
|
lines += left;
|
|
|
|
|
lines += 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,
|
2019-12-29 22:31:32 +00:00
|
|
|
newNumber: line.newNumber,
|
2019-11-26 23:57:47 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
} else if (oldLines.length || newLines.length) {
|
|
|
|
|
const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines);
|
|
|
|
|
lines += left;
|
|
|
|
|
lines += right;
|
2019-10-12 21:45:49 +00:00
|
|
|
} else {
|
2019-12-29 22:31:32 +00:00
|
|
|
console.error('Unknown state reached while processing groups of lines', contextLines, oldLines, newLines);
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
2019-11-26 16:07:53 +00:00
|
|
|
});
|
2019-10-12 21:45:49 +00:00
|
|
|
|
|
|
|
|
return lines;
|
|
|
|
|
})
|
2019-12-29 22:31:32 +00:00
|
|
|
.join('\n');
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
2019-11-26 09:44:09 +00:00
|
|
|
|
2019-11-26 23:57:47 +00:00
|
|
|
applyLineGroupping(block: DiffBlock): DiffLineGroups {
|
|
|
|
|
const blockLinesGroups: DiffLineGroups = [];
|
2019-11-26 16:07:53 +00:00
|
|
|
|
2019-11-26 23:57:47 +00:00
|
|
|
let oldLines: (DiffLineDeleted & DiffLineContent)[] = [];
|
|
|
|
|
let newLines: (DiffLineInserted & DiffLineContent)[] = [];
|
2019-11-26 16:07:53 +00:00
|
|
|
|
|
|
|
|
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(
|
2019-11-26 10:13:38 +00:00
|
|
|
oldLines: DiffLine[],
|
|
|
|
|
newLines: DiffLine[],
|
2019-12-29 22:31:32 +00:00
|
|
|
matcher: Rematch.MatcherFn<DiffLine>,
|
2019-11-26 16:07:53 +00:00
|
|
|
): DiffLine[][][] {
|
2019-11-26 10:13:38 +00:00
|
|
|
const comparisons = oldLines.length * newLines.length;
|
|
|
|
|
const maxLineSizeInBlock = Math.max.apply(
|
|
|
|
|
null,
|
2019-12-29 22:31:32 +00:00
|
|
|
[0].concat(oldLines.concat(newLines).map(elem => elem.content.length)),
|
2019-11-26 10:13:38 +00:00
|
|
|
);
|
|
|
|
|
const doMatching =
|
|
|
|
|
comparisons < this.config.matchingMaxComparisons &&
|
|
|
|
|
maxLineSizeInBlock < this.config.maxLineSizeInBlockForComparison &&
|
2019-12-29 22:31:32 +00:00
|
|
|
(this.config.matching === 'lines' || this.config.matching === 'words');
|
2019-11-26 10:13:38 +00:00
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
return doMatching ? matcher(oldLines, newLines) : [[oldLines, newLines]];
|
2019-11-26 16:07:53 +00:00
|
|
|
}
|
|
|
|
|
|
2019-11-26 23:57:47 +00:00
|
|
|
processChangedLines(isCombined: boolean, oldLines: DiffLine[], newLines: DiffLine[]): FileHtml {
|
|
|
|
|
const fileHtml = {
|
2019-12-29 22:31:32 +00:00
|
|
|
right: '',
|
|
|
|
|
left: '',
|
2019-11-26 23:57:47 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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,
|
2019-12-29 22:31:32 +00:00
|
|
|
type: renderUtils.CSSLineClass.DELETE_CHANGES,
|
2019-11-26 23:57:47 +00:00
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
...renderUtils.deconstructLine(oldLine.content, isCombined),
|
2019-12-29 22:31:32 +00:00
|
|
|
type: renderUtils.toCSSClass(oldLine.type),
|
2019-11-26 23:57:47 +00:00
|
|
|
}),
|
|
|
|
|
oldNumber: oldLine.oldNumber,
|
2019-12-29 22:31:32 +00:00
|
|
|
newNumber: oldLine.newNumber,
|
2019-11-26 23:57:47 +00:00
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const preparedNewLine =
|
|
|
|
|
newLine !== undefined && newLine.newNumber !== undefined
|
|
|
|
|
? {
|
|
|
|
|
...(diff !== undefined
|
|
|
|
|
? {
|
|
|
|
|
prefix: diff.newLine.prefix,
|
|
|
|
|
content: diff.newLine.content,
|
2019-12-29 22:31:32 +00:00
|
|
|
type: renderUtils.CSSLineClass.INSERT_CHANGES,
|
2019-11-26 23:57:47 +00:00
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
...renderUtils.deconstructLine(newLine.content, isCombined),
|
2019-12-29 22:31:32 +00:00
|
|
|
type: renderUtils.toCSSClass(newLine.type),
|
2019-11-26 23:57:47 +00:00
|
|
|
}),
|
|
|
|
|
oldNumber: newLine.oldNumber,
|
2019-12-29 22:31:32 +00:00
|
|
|
newNumber: newLine.newNumber,
|
2019-11-26 23:57:47 +00:00
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const { left, right } = this.generateLineHtml(preparedOldLine, preparedNewLine);
|
|
|
|
|
fileHtml.left += left;
|
|
|
|
|
fileHtml.right += right;
|
2019-11-26 16:07:53 +00:00
|
|
|
}
|
|
|
|
|
|
2019-11-26 23:57:47 +00:00
|
|
|
return fileHtml;
|
2019-11-26 10:13:38 +00:00
|
|
|
}
|
|
|
|
|
|
2019-11-26 23:57:47 +00:00
|
|
|
generateLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): FileHtml {
|
|
|
|
|
return {
|
|
|
|
|
left: this.generateSingleLineHtml(oldLine),
|
2019-12-29 22:31:32 +00:00
|
|
|
right: this.generateSingleLineHtml(newLine),
|
2019-11-26 23:57:47 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
generateSingleLineHtml(line?: DiffPreparedLine): string {
|
2019-12-29 22:31:32 +00:00
|
|
|
if (line === undefined) return '';
|
2019-11-26 23:57:47 +00:00
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
const lineNumberHtml = this.hoganUtils.render(baseTemplatesPath, 'numbers', {
|
|
|
|
|
oldNumber: line.oldNumber || '',
|
|
|
|
|
newNumber: line.newNumber || '',
|
2019-11-26 09:44:09 +00:00
|
|
|
});
|
|
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
return this.hoganUtils.render(genericTemplatesPath, 'line', {
|
2019-11-26 23:57:47 +00:00
|
|
|
type: line.type,
|
2019-12-29 22:31:32 +00:00
|
|
|
lineClass: 'd2h-code-linenumber',
|
|
|
|
|
contentClass: 'd2h-code-line',
|
|
|
|
|
prefix: line.prefix === ' ' ? ' ' : line.prefix,
|
2019-11-26 23:57:47 +00:00
|
|
|
content: line.content,
|
2019-12-29 22:31:32 +00:00
|
|
|
lineNumber: lineNumberHtml,
|
2019-11-26 09:44:09 +00:00
|
|
|
});
|
|
|
|
|
}
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
2019-11-26 23:57:47 +00:00
|
|
|
|
|
|
|
|
type DiffLineGroups = [
|
|
|
|
|
(DiffLineContext & DiffLineContent)[],
|
|
|
|
|
(DiffLineDeleted & DiffLineContent)[],
|
2019-12-29 22:31:32 +00:00
|
|
|
(DiffLineInserted & DiffLineContent)[],
|
2019-11-26 23:57:47 +00:00
|
|
|
][];
|
|
|
|
|
|
|
|
|
|
type DiffPreparedLine = {
|
|
|
|
|
type: renderUtils.CSSLineClass;
|
|
|
|
|
prefix: string;
|
|
|
|
|
content: string;
|
|
|
|
|
oldNumber?: number;
|
|
|
|
|
newNumber?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type FileHtml = {
|
|
|
|
|
left: string;
|
|
|
|
|
right: string;
|
|
|
|
|
};
|