diff2html
Version:
Fast Diff to colorized HTML
199 lines • 9.52 kB
JavaScript
import * as Rematch from './rematch';
import * as renderUtils from './render-utils';
import { LineType, } from './types';
export const defaultLineByLineRendererConfig = Object.assign(Object.assign({}, renderUtils.defaultRenderConfig), { renderNothingWhenEmpty: false, matchingMaxComparisons: 2500, maxLineSizeInBlockForComparison: 200 });
const genericTemplatesPath = 'generic';
const baseTemplatesPath = 'line-by-line';
const iconsBaseTemplatesPath = 'icon';
const tagsBaseTemplatesPath = 'tag';
export default class LineByLineRenderer {
constructor(hoganUtils, config = {}) {
this.hoganUtils = hoganUtils;
this.config = Object.assign(Object.assign({}, defaultLineByLineRendererConfig), config);
}
render(diffFiles) {
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');
return this.hoganUtils.render(genericTemplatesPath, 'wrapper', {
colorScheme: renderUtils.colorSchemeToCss(this.config.colorScheme),
content: diffsHtml,
});
}
makeFileDiffHtml(file, diffs) {
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,
}),
});
}
generateEmptyDiff() {
return this.hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-line',
CSSLineClass: renderUtils.CSSLineClass,
});
}
generateFileHtml(file) {
const matcher = Rematch.newMatcherFn(Rematch.newDistanceFn((e) => renderUtils.deconstructLine(e.content, file.isCombined).content));
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',
});
this.applyLineGroupping(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, 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(file, {
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, file.isCombined, oldLines, newLines);
lines += left;
lines += right;
}
else {
console.error('Unknown state reached while processing groups of lines', contextLines, oldLines, newLines);
}
});
return lines;
})
.join('\n');
}
applyLineGroupping(block) {
const blockLinesGroups = [];
let oldLines = [];
let newLines = [];
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(oldLines, newLines, matcher) {
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');
return doMatching ? matcher(oldLines, newLines) : [[oldLines, newLines]];
}
processChangedLines(file, isCombined, oldLines, newLines) {
const fileHtml = {
right: '',
left: '',
};
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
? Object.assign(Object.assign({}, (diff !== undefined
? {
prefix: diff.oldLine.prefix,
content: diff.oldLine.content,
type: renderUtils.CSSLineClass.DELETE_CHANGES,
}
: Object.assign(Object.assign({}, renderUtils.deconstructLine(oldLine.content, isCombined)), { type: renderUtils.toCSSClass(oldLine.type) }))), { oldNumber: oldLine.oldNumber, newNumber: oldLine.newNumber }) : undefined;
const preparedNewLine = newLine !== undefined && newLine.newNumber !== undefined
? Object.assign(Object.assign({}, (diff !== undefined
? {
prefix: diff.newLine.prefix,
content: diff.newLine.content,
type: renderUtils.CSSLineClass.INSERT_CHANGES,
}
: Object.assign(Object.assign({}, renderUtils.deconstructLine(newLine.content, isCombined)), { type: renderUtils.toCSSClass(newLine.type) }))), { oldNumber: newLine.oldNumber, newNumber: newLine.newNumber }) : undefined;
const { left, right } = this.generateLineHtml(file, preparedOldLine, preparedNewLine);
fileHtml.left += left;
fileHtml.right += right;
}
return fileHtml;
}
generateLineHtml(file, oldLine, newLine) {
return {
left: this.generateSingleLineHtml(file, oldLine),
right: this.generateSingleLineHtml(file, newLine),
};
}
generateSingleLineHtml(file, line) {
if (line === undefined)
return '';
const lineNumberHtml = this.hoganUtils.render(baseTemplatesPath, 'numbers', {
oldNumber: line.oldNumber || '',
newNumber: line.newNumber || '',
});
return this.hoganUtils.render(genericTemplatesPath, 'line', {
type: line.type,
lineClass: 'd2h-code-linenumber',
contentClass: 'd2h-code-line',
prefix: line.prefix === ' ' ? ' ' : line.prefix,
content: line.content,
lineNumber: lineNumberHtml,
line,
file,
});
}
}
//# sourceMappingURL=line-by-line-renderer.js.map