2019-12-29 22:31:32 +00:00
|
|
|
import * as jsDiff from 'diff';
|
2019-10-12 21:45:49 +00:00
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
import { unifyPath, hashCode } from './utils';
|
|
|
|
|
import * as rematch from './rematch';
|
2023-09-16 15:30:31 +00:00
|
|
|
import {
|
|
|
|
|
ColorSchemeType,
|
|
|
|
|
DiffFile,
|
|
|
|
|
DiffFileName,
|
|
|
|
|
DiffLineParts,
|
|
|
|
|
DiffStyleType,
|
|
|
|
|
LineMatchingType,
|
|
|
|
|
LineType,
|
|
|
|
|
} from './types';
|
2019-10-12 21:45:49 +00:00
|
|
|
|
2020-01-25 23:49:17 +00:00
|
|
|
export type CSSLineClass =
|
|
|
|
|
| 'd2h-ins'
|
|
|
|
|
| 'd2h-del'
|
|
|
|
|
| 'd2h-cntx'
|
|
|
|
|
| 'd2h-info'
|
|
|
|
|
| 'd2h-ins d2h-change'
|
|
|
|
|
| 'd2h-del d2h-change';
|
|
|
|
|
|
|
|
|
|
export const CSSLineClass: { [_: string]: CSSLineClass } = {
|
|
|
|
|
INSERTS: 'd2h-ins',
|
|
|
|
|
DELETES: 'd2h-del',
|
|
|
|
|
CONTEXT: 'd2h-cntx',
|
|
|
|
|
INFO: 'd2h-info',
|
|
|
|
|
INSERT_CHANGES: 'd2h-ins d2h-change',
|
|
|
|
|
DELETE_CHANGES: 'd2h-del d2h-change',
|
|
|
|
|
};
|
2019-10-12 21:45:49 +00:00
|
|
|
|
2019-10-21 22:37:42 +00:00
|
|
|
export type HighlightedLines = {
|
|
|
|
|
oldLine: {
|
|
|
|
|
prefix: string;
|
|
|
|
|
content: string;
|
|
|
|
|
};
|
|
|
|
|
newLine: {
|
|
|
|
|
prefix: string;
|
|
|
|
|
content: string;
|
|
|
|
|
};
|
2019-10-12 21:45:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export interface RenderConfig {
|
|
|
|
|
matching?: LineMatchingType;
|
|
|
|
|
matchWordsThreshold?: number;
|
|
|
|
|
maxLineLengthHighlight?: number;
|
|
|
|
|
diffStyle?: DiffStyleType;
|
2023-09-16 15:30:31 +00:00
|
|
|
colorScheme?: ColorSchemeType;
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const defaultRenderConfig = {
|
2019-10-21 22:37:42 +00:00
|
|
|
matching: LineMatchingType.NONE,
|
2019-10-12 21:45:49 +00:00
|
|
|
matchWordsThreshold: 0.25,
|
|
|
|
|
maxLineLengthHighlight: 10000,
|
2019-12-29 22:31:32 +00:00
|
|
|
diffStyle: DiffStyleType.WORD,
|
2023-09-16 15:30:31 +00:00
|
|
|
colorScheme: ColorSchemeType.LIGHT,
|
2019-10-12 21:45:49 +00:00
|
|
|
};
|
|
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
const separator = '/';
|
2019-10-12 21:45:49 +00:00
|
|
|
const distance = rematch.newDistanceFn((change: jsDiff.Change) => change.value);
|
|
|
|
|
const matcher = rematch.newMatcherFn(distance);
|
|
|
|
|
|
|
|
|
|
function isDevNullName(name: string): boolean {
|
2019-12-29 22:31:32 +00:00
|
|
|
return name.indexOf('dev/null') !== -1;
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeInsElements(line: string): string {
|
2019-12-29 22:31:32 +00:00
|
|
|
return line.replace(/(<ins[^>]*>((.|\n)*?)<\/ins>)/g, '');
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeDelElements(line: string): string {
|
2019-12-29 22:31:32 +00:00
|
|
|
return line.replace(/(<del[^>]*>((.|\n)*?)<\/del>)/g, '');
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Convert from LineType to CSSLineClass
|
|
|
|
|
*/
|
|
|
|
|
export function toCSSClass(lineType: LineType): CSSLineClass {
|
|
|
|
|
switch (lineType) {
|
|
|
|
|
case LineType.CONTEXT:
|
|
|
|
|
return CSSLineClass.CONTEXT;
|
|
|
|
|
case LineType.INSERT:
|
|
|
|
|
return CSSLineClass.INSERTS;
|
|
|
|
|
case LineType.DELETE:
|
|
|
|
|
return CSSLineClass.DELETES;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-16 15:30:31 +00:00
|
|
|
export function colorSchemeToCss(colorScheme: ColorSchemeType): string {
|
|
|
|
|
switch (colorScheme) {
|
|
|
|
|
case ColorSchemeType.DARK:
|
2023-09-26 23:06:06 +00:00
|
|
|
return 'd2h-dark-color-scheme';
|
2023-09-16 15:30:31 +00:00
|
|
|
case ColorSchemeType.AUTO:
|
2023-09-26 23:06:06 +00:00
|
|
|
return 'd2h-auto-color-scheme';
|
|
|
|
|
case ColorSchemeType.LIGHT:
|
2023-09-16 15:30:31 +00:00
|
|
|
default:
|
2023-09-26 23:06:06 +00:00
|
|
|
return 'd2h-light-color-scheme';
|
2023-09-16 15:30:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-12 21:45:49 +00:00
|
|
|
/**
|
|
|
|
|
* Prefix length of the hunk lines in the diff
|
|
|
|
|
*/
|
2019-10-21 22:37:42 +00:00
|
|
|
function prefixLength(isCombined: boolean): number {
|
2019-10-12 21:45:49 +00:00
|
|
|
return isCombined ? 2 : 1;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-26 09:44:09 +00:00
|
|
|
/**
|
|
|
|
|
* Escapes all required characters for safe HTML rendering
|
|
|
|
|
*/
|
|
|
|
|
// TODO: Test this method inside deconstructLine since it should not be used anywhere else
|
|
|
|
|
export function escapeForHtml(str: string): string {
|
|
|
|
|
return str
|
|
|
|
|
.slice(0)
|
2019-12-29 22:31:32 +00:00
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, ''')
|
|
|
|
|
.replace(/\//g, '/');
|
2019-11-26 09:44:09 +00:00
|
|
|
}
|
|
|
|
|
|
2019-10-12 21:45:49 +00:00
|
|
|
/**
|
|
|
|
|
* Deconstructs diff @line by separating the content from the prefix type
|
|
|
|
|
*/
|
2020-01-08 22:40:46 +00:00
|
|
|
export function deconstructLine(line: string, isCombined: boolean, escape = true): DiffLineParts {
|
2019-10-12 21:45:49 +00:00
|
|
|
const indexToSplit = prefixLength(isCombined);
|
|
|
|
|
return {
|
|
|
|
|
prefix: line.substring(0, indexToSplit),
|
2020-01-08 22:40:46 +00:00
|
|
|
content: escape ? escapeForHtml(line.substring(indexToSplit)) : line.substring(indexToSplit),
|
2019-10-12 21:45:49 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generates pretty filename diffs
|
|
|
|
|
*
|
|
|
|
|
* e.g.:
|
|
|
|
|
* 1. file = { oldName: "my/path/to/file.js", newName: "my/path/to/new-file.js" }
|
|
|
|
|
* returns "my/path/to/{file.js → new-file.js}"
|
|
|
|
|
* 2. file = { oldName: "my/path/to/file.js", newName: "very/new/path/to/new-file.js" }
|
|
|
|
|
* returns "my/path/to/file.js → very/new/path/to/new-file.js"
|
|
|
|
|
* 3. file = { oldName: "my/path/to/file.js", newName: "my/path/for/file.js" }
|
|
|
|
|
* returns "my/path/{to → for}/file.js"
|
|
|
|
|
*/
|
|
|
|
|
export function filenameDiff(file: DiffFileName): string {
|
|
|
|
|
// TODO: Move unify path to parsing
|
|
|
|
|
const oldFilename = unifyPath(file.oldName);
|
|
|
|
|
const newFilename = unifyPath(file.newName);
|
|
|
|
|
|
|
|
|
|
if (oldFilename !== newFilename && !isDevNullName(oldFilename) && !isDevNullName(newFilename)) {
|
|
|
|
|
const prefixPaths = [];
|
|
|
|
|
const suffixPaths = [];
|
|
|
|
|
|
|
|
|
|
const oldFilenameParts = oldFilename.split(separator);
|
|
|
|
|
const newFilenameParts = newFilename.split(separator);
|
|
|
|
|
|
|
|
|
|
const oldFilenamePartsSize = oldFilenameParts.length;
|
|
|
|
|
const newFilenamePartsSize = newFilenameParts.length;
|
|
|
|
|
|
|
|
|
|
let i = 0;
|
|
|
|
|
let j = oldFilenamePartsSize - 1;
|
|
|
|
|
let k = newFilenamePartsSize - 1;
|
|
|
|
|
|
|
|
|
|
while (i < j && i < k) {
|
|
|
|
|
if (oldFilenameParts[i] === newFilenameParts[i]) {
|
|
|
|
|
prefixPaths.push(newFilenameParts[i]);
|
|
|
|
|
i += 1;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (j > i && k > i) {
|
|
|
|
|
if (oldFilenameParts[j] === newFilenameParts[k]) {
|
|
|
|
|
suffixPaths.unshift(newFilenameParts[k]);
|
|
|
|
|
j -= 1;
|
|
|
|
|
k -= 1;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const finalPrefix = prefixPaths.join(separator);
|
|
|
|
|
const finalSuffix = suffixPaths.join(separator);
|
|
|
|
|
|
|
|
|
|
const oldRemainingPath = oldFilenameParts.slice(i, j + 1).join(separator);
|
|
|
|
|
const newRemainingPath = newFilenameParts.slice(i, k + 1).join(separator);
|
|
|
|
|
|
|
|
|
|
if (finalPrefix.length && finalSuffix.length) {
|
|
|
|
|
return (
|
2019-12-29 22:31:32 +00:00
|
|
|
finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix
|
2019-10-12 21:45:49 +00:00
|
|
|
);
|
|
|
|
|
} else if (finalPrefix.length) {
|
2019-12-29 22:31:32 +00:00
|
|
|
return finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}';
|
2019-10-12 21:45:49 +00:00
|
|
|
} else if (finalSuffix.length) {
|
2019-12-29 22:31:32 +00:00
|
|
|
return '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix;
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
2019-12-29 22:31:32 +00:00
|
|
|
return oldFilename + ' → ' + newFilename;
|
2019-10-12 21:45:49 +00:00
|
|
|
} else if (!isDevNullName(newFilename)) {
|
|
|
|
|
return newFilename;
|
|
|
|
|
} else {
|
|
|
|
|
return oldFilename;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generates a unique string numerical identifier based on the names of the file diff
|
|
|
|
|
*/
|
|
|
|
|
export function getHtmlId(file: DiffFileName): string {
|
2020-05-09 11:05:18 +00:00
|
|
|
return `d2h-${hashCode(filenameDiff(file)).toString().slice(-6)}`;
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Selects the correct icon name for the file
|
|
|
|
|
*/
|
|
|
|
|
export function getFileIcon(file: DiffFile): string {
|
2019-12-29 22:31:32 +00:00
|
|
|
let templateName = 'file-changed';
|
2019-10-12 21:45:49 +00:00
|
|
|
|
|
|
|
|
if (file.isRename) {
|
2019-12-29 22:31:32 +00:00
|
|
|
templateName = 'file-renamed';
|
2019-10-12 21:45:49 +00:00
|
|
|
} else if (file.isCopy) {
|
2019-12-29 22:31:32 +00:00
|
|
|
templateName = 'file-renamed';
|
2019-10-12 21:45:49 +00:00
|
|
|
} else if (file.isNew) {
|
2019-12-29 22:31:32 +00:00
|
|
|
templateName = 'file-added';
|
2019-10-12 21:45:49 +00:00
|
|
|
} else if (file.isDeleted) {
|
2019-12-29 22:31:32 +00:00
|
|
|
templateName = 'file-deleted';
|
2019-10-12 21:45:49 +00:00
|
|
|
} else if (file.newName !== file.oldName) {
|
|
|
|
|
// If file is not Added, not Deleted and the names changed it must be a rename :)
|
2019-12-29 22:31:32 +00:00
|
|
|
templateName = 'file-renamed';
|
2019-10-12 21:45:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return templateName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2019-10-21 22:37:42 +00:00
|
|
|
* Highlight differences between @diffLine1 and @diffLine2 using <ins> and <del> tags
|
2019-10-12 21:45:49 +00:00
|
|
|
*/
|
|
|
|
|
export function diffHighlight(
|
|
|
|
|
diffLine1: string,
|
|
|
|
|
diffLine2: string,
|
|
|
|
|
isCombined: boolean,
|
2019-12-29 22:31:32 +00:00
|
|
|
config: RenderConfig = {},
|
2019-10-12 21:45:49 +00:00
|
|
|
): HighlightedLines {
|
|
|
|
|
const { matching, maxLineLengthHighlight, matchWordsThreshold, diffStyle } = { ...defaultRenderConfig, ...config };
|
|
|
|
|
|
2020-01-08 22:40:46 +00:00
|
|
|
const line1 = deconstructLine(diffLine1, isCombined, false);
|
|
|
|
|
const line2 = deconstructLine(diffLine2, isCombined, false);
|
2019-10-12 21:45:49 +00:00
|
|
|
|
2019-10-21 22:37:42 +00:00
|
|
|
if (line1.content.length > maxLineLengthHighlight || line2.content.length > maxLineLengthHighlight) {
|
2019-10-12 21:45:49 +00:00
|
|
|
return {
|
|
|
|
|
oldLine: {
|
2019-10-21 22:37:42 +00:00
|
|
|
prefix: line1.prefix,
|
2020-07-26 12:53:06 +00:00
|
|
|
content: escapeForHtml(line1.content),
|
2019-10-12 21:45:49 +00:00
|
|
|
},
|
|
|
|
|
newLine: {
|
2019-10-21 22:37:42 +00:00
|
|
|
prefix: line2.prefix,
|
2020-07-26 12:53:06 +00:00
|
|
|
content: escapeForHtml(line2.content),
|
2019-12-29 22:31:32 +00:00
|
|
|
},
|
2019-10-12 21:45:49 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const diff =
|
2019-12-29 22:31:32 +00:00
|
|
|
diffStyle === 'char'
|
2019-10-21 22:37:42 +00:00
|
|
|
? jsDiff.diffChars(line1.content, line2.content)
|
|
|
|
|
: jsDiff.diffWordsWithSpace(line1.content, line2.content);
|
2019-10-12 21:45:49 +00:00
|
|
|
|
|
|
|
|
const changedWords: jsDiff.Change[] = [];
|
2019-12-29 22:31:32 +00:00
|
|
|
if (diffStyle === 'word' && matching === 'words') {
|
2019-10-12 21:45:49 +00:00
|
|
|
const removed = diff.filter(element => element.removed);
|
|
|
|
|
const added = diff.filter(element => element.added);
|
|
|
|
|
const chunks = matcher(added, removed);
|
|
|
|
|
chunks.forEach(chunk => {
|
|
|
|
|
if (chunk[0].length === 1 && chunk[1].length === 1) {
|
|
|
|
|
const dist = distance(chunk[0][0], chunk[1][0]);
|
2019-11-26 16:07:53 +00:00
|
|
|
if (dist < matchWordsThreshold) {
|
2019-10-12 21:45:49 +00:00
|
|
|
changedWords.push(chunk[0][0]);
|
|
|
|
|
changedWords.push(chunk[1][0]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const highlightedLine = diff.reduce((highlightedLine, part) => {
|
2019-12-29 22:31:32 +00:00
|
|
|
const elemType = part.added ? 'ins' : part.removed ? 'del' : null;
|
|
|
|
|
const addClass = changedWords.indexOf(part) > -1 ? ' class="d2h-change"' : '';
|
2020-01-08 22:40:46 +00:00
|
|
|
const escapedValue = escapeForHtml(part.value);
|
2019-10-12 21:45:49 +00:00
|
|
|
|
|
|
|
|
return elemType !== null
|
2020-01-08 22:40:46 +00:00
|
|
|
? `${highlightedLine}<${elemType}${addClass}>${escapedValue}</${elemType}>`
|
|
|
|
|
: `${highlightedLine}${escapedValue}`;
|
2019-12-29 22:31:32 +00:00
|
|
|
}, '');
|
2019-10-12 21:45:49 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
oldLine: {
|
2019-10-21 22:37:42 +00:00
|
|
|
prefix: line1.prefix,
|
2019-12-29 22:31:32 +00:00
|
|
|
content: removeInsElements(highlightedLine),
|
2019-10-12 21:45:49 +00:00
|
|
|
},
|
|
|
|
|
newLine: {
|
2019-10-21 22:37:42 +00:00
|
|
|
prefix: line2.prefix,
|
2019-12-29 22:31:32 +00:00
|
|
|
content: removeDelElements(highlightedLine),
|
|
|
|
|
},
|
2019-10-12 21:45:49 +00:00
|
|
|
};
|
|
|
|
|
}
|