2019-10-12 21:45:49 +00:00
|
|
|
import HoganJsUtils from "./hoganjs-utils";
|
|
|
|
|
import * as Rematch from "./rematch";
|
|
|
|
|
import * as renderUtils from "./render-utils";
|
2019-11-26 16:07:53 +00:00
|
|
|
import { DiffFile, DiffLine, LineType, DiffBlock } 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-10-13 18:21:19 +00:00
|
|
|
maxLineSizeInBlockForComparison: 200
|
2019-10-12 21:45:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const genericTemplatesPath = "generic";
|
|
|
|
|
const baseTemplatesPath = "line-by-line";
|
|
|
|
|
const iconsBaseTemplatesPath = "icon";
|
|
|
|
|
const tagsBaseTemplatesPath = "tag";
|
|
|
|
|
|
|
|
|
|
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-10-21 22:37:42 +00:00
|
|
|
render(diffFiles: DiffFile[]): string | undefined {
|
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);
|
|
|
|
|
})
|
|
|
|
|
.join("\n");
|
2019-10-12 21:45:49 +00:00
|
|
|
|
2019-11-26 09:09:59 +00:00
|
|
|
return this.hoganUtils.render(genericTemplatesPath, "wrapper", { content: diffsHtml });
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Make this private after improving tests
|
2019-10-21 22:37:42 +00:00
|
|
|
makeFileDiffHtml(file: DiffFile, diffs: string): string {
|
2019-10-12 21:45:49 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Make this private after improving tests
|
|
|
|
|
generateEmptyDiff(): string {
|
|
|
|
|
return this.hoganUtils.render(genericTemplatesPath, "empty-diff", {
|
|
|
|
|
contentClass: "d2h-code-line",
|
|
|
|
|
CSSLineClass: renderUtils.CSSLineClass
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-21 22:37:42 +00:00
|
|
|
generateFileHtml(file: DiffFile): string {
|
2019-11-26 09:44:09 +00:00
|
|
|
const matcher = Rematch.newMatcherFn(
|
|
|
|
|
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-11-26 10:13:38 +00:00
|
|
|
let lines = this.hoganUtils.render(genericTemplatesPath, "block-header", {
|
2019-11-26 09:44:09 +00:00
|
|
|
CSSLineClass: renderUtils.CSSLineClass,
|
|
|
|
|
blockHeader: block.header,
|
|
|
|
|
lineClass: "d2h-code-linenumber",
|
|
|
|
|
contentClass: "d2h-code-line"
|
|
|
|
|
});
|
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) {
|
|
|
|
|
lines += this.applyRematchMatching(oldLines, newLines, matcher)
|
|
|
|
|
.map(([oldLines, newLines]) => this.applyLineDiff(file, oldLines, newLines))
|
|
|
|
|
.join("");
|
|
|
|
|
} else if (oldLines.length || newLines.length || contextLines.length) {
|
|
|
|
|
lines += (contextLines || [])
|
|
|
|
|
.concat((oldLines || []).concat(newLines || []))
|
|
|
|
|
.map(line => {
|
|
|
|
|
const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined);
|
|
|
|
|
return this.makeLineHtml(
|
|
|
|
|
renderUtils.toCSSClass(line.type),
|
|
|
|
|
prefix,
|
|
|
|
|
content,
|
|
|
|
|
line.oldNumber,
|
|
|
|
|
line.newNumber
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
.join("");
|
2019-10-12 21:45:49 +00:00
|
|
|
} else {
|
2019-11-26 16:07:53 +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;
|
|
|
|
|
})
|
|
|
|
|
.join("\n");
|
|
|
|
|
}
|
2019-11-26 09:44:09 +00:00
|
|
|
|
2019-11-26 16:07:53 +00:00
|
|
|
applyLineGroupping(block: DiffBlock): DiffLine[][][] {
|
|
|
|
|
const blockLinesGroups: DiffLine[][][] = [];
|
|
|
|
|
|
|
|
|
|
let oldLines: DiffLine[] = [];
|
|
|
|
|
let newLines: DiffLine[] = [];
|
|
|
|
|
|
|
|
|
|
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[],
|
|
|
|
|
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,
|
|
|
|
|
[0].concat(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");
|
|
|
|
|
|
2019-11-26 16:07:53 +00:00
|
|
|
const matches = doMatching ? matcher(oldLines, newLines) : [[oldLines, newLines]];
|
2019-11-26 10:13:38 +00:00
|
|
|
|
2019-11-26 16:07:53 +00:00
|
|
|
return matches;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applyLineDiff(file: DiffFile, oldLines: DiffLine[], newLines: DiffLine[]): string {
|
|
|
|
|
let oldLinesHtml = "";
|
|
|
|
|
let newLinesHtml = "";
|
|
|
|
|
|
|
|
|
|
const common = Math.min(oldLines.length, newLines.length);
|
|
|
|
|
|
|
|
|
|
let oldLine, newLine;
|
|
|
|
|
for (let j = 0; j < common; j++) {
|
|
|
|
|
oldLine = oldLines[j];
|
|
|
|
|
newLine = newLines[j];
|
|
|
|
|
|
|
|
|
|
const diff = renderUtils.diffHighlight(oldLine.content, newLine.content, file.isCombined, this.config);
|
2019-11-26 10:13:38 +00:00
|
|
|
|
2019-11-26 16:07:53 +00:00
|
|
|
oldLinesHtml += this.makeLineHtml(
|
|
|
|
|
renderUtils.CSSLineClass.DELETE_CHANGES,
|
|
|
|
|
diff.oldLine.prefix,
|
|
|
|
|
diff.oldLine.content,
|
|
|
|
|
oldLine.oldNumber,
|
|
|
|
|
oldLine.newNumber
|
|
|
|
|
);
|
|
|
|
|
newLinesHtml += this.makeLineHtml(
|
|
|
|
|
renderUtils.CSSLineClass.INSERT_CHANGES,
|
|
|
|
|
diff.newLine.prefix,
|
|
|
|
|
diff.newLine.content,
|
|
|
|
|
newLine.oldNumber,
|
|
|
|
|
newLine.newNumber
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const remainingLines = oldLines
|
|
|
|
|
.slice(common)
|
|
|
|
|
.concat(newLines.slice(common))
|
|
|
|
|
.map(line => {
|
|
|
|
|
const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined);
|
|
|
|
|
return this.makeLineHtml(renderUtils.toCSSClass(line.type), prefix, content, line.oldNumber, line.newNumber);
|
|
|
|
|
})
|
|
|
|
|
.join("");
|
|
|
|
|
|
|
|
|
|
return oldLinesHtml + newLinesHtml + remainingLines;
|
2019-11-26 10:13:38 +00:00
|
|
|
}
|
|
|
|
|
|
2019-11-26 09:44:09 +00:00
|
|
|
// TODO: Make this private after improving tests
|
|
|
|
|
makeLineHtml(
|
|
|
|
|
type: renderUtils.CSSLineClass,
|
2019-11-26 16:07:53 +00:00
|
|
|
prefix: string,
|
2019-11-26 09:44:09 +00:00
|
|
|
content: string,
|
|
|
|
|
oldNumber?: number,
|
2019-11-26 16:07:53 +00:00
|
|
|
newNumber?: number
|
2019-11-26 09:44:09 +00:00
|
|
|
): string {
|
2019-11-26 16:07:53 +00:00
|
|
|
const lineNumberHtml = this.hoganUtils.render(baseTemplatesPath, "numbers", {
|
2019-11-26 09:44:09 +00:00
|
|
|
oldNumber: oldNumber || "",
|
|
|
|
|
newNumber: newNumber || ""
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.hoganUtils.render(genericTemplatesPath, "line", {
|
|
|
|
|
type: type,
|
|
|
|
|
lineClass: "d2h-code-linenumber",
|
|
|
|
|
contentClass: "d2h-code-line",
|
2019-11-26 16:07:53 +00:00
|
|
|
prefix: prefix === " " ? " " : prefix,
|
|
|
|
|
content: content,
|
|
|
|
|
lineNumber: lineNumberHtml
|
2019-11-26 09:44:09 +00:00
|
|
|
});
|
|
|
|
|
}
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|