UNPKG

@sanity/diff-match-patch

Version:

Robust diff, match and patch algorithms to perform operations required for synchronizing plain text

1 lines 124 kB
{"version":3,"file":"index.cjs","sources":["../src/diff/clone.ts","../src/diff/commonOverlap.ts","../src/diff/commonPrefix.ts","../src/diff/commonSuffix.ts","../src/utils/surrogatePairs.ts","../src/diff/bisect.ts","../src/diff/halfMatch.ts","../src/diff/charsToLines.ts","../src/diff/linesToChars.ts","../src/diff/lineMode.ts","../src/diff/compute.ts","../src/diff/diff.ts","../src/diff/cleanup.ts","../src/match/bitap.ts","../src/match/match.ts","../src/diff/diffText.ts","../src/diff/levenshtein.ts","../src/diff/xIndex.ts","../src/utils/utf8Indices.ts","../src/patch/constants.ts","../src/patch/addPadding.ts","../src/patch/createPatchObject.ts","../src/patch/splitMax.ts","../src/patch/apply.ts","../src/patch/make.ts","../src/patch/parse.ts","../src/patch/stringify.ts"],"sourcesContent":["import {type Diff} from './diff.js'\n\n/**\n * Clones a diff tuple.\n *\n * @param diff - Tuple to clone.\n * @returns New, cloned tuple.\n * @internal\n */\nexport function cloneDiff(diff: Diff): Diff {\n const [type, patch] = diff\n return [type, patch]\n}\n","/**\n * Determine if the suffix of one string is the prefix of another.\n *\n * @param textA - First string.\n * @param textB - Second string.\n * @returns Number of characters common to the end of the first string\n * and the start of the second string.\n * @internal\n */\nexport function getCommonOverlap(textA: string, textB: string): number {\n let text1 = textA\n let text2 = textB\n\n // Cache the text lengths to prevent multiple calls.\n const text1Length = text1.length\n const text2Length = text2.length\n\n // Eliminate the null case.\n if (text1Length === 0 || text2Length === 0) {\n return 0\n }\n\n // Truncate the longer string.\n if (text1Length > text2Length) {\n text1 = text1.substring(text1Length - text2Length)\n } else if (text1Length < text2Length) {\n text2 = text2.substring(0, text1Length)\n }\n const textLength = Math.min(text1Length, text2Length)\n\n // Quick check for the worst case.\n if (text1 === text2) {\n return textLength\n }\n\n // Start by looking for a single character match\n // and increase length until no match is found.\n // Performance analysis: http://neil.fraser.name/news/2010/11/04/\n let best = 0\n let length = 1\n\n for (let found = 0; found !== -1; ) {\n const pattern = text1.substring(textLength - length)\n found = text2.indexOf(pattern)\n if (found === -1) {\n return best\n }\n length += found\n if (found === 0 || text1.substring(textLength - length) === text2.substring(0, length)) {\n best = length\n length++\n }\n }\n\n // Only for typescript, never reached\n return best\n}\n","/**\n * Determine the common prefix of two strings.\n *\n * @param text1 - First string.\n * @param text2 - Second string.\n * @returns The number of characters common to the start of each string.\n * @internal\n */\nexport function getCommonPrefix(text1: string, text2: string): number {\n // Quick check for common null cases.\n if (!text1 || !text2 || text1[0] !== text2[0]) {\n return 0\n }\n\n // Binary search.\n // Performance analysis: http://neil.fraser.name/news/2007/10/09/\n let pointerMin = 0\n let pointerMax = Math.min(text1.length, text2.length)\n let pointerMid = pointerMax\n let pointerStart = 0\n while (pointerMin < pointerMid) {\n if (text1.substring(pointerStart, pointerMid) === text2.substring(pointerStart, pointerMid)) {\n pointerMin = pointerMid\n pointerStart = pointerMin\n } else {\n pointerMax = pointerMid\n }\n pointerMid = Math.floor((pointerMax - pointerMin) / 2 + pointerMin)\n }\n return pointerMid\n}\n","/**\n * Determine the common suffix of two strings.\n *\n * @param text1 - First string.\n * @param text2 - Second string.\n * @returns The number of characters common to the end of each string.\n * @internal\n */\nexport function getCommonSuffix(text1: string, text2: string): number {\n // Quick check for common null cases.\n if (!text1 || !text2 || text1[text1.length - 1] !== text2[text2.length - 1]) {\n return 0\n }\n\n // Binary search.\n // Performance analysis: http://neil.fraser.name/news/2007/10/09/\n let pointerMin = 0\n let pointerMax = Math.min(text1.length, text2.length)\n let pointerMid = pointerMax\n let pointerEnd = 0\n while (pointerMin < pointerMid) {\n if (\n text1.substring(text1.length - pointerMid, text1.length - pointerEnd) ===\n text2.substring(text2.length - pointerMid, text2.length - pointerEnd)\n ) {\n pointerMin = pointerMid\n pointerEnd = pointerMin\n } else {\n pointerMax = pointerMid\n }\n pointerMid = Math.floor((pointerMax - pointerMin) / 2 + pointerMin)\n }\n\n return pointerMid\n}\n","/**\n * Checks if the character is a high surrogate\n *\n * @param char - Character to check\n * @returns True if high surrogate, false otherwise\n */\nexport function isHighSurrogate(char: string): boolean {\n const charCode = char.charCodeAt(0)\n return charCode >= 0xd800 && charCode <= 0xdbff\n}\n\n/**\n * Checks if the character is a low surrogate\n *\n * @param char - Character to check\n * @returns True if low surrogate, false otherwise\n */\nexport function isLowSurrogate(char: string): boolean {\n const charCode = char.charCodeAt(0)\n return charCode >= 0xdc00 && charCode <= 0xdfff\n}\n","import {type Diff, DIFF_DELETE, DIFF_INSERT, doDiff} from './diff.js'\n\n/**\n * Find the 'middle snake' of a diff, split the problem in two\n * and return the recursively constructed diff.\n * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.\n *\n * @param text1 - Old string to be diffed.\n * @param text2 - New string to be diffed.\n * @returns Array of diff tuples.\n * @internal\n */\nexport function bisect(text1: string, text2: string, deadline: number): Diff[] {\n // Cache the text lengths to prevent multiple calls.\n const text1Length = text1.length\n const text2Length = text2.length\n const maxD = Math.ceil((text1Length + text2Length) / 2)\n const vOffset = maxD\n const vLength = 2 * maxD\n const v1 = new Array(vLength)\n const v2 = new Array(vLength)\n // Setting all elements to -1 is faster in Chrome & Firefox than mixing\n // integers and undefined.\n for (let x = 0; x < vLength; x++) {\n v1[x] = -1\n v2[x] = -1\n }\n v1[vOffset + 1] = 0\n v2[vOffset + 1] = 0\n const delta = text1Length - text2Length\n // If the total number of characters is odd, then the front path will collide\n // with the reverse path.\n const front = delta % 2 !== 0\n // Offsets for start and end of k loop.\n // Prevents mapping of space beyond the grid.\n let k1start = 0\n let k1end = 0\n let k2start = 0\n let k2end = 0\n for (let d = 0; d < maxD; d++) {\n // Bail out if deadline is reached.\n if (Date.now() > deadline) {\n break\n }\n // Walk the front path one step.\n for (let k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {\n const k1Offset = vOffset + k1\n let x1\n if (k1 === -d || (k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1])) {\n x1 = v1[k1Offset + 1]\n } else {\n x1 = v1[k1Offset - 1] + 1\n }\n let y1 = x1 - k1\n while (x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)) {\n x1++\n y1++\n }\n v1[k1Offset] = x1\n if (x1 > text1Length) {\n // Ran off the right of the graph.\n k1end += 2\n } else if (y1 > text2Length) {\n // Ran off the bottom of the graph.\n k1start += 2\n } else if (front) {\n const k2Offset = vOffset + delta - k1\n if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) {\n // Mirror x2 onto top-left coordinate system.\n const x2 = text1Length - v2[k2Offset]\n if (x1 >= x2) {\n // Overlap detected.\n return bisectSplit(text1, text2, x1, y1, deadline)\n }\n }\n }\n }\n\n // Walk the reverse path one step.\n for (let k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {\n const k2Offset = vOffset + k2\n let x2\n if (k2 === -d || (k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1])) {\n x2 = v2[k2Offset + 1]\n } else {\n x2 = v2[k2Offset - 1] + 1\n }\n let y2 = x2 - k2\n while (\n x2 < text1Length &&\n y2 < text2Length &&\n text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)\n ) {\n x2++\n y2++\n }\n v2[k2Offset] = x2\n if (x2 > text1Length) {\n // Ran off the left of the graph.\n k2end += 2\n } else if (y2 > text2Length) {\n // Ran off the top of the graph.\n k2start += 2\n } else if (!front) {\n const k1Offset = vOffset + delta - k2\n if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) {\n const x1 = v1[k1Offset]\n const y1 = vOffset + x1 - k1Offset\n // Mirror x2 onto top-left coordinate system.\n x2 = text1Length - x2\n if (x1 >= x2) {\n // Overlap detected.\n return bisectSplit(text1, text2, x1, y1, deadline)\n }\n }\n }\n }\n }\n // Number of diffs equals number of characters, no commonality at all.\n return [\n [DIFF_DELETE, text1],\n [DIFF_INSERT, text2],\n ]\n}\n\n/**\n * Given the location of the 'middle snake', split the diff in two parts\n * and recurse.\n *\n * @param text1 - Old string to be diffed.\n * @param text2 - New string to be diffed.\n * @param x - Index of split point in text1.\n * @param y - Index of split point in text2.\n * @param deadline - Time at which to bail if not yet complete.\n * @returns Array of diff tuples.\n * @internal\n */\nfunction bisectSplit(text1: string, text2: string, x: number, y: number, deadline: number): Diff[] {\n const text1a = text1.substring(0, x)\n const text2a = text2.substring(0, y)\n const text1b = text1.substring(x)\n const text2b = text2.substring(y)\n\n // Compute both diffs serially.\n const diffs = doDiff(text1a, text2a, {checkLines: false, deadline})\n const diffsb = doDiff(text1b, text2b, {checkLines: false, deadline})\n\n return diffs.concat(diffsb)\n}\n","import {getCommonPrefix} from './commonPrefix.js'\nimport {getCommonSuffix} from './commonSuffix.js'\n\ntype HalfMatch = [string, string, string, string, string]\n\n/**\n * Does a slice of shorttext exist within longtext such that the slice\n * is at least half the length of longtext?\n *\n * @param longtext - Longer string.\n * @param shorttext - Shorter string.\n * @param i - Start index of quarter length slice within longtext.\n * @returns Five element Array, containing the prefix of\n * longtext, the suffix of longtext, the prefix of shorttext, the suffix\n * of shorttext and the common middle. Or null if there was no match.\n * @internal\n */\nexport function findHalfMatch(text1: string, text2: string, timeout = 1): null | HalfMatch {\n if (timeout <= 0) {\n // Don't risk returning a non-optimal diff if we have unlimited time.\n return null\n }\n\n const longText = text1.length > text2.length ? text1 : text2\n const shortText = text1.length > text2.length ? text2 : text1\n if (longText.length < 4 || shortText.length * 2 < longText.length) {\n return null // Pointless.\n }\n\n // First check if the second quarter is the seed for a half-match.\n const halfMatch1 = halfMatchI(longText, shortText, Math.ceil(longText.length / 4))\n // Check again based on the third quarter.\n const halfMatch2 = halfMatchI(longText, shortText, Math.ceil(longText.length / 2))\n\n let halfMatch\n if (halfMatch1 && halfMatch2) {\n // Both matched. Select the longest.\n halfMatch = halfMatch1[4].length > halfMatch2[4].length ? halfMatch1 : halfMatch2\n } else if (!halfMatch1 && !halfMatch2) {\n return null\n } else if (!halfMatch2) {\n halfMatch = halfMatch1\n } else if (!halfMatch1) {\n halfMatch = halfMatch2\n }\n\n if (!halfMatch) {\n throw new Error('Unable to find a half match.')\n }\n\n // A half-match was found, sort out the return data.\n let text1A: string\n let text1B: string\n let text2A: string\n let text2B: string\n\n if (text1.length > text2.length) {\n text1A = halfMatch[0]\n text1B = halfMatch[1]\n text2A = halfMatch[2]\n text2B = halfMatch[3]\n } else {\n text2A = halfMatch[0]\n text2B = halfMatch[1]\n text1A = halfMatch[2]\n text1B = halfMatch[3]\n }\n const midCommon = halfMatch[4]\n return [text1A, text1B, text2A, text2B, midCommon]\n}\n\n/**\n * Do the two texts share a slice which is at least half the length of the\n * longer text?\n * This speedup can produce non-minimal diffs.\n *\n * @param longText - First string.\n * @param shortText - Second string.\n * @returns Five element array, containing the prefix of longText,\n * the suffix of longText, the prefix of shortText, the suffix of\n * shortText and the common middle. Or null if there was no match.\n * @internal\n */\nfunction halfMatchI(longText: string, shortText: string, i: number): null | HalfMatch {\n // Start with a 1/4 length slice at position i as a seed.\n const seed = longText.slice(i, i + Math.floor(longText.length / 4))\n let j = -1\n let bestCommon = ''\n let bestLongTextA\n let bestLongTextB\n let bestShortTextA\n let bestShortTextB\n\n while ((j = shortText.indexOf(seed, j + 1)) !== -1) {\n const prefixLength = getCommonPrefix(longText.slice(i), shortText.slice(j))\n const suffixLength = getCommonSuffix(longText.slice(0, i), shortText.slice(0, j))\n if (bestCommon.length < suffixLength + prefixLength) {\n bestCommon = shortText.slice(j - suffixLength, j) + shortText.slice(j, j + prefixLength)\n bestLongTextA = longText.slice(0, i - suffixLength)\n bestLongTextB = longText.slice(i + prefixLength)\n bestShortTextA = shortText.slice(0, j - suffixLength)\n bestShortTextB = shortText.slice(j + prefixLength)\n }\n }\n if (bestCommon.length * 2 >= longText.length) {\n return [\n bestLongTextA || '',\n bestLongTextB || '',\n bestShortTextA || '',\n bestShortTextB || '',\n bestCommon || '',\n ]\n }\n\n return null\n}\n","import {type Diff} from './diff.js'\n\n/**\n * Rehydrate the text in a diff from a string of line hashes to real lines of text.\n *\n * @param diffs - Array of diff tuples.\n * @param lineArray - Array of unique strings.\n * @internal\n */\nexport function charsToLines(diffs: Diff[], lineArray: string[]): void {\n for (let x = 0; x < diffs.length; x++) {\n const chars = diffs[x][1]\n const text: string[] = []\n for (let y = 0; y < chars.length; y++) {\n text[y] = lineArray[chars.charCodeAt(y)]\n }\n diffs[x][1] = text.join('')\n }\n}\n","/**\n * Split two texts into an array of strings. Reduce the texts to a string of\n * hashes where each Unicode character represents one line.\n *\n * @param textA - First string.\n * @param textB - Second string.\n * @returns An object containing the encoded textA, the encoded textB and\n * the array of unique strings. The zeroth element of the array of unique\n * strings is intentionally blank.\n * @internal\n */\nexport function linesToChars(\n textA: string,\n textB: string,\n): {\n chars1: string\n chars2: string\n lineArray: string[]\n} {\n const lineArray: string[] = [] // e.g. lineArray[4] === 'Hello\\n'\n const lineHash: {[key: string]: number} = {} // e.g. lineHash['Hello\\n'] === 4\n\n // '\\x00' is a valid character, but various debuggers don't like it.\n // So we'll insert a junk entry to avoid generating a null character.\n lineArray[0] = ''\n\n /**\n * Split a text into an array of strings. Reduce the texts to a string of\n * hashes where each Unicode character represents one line.\n * Modifies linearray and linehash through being a closure.\n *\n * @param text - String to encode.\n * @returns Encoded string.\n * @internal\n */\n function diffLinesToMunge(text: string) {\n let chars = ''\n // Walk the text, pulling out a substring for each line.\n // text.split('\\n') would would temporarily double our memory footprint.\n // Modifying text would create many large strings to garbage collect.\n let lineStart = 0\n let lineEnd = -1\n // Keeping our own length variable is faster than looking it up.\n let lineArrayLength = lineArray.length\n while (lineEnd < text.length - 1) {\n lineEnd = text.indexOf('\\n', lineStart)\n if (lineEnd === -1) {\n lineEnd = text.length - 1\n }\n let line = text.slice(lineStart, lineEnd + 1)\n\n // eslint-disable-next-line no-prototype-builtins\n if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : lineHash[line] !== undefined) {\n chars += String.fromCharCode(lineHash[line])\n } else {\n if (lineArrayLength === maxLines) {\n // Bail out at 65535 because\n // String.fromCharCode(65536) == String.fromCharCode(0)\n line = text.slice(lineStart)\n lineEnd = text.length\n }\n chars += String.fromCharCode(lineArrayLength)\n lineHash[line] = lineArrayLength\n lineArray[lineArrayLength++] = line\n }\n lineStart = lineEnd + 1\n }\n return chars\n }\n // Allocate 2/3rds of the space for textA, the rest for textB.\n let maxLines = 40000\n const chars1 = diffLinesToMunge(textA)\n maxLines = 65535\n const chars2 = diffLinesToMunge(textB)\n return {chars1, chars2, lineArray}\n}\n","import {charsToLines} from './charsToLines.js'\nimport {cleanupSemantic} from './cleanup.js'\nimport {\n type Diff,\n DIFF_DELETE,\n DIFF_EQUAL,\n DIFF_INSERT,\n doDiff,\n type InternalDiffOptions,\n} from './diff.js'\nimport {linesToChars} from './linesToChars.js'\n\n/**\n * Do a quick line-level diff on both strings, then rediff the parts for\n * greater accuracy.\n * This speedup can produce non-minimal diffs.\n *\n * @param textA - Old string to be diffed.\n * @param textB - New string to be diffed.\n * @param options - Options for the differ.\n * @returns Array of diff tuples.\n * @internal\n */\nexport function doLineModeDiff(textA: string, textB: string, opts: InternalDiffOptions): Diff[] {\n // Don't reassign fn params\n let text1 = textA\n let text2 = textB\n\n // Scan the text on a line-by-line basis first.\n const a = linesToChars(text1, text2)\n text1 = a.chars1\n text2 = a.chars2\n const linearray = a.lineArray\n\n let diffs = doDiff(text1, text2, {\n checkLines: false,\n deadline: opts.deadline,\n })\n\n // Convert the diff back to original text.\n charsToLines(diffs, linearray)\n // Eliminate freak matches (e.g. blank lines)\n diffs = cleanupSemantic(diffs)\n\n // Rediff any replacement blocks, this time character-by-character.\n // Add a dummy entry at the end.\n diffs.push([DIFF_EQUAL, ''])\n let pointer = 0\n let countDelete = 0\n let countInsert = 0\n let textDelete = ''\n let textInsert = ''\n while (pointer < diffs.length) {\n switch (diffs[pointer][0]) {\n case DIFF_INSERT:\n countInsert++\n textInsert += diffs[pointer][1]\n break\n case DIFF_DELETE:\n countDelete++\n textDelete += diffs[pointer][1]\n break\n case DIFF_EQUAL:\n // Upon reaching an equality, check for prior redundancies.\n if (countDelete >= 1 && countInsert >= 1) {\n // Delete the offending records and add the merged ones.\n diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert)\n pointer = pointer - countDelete - countInsert\n const aa = doDiff(textDelete, textInsert, {\n checkLines: false,\n deadline: opts.deadline,\n })\n for (let j = aa.length - 1; j >= 0; j--) {\n diffs.splice(pointer, 0, aa[j])\n }\n pointer += aa.length\n }\n countInsert = 0\n countDelete = 0\n textDelete = ''\n textInsert = ''\n break\n default:\n throw new Error('Unknown diff operation.')\n }\n pointer++\n }\n diffs.pop() // Remove the dummy entry at the end.\n\n return diffs\n}\n","import {bisect} from './bisect.js'\nimport {\n type Diff,\n DIFF_DELETE,\n DIFF_EQUAL,\n DIFF_INSERT,\n doDiff,\n type InternalDiffOptions,\n} from './diff.js'\nimport {findHalfMatch} from './halfMatch.js'\nimport {doLineModeDiff} from './lineMode.js'\n\n/**\n * Find the differences between two texts. Assumes that the texts do not\n * have any common prefix or suffix.\n *\n * @param text1 - Old string to be diffed.\n * @param text2 - New string to be diffed.\n * @param checklines - Speedup flag. If false, then don't run a\n * line-level diff first to identify the changed areas.\n * If true, then run a faster, slightly less optimal diff.\n * @param deadline - Time when the diff should be complete by.\n * @returns Array of diff tuples.\n * @internal\n */\nexport function computeDiff(text1: string, text2: string, opts: InternalDiffOptions): Diff[] {\n let diffs: Diff[]\n\n if (!text1) {\n // Just add some text (speedup).\n return [[DIFF_INSERT, text2]]\n }\n\n if (!text2) {\n // Just delete some text (speedup).\n return [[DIFF_DELETE, text1]]\n }\n\n const longtext = text1.length > text2.length ? text1 : text2\n const shorttext = text1.length > text2.length ? text2 : text1\n const i = longtext.indexOf(shorttext)\n if (i !== -1) {\n // Shorter text is inside the longer text (speedup).\n diffs = [\n [DIFF_INSERT, longtext.substring(0, i)],\n [DIFF_EQUAL, shorttext],\n [DIFF_INSERT, longtext.substring(i + shorttext.length)],\n ]\n // Swap insertions for deletions if diff is reversed.\n if (text1.length > text2.length) {\n diffs[0][0] = DIFF_DELETE\n diffs[2][0] = DIFF_DELETE\n }\n return diffs\n }\n\n if (shorttext.length === 1) {\n // Single character string.\n // After the previous speedup, the character can't be an equality.\n return [\n [DIFF_DELETE, text1],\n [DIFF_INSERT, text2],\n ]\n }\n\n // Check to see if the problem can be split in two.\n const halfMatch = findHalfMatch(text1, text2)\n if (halfMatch) {\n // A half-match was found, sort out the return data.\n const text1A = halfMatch[0]\n const text1B = halfMatch[1]\n const text2A = halfMatch[2]\n const text2B = halfMatch[3]\n const midCommon = halfMatch[4]\n // Send both pairs off for separate processing.\n const diffsA = doDiff(text1A, text2A, opts)\n const diffsB = doDiff(text1B, text2B, opts)\n // Merge the results.\n return diffsA.concat([[DIFF_EQUAL, midCommon]], diffsB)\n }\n\n if (opts.checkLines && text1.length > 100 && text2.length > 100) {\n return doLineModeDiff(text1, text2, opts)\n }\n\n return bisect(text1, text2, opts.deadline)\n}\n","import {isHighSurrogate, isLowSurrogate} from '../utils/surrogatePairs.js'\nimport {cleanupMerge} from './cleanup.js'\nimport {getCommonPrefix} from './commonPrefix.js'\nimport {getCommonSuffix} from './commonSuffix.js'\nimport {computeDiff} from './compute.js'\n\n/**\n * Diff type for deleted text.\n *\n * @public\n */\nexport const DIFF_DELETE = -1\n\n/**\n * Diff type for inserted text.\n *\n * @public\n */\nexport const DIFF_INSERT = 1\n\n/**\n * Diff type for text that is equal.\n *\n * @public\n */\nexport const DIFF_EQUAL = 0\n\n/**\n * The three different types of changes possible in a diff:\n * - `DIFF_DELETE`: a deletion of text\n * - `DIFF_INSERT`: an insertion of text\n * - `DIFF_EQUAL` : an equal text\n *\n * @public\n */\nexport type DiffType = typeof DIFF_DELETE | typeof DIFF_INSERT | typeof DIFF_EQUAL\n\n/**\n * The data structure representing a diff is an array of tuples:\n * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]\n * which means: delete 'Hello', add 'Goodbye' and keep ' world.'\n *\n * @public\n */\nexport type Diff = [DiffType, string]\n\n/**\n * Options for generating a diff.\n *\n * @public\n */\nexport interface DiffOptions {\n checkLines: boolean\n timeout: number\n}\n\n/**\n * @internal\n */\nexport interface InternalDiffOptions {\n checkLines: boolean\n\n /**\n * Time when the diff should be complete by.\n */\n deadline: number\n}\n\n/**\n * Find the differences between two texts. Simplifies the problem by stripping\n * any common prefix or suffix off the texts before diffing.\n *\n * @param textA - Old string to be diffed.\n * @param textA - New string to be diffed.\n * @returns Array of diff tuples.\n * @public\n */\nexport function diff(\n textA: null | string,\n textB: null | string,\n opts?: Partial<DiffOptions>,\n): Diff[] {\n // Check for null inputs.\n if (textA === null || textB === null) {\n throw new Error('Null input. (diff)')\n }\n\n const diffs = doDiff(textA, textB, createInternalOpts(opts || {}))\n adjustDiffForSurrogatePairs(diffs)\n return diffs\n}\n\n/**\n * Find the differences between two texts. Simplifies the problem by stripping\n * any common prefix or suffix off the texts before diffing.\n *\n * @param textA - Old string to be diffed.\n * @param textB - New string to be diffed.\n * @returns Array of diff tuples.\n * @internal\n */\nexport function doDiff(textA: string, textB: string, options: InternalDiffOptions): Diff[] {\n // Don't reassign fn params\n let text1 = textA\n let text2 = textB\n\n // Check for equality (speedup).\n if (text1 === text2) {\n return text1 ? [[DIFF_EQUAL, text1]] : []\n }\n\n // Trim off common prefix (speedup).\n let commonlength = getCommonPrefix(text1, text2)\n const commonprefix = text1.substring(0, commonlength)\n text1 = text1.substring(commonlength)\n text2 = text2.substring(commonlength)\n\n // Trim off common suffix (speedup).\n commonlength = getCommonSuffix(text1, text2)\n const commonsuffix = text1.substring(text1.length - commonlength)\n text1 = text1.substring(0, text1.length - commonlength)\n text2 = text2.substring(0, text2.length - commonlength)\n\n // Compute the diff on the middle block.\n let diffs = computeDiff(text1, text2, options)\n\n // Restore the prefix and suffix.\n if (commonprefix) {\n diffs.unshift([DIFF_EQUAL, commonprefix])\n }\n if (commonsuffix) {\n diffs.push([DIFF_EQUAL, commonsuffix])\n }\n diffs = cleanupMerge(diffs)\n return diffs\n}\n\nfunction createDeadLine(timeout: undefined | number): number {\n let t = 1\n if (typeof timeout !== 'undefined') {\n t = timeout <= 0 ? Number.MAX_VALUE : timeout\n }\n return Date.now() + t * 1000\n}\n\nfunction createInternalOpts(opts: Partial<DiffOptions>): InternalDiffOptions {\n return {\n checkLines: true,\n deadline: createDeadLine(opts.timeout || 1.0),\n ...opts,\n }\n}\n\nfunction combineChar(data: string, char: string, dir: 1 | -1) {\n return dir === 1 ? data + char : char + data\n}\n\n/**\n * Splits out a character in a given direction.\n */\nfunction splitChar(data: string, dir: 1 | -1): [string, string] {\n return dir === 1\n ? [data.substring(0, data.length - 1), data[data.length - 1]]\n : [data.substring(1), data[0]]\n}\n\n/**\n * Checks if two entries of the diff has the same character in the same \"direction\".\n */\nfunction hasSharedChar(diffs: Diff[], i: number, j: number, dir: 1 | -1): boolean {\n return dir === 1\n ? diffs[i][1][diffs[i][1].length - 1] === diffs[j][1][diffs[j][1].length - 1]\n : diffs[i][1][0] === diffs[j][1][0]\n}\n\n/**\n * Takes in a position of an EQUAL diff-type and attempts to \"deisolate\" the character for a given direction.\n * By this we mean that we attempt to either \"shift\" it to the later diffs, or bring another character next into this one.\n *\n * It's easier to understand with an example:\n * [INSERT a, DELETE b, EQUAL cde, INSERT f, DELETE g]\n * shifting this forward will produce\n * [INSERT a, DELETE b, EQUAL cd, INSERT ef, DELETE eg]\n *\n * This behavior is useful when `e` is actually a high surrogate character.\n *\n * Shifting it backwards produces\n * [INSERT ac, DELETE bc, EQUAL cde, INSERT f, DELETE g]\n * which is useful when `c` is a low surrogate character.\n *\n * Note that these diffs are 100% semantically equal.\n *\n * If there's not a matching INSERT/DELETE then it's forced to insert an additional entry:\n * [EQUAL abc, INSERT d, EQUAL e]\n * shifted forward becomes:\n * [EQUAL ab, INSERT cd, DELETE c, EQUAL e]\n *\n * If the INSERT and DELETE ends with the same character it will instead deisolate it by\n * bring that charcter into _this_ equal:\n * [EQUAL abc, INSERT de, DELETE df]\n * shifted forward actually becomes\n * [EQUAL abcd, INSERT e, DELETE f]\n *\n * The original diff here is typically never produced by the diff algorithm directly,\n * but they occur when we isolate characters in other places.\n */\nfunction deisolateChar(diffs: Diff[], i: number, dir: 1 | -1) {\n const inv = dir === 1 ? -1 : 1\n let insertIdx: null | number = null\n let deleteIdx: null | number = null\n\n let j = i + dir\n for (; j >= 0 && j < diffs.length && (insertIdx === null || deleteIdx === null); j += dir) {\n const [op, text] = diffs[j]\n if (text.length === 0) {\n continue\n }\n\n if (op === DIFF_INSERT) {\n if (insertIdx === null) {\n insertIdx = j\n }\n continue\n } else if (op === DIFF_DELETE) {\n if (deleteIdx === null) {\n deleteIdx = j\n }\n continue\n } else if (op === DIFF_EQUAL) {\n if (insertIdx === null && deleteIdx === null) {\n // This means that there was two consecutive EQUAL. Kinda weird, but easy to handle.\n const [rest, char] = splitChar(diffs[i][1], dir)\n diffs[i][1] = rest\n diffs[j][1] = combineChar(diffs[j][1], char, inv)\n return\n }\n break\n }\n }\n\n if (insertIdx !== null && deleteIdx !== null && hasSharedChar(diffs, insertIdx, deleteIdx, dir)) {\n // Special case.\n const [insertText, insertChar] = splitChar(diffs[insertIdx][1], inv)\n const [deleteText] = splitChar(diffs[deleteIdx][1], inv)\n diffs[insertIdx][1] = insertText\n diffs[deleteIdx][1] = deleteText\n diffs[i][1] = combineChar(diffs[i][1], insertChar, dir)\n return\n }\n\n const [text, char] = splitChar(diffs[i][1], dir)\n diffs[i][1] = text\n\n if (insertIdx === null) {\n diffs.splice(j, 0, [DIFF_INSERT, char])\n\n // We need to adjust deleteIdx here since it's been shifted\n if (deleteIdx !== null && deleteIdx >= j) deleteIdx++\n } else {\n diffs[insertIdx][1] = combineChar(diffs[insertIdx][1], char, inv)\n }\n\n if (deleteIdx === null) {\n diffs.splice(j, 0, [DIFF_DELETE, char])\n } else {\n diffs[deleteIdx][1] = combineChar(diffs[deleteIdx][1], char, inv)\n }\n}\n\nfunction adjustDiffForSurrogatePairs(diffs: Diff[]) {\n // Go over each pair of diffs and see if there was a split at a surrogate pair\n for (let i = 0; i < diffs.length; i++) {\n const [diffType, diffText] = diffs[i]\n\n if (diffText.length === 0) continue\n\n const firstChar = diffText[0]\n const lastChar = diffText[diffText.length - 1]\n\n if (isHighSurrogate(lastChar) && diffType === DIFF_EQUAL) {\n deisolateChar(diffs, i, 1)\n }\n\n if (isLowSurrogate(firstChar) && diffType === DIFF_EQUAL) {\n deisolateChar(diffs, i, -1)\n }\n }\n\n for (let i = 0; i < diffs.length; i++) {\n // Remove any empty diffs\n if (diffs[i][1].length === 0) {\n diffs.splice(i, 1)\n }\n }\n}\n","import {cloneDiff} from './clone.js'\nimport {getCommonOverlap} from './commonOverlap.js'\nimport {getCommonPrefix} from './commonPrefix.js'\nimport {getCommonSuffix} from './commonSuffix.js'\nimport {type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT} from './diff.js'\n\n/**\n * Reduce the number of edits by eliminating semantically trivial equalities.\n *\n * @param rawDiffs - Array of diff tuples.\n * @returns Array of diff tuples.\n * @public\n */\nexport function cleanupSemantic(rawDiffs: Diff[]): Diff[] {\n let diffs: Diff[] = rawDiffs.map((diff) => cloneDiff(diff))\n\n let hasChanges = false\n const equalities: number[] = [] // Stack of indices where equalities are found.\n let equalitiesLength = 0 // Keeping our own length var is faster in JS.\n /** @type {?string} */\n let lastEquality = null\n // Always equal to diffs[equalities[equalitiesLength - 1]][1]\n let pointer = 0 // Index of current position.\n // Number of characters that changed prior to the equality.\n let lengthInsertions1 = 0\n let lengthDeletions1 = 0\n // Number of characters that changed after the equality.\n let lengthInsertions2 = 0\n let lengthDeletions2 = 0\n while (pointer < diffs.length) {\n if (diffs[pointer][0] === DIFF_EQUAL) {\n // Equality found.\n equalities[equalitiesLength++] = pointer\n lengthInsertions1 = lengthInsertions2\n lengthDeletions1 = lengthDeletions2\n lengthInsertions2 = 0\n lengthDeletions2 = 0\n lastEquality = diffs[pointer][1]\n } else {\n // An insertion or deletion.\n if (diffs[pointer][0] === DIFF_INSERT) {\n lengthInsertions2 += diffs[pointer][1].length\n } else {\n lengthDeletions2 += diffs[pointer][1].length\n }\n // Eliminate an equality that is smaller or equal to the edits on both\n // sides of it.\n if (\n lastEquality &&\n lastEquality.length <= Math.max(lengthInsertions1, lengthDeletions1) &&\n lastEquality.length <= Math.max(lengthInsertions2, lengthDeletions2)\n ) {\n // Duplicate record.\n diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastEquality])\n // Change second copy to insert.\n diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT\n // Throw away the equality we just deleted.\n equalitiesLength--\n // Throw away the previous equality (it needs to be reevaluated).\n equalitiesLength--\n pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1\n lengthInsertions1 = 0 // Reset the counters.\n lengthDeletions1 = 0\n lengthInsertions2 = 0\n lengthDeletions2 = 0\n lastEquality = null\n hasChanges = true\n }\n }\n pointer++\n }\n\n // Normalize the diff.\n if (hasChanges) {\n diffs = cleanupMerge(diffs)\n }\n diffs = cleanupSemanticLossless(diffs)\n\n // Find any overlaps between deletions and insertions.\n // e.g: <del>abczzz</del><ins>zzzdef</ins>\n // -> <del>abc</del>zzz<ins>def</ins>\n // e.g: <del>zzzabc</del><ins>defzzz</ins>\n // -> <ins>def</ins>zzz<del>abc</del>\n // Only extract an overlap if it is as big as the edit ahead or behind it.\n pointer = 1\n while (pointer < diffs.length) {\n if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) {\n const deletion = diffs[pointer - 1][1]\n const insertion = diffs[pointer][1]\n const overlapLength1 = getCommonOverlap(deletion, insertion)\n const overlapLength2 = getCommonOverlap(insertion, deletion)\n if (overlapLength1 >= overlapLength2) {\n if (overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2) {\n // Overlap found. Insert an equality and trim the surrounding edits.\n diffs.splice(pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlapLength1)])\n diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlapLength1)\n diffs[pointer + 1][1] = insertion.substring(overlapLength1)\n pointer++\n }\n } else if (overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2) {\n // Reverse overlap found.\n // Insert an equality and swap and trim the surrounding edits.\n diffs.splice(pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlapLength2)])\n diffs[pointer - 1][0] = DIFF_INSERT\n diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlapLength2)\n diffs[pointer + 1][0] = DIFF_DELETE\n diffs[pointer + 1][1] = deletion.substring(overlapLength2)\n pointer++\n }\n pointer++\n }\n pointer++\n }\n return diffs\n}\n\n// Define some regex patterns for matching boundaries.\nconst nonAlphaNumericRegex = /[^a-zA-Z0-9]/\nconst whitespaceRegex = /\\s/\nconst linebreakRegex = /[\\r\\n]/\nconst blanklineEndRegex = /\\n\\r?\\n$/\nconst blanklineStartRegex = /^\\r?\\n\\r?\\n/\n\n/**\n * Look for single edits surrounded on both sides by equalities\n * which can be shifted sideways to align the edit to a word boundary.\n * e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.\n *\n * @param rawDiffs - Array of diff tuples.\n * @returns Array of diff tuples.\n * @public\n */\nexport function cleanupSemanticLossless(rawDiffs: Diff[]): Diff[] {\n const diffs = rawDiffs.map((diff) => cloneDiff(diff))\n\n /**\n * Given two strings, compute a score representing whether the internal\n * boundary falls on logical boundaries.\n * Scores range from 6 (best) to 0 (worst).\n * Closure, but does not reference any external variables.\n *\n * @param one - First string.\n * @param two - Second string.\n * @returns The score.\n * @internal\n */\n function diffCleanupSemanticScore(one: string, two: string) {\n if (!one || !two) {\n // Edges are the best.\n return 6\n }\n\n // Each port of this function behaves slightly differently due to\n // subtle differences in each language's definition of things like\n // 'whitespace'. Since this function's purpose is largely cosmetic,\n // the choice has been made to use each language's native features\n // rather than force total conformity.\n const char1 = one.charAt(one.length - 1)\n const char2 = two.charAt(0)\n const nonAlphaNumeric1 = char1.match(nonAlphaNumericRegex)\n const nonAlphaNumeric2 = char2.match(nonAlphaNumericRegex)\n const whitespace1 = nonAlphaNumeric1 && char1.match(whitespaceRegex)\n const whitespace2 = nonAlphaNumeric2 && char2.match(whitespaceRegex)\n const lineBreak1 = whitespace1 && char1.match(linebreakRegex)\n const lineBreak2 = whitespace2 && char2.match(linebreakRegex)\n const blankLine1 = lineBreak1 && one.match(blanklineEndRegex)\n const blankLine2 = lineBreak2 && two.match(blanklineStartRegex)\n\n if (blankLine1 || blankLine2) {\n // Five points for blank lines.\n return 5\n } else if (lineBreak1 || lineBreak2) {\n // Four points for line breaks.\n return 4\n } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) {\n // Three points for end of sentences.\n return 3\n } else if (whitespace1 || whitespace2) {\n // Two points for whitespace.\n return 2\n } else if (nonAlphaNumeric1 || nonAlphaNumeric2) {\n // One point for non-alphanumeric.\n return 1\n }\n return 0\n }\n\n let pointer = 1\n // Intentionally ignore the first and last element (don't need checking).\n while (pointer < diffs.length - 1) {\n if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) {\n // This is a single edit surrounded by equalities.\n let equality1 = diffs[pointer - 1][1]\n let edit = diffs[pointer][1]\n let equality2 = diffs[pointer + 1][1]\n\n // First, shift the edit as far left as possible.\n const commonOffset = getCommonSuffix(equality1, edit)\n if (commonOffset) {\n const commonString = edit.substring(edit.length - commonOffset)\n equality1 = equality1.substring(0, equality1.length - commonOffset)\n edit = commonString + edit.substring(0, edit.length - commonOffset)\n equality2 = commonString + equality2\n }\n\n // Second, step character by character right, looking for the best fit.\n let bestEquality1 = equality1\n let bestEdit = edit\n let bestEquality2 = equality2\n let bestScore =\n diffCleanupSemanticScore(equality1, edit) + diffCleanupSemanticScore(edit, equality2)\n while (edit.charAt(0) === equality2.charAt(0)) {\n equality1 += edit.charAt(0)\n edit = edit.substring(1) + equality2.charAt(0)\n equality2 = equality2.substring(1)\n const score =\n diffCleanupSemanticScore(equality1, edit) + diffCleanupSemanticScore(edit, equality2)\n // The >= encourages trailing rather than leading whitespace on edits.\n if (score >= bestScore) {\n bestScore = score\n bestEquality1 = equality1\n bestEdit = edit\n bestEquality2 = equality2\n }\n }\n\n if (diffs[pointer - 1][1] !== bestEquality1) {\n // We have an improvement, save it back to the diff.\n if (bestEquality1) {\n diffs[pointer - 1][1] = bestEquality1\n } else {\n diffs.splice(pointer - 1, 1)\n pointer--\n }\n diffs[pointer][1] = bestEdit\n if (bestEquality2) {\n diffs[pointer + 1][1] = bestEquality2\n } else {\n diffs.splice(pointer + 1, 1)\n pointer--\n }\n }\n }\n pointer++\n }\n\n return diffs\n}\n\n/**\n * Reorder and merge like edit sections. Merge equalities.\n * Any edit section can move as long as it doesn't cross an equality.\n *\n * @param rawDiffs - Array of diff tuples.\n * @returns Array of diff tuples.\n * @public\n */\nexport function cleanupMerge(rawDiffs: Diff[]): Diff[] {\n let diffs = rawDiffs.map((diff) => cloneDiff(diff))\n\n // Add a dummy entry at the end.\n diffs.push([DIFF_EQUAL, ''])\n let pointer = 0\n let countDelete = 0\n let countInsert = 0\n let textDelete = ''\n let textInsert = ''\n let commonlength\n while (pointer < diffs.length) {\n switch (diffs[pointer][0]) {\n case DIFF_INSERT:\n countInsert++\n textInsert += diffs[pointer][1]\n pointer++\n break\n case DIFF_DELETE:\n countDelete++\n textDelete += diffs[pointer][1]\n pointer++\n break\n case DIFF_EQUAL:\n // Upon reaching an equality, check for prior redundancies.\n if (countDelete + countInsert > 1) {\n if (countDelete !== 0 && countInsert !== 0) {\n // Factor out any common prefixies.\n commonlength = getCommonPrefix(textInsert, textDelete)\n if (commonlength !== 0) {\n if (\n pointer - countDelete - countInsert > 0 &&\n diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL\n ) {\n diffs[pointer - countDelete - countInsert - 1][1] += textInsert.substring(\n 0,\n commonlength,\n )\n } else {\n diffs.splice(0, 0, [DIFF_EQUAL, textInsert.substring(0, commonlength)])\n pointer++\n }\n textInsert = textInsert.substring(commonlength)\n textDelete = textDelete.substring(commonlength)\n }\n // Factor out any common suffixies.\n commonlength = getCommonSuffix(textInsert, textDelete)\n if (commonlength !== 0) {\n diffs[pointer][1] =\n textInsert.substring(textInsert.length - commonlength) + diffs[pointer][1]\n textInsert = textInsert.substring(0, textInsert.length - commonlength)\n textDelete = textDelete.substring(0, textDelete.length - commonlength)\n }\n }\n // Delete the offending records and add the merged ones.\n pointer -= countDelete + countInsert\n diffs.splice(pointer, countDelete + countInsert)\n if (textDelete.length) {\n diffs.splice(pointer, 0, [DIFF_DELETE, textDelete])\n pointer++\n }\n if (textInsert.length) {\n diffs.splice(pointer, 0, [DIFF_INSERT, textInsert])\n pointer++\n }\n pointer++\n } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {\n // Merge this equality with the previous one.\n diffs[pointer - 1][1] += diffs[pointer][1]\n diffs.splice(pointer, 1)\n } else {\n pointer++\n }\n countInsert = 0\n countDelete = 0\n textDelete = ''\n textInsert = ''\n break\n default:\n throw new Error('Unknown diff operation')\n }\n }\n if (diffs[diffs.length - 1][1] === '') {\n diffs.pop() // Remove the dummy entry at the end.\n }\n\n // Second pass: look for single edits surrounded on both sides by equalities\n // which can be shifted sideways to eliminate an equality.\n // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC\n let hasChanges = false\n pointer = 1\n // Intentionally ignore the first and last element (don't need checking).\n while (pointer < diffs.length - 1) {\n if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) {\n // This is a single edit surrounded by equalities.\n if (\n diffs[pointer][1].substring(diffs[pointer][1].length - diffs[pointer - 1][1].length) ===\n diffs[pointer - 1][1]\n ) {\n // Shift the edit over the previous equality.\n diffs[pointer][1] =\n diffs[pointer - 1][1] +\n diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length)\n diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]\n diffs.splice(pointer - 1, 1)\n hasChanges = true\n } else if (\n diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]\n ) {\n // Shift the edit over the next equality.\n diffs[pointer - 1][1] += diffs[pointer + 1][1]\n diffs[pointer][1] =\n diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]\n diffs.splice(pointer + 1, 1)\n hasChanges = true\n }\n }\n pointer++\n }\n // If shifts were made, the diff needs reordering and another shift sweep.\n if (hasChanges) {\n diffs = cleanupMerge(diffs)\n }\n\n return diffs\n}\n\nfunction trueCount(...args: boolean[]) {\n return args.reduce((n, bool) => n + (bool ? 1 : 0), 0)\n}\n\n/**\n * Reduce the number of edits by eliminating operationally trivial equalities.\n *\n * @param rawDiffs - Array of diff tuples.\n * @param editCost - Cost of an empty edit operation in terms of edit characters.\n * @returns Array of diff tuples.\n * @public\n */\nexport function cleanupEfficiency(rawDiffs: Diff[], editCost: number = 4): Diff[] {\n let diffs = rawDiffs.map((diff) => cloneDiff(diff))\n let hasChanges = false\n const equalities: number[] = [] // Stack of indices where equalities are found.\n let equalitiesLength = 0 // Keeping our own length var is faster in JS.\n let lastEquality: string | null = null\n // Always equal to diffs[equalities[equalitiesLength - 1]][1]\n let pointer = 0 // Index of current position.\n // Is there an insertion operation before the last equality.\n let preIns = false\n // Is there a deletion operation before the last equality.\n let preDel = false\n // Is there an insertion operation after the last equality.\n let postIns = false\n // Is there a deletion operation after the last equality.\n let postDel = false\n while (pointer < diffs.length) {\n if (diffs[pointer][0] === DIFF_EQUAL) {\n // Equality found.\n if (diffs[pointer][1].length < editCost && (postIns || postDel)) {\n // Candidate found.\n equalities[equalitiesLength++] = pointer\n preIns = postIns\n preDel = postDel\n lastEquality = diffs[pointer][1]\n } else {\n // Not a candidate, and can never become one.\n equalitiesLength = 0\n lastEquality = null\n }\n postIns = false\n postDel = false\n } else {\n // An insertion or deletion.\n if (diffs[pointer][0] === DIFF_DELETE) {\n postDel = true\n } else {\n postIns = true\n }\n /*\n * Five types to be split:\n * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>\n * <ins>A</ins>X<ins>C</ins><del>D</del>\n * <ins>A</ins><del>B</del>X<ins>C</ins>\n * <ins>A</del>X<ins>C</ins><del>D</del>\n * <ins>A</ins><del>B</del>X<del>C</del>\n */\n if (\n lastEquality &&\n ((preIns && preDel && postIns && postDel) ||\n (lastEquality.length < editCost / 2 && trueCount(preIns, preDel, postIns, postDel) === 3))\n ) {\n // Duplicate record.\n diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastEquality])\n // Change second copy to insert.\n diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT\n equalitiesLength-- // Throw away the equality we just deleted;\n lastEquality = null\n if (preIns && preDel) {\n // No hasChanges made which could affect previous entry, keep going.\n postIns = true\n postDel = true\n equalitiesLength = 0\n } else {\n equalitiesLength-- // Throw away the previous equality.\n pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1\n postIns = false\n postDel = false\n }\n hasChanges = true\n }\n }\n pointer++\n }\n\n if (hasChanges) {\n diffs = cleanupMerge(diffs)\n }\n\n return diffs\n}\n","interface BitapOptions {\n threshold: number\n distance: number\n}\n\ninterface Alphabet {\n [char: string]: number\n}\n\nconst DEFAULT_OPTIONS: BitapOptions = {\n /**\n * At what point is no match declared (0.0 = perfection, 1.0 = very loose).\n