UNPKG

generate-differences

Version:

Generate differences between original source and formmated source

731 lines (726 loc) 25.4 kB
const defaultOptions = /* @__PURE__ */ Object.freeze({ diffTimeout: 1, diffEditCost: 4, matchThreshold: 0.5, matchDistance: 1e3, patchDeleteThreshold: 0.5, patchMargin: 4, matchMaxBits: 32 }); function resolveOptions(options) { if (options?.__resolved) return options; const resolved = { ...defaultOptions, ...options }; Object.defineProperty(resolved, "__resolved", { value: true, enumerable: false }); return resolved; } const DIFF_DELETE = -1; const DIFF_INSERT = 1; const DIFF_EQUAL = 0; function createDiff(op, text) { return [op, text]; } function diffMain(text1, text2, options, opt_checklines = true, opt_deadline) { const resolved = resolveOptions(options); if (typeof opt_deadline == "undefined") { if (resolved.diffTimeout <= 0) opt_deadline = Number.MAX_VALUE; else opt_deadline = (/* @__PURE__ */ new Date()).getTime() + resolved.diffTimeout * 1e3; } const deadline = opt_deadline; if (text1 == null || text2 == null) throw new Error("Null input. (diff_main)"); if (text1 === text2) { if (text1) return [createDiff(DIFF_EQUAL, text1)]; return []; } const checklines = opt_checklines; let commonlength = diffCommonPrefix(text1, text2); const commonprefix = text1.substring(0, commonlength); text1 = text1.substring(commonlength); text2 = text2.substring(commonlength); commonlength = diffCommonSuffix(text1, text2); const commonsuffix = text1.substring(text1.length - commonlength); text1 = text1.substring(0, text1.length - commonlength); text2 = text2.substring(0, text2.length - commonlength); const diffs = diffCompute(text1, text2, resolved, checklines, deadline); if (commonprefix) diffs.unshift(createDiff(DIFF_EQUAL, commonprefix)); if (commonsuffix) diffs.push(createDiff(DIFF_EQUAL, commonsuffix)); diffCleanupMerge(diffs); return diffs; } function diffCompute(text1, text2, options, checklines, deadline) { let diffs; if (!text1) { return [createDiff(DIFF_INSERT, text2)]; } if (!text2) { return [createDiff(DIFF_DELETE, text1)]; } const longtext = text1.length > text2.length ? text1 : text2; const shorttext = text1.length > text2.length ? text2 : text1; const i = longtext.indexOf(shorttext); if (i !== -1) { diffs = [createDiff(DIFF_INSERT, longtext.substring(0, i)), createDiff(DIFF_EQUAL, shorttext), createDiff(DIFF_INSERT, longtext.substring(i + shorttext.length))]; if (text1.length > text2.length) diffs[0][0] = diffs[2][0] = DIFF_DELETE; return diffs; } if (shorttext.length === 1) { return [createDiff(DIFF_DELETE, text1), createDiff(DIFF_INSERT, text2)]; } const hm = diffHalfMatch(text1, text2, options); if (hm) { const text1_a = hm[0]; const text1_b = hm[1]; const text2_a = hm[2]; const text2_b = hm[3]; const mid_common = hm[4]; const diffs_a = diffMain(text1_a, text2_a, options, checklines, deadline); const diffs_b = diffMain(text1_b, text2_b, options, checklines, deadline); return diffs_a.concat([createDiff(DIFF_EQUAL, mid_common)], diffs_b); } if (checklines && text1.length > 100 && text2.length > 100) return diffLineMode(text1, text2, options, deadline); return diffBisect(text1, text2, options, deadline); } function diffLineMode(text1, text2, options, deadline) { const a = diffLinesToChars(text1, text2); text1 = a.chars1; text2 = a.chars2; const linearray = a.lineArray; const diffs = diffMain(text1, text2, options, false, deadline); diffCharsToLines(diffs, linearray); diffCleanupSemantic(diffs); diffs.push(createDiff(DIFF_EQUAL, "")); let pointer = 0; let count_delete = 0; let count_insert = 0; let text_delete = ""; let text_insert = ""; while (pointer < diffs.length) { switch (diffs[pointer][0]) { case DIFF_INSERT: count_insert++; text_insert += diffs[pointer][1]; break; case DIFF_DELETE: count_delete++; text_delete += diffs[pointer][1]; break; case DIFF_EQUAL: if (count_delete >= 1 && count_insert >= 1) { diffs.splice(pointer - count_delete - count_insert, count_delete + count_insert); pointer = pointer - count_delete - count_insert; const subDiff = diffMain(text_delete, text_insert, options, false, deadline); for (let j = subDiff.length - 1; j >= 0; j--) diffs.splice(pointer, 0, subDiff[j]); pointer = pointer + subDiff.length; } count_insert = 0; count_delete = 0; text_delete = ""; text_insert = ""; break; } pointer++; } diffs.pop(); return diffs; } function diffBisect(text1, text2, options, deadline) { const text1_length = text1.length; const text2_length = text2.length; const max_d = Math.ceil((text1_length + text2_length) / 2); const v_offset = max_d; const v_length = 2 * max_d; const v1 = new Array(v_length); const v2 = new Array(v_length); for (let x = 0; x < v_length; x++) { v1[x] = -1; v2[x] = -1; } v1[v_offset + 1] = 0; v2[v_offset + 1] = 0; const delta = text1_length - text2_length; const front = delta % 2 !== 0; let k1start = 0; let k1end = 0; let k2start = 0; let k2end = 0; for (let d = 0; d < max_d; d++) { if ((/* @__PURE__ */ new Date()).getTime() > deadline) break; for (let k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { const k1_offset = v_offset + k1; let x1; if (k1 === -d || k1 !== d && v1[k1_offset - 1] < v1[k1_offset + 1]) x1 = v1[k1_offset + 1]; else x1 = v1[k1_offset - 1] + 1; let y1 = x1 - k1; while (x1 < text1_length && y1 < text2_length && text1.charAt(x1) === text2.charAt(y1)) { x1++; y1++; } v1[k1_offset] = x1; if (x1 > text1_length) { k1end += 2; } else if (y1 > text2_length) { k1start += 2; } else if (front) { const k2_offset = v_offset + delta - k1; if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] !== -1) { const x2 = text1_length - v2[k2_offset]; if (x1 >= x2) { return diffBisectSplit(text1, text2, options, x1, y1, deadline); } } } } for (let k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { const k2_offset = v_offset + k2; let x2; if (k2 === -d || k2 !== d && v2[k2_offset - 1] < v2[k2_offset + 1]) x2 = v2[k2_offset + 1]; else x2 = v2[k2_offset - 1] + 1; let y2 = x2 - k2; while (x2 < text1_length && y2 < text2_length && text1.charAt(text1_length - x2 - 1) === text2.charAt(text2_length - y2 - 1)) { x2++; y2++; } v2[k2_offset] = x2; if (x2 > text1_length) { k2end += 2; } else if (y2 > text2_length) { k2start += 2; } else if (!front) { const k1_offset = v_offset + delta - k2; if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] !== -1) { const x1 = v1[k1_offset]; const y1 = v_offset + x1 - k1_offset; x2 = text1_length - x2; if (x1 >= x2) { return diffBisectSplit(text1, text2, options, x1, y1, deadline); } } } } } return [createDiff(DIFF_DELETE, text1), createDiff(DIFF_INSERT, text2)]; } function diffBisectSplit(text1, text2, options, x, y, deadline) { const text1a = text1.substring(0, x); const text2a = text2.substring(0, y); const text1b = text1.substring(x); const text2b = text2.substring(y); const diffs = diffMain(text1a, text2a, options, false, deadline); const diffsb = diffMain(text1b, text2b, options, false, deadline); return diffs.concat(diffsb); } function diffLinesToChars(text1, text2) { const lineArray = []; const lineHash = {}; let maxLines = 4e4; lineArray[0] = ""; function diffLinesToCharsMunge(text) { let chars = ""; let lineStart = 0; let lineEnd = -1; let lineArrayLength = lineArray.length; while (lineEnd < text.length - 1) { lineEnd = text.indexOf("\n", lineStart); if (lineEnd === -1) lineEnd = text.length - 1; let line = text.substring(lineStart, lineEnd + 1); if (lineHash.hasOwnProperty ? Object.prototype.hasOwnProperty.call(lineHash, line) : lineHash[line] !== undefined) { chars += String.fromCharCode(lineHash[line]); } else { if (lineArrayLength === maxLines) { line = text.substring(lineStart); lineEnd = text.length; } chars += String.fromCharCode(lineArrayLength); lineHash[line] = lineArrayLength; lineArray[lineArrayLength++] = line; } lineStart = lineEnd + 1; } return chars; } const chars1 = diffLinesToCharsMunge(text1); maxLines = 65535; const chars2 = diffLinesToCharsMunge(text2); return { chars1, chars2, lineArray }; } function diffCharsToLines(diffs, lineArray) { for (let i = 0; i < diffs.length; i++) { const chars = diffs[i][1]; const text = []; for (let j = 0; j < chars.length; j++) text[j] = lineArray[chars.charCodeAt(j)]; diffs[i][1] = text.join(""); } } function diffCommonPrefix(text1, text2) { if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) return 0; let pointermin = 0; let pointermax = Math.min(text1.length, text2.length); let pointermid = pointermax; let pointerstart = 0; while (pointermin < pointermid) { if (text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)) { pointermin = pointermid; pointerstart = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); } return pointermid; } function diffCommonSuffix(text1, text2) { if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { return 0; } let pointermin = 0; let pointermax = Math.min(text1.length, text2.length); let pointermid = pointermax; let pointerend = 0; while (pointermin < pointermid) { if (text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)) { pointermin = pointermid; pointerend = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); } return pointermid; } function diffCommonOverlap(text1, text2) { const text1_length = text1.length; const text2_length = text2.length; if (text1_length === 0 || text2_length === 0) return 0; if (text1_length > text2_length) text1 = text1.substring(text1_length - text2_length); else if (text1_length < text2_length) text2 = text2.substring(0, text1_length); const text_length = Math.min(text1_length, text2_length); if (text1 === text2) return text_length; let best = 0; let length = 1; while (true) { const pattern = text1.substring(text_length - length); const found = text2.indexOf(pattern); if (found === -1) return best; length += found; if (found === 0 || text1.substring(text_length - length) === text2.substring(0, length)) { best = length; length++; } } } function diffHalfMatch(text1, text2, options) { if (options.diffTimeout <= 0) { return null; } const longtext = text1.length > text2.length ? text1 : text2; const shorttext = text1.length > text2.length ? text2 : text1; if (longtext.length < 4 || shorttext.length * 2 < longtext.length) return null; function diffHalfMatchI(longtext2, shorttext2, i) { const seed = longtext2.substring(i, i + Math.floor(longtext2.length / 4)); let j = -1; let best_common = ""; let best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; while ((j = shorttext2.indexOf(seed, j + 1)) !== -1) { const prefixLength = diffCommonPrefix(longtext2.substring(i), shorttext2.substring(j)); const suffixLength = diffCommonSuffix(longtext2.substring(0, i), shorttext2.substring(0, j)); if (best_common.length < suffixLength + prefixLength) { best_common = shorttext2.substring(j - suffixLength, j) + shorttext2.substring(j, j + prefixLength); best_longtext_a = longtext2.substring(0, i - suffixLength); best_longtext_b = longtext2.substring(i + prefixLength); best_shorttext_a = shorttext2.substring(0, j - suffixLength); best_shorttext_b = shorttext2.substring(j + prefixLength); } } if (best_common.length * 2 >= longtext2.length) return [best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b, best_common]; else return null; } const hm1 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 4)); const hm2 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 2)); let hm; if (!hm1 && !hm2) { return null; } else if (!hm2) { hm = hm1; } else if (!hm1) { hm = hm2; } else { hm = hm1[4].length > hm2[4].length ? hm1 : hm2; } let text1_a, text1_b, text2_a, text2_b; if (text1.length > text2.length) { text1_a = hm[0]; text1_b = hm[1]; text2_a = hm[2]; text2_b = hm[3]; } else { text2_a = hm[0]; text2_b = hm[1]; text1_a = hm[2]; text1_b = hm[3]; } const mid_common = hm[4]; return [text1_a, text1_b, text2_a, text2_b, mid_common]; } function diffCleanupSemantic(diffs) { let changes = false; const equalities = []; let equalitiesLength = 0; let lastEquality = null; let pointer = 0; let length_insertions1 = 0; let length_deletions1 = 0; let length_insertions2 = 0; let length_deletions2 = 0; while (pointer < diffs.length) { if (diffs[pointer][0] === DIFF_EQUAL) { equalities[equalitiesLength++] = pointer; length_insertions1 = length_insertions2; length_deletions1 = length_deletions2; length_insertions2 = 0; length_deletions2 = 0; lastEquality = diffs[pointer][1]; } else { if (diffs[pointer][0] === DIFF_INSERT) length_insertions2 += diffs[pointer][1].length; else length_deletions2 += diffs[pointer][1].length; if (lastEquality && lastEquality.length <= Math.max(length_insertions1, length_deletions1) && lastEquality.length <= Math.max(length_insertions2, length_deletions2)) { diffs.splice(equalities[equalitiesLength - 1], 0, createDiff(DIFF_DELETE, lastEquality)); diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; equalitiesLength--; equalitiesLength--; pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; length_insertions1 = 0; length_deletions1 = 0; length_insertions2 = 0; length_deletions2 = 0; lastEquality = null; changes = true; } } pointer++; } if (changes) diffCleanupMerge(diffs); diffCleanupSemanticLossless(diffs); pointer = 1; while (pointer < diffs.length) { if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) { const deletion = diffs[pointer - 1][1]; const insertion = diffs[pointer][1]; const overlap_length1 = diffCommonOverlap(deletion, insertion); const overlap_length2 = diffCommonOverlap(insertion, deletion); if (overlap_length1 >= overlap_length2) { if (overlap_length1 >= deletion.length / 2 || overlap_length1 >= insertion.length / 2) { diffs.splice(pointer, 0, createDiff(DIFF_EQUAL, insertion.substring(0, overlap_length1))); diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlap_length1); diffs[pointer + 1][1] = insertion.substring(overlap_length1); pointer++; } } else { if (overlap_length2 >= deletion.length / 2 || overlap_length2 >= insertion.length / 2) { diffs.splice(pointer, 0, createDiff(DIFF_EQUAL, deletion.substring(0, overlap_length2))); diffs[pointer - 1][0] = DIFF_INSERT; diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlap_length2); diffs[pointer + 1][0] = DIFF_DELETE; diffs[pointer + 1][1] = deletion.substring(overlap_length2); pointer++; } } pointer++; } pointer++; } } const nonAlphaNumericRegex_ = /[^a-z0-9]/i; const whitespaceRegex_ = /\s/; const linebreakRegex_ = /[\r\n]/; const blanklineEndRegex_ = /\n\r?\n$/; const blanklineStartRegex_ = /^\r?\n\r?\n/; function diffCleanupSemanticLossless(diffs) { function diffCleanupSemanticScore(one, two) { if (!one || !two) { return 6; } const char1 = one.charAt(one.length - 1); const char2 = two.charAt(0); const nonAlphaNumeric1 = char1.match(nonAlphaNumericRegex_); const nonAlphaNumeric2 = char2.match(nonAlphaNumericRegex_); const whitespace1 = nonAlphaNumeric1 && char1.match(whitespaceRegex_); const whitespace2 = nonAlphaNumeric2 && char2.match(whitespaceRegex_); const lineBreak1 = whitespace1 && char1.match(linebreakRegex_); const lineBreak2 = whitespace2 && char2.match(linebreakRegex_); const blankLine1 = lineBreak1 && one.match(blanklineEndRegex_); const blankLine2 = lineBreak2 && two.match(blanklineStartRegex_); if (blankLine1 || blankLine2) { return 5; } else if (lineBreak1 || lineBreak2) { return 4; } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { return 3; } else if (whitespace1 || whitespace2) { return 2; } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { return 1; } return 0; } let pointer = 1; while (pointer < diffs.length - 1) { if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { let equality1 = diffs[pointer - 1][1]; let edit = diffs[pointer][1]; let equality2 = diffs[pointer + 1][1]; const commonOffset = diffCommonSuffix(equality1, edit); if (commonOffset) { const commonString = edit.substring(edit.length - commonOffset); equality1 = equality1.substring(0, equality1.length - commonOffset); edit = commonString + edit.substring(0, edit.length - commonOffset); equality2 = commonString + equality2; } let bestEquality1 = equality1; let bestEdit = edit; let bestEquality2 = equality2; let bestScore = diffCleanupSemanticScore(equality1, edit) + diffCleanupSemanticScore(edit, equality2); while (edit.charAt(0) === equality2.charAt(0)) { equality1 += edit.charAt(0); edit = edit.substring(1) + equality2.charAt(0); equality2 = equality2.substring(1); const score = diffCleanupSemanticScore(equality1, edit) + diffCleanupSemanticScore(edit, equality2); if (score >= bestScore) { bestScore = score; bestEquality1 = equality1; bestEdit = edit; bestEquality2 = equality2; } } if (diffs[pointer - 1][1] !== bestEquality1) { if (bestEquality1) { diffs[pointer - 1][1] = bestEquality1; } else { diffs.splice(pointer - 1, 1); pointer--; } diffs[pointer][1] = bestEdit; if (bestEquality2) { diffs[pointer + 1][1] = bestEquality2; } else { diffs.splice(pointer + 1, 1); pointer--; } } } pointer++; } } function diffCleanupMerge(diffs) { diffs.push(createDiff(DIFF_EQUAL, "")); let pointer = 0; let count_delete = 0; let count_insert = 0; let text_delete = ""; let text_insert = ""; let commonlength; while (pointer < diffs.length) { switch (diffs[pointer][0]) { case DIFF_INSERT: count_insert++; text_insert += diffs[pointer][1]; pointer++; break; case DIFF_DELETE: count_delete++; text_delete += diffs[pointer][1]; pointer++; break; case DIFF_EQUAL: if (count_delete + count_insert > 1) { if (count_delete !== 0 && count_insert !== 0) { commonlength = diffCommonPrefix(text_insert, text_delete); if (commonlength !== 0) { if (pointer - count_delete - count_insert > 0 && diffs[pointer - count_delete - count_insert - 1][0] === DIFF_EQUAL) { diffs[pointer - count_delete - count_insert - 1][1] += text_insert.substring(0, commonlength); } else { diffs.splice(0, 0, createDiff(DIFF_EQUAL, text_insert.substring(0, commonlength))); pointer++; } text_insert = text_insert.substring(commonlength); text_delete = text_delete.substring(commonlength); } commonlength = diffCommonSuffix(text_insert, text_delete); if (commonlength !== 0) { diffs[pointer][1] = text_insert.substring(text_insert.length - commonlength) + diffs[pointer][1]; text_insert = text_insert.substring(0, text_insert.length - commonlength); text_delete = text_delete.substring(0, text_delete.length - commonlength); } } pointer -= count_delete + count_insert; diffs.splice(pointer, count_delete + count_insert); if (text_delete.length) { diffs.splice(pointer, 0, createDiff(DIFF_DELETE, text_delete)); pointer++; } if (text_insert.length) { diffs.splice(pointer, 0, createDiff(DIFF_INSERT, text_insert)); pointer++; } pointer++; } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { diffs[pointer - 1][1] += diffs[pointer][1]; diffs.splice(pointer, 1); } else { pointer++; } count_insert = 0; count_delete = 0; text_delete = ""; text_insert = ""; break; } } if (diffs[diffs.length - 1][1] === "") diffs.pop(); let changes = false; pointer = 1; while (pointer < diffs.length - 1) { if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { if (diffs[pointer][1].substring(diffs[pointer][1].length - diffs[pointer - 1][1].length) === diffs[pointer - 1][1]) { diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length); diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; diffs.splice(pointer - 1, 1); changes = true; } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]) { diffs[pointer - 1][1] += diffs[pointer + 1][1]; diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]; diffs.splice(pointer + 1, 1); changes = true; } } pointer++; } if (changes) diffCleanupMerge(diffs); } const DIFFERENCE = { DELETE: "delete", INSERT: "insert", REPLACE: "replace" }; const DIFF = { DELETE: -1, EQUAL: 0, INSERT: 1 }; const RE_LINE_ENDING = /\r\n|[\r\n\u2028\u2029]/; function generateDifferences(source, formatted, options = {}) { const differences = []; const batch = []; let { // INSERT never advances the offset offset = 0, ...diffMatchPathOptions } = options; const results = diffMain(source, formatted, diffMatchPathOptions); function flush() { let aheadDeleteText = ""; let aheadInsertText = ""; while (batch.length) { const next = batch.shift(); const op = next[0]; const text = next[1]; switch (op) { case DIFF.DELETE: aheadDeleteText += text; break; case DIFF.EQUAL: aheadDeleteText += text; aheadInsertText += text; break; case DIFF.INSERT: aheadInsertText += text; break; } } if (aheadDeleteText && aheadInsertText) { differences.push({ deleteText: aheadDeleteText, insertText: aheadInsertText, offset, operation: DIFFERENCE.REPLACE }); } else if (!aheadDeleteText && aheadInsertText) { differences.push({ insertText: aheadInsertText, offset, operation: DIFFERENCE.INSERT }); } else if (aheadDeleteText && !aheadInsertText) { differences.push({ deleteText: aheadDeleteText, offset, operation: DIFFERENCE.DELETE }); } offset += aheadDeleteText.length; } while (results.length) { const result = results.shift(); const op = result[0]; const text = result[1]; switch (op) { case DIFF.DELETE: case DIFF.INSERT: batch.push(result); break; case DIFF.EQUAL: if (results.length) { if (batch.length) { if (RE_LINE_ENDING.test(text)) { flush(); offset += text.length; } else { batch.push(result); } } else { offset += text.length; } } break; default: throw new Error(`Unexpected diff operation ${op}`); } } if (batch.length && !results.length) { flush(); } return differences; } export { DIFFERENCE, generateDifferences };