Download node_modules/@vitest/utils/dist/diff.js from metarm/QMetaRam: direct link, hf CLI and curl.
- Browser
- Download file 78.7 kB
-
https://huggingface.co/metarm/QMetaRam/resolve/main/node_modules/%40vitest/utils/dist/diff.js
- Command line
-
hf download hf://metarm/QMetaRam/node_modules/@vitest/utils/dist/diff.js
-
curl -L -o diff.js https://huggingface.co/metarm/QMetaRam/resolve/main/node_modules/%40vitest/utils/dist/diff.js
78.7 kB
| import { plugins, format } from '@vitest/pretty-format'; | |
| import c from 'tinyrainbow'; | |
| import { g as getDefaultExportFromCjs, s as stringify } from './chunk-_commonjsHelpers.js'; | |
| import { deepClone, getOwnProperties, getType as getType$1 } from './helpers.js'; | |
| import 'loupe'; | |
| /** | |
| * Diff Match and Patch | |
| * Copyright 2018 The diff-match-patch Authors. | |
| * https://github.com/google/diff-match-patch | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| /** | |
| * @fileoverview Computes the difference between two texts to create a patch. | |
| * Applies the patch onto another text, allowing for errors. | |
| * @author fraser@google.com (Neil Fraser) | |
| */ | |
| /** | |
| * CHANGES by pedrottimark to diff_match_patch_uncompressed.ts file: | |
| * | |
| * 1. Delete anything not needed to use diff_cleanupSemantic method | |
| * 2. Convert from prototype properties to var declarations | |
| * 3. Convert Diff to class from constructor and prototype | |
| * 4. Add type annotations for arguments and return values | |
| * 5. Add exports | |
| */ | |
| /** | |
| * The data structure representing a diff is an array of tuples: | |
| * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] | |
| * which means: delete 'Hello', add 'Goodbye' and keep ' world.' | |
| */ | |
| const DIFF_DELETE = -1; | |
| const DIFF_INSERT = 1; | |
| const DIFF_EQUAL = 0; | |
| /** | |
| * Class representing one diff tuple. | |
| * Attempts to look like a two-element array (which is what this used to be). | |
| * @param {number} op Operation, one of: DIFF_DELETE, DIFF_INSERT, DIFF_EQUAL. | |
| * @param {string} text Text to be deleted, inserted, or retained. | |
| * @constructor | |
| */ | |
| class Diff { | |
| 0; | |
| 1; | |
| constructor(op, text) { | |
| this[0] = op; | |
| this[1] = text; | |
| } | |
| } | |
| /** | |
| * Determine the common prefix of two strings. | |
| * @param {string} text1 First string. | |
| * @param {string} text2 Second string. | |
| * @return {number} The number of characters common to the start of each | |
| * string. | |
| */ | |
| function diff_commonPrefix(text1, text2) { | |
| // Quick check for common null cases. | |
| if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) { | |
| return 0; | |
| } | |
| // Binary search. | |
| // Performance analysis: https://neil.fraser.name/news/2007/10/09/ | |
| 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; | |
| } | |
| /** | |
| * Determine the common suffix of two strings. | |
| * @param {string} text1 First string. | |
| * @param {string} text2 Second string. | |
| * @return {number} The number of characters common to the end of each string. | |
| */ | |
| function diff_commonSuffix(text1, text2) { | |
| // Quick check for common null cases. | |
| if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { | |
| return 0; | |
| } | |
| // Binary search. | |
| // Performance analysis: https://neil.fraser.name/news/2007/10/09/ | |
| 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; | |
| } | |
| /** | |
| * Determine if the suffix of one string is the prefix of another. | |
| * @param {string} text1 First string. | |
| * @param {string} text2 Second string. | |
| * @return {number} The number of characters common to the end of the first | |
| * string and the start of the second string. | |
| * @private | |
| */ | |
| function diff_commonOverlap_(text1, text2) { | |
| // Cache the text lengths to prevent multiple calls. | |
| const text1_length = text1.length; | |
| const text2_length = text2.length; | |
| // Eliminate the null case. | |
| if (text1_length === 0 || text2_length === 0) { | |
| return 0; | |
| } | |
| // Truncate the longer string. | |
| 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); | |
| // Quick check for the worst case. | |
| if (text1 === text2) { | |
| return text_length; | |
| } | |
| // Start by looking for a single character match | |
| // and increase length until no match is found. | |
| // Performance analysis: https://neil.fraser.name/news/2010/11/04/ | |
| 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++; | |
| } | |
| } | |
| } | |
| /** | |
| * Reduce the number of edits by eliminating semantically trivial equalities. | |
| * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. | |
| */ | |
| function diff_cleanupSemantic(diffs) { | |
| let changes = false; | |
| const equalities = []; | |
| let equalitiesLength = 0; | |
| /** @type {?string} */ | |
| let lastEquality = null; | |
| // Always equal to diffs[equalities[equalitiesLength - 1]][1] | |
| let pointer = 0; | |
| // Number of characters that changed prior to the equality. | |
| let length_insertions1 = 0; | |
| let length_deletions1 = 0; | |
| // Number of characters that changed after the equality. | |
| let length_insertions2 = 0; | |
| let length_deletions2 = 0; | |
| while (pointer < diffs.length) { | |
| if (diffs[pointer][0] === DIFF_EQUAL) { | |
| // Equality found. | |
| equalities[equalitiesLength++] = pointer; | |
| length_insertions1 = length_insertions2; | |
| length_deletions1 = length_deletions2; | |
| length_insertions2 = 0; | |
| length_deletions2 = 0; | |
| lastEquality = diffs[pointer][1]; | |
| } else { | |
| // An insertion or deletion. | |
| if (diffs[pointer][0] === DIFF_INSERT) { | |
| length_insertions2 += diffs[pointer][1].length; | |
| } else { | |
| length_deletions2 += diffs[pointer][1].length; | |
| } | |
| // Eliminate an equality that is smaller or equal to the edits on both | |
| // sides of it. | |
| if (lastEquality && lastEquality.length <= Math.max(length_insertions1, length_deletions1) && lastEquality.length <= Math.max(length_insertions2, length_deletions2)) { | |
| // Duplicate record. | |
| diffs.splice(equalities[equalitiesLength - 1], 0, new Diff(DIFF_DELETE, lastEquality)); | |
| // Change second copy to insert. | |
| diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; | |
| // Throw away the equality we just deleted. | |
| equalitiesLength--; | |
| // Throw away the previous equality (it needs to be reevaluated). | |
| 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++; | |
| } | |
| // Normalize the diff. | |
| if (changes) { | |
| diff_cleanupMerge(diffs); | |
| } | |
| diff_cleanupSemanticLossless(diffs); | |
| // Find any overlaps between deletions and insertions. | |
| // e.g: <del>abcxxx</del><ins>xxxdef</ins> | |
| // -> <del>abc</del>xxx<ins>def</ins> | |
| // e.g: <del>xxxabc</del><ins>defxxx</ins> | |
| // -> <ins>def</ins>xxx<del>abc</del> | |
| // Only extract an overlap if it is as big as the edit ahead or behind it. | |
| 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 = diff_commonOverlap_(deletion, insertion); | |
| const overlap_length2 = diff_commonOverlap_(insertion, deletion); | |
| if (overlap_length1 >= overlap_length2) { | |
| if (overlap_length1 >= deletion.length / 2 || overlap_length1 >= insertion.length / 2) { | |
| // Overlap found. Insert an equality and trim the surrounding edits. | |
| diffs.splice(pointer, 0, new Diff(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) { | |
| // Reverse overlap found. | |
| // Insert an equality and swap and trim the surrounding edits. | |
| diffs.splice(pointer, 0, new Diff(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++; | |
| } | |
| } | |
| // Define some regex patterns for matching boundaries. | |
| const nonAlphaNumericRegex_ = /[^a-z0-9]/i; | |
| const whitespaceRegex_ = /\s/; | |
| const linebreakRegex_ = /[\r\n]/; | |
| const blanklineEndRegex_ = /\n\r?\n$/; | |
| const blanklineStartRegex_ = /^\r?\n\r?\n/; | |
| /** | |
| * Look for single edits surrounded on both sides by equalities | |
| * which can be shifted sideways to align the edit to a word boundary. | |
| * e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came. | |
| * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. | |
| */ | |
| function diff_cleanupSemanticLossless(diffs) { | |
| let pointer = 1; | |
| // Intentionally ignore the first and last element (don't need checking). | |
| while (pointer < diffs.length - 1) { | |
| if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { | |
| // This is a single edit surrounded by equalities. | |
| let equality1 = diffs[pointer - 1][1]; | |
| let edit = diffs[pointer][1]; | |
| let equality2 = diffs[pointer + 1][1]; | |
| // First, shift the edit as far left as possible. | |
| const commonOffset = diff_commonSuffix(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; | |
| } | |
| // Second, step character by character right, looking for the best fit. | |
| let bestEquality1 = equality1; | |
| let bestEdit = edit; | |
| let bestEquality2 = equality2; | |
| let bestScore = diff_cleanupSemanticScore_(equality1, edit) + diff_cleanupSemanticScore_(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 = diff_cleanupSemanticScore_(equality1, edit) + diff_cleanupSemanticScore_(edit, equality2); | |
| // The >= encourages trailing rather than leading whitespace on edits. | |
| if (score >= bestScore) { | |
| bestScore = score; | |
| bestEquality1 = equality1; | |
| bestEdit = edit; | |
| bestEquality2 = equality2; | |
| } | |
| } | |
| if (diffs[pointer - 1][1] !== bestEquality1) { | |
| // We have an improvement, save it back to the diff. | |
| 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++; | |
| } | |
| } | |
| /** | |
| * Reorder and merge like edit sections. Merge equalities. | |
| * Any edit section can move as long as it doesn't cross an equality. | |
| * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. | |
| */ | |
| function diff_cleanupMerge(diffs) { | |
| // Add a dummy entry at the end. | |
| diffs.push(new Diff(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: | |
| // Upon reaching an equality, check for prior redundancies. | |
| if (count_delete + count_insert > 1) { | |
| if (count_delete !== 0 && count_insert !== 0) { | |
| // Factor out any common prefixes. | |
| commonlength = diff_commonPrefix(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, new Diff(DIFF_EQUAL, text_insert.substring(0, commonlength))); | |
| pointer++; | |
| } | |
| text_insert = text_insert.substring(commonlength); | |
| text_delete = text_delete.substring(commonlength); | |
| } | |
| // Factor out any common suffixes. | |
| commonlength = diff_commonSuffix(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); | |
| } | |
| } | |
| // Delete the offending records and add the merged ones. | |
| pointer -= count_delete + count_insert; | |
| diffs.splice(pointer, count_delete + count_insert); | |
| if (text_delete.length) { | |
| diffs.splice(pointer, 0, new Diff(DIFF_DELETE, text_delete)); | |
| pointer++; | |
| } | |
| if (text_insert.length) { | |
| diffs.splice(pointer, 0, new Diff(DIFF_INSERT, text_insert)); | |
| pointer++; | |
| } | |
| pointer++; | |
| } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { | |
| // Merge this equality with the previous one. | |
| 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(); | |
| } | |
| // Second pass: look for single edits surrounded on both sides by equalities | |
| // which can be shifted sideways to eliminate an equality. | |
| // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC | |
| let changes = false; | |
| pointer = 1; | |
| // Intentionally ignore the first and last element (don't need checking). | |
| while (pointer < diffs.length - 1) { | |
| if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { | |
| // This is a single edit surrounded by equalities. | |
| if (diffs[pointer][1].substring(diffs[pointer][1].length - diffs[pointer - 1][1].length) === diffs[pointer - 1][1]) { | |
| // Shift the edit over the previous equality. | |
| 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]) { | |
| // Shift the edit over the next equality. | |
| 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 shifts were made, the diff needs reordering and another shift sweep. | |
| if (changes) { | |
| diff_cleanupMerge(diffs); | |
| } | |
| } | |
| /** | |
| * Given two strings, compute a score representing whether the internal | |
| * boundary falls on logical boundaries. | |
| * Scores range from 6 (best) to 0 (worst). | |
| * Closure, but does not reference any external variables. | |
| * @param {string} one First string. | |
| * @param {string} two Second string. | |
| * @return {number} The score. | |
| * @private | |
| */ | |
| function diff_cleanupSemanticScore_(one, two) { | |
| if (!one || !two) { | |
| // Edges are the best. | |
| return 6; | |
| } | |
| // Each port of this function behaves slightly differently due to | |
| // subtle differences in each language's definition of things like | |
| // 'whitespace'. Since this function's purpose is largely cosmetic, | |
| // the choice has been made to use each language's native features | |
| // rather than force total conformity. | |
| 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) { | |
| // Five points for blank lines. | |
| return 5; | |
| } else if (lineBreak1 || lineBreak2) { | |
| // Four points for line breaks. | |
| return 4; | |
| } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { | |
| // Three points for end of sentences. | |
| return 3; | |
| } else if (whitespace1 || whitespace2) { | |
| // Two points for whitespace. | |
| return 2; | |
| } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { | |
| // One point for non-alphanumeric. | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| /** | |
| * Copyright (c) Meta Platforms, Inc. and affiliates. | |
| * | |
| * This source code is licensed under the MIT license found in the | |
| * LICENSE file in the root directory of this source tree. | |
| */ | |
| const NO_DIFF_MESSAGE = "Compared values have no visual difference."; | |
| const SIMILAR_MESSAGE = "Compared values serialize to the same structure.\n" + "Printing internal object structure without calling `toJSON` instead."; | |
| var build = {}; | |
| var hasRequiredBuild; | |
| function requireBuild () { | |
| if (hasRequiredBuild) return build; | |
| hasRequiredBuild = 1; | |
| Object.defineProperty(build, '__esModule', { | |
| value: true | |
| }); | |
| build.default = diffSequence; | |
| /** | |
| * Copyright (c) Meta Platforms, Inc. and affiliates. | |
| * | |
| * This source code is licensed under the MIT license found in the | |
| * LICENSE file in the root directory of this source tree. | |
| * | |
| */ | |
| // This diff-sequences package implements the linear space variation in | |
| // An O(ND) Difference Algorithm and Its Variations by Eugene W. Myers | |
| // Relationship in notation between Myers paper and this package: | |
| // A is a | |
| // N is aLength, aEnd - aStart, and so on | |
| // x is aIndex, aFirst, aLast, and so on | |
| // B is b | |
| // M is bLength, bEnd - bStart, and so on | |
| // y is bIndex, bFirst, bLast, and so on | |
| // Δ = N - M is negative of baDeltaLength = bLength - aLength | |
| // D is d | |
| // k is kF | |
| // k + Δ is kF = kR - baDeltaLength | |
| // V is aIndexesF or aIndexesR (see comment below about Indexes type) | |
| // index intervals [1, N] and [1, M] are [0, aLength) and [0, bLength) | |
| // starting point in forward direction (0, 0) is (-1, -1) | |
| // starting point in reverse direction (N + 1, M + 1) is (aLength, bLength) | |
| // The “edit graph” for sequences a and b corresponds to items: | |
| // in a on the horizontal axis | |
| // in b on the vertical axis | |
| // | |
| // Given a-coordinate of a point in a diagonal, you can compute b-coordinate. | |
| // | |
| // Forward diagonals kF: | |
| // zero diagonal intersects top left corner | |
| // positive diagonals intersect top edge | |
| // negative diagonals insersect left edge | |
| // | |
| // Reverse diagonals kR: | |
| // zero diagonal intersects bottom right corner | |
| // positive diagonals intersect right edge | |
| // negative diagonals intersect bottom edge | |
| // The graph contains a directed acyclic graph of edges: | |
| // horizontal: delete an item from a | |
| // vertical: insert an item from b | |
| // diagonal: common item in a and b | |
| // | |
| // The algorithm solves dual problems in the graph analogy: | |
| // Find longest common subsequence: path with maximum number of diagonal edges | |
| // Find shortest edit script: path with minimum number of non-diagonal edges | |
| // Input callback function compares items at indexes in the sequences. | |
| // Output callback function receives the number of adjacent items | |
| // and starting indexes of each common subsequence. | |
| // Either original functions or wrapped to swap indexes if graph is transposed. | |
| // Indexes in sequence a of last point of forward or reverse paths in graph. | |
| // Myers algorithm indexes by diagonal k which for negative is bad deopt in V8. | |
| // This package indexes by iF and iR which are greater than or equal to zero. | |
| // and also updates the index arrays in place to cut memory in half. | |
| // kF = 2 * iF - d | |
| // kR = d - 2 * iR | |
| // Division of index intervals in sequences a and b at the middle change. | |
| // Invariant: intervals do not have common items at the start or end. | |
| const pkg = 'diff-sequences'; // for error messages | |
| const NOT_YET_SET = 0; // small int instead of undefined to avoid deopt in V8 | |
| // Return the number of common items that follow in forward direction. | |
| // The length of what Myers paper calls a “snake” in a forward path. | |
| const countCommonItemsF = (aIndex, aEnd, bIndex, bEnd, isCommon) => { | |
| let nCommon = 0; | |
| while (aIndex < aEnd && bIndex < bEnd && isCommon(aIndex, bIndex)) { | |
| aIndex += 1; | |
| bIndex += 1; | |
| nCommon += 1; | |
| } | |
| return nCommon; | |
| }; | |
| // Return the number of common items that precede in reverse direction. | |
| // The length of what Myers paper calls a “snake” in a reverse path. | |
| const countCommonItemsR = (aStart, aIndex, bStart, bIndex, isCommon) => { | |
| let nCommon = 0; | |
| while (aStart <= aIndex && bStart <= bIndex && isCommon(aIndex, bIndex)) { | |
| aIndex -= 1; | |
| bIndex -= 1; | |
| nCommon += 1; | |
| } | |
| return nCommon; | |
| }; | |
| // A simple function to extend forward paths from (d - 1) to d changes | |
| // when forward and reverse paths cannot yet overlap. | |
| const extendPathsF = ( | |
| d, | |
| aEnd, | |
| bEnd, | |
| bF, | |
| isCommon, | |
| aIndexesF, | |
| iMaxF // return the value because optimization might decrease it | |
| ) => { | |
| // Unroll the first iteration. | |
| let iF = 0; | |
| let kF = -d; // kF = 2 * iF - d | |
| let aFirst = aIndexesF[iF]; // in first iteration always insert | |
| let aIndexPrev1 = aFirst; // prev value of [iF - 1] in next iteration | |
| aIndexesF[iF] += countCommonItemsF( | |
| aFirst + 1, | |
| aEnd, | |
| bF + aFirst - kF + 1, | |
| bEnd, | |
| isCommon | |
| ); | |
| // Optimization: skip diagonals in which paths cannot ever overlap. | |
| const nF = d < iMaxF ? d : iMaxF; | |
| // The diagonals kF are odd when d is odd and even when d is even. | |
| for (iF += 1, kF += 2; iF <= nF; iF += 1, kF += 2) { | |
| // To get first point of path segment, move one change in forward direction | |
| // from last point of previous path segment in an adjacent diagonal. | |
| // In last possible iteration when iF === d and kF === d always delete. | |
| if (iF !== d && aIndexPrev1 < aIndexesF[iF]) { | |
| aFirst = aIndexesF[iF]; // vertical to insert from b | |
| } else { | |
| aFirst = aIndexPrev1 + 1; // horizontal to delete from a | |
| if (aEnd <= aFirst) { | |
| // Optimization: delete moved past right of graph. | |
| return iF - 1; | |
| } | |
| } | |
| // To get last point of path segment, move along diagonal of common items. | |
| aIndexPrev1 = aIndexesF[iF]; | |
| aIndexesF[iF] = | |
| aFirst + | |
| countCommonItemsF(aFirst + 1, aEnd, bF + aFirst - kF + 1, bEnd, isCommon); | |
| } | |
| return iMaxF; | |
| }; | |
| // A simple function to extend reverse paths from (d - 1) to d changes | |
| // when reverse and forward paths cannot yet overlap. | |
| const extendPathsR = ( | |
| d, | |
| aStart, | |
| bStart, | |
| bR, | |
| isCommon, | |
| aIndexesR, | |
| iMaxR // return the value because optimization might decrease it | |
| ) => { | |
| // Unroll the first iteration. | |
| let iR = 0; | |
| let kR = d; // kR = d - 2 * iR | |
| let aFirst = aIndexesR[iR]; // in first iteration always insert | |
| let aIndexPrev1 = aFirst; // prev value of [iR - 1] in next iteration | |
| aIndexesR[iR] -= countCommonItemsR( | |
| aStart, | |
| aFirst - 1, | |
| bStart, | |
| bR + aFirst - kR - 1, | |
| isCommon | |
| ); | |
| // Optimization: skip diagonals in which paths cannot ever overlap. | |
| const nR = d < iMaxR ? d : iMaxR; | |
| // The diagonals kR are odd when d is odd and even when d is even. | |
| for (iR += 1, kR -= 2; iR <= nR; iR += 1, kR -= 2) { | |
| // To get first point of path segment, move one change in reverse direction | |
| // from last point of previous path segment in an adjacent diagonal. | |
| // In last possible iteration when iR === d and kR === -d always delete. | |
| if (iR !== d && aIndexesR[iR] < aIndexPrev1) { | |
| aFirst = aIndexesR[iR]; // vertical to insert from b | |
| } else { | |
| aFirst = aIndexPrev1 - 1; // horizontal to delete from a | |
| if (aFirst < aStart) { | |
| // Optimization: delete moved past left of graph. | |
| return iR - 1; | |
| } | |
| } | |
| // To get last point of path segment, move along diagonal of common items. | |
| aIndexPrev1 = aIndexesR[iR]; | |
| aIndexesR[iR] = | |
| aFirst - | |
| countCommonItemsR( | |
| aStart, | |
| aFirst - 1, | |
| bStart, | |
| bR + aFirst - kR - 1, | |
| isCommon | |
| ); | |
| } | |
| return iMaxR; | |
| }; | |
| // A complete function to extend forward paths from (d - 1) to d changes. | |
| // Return true if a path overlaps reverse path of (d - 1) changes in its diagonal. | |
| const extendOverlappablePathsF = ( | |
| d, | |
| aStart, | |
| aEnd, | |
| bStart, | |
| bEnd, | |
| isCommon, | |
| aIndexesF, | |
| iMaxF, | |
| aIndexesR, | |
| iMaxR, | |
| division // update prop values if return true | |
| ) => { | |
| const bF = bStart - aStart; // bIndex = bF + aIndex - kF | |
| const aLength = aEnd - aStart; | |
| const bLength = bEnd - bStart; | |
| const baDeltaLength = bLength - aLength; // kF = kR - baDeltaLength | |
| // Range of diagonals in which forward and reverse paths might overlap. | |
| const kMinOverlapF = -baDeltaLength - (d - 1); // -(d - 1) <= kR | |
| const kMaxOverlapF = -baDeltaLength + (d - 1); // kR <= (d - 1) | |
| let aIndexPrev1 = NOT_YET_SET; // prev value of [iF - 1] in next iteration | |
| // Optimization: skip diagonals in which paths cannot ever overlap. | |
| const nF = d < iMaxF ? d : iMaxF; | |
| // The diagonals kF = 2 * iF - d are odd when d is odd and even when d is even. | |
| for (let iF = 0, kF = -d; iF <= nF; iF += 1, kF += 2) { | |
| // To get first point of path segment, move one change in forward direction | |
| // from last point of previous path segment in an adjacent diagonal. | |
| // In first iteration when iF === 0 and kF === -d always insert. | |
| // In last possible iteration when iF === d and kF === d always delete. | |
| const insert = iF === 0 || (iF !== d && aIndexPrev1 < aIndexesF[iF]); | |
| const aLastPrev = insert ? aIndexesF[iF] : aIndexPrev1; | |
| const aFirst = insert | |
| ? aLastPrev // vertical to insert from b | |
| : aLastPrev + 1; // horizontal to delete from a | |
| // To get last point of path segment, move along diagonal of common items. | |
| const bFirst = bF + aFirst - kF; | |
| const nCommonF = countCommonItemsF( | |
| aFirst + 1, | |
| aEnd, | |
| bFirst + 1, | |
| bEnd, | |
| isCommon | |
| ); | |
| const aLast = aFirst + nCommonF; | |
| aIndexPrev1 = aIndexesF[iF]; | |
| aIndexesF[iF] = aLast; | |
| if (kMinOverlapF <= kF && kF <= kMaxOverlapF) { | |
| // Solve for iR of reverse path with (d - 1) changes in diagonal kF: | |
| // kR = kF + baDeltaLength | |
| // kR = (d - 1) - 2 * iR | |
| const iR = (d - 1 - (kF + baDeltaLength)) / 2; | |
| // If this forward path overlaps the reverse path in this diagonal, | |
| // then this is the middle change of the index intervals. | |
| if (iR <= iMaxR && aIndexesR[iR] - 1 <= aLast) { | |
| // Unlike the Myers algorithm which finds only the middle “snake” | |
| // this package can find two common subsequences per division. | |
| // Last point of previous path segment is on an adjacent diagonal. | |
| const bLastPrev = bF + aLastPrev - (insert ? kF + 1 : kF - 1); | |
| // Because of invariant that intervals preceding the middle change | |
| // cannot have common items at the end, | |
| // move in reverse direction along a diagonal of common items. | |
| const nCommonR = countCommonItemsR( | |
| aStart, | |
| aLastPrev, | |
| bStart, | |
| bLastPrev, | |
| isCommon | |
| ); | |
| const aIndexPrevFirst = aLastPrev - nCommonR; | |
| const bIndexPrevFirst = bLastPrev - nCommonR; | |
| const aEndPreceding = aIndexPrevFirst + 1; | |
| const bEndPreceding = bIndexPrevFirst + 1; | |
| division.nChangePreceding = d - 1; | |
| if (d - 1 === aEndPreceding + bEndPreceding - aStart - bStart) { | |
| // Optimization: number of preceding changes in forward direction | |
| // is equal to number of items in preceding interval, | |
| // therefore it cannot contain any common items. | |
| division.aEndPreceding = aStart; | |
| division.bEndPreceding = bStart; | |
| } else { | |
| division.aEndPreceding = aEndPreceding; | |
| division.bEndPreceding = bEndPreceding; | |
| } | |
| division.nCommonPreceding = nCommonR; | |
| if (nCommonR !== 0) { | |
| division.aCommonPreceding = aEndPreceding; | |
| division.bCommonPreceding = bEndPreceding; | |
| } | |
| division.nCommonFollowing = nCommonF; | |
| if (nCommonF !== 0) { | |
| division.aCommonFollowing = aFirst + 1; | |
| division.bCommonFollowing = bFirst + 1; | |
| } | |
| const aStartFollowing = aLast + 1; | |
| const bStartFollowing = bFirst + nCommonF + 1; | |
| division.nChangeFollowing = d - 1; | |
| if (d - 1 === aEnd + bEnd - aStartFollowing - bStartFollowing) { | |
| // Optimization: number of changes in reverse direction | |
| // is equal to number of items in following interval, | |
| // therefore it cannot contain any common items. | |
| division.aStartFollowing = aEnd; | |
| division.bStartFollowing = bEnd; | |
| } else { | |
| division.aStartFollowing = aStartFollowing; | |
| division.bStartFollowing = bStartFollowing; | |
| } | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| }; | |
| // A complete function to extend reverse paths from (d - 1) to d changes. | |
| // Return true if a path overlaps forward path of d changes in its diagonal. | |
| const extendOverlappablePathsR = ( | |
| d, | |
| aStart, | |
| aEnd, | |
| bStart, | |
| bEnd, | |
| isCommon, | |
| aIndexesF, | |
| iMaxF, | |
| aIndexesR, | |
| iMaxR, | |
| division // update prop values if return true | |
| ) => { | |
| const bR = bEnd - aEnd; // bIndex = bR + aIndex - kR | |
| const aLength = aEnd - aStart; | |
| const bLength = bEnd - bStart; | |
| const baDeltaLength = bLength - aLength; // kR = kF + baDeltaLength | |
| // Range of diagonals in which forward and reverse paths might overlap. | |
| const kMinOverlapR = baDeltaLength - d; // -d <= kF | |
| const kMaxOverlapR = baDeltaLength + d; // kF <= d | |
| let aIndexPrev1 = NOT_YET_SET; // prev value of [iR - 1] in next iteration | |
| // Optimization: skip diagonals in which paths cannot ever overlap. | |
| const nR = d < iMaxR ? d : iMaxR; | |
| // The diagonals kR = d - 2 * iR are odd when d is odd and even when d is even. | |
| for (let iR = 0, kR = d; iR <= nR; iR += 1, kR -= 2) { | |
| // To get first point of path segment, move one change in reverse direction | |
| // from last point of previous path segment in an adjacent diagonal. | |
| // In first iteration when iR === 0 and kR === d always insert. | |
| // In last possible iteration when iR === d and kR === -d always delete. | |
| const insert = iR === 0 || (iR !== d && aIndexesR[iR] < aIndexPrev1); | |
| const aLastPrev = insert ? aIndexesR[iR] : aIndexPrev1; | |
| const aFirst = insert | |
| ? aLastPrev // vertical to insert from b | |
| : aLastPrev - 1; // horizontal to delete from a | |
| // To get last point of path segment, move along diagonal of common items. | |
| const bFirst = bR + aFirst - kR; | |
| const nCommonR = countCommonItemsR( | |
| aStart, | |
| aFirst - 1, | |
| bStart, | |
| bFirst - 1, | |
| isCommon | |
| ); | |
| const aLast = aFirst - nCommonR; | |
| aIndexPrev1 = aIndexesR[iR]; | |
| aIndexesR[iR] = aLast; | |
| if (kMinOverlapR <= kR && kR <= kMaxOverlapR) { | |
| // Solve for iF of forward path with d changes in diagonal kR: | |
| // kF = kR - baDeltaLength | |
| // kF = 2 * iF - d | |
| const iF = (d + (kR - baDeltaLength)) / 2; | |
| // If this reverse path overlaps the forward path in this diagonal, | |
| // then this is a middle change of the index intervals. | |
| if (iF <= iMaxF && aLast - 1 <= aIndexesF[iF]) { | |
| const bLast = bFirst - nCommonR; | |
| division.nChangePreceding = d; | |
| if (d === aLast + bLast - aStart - bStart) { | |
| // Optimization: number of changes in reverse direction | |
| // is equal to number of items in preceding interval, | |
| // therefore it cannot contain any common items. | |
| division.aEndPreceding = aStart; | |
| division.bEndPreceding = bStart; | |
| } else { | |
| division.aEndPreceding = aLast; | |
| division.bEndPreceding = bLast; | |
| } | |
| division.nCommonPreceding = nCommonR; | |
| if (nCommonR !== 0) { | |
| // The last point of reverse path segment is start of common subsequence. | |
| division.aCommonPreceding = aLast; | |
| division.bCommonPreceding = bLast; | |
| } | |
| division.nChangeFollowing = d - 1; | |
| if (d === 1) { | |
| // There is no previous path segment. | |
| division.nCommonFollowing = 0; | |
| division.aStartFollowing = aEnd; | |
| division.bStartFollowing = bEnd; | |
| } else { | |
| // Unlike the Myers algorithm which finds only the middle “snake” | |
| // this package can find two common subsequences per division. | |
| // Last point of previous path segment is on an adjacent diagonal. | |
| const bLastPrev = bR + aLastPrev - (insert ? kR - 1 : kR + 1); | |
| // Because of invariant that intervals following the middle change | |
| // cannot have common items at the start, | |
| // move in forward direction along a diagonal of common items. | |
| const nCommonF = countCommonItemsF( | |
| aLastPrev, | |
| aEnd, | |
| bLastPrev, | |
| bEnd, | |
| isCommon | |
| ); | |
| division.nCommonFollowing = nCommonF; | |
| if (nCommonF !== 0) { | |
| // The last point of reverse path segment is start of common subsequence. | |
| division.aCommonFollowing = aLastPrev; | |
| division.bCommonFollowing = bLastPrev; | |
| } | |
| const aStartFollowing = aLastPrev + nCommonF; // aFirstPrev | |
| const bStartFollowing = bLastPrev + nCommonF; // bFirstPrev | |
| if (d - 1 === aEnd + bEnd - aStartFollowing - bStartFollowing) { | |
| // Optimization: number of changes in forward direction | |
| // is equal to number of items in following interval, | |
| // therefore it cannot contain any common items. | |
| division.aStartFollowing = aEnd; | |
| division.bStartFollowing = bEnd; | |
| } else { | |
| division.aStartFollowing = aStartFollowing; | |
| division.bStartFollowing = bStartFollowing; | |
| } | |
| } | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| }; | |
| // Given index intervals and input function to compare items at indexes, | |
| // divide at the middle change. | |
| // | |
| // DO NOT CALL if start === end, because interval cannot contain common items | |
| // and because this function will throw the “no overlap” error. | |
| const divide = ( | |
| nChange, | |
| aStart, | |
| aEnd, | |
| bStart, | |
| bEnd, | |
| isCommon, | |
| aIndexesF, | |
| aIndexesR, | |
| division // output | |
| ) => { | |
| const bF = bStart - aStart; // bIndex = bF + aIndex - kF | |
| const bR = bEnd - aEnd; // bIndex = bR + aIndex - kR | |
| const aLength = aEnd - aStart; | |
| const bLength = bEnd - bStart; | |
| // Because graph has square or portrait orientation, | |
| // length difference is minimum number of items to insert from b. | |
| // Corresponding forward and reverse diagonals in graph | |
| // depend on length difference of the sequences: | |
| // kF = kR - baDeltaLength | |
| // kR = kF + baDeltaLength | |
| const baDeltaLength = bLength - aLength; | |
| // Optimization: max diagonal in graph intersects corner of shorter side. | |
| let iMaxF = aLength; | |
| let iMaxR = aLength; | |
| // Initialize no changes yet in forward or reverse direction: | |
| aIndexesF[0] = aStart - 1; // at open start of interval, outside closed start | |
| aIndexesR[0] = aEnd; // at open end of interval | |
| if (baDeltaLength % 2 === 0) { | |
| // The number of changes in paths is 2 * d if length difference is even. | |
| const dMin = (nChange || baDeltaLength) / 2; | |
| const dMax = (aLength + bLength) / 2; | |
| for (let d = 1; d <= dMax; d += 1) { | |
| iMaxF = extendPathsF(d, aEnd, bEnd, bF, isCommon, aIndexesF, iMaxF); | |
| if (d < dMin) { | |
| iMaxR = extendPathsR(d, aStart, bStart, bR, isCommon, aIndexesR, iMaxR); | |
| } else if ( | |
| // If a reverse path overlaps a forward path in the same diagonal, | |
| // return a division of the index intervals at the middle change. | |
| extendOverlappablePathsR( | |
| d, | |
| aStart, | |
| aEnd, | |
| bStart, | |
| bEnd, | |
| isCommon, | |
| aIndexesF, | |
| iMaxF, | |
| aIndexesR, | |
| iMaxR, | |
| division | |
| ) | |
| ) { | |
| return; | |
| } | |
| } | |
| } else { | |
| // The number of changes in paths is 2 * d - 1 if length difference is odd. | |
| const dMin = ((nChange || baDeltaLength) + 1) / 2; | |
| const dMax = (aLength + bLength + 1) / 2; | |
| // Unroll first half iteration so loop extends the relevant pairs of paths. | |
| // Because of invariant that intervals have no common items at start or end, | |
| // and limitation not to call divide with empty intervals, | |
| // therefore it cannot be called if a forward path with one change | |
| // would overlap a reverse path with no changes, even if dMin === 1. | |
| let d = 1; | |
| iMaxF = extendPathsF(d, aEnd, bEnd, bF, isCommon, aIndexesF, iMaxF); | |
| for (d += 1; d <= dMax; d += 1) { | |
| iMaxR = extendPathsR( | |
| d - 1, | |
| aStart, | |
| bStart, | |
| bR, | |
| isCommon, | |
| aIndexesR, | |
| iMaxR | |
| ); | |
| if (d < dMin) { | |
| iMaxF = extendPathsF(d, aEnd, bEnd, bF, isCommon, aIndexesF, iMaxF); | |
| } else if ( | |
| // If a forward path overlaps a reverse path in the same diagonal, | |
| // return a division of the index intervals at the middle change. | |
| extendOverlappablePathsF( | |
| d, | |
| aStart, | |
| aEnd, | |
| bStart, | |
| bEnd, | |
| isCommon, | |
| aIndexesF, | |
| iMaxF, | |
| aIndexesR, | |
| iMaxR, | |
| division | |
| ) | |
| ) { | |
| return; | |
| } | |
| } | |
| } | |
| /* istanbul ignore next */ | |
| throw new Error( | |
| `${pkg}: no overlap aStart=${aStart} aEnd=${aEnd} bStart=${bStart} bEnd=${bEnd}` | |
| ); | |
| }; | |
| // Given index intervals and input function to compare items at indexes, | |
| // return by output function the number of adjacent items and starting indexes | |
| // of each common subsequence. Divide and conquer with only linear space. | |
| // | |
| // The index intervals are half open [start, end) like array slice method. | |
| // DO NOT CALL if start === end, because interval cannot contain common items | |
| // and because divide function will throw the “no overlap” error. | |
| const findSubsequences = ( | |
| nChange, | |
| aStart, | |
| aEnd, | |
| bStart, | |
| bEnd, | |
| transposed, | |
| callbacks, | |
| aIndexesF, | |
| aIndexesR, | |
| division // temporary memory, not input nor output | |
| ) => { | |
| if (bEnd - bStart < aEnd - aStart) { | |
| // Transpose graph so it has portrait instead of landscape orientation. | |
| // Always compare shorter to longer sequence for consistency and optimization. | |
| transposed = !transposed; | |
| if (transposed && callbacks.length === 1) { | |
| // Lazily wrap callback functions to swap args if graph is transposed. | |
| const {foundSubsequence, isCommon} = callbacks[0]; | |
| callbacks[1] = { | |
| foundSubsequence: (nCommon, bCommon, aCommon) => { | |
| foundSubsequence(nCommon, aCommon, bCommon); | |
| }, | |
| isCommon: (bIndex, aIndex) => isCommon(aIndex, bIndex) | |
| }; | |
| } | |
| const tStart = aStart; | |
| const tEnd = aEnd; | |
| aStart = bStart; | |
| aEnd = bEnd; | |
| bStart = tStart; | |
| bEnd = tEnd; | |
| } | |
| const {foundSubsequence, isCommon} = callbacks[transposed ? 1 : 0]; | |
| // Divide the index intervals at the middle change. | |
| divide( | |
| nChange, | |
| aStart, | |
| aEnd, | |
| bStart, | |
| bEnd, | |
| isCommon, | |
| aIndexesF, | |
| aIndexesR, | |
| division | |
| ); | |
| const { | |
| nChangePreceding, | |
| aEndPreceding, | |
| bEndPreceding, | |
| nCommonPreceding, | |
| aCommonPreceding, | |
| bCommonPreceding, | |
| nCommonFollowing, | |
| aCommonFollowing, | |
| bCommonFollowing, | |
| nChangeFollowing, | |
| aStartFollowing, | |
| bStartFollowing | |
| } = division; | |
| // Unless either index interval is empty, they might contain common items. | |
| if (aStart < aEndPreceding && bStart < bEndPreceding) { | |
| // Recursely find and return common subsequences preceding the division. | |
| findSubsequences( | |
| nChangePreceding, | |
| aStart, | |
| aEndPreceding, | |
| bStart, | |
| bEndPreceding, | |
| transposed, | |
| callbacks, | |
| aIndexesF, | |
| aIndexesR, | |
| division | |
| ); | |
| } | |
| // Return common subsequences that are adjacent to the middle change. | |
| if (nCommonPreceding !== 0) { | |
| foundSubsequence(nCommonPreceding, aCommonPreceding, bCommonPreceding); | |
| } | |
| if (nCommonFollowing !== 0) { | |
| foundSubsequence(nCommonFollowing, aCommonFollowing, bCommonFollowing); | |
| } | |
| // Unless either index interval is empty, they might contain common items. | |
| if (aStartFollowing < aEnd && bStartFollowing < bEnd) { | |
| // Recursely find and return common subsequences following the division. | |
| findSubsequences( | |
| nChangeFollowing, | |
| aStartFollowing, | |
| aEnd, | |
| bStartFollowing, | |
| bEnd, | |
| transposed, | |
| callbacks, | |
| aIndexesF, | |
| aIndexesR, | |
| division | |
| ); | |
| } | |
| }; | |
| const validateLength = (name, arg) => { | |
| if (typeof arg !== 'number') { | |
| throw new TypeError(`${pkg}: ${name} typeof ${typeof arg} is not a number`); | |
| } | |
| if (!Number.isSafeInteger(arg)) { | |
| throw new RangeError(`${pkg}: ${name} value ${arg} is not a safe integer`); | |
| } | |
| if (arg < 0) { | |
| throw new RangeError(`${pkg}: ${name} value ${arg} is a negative integer`); | |
| } | |
| }; | |
| const validateCallback = (name, arg) => { | |
| const type = typeof arg; | |
| if (type !== 'function') { | |
| throw new TypeError(`${pkg}: ${name} typeof ${type} is not a function`); | |
| } | |
| }; | |
| // Compare items in two sequences to find a longest common subsequence. | |
| // Given lengths of sequences and input function to compare items at indexes, | |
| // return by output function the number of adjacent items and starting indexes | |
| // of each common subsequence. | |
| function diffSequence(aLength, bLength, isCommon, foundSubsequence) { | |
| validateLength('aLength', aLength); | |
| validateLength('bLength', bLength); | |
| validateCallback('isCommon', isCommon); | |
| validateCallback('foundSubsequence', foundSubsequence); | |
| // Count common items from the start in the forward direction. | |
| const nCommonF = countCommonItemsF(0, aLength, 0, bLength, isCommon); | |
| if (nCommonF !== 0) { | |
| foundSubsequence(nCommonF, 0, 0); | |
| } | |
| // Unless both sequences consist of common items only, | |
| // find common items in the half-trimmed index intervals. | |
| if (aLength !== nCommonF || bLength !== nCommonF) { | |
| // Invariant: intervals do not have common items at the start. | |
| // The start of an index interval is closed like array slice method. | |
| const aStart = nCommonF; | |
| const bStart = nCommonF; | |
| // Count common items from the end in the reverse direction. | |
| const nCommonR = countCommonItemsR( | |
| aStart, | |
| aLength - 1, | |
| bStart, | |
| bLength - 1, | |
| isCommon | |
| ); | |
| // Invariant: intervals do not have common items at the end. | |
| // The end of an index interval is open like array slice method. | |
| const aEnd = aLength - nCommonR; | |
| const bEnd = bLength - nCommonR; | |
| // Unless one sequence consists of common items only, | |
| // therefore the other trimmed index interval consists of changes only, | |
| // find common items in the trimmed index intervals. | |
| const nCommonFR = nCommonF + nCommonR; | |
| if (aLength !== nCommonFR && bLength !== nCommonFR) { | |
| const nChange = 0; // number of change items is not yet known | |
| const transposed = false; // call the original unwrapped functions | |
| const callbacks = [ | |
| { | |
| foundSubsequence, | |
| isCommon | |
| } | |
| ]; | |
| // Indexes in sequence a of last points in furthest reaching paths | |
| // from outside the start at top left in the forward direction: | |
| const aIndexesF = [NOT_YET_SET]; | |
| // from the end at bottom right in the reverse direction: | |
| const aIndexesR = [NOT_YET_SET]; | |
| // Initialize one object as output of all calls to divide function. | |
| const division = { | |
| aCommonFollowing: NOT_YET_SET, | |
| aCommonPreceding: NOT_YET_SET, | |
| aEndPreceding: NOT_YET_SET, | |
| aStartFollowing: NOT_YET_SET, | |
| bCommonFollowing: NOT_YET_SET, | |
| bCommonPreceding: NOT_YET_SET, | |
| bEndPreceding: NOT_YET_SET, | |
| bStartFollowing: NOT_YET_SET, | |
| nChangeFollowing: NOT_YET_SET, | |
| nChangePreceding: NOT_YET_SET, | |
| nCommonFollowing: NOT_YET_SET, | |
| nCommonPreceding: NOT_YET_SET | |
| }; | |
| // Find and return common subsequences in the trimmed index intervals. | |
| findSubsequences( | |
| nChange, | |
| aStart, | |
| aEnd, | |
| bStart, | |
| bEnd, | |
| transposed, | |
| callbacks, | |
| aIndexesF, | |
| aIndexesR, | |
| division | |
| ); | |
| } | |
| if (nCommonR !== 0) { | |
| foundSubsequence(nCommonR, aEnd, bEnd); | |
| } | |
| } | |
| } | |
| return build; | |
| } | |
| var buildExports = requireBuild(); | |
| var diffSequences = /*@__PURE__*/getDefaultExportFromCjs(buildExports); | |
| function formatTrailingSpaces(line, trailingSpaceFormatter) { | |
| return line.replace(/\s+$/, (match) => trailingSpaceFormatter(match)); | |
| } | |
| function printDiffLine(line, isFirstOrLast, color, indicator, trailingSpaceFormatter, emptyFirstOrLastLinePlaceholder) { | |
| return line.length !== 0 ? color(`${indicator} ${formatTrailingSpaces(line, trailingSpaceFormatter)}`) : indicator !== " " ? color(indicator) : isFirstOrLast && emptyFirstOrLastLinePlaceholder.length !== 0 ? color(`${indicator} ${emptyFirstOrLastLinePlaceholder}`) : ""; | |
| } | |
| function printDeleteLine(line, isFirstOrLast, { aColor, aIndicator, changeLineTrailingSpaceColor, emptyFirstOrLastLinePlaceholder }) { | |
| return printDiffLine(line, isFirstOrLast, aColor, aIndicator, changeLineTrailingSpaceColor, emptyFirstOrLastLinePlaceholder); | |
| } | |
| function printInsertLine(line, isFirstOrLast, { bColor, bIndicator, changeLineTrailingSpaceColor, emptyFirstOrLastLinePlaceholder }) { | |
| return printDiffLine(line, isFirstOrLast, bColor, bIndicator, changeLineTrailingSpaceColor, emptyFirstOrLastLinePlaceholder); | |
| } | |
| function printCommonLine(line, isFirstOrLast, { commonColor, commonIndicator, commonLineTrailingSpaceColor, emptyFirstOrLastLinePlaceholder }) { | |
| return printDiffLine(line, isFirstOrLast, commonColor, commonIndicator, commonLineTrailingSpaceColor, emptyFirstOrLastLinePlaceholder); | |
| } | |
| // In GNU diff format, indexes are one-based instead of zero-based. | |
| function createPatchMark(aStart, aEnd, bStart, bEnd, { patchColor }) { | |
| return patchColor(`@@ -${aStart + 1},${aEnd - aStart} +${bStart + 1},${bEnd - bStart} @@`); | |
| } | |
| // jest --no-expand | |
| // | |
| // Given array of aligned strings with inverse highlight formatting, | |
| // return joined lines with diff formatting (and patch marks, if needed). | |
| function joinAlignedDiffsNoExpand(diffs, options) { | |
| const iLength = diffs.length; | |
| const nContextLines = options.contextLines; | |
| const nContextLines2 = nContextLines + nContextLines; | |
| // First pass: count output lines and see if it has patches. | |
| let jLength = iLength; | |
| let hasExcessAtStartOrEnd = false; | |
| let nExcessesBetweenChanges = 0; | |
| let i = 0; | |
| while (i !== iLength) { | |
| const iStart = i; | |
| while (i !== iLength && diffs[i][0] === DIFF_EQUAL) { | |
| i += 1; | |
| } | |
| if (iStart !== i) { | |
| if (iStart === 0) { | |
| // at start | |
| if (i > nContextLines) { | |
| jLength -= i - nContextLines; | |
| hasExcessAtStartOrEnd = true; | |
| } | |
| } else if (i === iLength) { | |
| // at end | |
| const n = i - iStart; | |
| if (n > nContextLines) { | |
| jLength -= n - nContextLines; | |
| hasExcessAtStartOrEnd = true; | |
| } | |
| } else { | |
| // between changes | |
| const n = i - iStart; | |
| if (n > nContextLines2) { | |
| jLength -= n - nContextLines2; | |
| nExcessesBetweenChanges += 1; | |
| } | |
| } | |
| } | |
| while (i !== iLength && diffs[i][0] !== DIFF_EQUAL) { | |
| i += 1; | |
| } | |
| } | |
| const hasPatch = nExcessesBetweenChanges !== 0 || hasExcessAtStartOrEnd; | |
| if (nExcessesBetweenChanges !== 0) { | |
| jLength += nExcessesBetweenChanges + 1; | |
| } else if (hasExcessAtStartOrEnd) { | |
| jLength += 1; | |
| } | |
| const jLast = jLength - 1; | |
| const lines = []; | |
| let jPatchMark = 0; | |
| if (hasPatch) { | |
| lines.push(""); | |
| } | |
| // Indexes of expected or received lines in current patch: | |
| let aStart = 0; | |
| let bStart = 0; | |
| let aEnd = 0; | |
| let bEnd = 0; | |
| const pushCommonLine = (line) => { | |
| const j = lines.length; | |
| lines.push(printCommonLine(line, j === 0 || j === jLast, options)); | |
| aEnd += 1; | |
| bEnd += 1; | |
| }; | |
| const pushDeleteLine = (line) => { | |
| const j = lines.length; | |
| lines.push(printDeleteLine(line, j === 0 || j === jLast, options)); | |
| aEnd += 1; | |
| }; | |
| const pushInsertLine = (line) => { | |
| const j = lines.length; | |
| lines.push(printInsertLine(line, j === 0 || j === jLast, options)); | |
| bEnd += 1; | |
| }; | |
| // Second pass: push lines with diff formatting (and patch marks, if needed). | |
| i = 0; | |
| while (i !== iLength) { | |
| let iStart = i; | |
| while (i !== iLength && diffs[i][0] === DIFF_EQUAL) { | |
| i += 1; | |
| } | |
| if (iStart !== i) { | |
| if (iStart === 0) { | |
| // at beginning | |
| if (i > nContextLines) { | |
| iStart = i - nContextLines; | |
| aStart = iStart; | |
| bStart = iStart; | |
| aEnd = aStart; | |
| bEnd = bStart; | |
| } | |
| for (let iCommon = iStart; iCommon !== i; iCommon += 1) { | |
| pushCommonLine(diffs[iCommon][1]); | |
| } | |
| } else if (i === iLength) { | |
| // at end | |
| const iEnd = i - iStart > nContextLines ? iStart + nContextLines : i; | |
| for (let iCommon = iStart; iCommon !== iEnd; iCommon += 1) { | |
| pushCommonLine(diffs[iCommon][1]); | |
| } | |
| } else { | |
| // between changes | |
| const nCommon = i - iStart; | |
| if (nCommon > nContextLines2) { | |
| const iEnd = iStart + nContextLines; | |
| for (let iCommon = iStart; iCommon !== iEnd; iCommon += 1) { | |
| pushCommonLine(diffs[iCommon][1]); | |
| } | |
| lines[jPatchMark] = createPatchMark(aStart, aEnd, bStart, bEnd, options); | |
| jPatchMark = lines.length; | |
| lines.push(""); | |
| const nOmit = nCommon - nContextLines2; | |
| aStart = aEnd + nOmit; | |
| bStart = bEnd + nOmit; | |
| aEnd = aStart; | |
| bEnd = bStart; | |
| for (let iCommon = i - nContextLines; iCommon !== i; iCommon += 1) { | |
| pushCommonLine(diffs[iCommon][1]); | |
| } | |
| } else { | |
| for (let iCommon = iStart; iCommon !== i; iCommon += 1) { | |
| pushCommonLine(diffs[iCommon][1]); | |
| } | |
| } | |
| } | |
| } | |
| while (i !== iLength && diffs[i][0] === DIFF_DELETE) { | |
| pushDeleteLine(diffs[i][1]); | |
| i += 1; | |
| } | |
| while (i !== iLength && diffs[i][0] === DIFF_INSERT) { | |
| pushInsertLine(diffs[i][1]); | |
| i += 1; | |
| } | |
| } | |
| if (hasPatch) { | |
| lines[jPatchMark] = createPatchMark(aStart, aEnd, bStart, bEnd, options); | |
| } | |
| return lines.join("\n"); | |
| } | |
| // jest --expand | |
| // | |
| // Given array of aligned strings with inverse highlight formatting, | |
| // return joined lines with diff formatting. | |
| function joinAlignedDiffsExpand(diffs, options) { | |
| return diffs.map((diff, i, diffs) => { | |
| const line = diff[1]; | |
| const isFirstOrLast = i === 0 || i === diffs.length - 1; | |
| switch (diff[0]) { | |
| case DIFF_DELETE: return printDeleteLine(line, isFirstOrLast, options); | |
| case DIFF_INSERT: return printInsertLine(line, isFirstOrLast, options); | |
| default: return printCommonLine(line, isFirstOrLast, options); | |
| } | |
| }).join("\n"); | |
| } | |
| const noColor = (string) => string; | |
| const DIFF_CONTEXT_DEFAULT = 5; | |
| const DIFF_TRUNCATE_THRESHOLD_DEFAULT = 0; | |
| function getDefaultOptions() { | |
| return { | |
| aAnnotation: "Expected", | |
| aColor: c.green, | |
| aIndicator: "-", | |
| bAnnotation: "Received", | |
| bColor: c.red, | |
| bIndicator: "+", | |
| changeColor: c.inverse, | |
| changeLineTrailingSpaceColor: noColor, | |
| commonColor: c.dim, | |
| commonIndicator: " ", | |
| commonLineTrailingSpaceColor: noColor, | |
| compareKeys: undefined, | |
| contextLines: DIFF_CONTEXT_DEFAULT, | |
| emptyFirstOrLastLinePlaceholder: "", | |
| expand: false, | |
| includeChangeCounts: false, | |
| omitAnnotationLines: false, | |
| patchColor: c.yellow, | |
| printBasicPrototype: false, | |
| truncateThreshold: DIFF_TRUNCATE_THRESHOLD_DEFAULT, | |
| truncateAnnotation: "... Diff result is truncated", | |
| truncateAnnotationColor: noColor | |
| }; | |
| } | |
| function getCompareKeys(compareKeys) { | |
| return compareKeys && typeof compareKeys === "function" ? compareKeys : undefined; | |
| } | |
| function getContextLines(contextLines) { | |
| return typeof contextLines === "number" && Number.isSafeInteger(contextLines) && contextLines >= 0 ? contextLines : DIFF_CONTEXT_DEFAULT; | |
| } | |
| // Pure function returns options with all properties. | |
| function normalizeDiffOptions(options = {}) { | |
| return { | |
| ...getDefaultOptions(), | |
| ...options, | |
| compareKeys: getCompareKeys(options.compareKeys), | |
| contextLines: getContextLines(options.contextLines) | |
| }; | |
| } | |
| function isEmptyString(lines) { | |
| return lines.length === 1 && lines[0].length === 0; | |
| } | |
| function countChanges(diffs) { | |
| let a = 0; | |
| let b = 0; | |
| diffs.forEach((diff) => { | |
| switch (diff[0]) { | |
| case DIFF_DELETE: | |
| a += 1; | |
| break; | |
| case DIFF_INSERT: | |
| b += 1; | |
| break; | |
| } | |
| }); | |
| return { | |
| a, | |
| b | |
| }; | |
| } | |
| function printAnnotation({ aAnnotation, aColor, aIndicator, bAnnotation, bColor, bIndicator, includeChangeCounts, omitAnnotationLines }, changeCounts) { | |
| if (omitAnnotationLines) { | |
| return ""; | |
| } | |
| let aRest = ""; | |
| let bRest = ""; | |
| if (includeChangeCounts) { | |
| const aCount = String(changeCounts.a); | |
| const bCount = String(changeCounts.b); | |
| // Padding right aligns the ends of the annotations. | |
| const baAnnotationLengthDiff = bAnnotation.length - aAnnotation.length; | |
| const aAnnotationPadding = " ".repeat(Math.max(0, baAnnotationLengthDiff)); | |
| const bAnnotationPadding = " ".repeat(Math.max(0, -baAnnotationLengthDiff)); | |
| // Padding left aligns the ends of the counts. | |
| const baCountLengthDiff = bCount.length - aCount.length; | |
| const aCountPadding = " ".repeat(Math.max(0, baCountLengthDiff)); | |
| const bCountPadding = " ".repeat(Math.max(0, -baCountLengthDiff)); | |
| aRest = `${aAnnotationPadding} ${aIndicator} ${aCountPadding}${aCount}`; | |
| bRest = `${bAnnotationPadding} ${bIndicator} ${bCountPadding}${bCount}`; | |
| } | |
| const a = `${aIndicator} ${aAnnotation}${aRest}`; | |
| const b = `${bIndicator} ${bAnnotation}${bRest}`; | |
| return `${aColor(a)}\n${bColor(b)}\n\n`; | |
| } | |
| function printDiffLines(diffs, truncated, options) { | |
| return printAnnotation(options, countChanges(diffs)) + (options.expand ? joinAlignedDiffsExpand(diffs, options) : joinAlignedDiffsNoExpand(diffs, options)) + (truncated ? options.truncateAnnotationColor(`\n${options.truncateAnnotation}`) : ""); | |
| } | |
| // Compare two arrays of strings line-by-line. Format as comparison lines. | |
| function diffLinesUnified(aLines, bLines, options) { | |
| const normalizedOptions = normalizeDiffOptions(options); | |
| const [diffs, truncated] = diffLinesRaw(isEmptyString(aLines) ? [] : aLines, isEmptyString(bLines) ? [] : bLines, normalizedOptions); | |
| return printDiffLines(diffs, truncated, normalizedOptions); | |
| } | |
| // Given two pairs of arrays of strings: | |
| // Compare the pair of comparison arrays line-by-line. | |
| // Format the corresponding lines in the pair of displayable arrays. | |
| function diffLinesUnified2(aLinesDisplay, bLinesDisplay, aLinesCompare, bLinesCompare, options) { | |
| if (isEmptyString(aLinesDisplay) && isEmptyString(aLinesCompare)) { | |
| aLinesDisplay = []; | |
| aLinesCompare = []; | |
| } | |
| if (isEmptyString(bLinesDisplay) && isEmptyString(bLinesCompare)) { | |
| bLinesDisplay = []; | |
| bLinesCompare = []; | |
| } | |
| if (aLinesDisplay.length !== aLinesCompare.length || bLinesDisplay.length !== bLinesCompare.length) { | |
| // Fall back to diff of display lines. | |
| return diffLinesUnified(aLinesDisplay, bLinesDisplay, options); | |
| } | |
| const [diffs, truncated] = diffLinesRaw(aLinesCompare, bLinesCompare, options); | |
| // Replace comparison lines with displayable lines. | |
| let aIndex = 0; | |
| let bIndex = 0; | |
| diffs.forEach((diff) => { | |
| switch (diff[0]) { | |
| case DIFF_DELETE: | |
| diff[1] = aLinesDisplay[aIndex]; | |
| aIndex += 1; | |
| break; | |
| case DIFF_INSERT: | |
| diff[1] = bLinesDisplay[bIndex]; | |
| bIndex += 1; | |
| break; | |
| default: | |
| diff[1] = bLinesDisplay[bIndex]; | |
| aIndex += 1; | |
| bIndex += 1; | |
| } | |
| }); | |
| return printDiffLines(diffs, truncated, normalizeDiffOptions(options)); | |
| } | |
| // Compare two arrays of strings line-by-line. | |
| function diffLinesRaw(aLines, bLines, options) { | |
| const truncate = (options === null || options === void 0 ? void 0 : options.truncateThreshold) ?? false; | |
| const truncateThreshold = Math.max(Math.floor((options === null || options === void 0 ? void 0 : options.truncateThreshold) ?? 0), 0); | |
| const aLength = truncate ? Math.min(aLines.length, truncateThreshold) : aLines.length; | |
| const bLength = truncate ? Math.min(bLines.length, truncateThreshold) : bLines.length; | |
| const truncated = aLength !== aLines.length || bLength !== bLines.length; | |
| const isCommon = (aIndex, bIndex) => aLines[aIndex] === bLines[bIndex]; | |
| const diffs = []; | |
| let aIndex = 0; | |
| let bIndex = 0; | |
| const foundSubsequence = (nCommon, aCommon, bCommon) => { | |
| for (; aIndex !== aCommon; aIndex += 1) { | |
| diffs.push(new Diff(DIFF_DELETE, aLines[aIndex])); | |
| } | |
| for (; bIndex !== bCommon; bIndex += 1) { | |
| diffs.push(new Diff(DIFF_INSERT, bLines[bIndex])); | |
| } | |
| for (; nCommon !== 0; nCommon -= 1, aIndex += 1, bIndex += 1) { | |
| diffs.push(new Diff(DIFF_EQUAL, bLines[bIndex])); | |
| } | |
| }; | |
| diffSequences(aLength, bLength, isCommon, foundSubsequence); | |
| // After the last common subsequence, push remaining change items. | |
| for (; aIndex !== aLength; aIndex += 1) { | |
| diffs.push(new Diff(DIFF_DELETE, aLines[aIndex])); | |
| } | |
| for (; bIndex !== bLength; bIndex += 1) { | |
| diffs.push(new Diff(DIFF_INSERT, bLines[bIndex])); | |
| } | |
| return [diffs, truncated]; | |
| } | |
| // get the type of a value with handling the edge cases like `typeof []` | |
| // and `typeof null` | |
| function getType(value) { | |
| if (value === undefined) { | |
| return "undefined"; | |
| } else if (value === null) { | |
| return "null"; | |
| } else if (Array.isArray(value)) { | |
| return "array"; | |
| } else if (typeof value === "boolean") { | |
| return "boolean"; | |
| } else if (typeof value === "function") { | |
| return "function"; | |
| } else if (typeof value === "number") { | |
| return "number"; | |
| } else if (typeof value === "string") { | |
| return "string"; | |
| } else if (typeof value === "bigint") { | |
| return "bigint"; | |
| } else if (typeof value === "object") { | |
| if (value != null) { | |
| if (value.constructor === RegExp) { | |
| return "regexp"; | |
| } else if (value.constructor === Map) { | |
| return "map"; | |
| } else if (value.constructor === Set) { | |
| return "set"; | |
| } else if (value.constructor === Date) { | |
| return "date"; | |
| } | |
| } | |
| return "object"; | |
| } else if (typeof value === "symbol") { | |
| return "symbol"; | |
| } | |
| throw new Error(`value of unknown type: ${value}`); | |
| } | |
| // platforms compatible | |
| function getNewLineSymbol(string) { | |
| return string.includes("\r\n") ? "\r\n" : "\n"; | |
| } | |
| function diffStrings(a, b, options) { | |
| const truncate = (options === null || options === void 0 ? void 0 : options.truncateThreshold) ?? false; | |
| const truncateThreshold = Math.max(Math.floor((options === null || options === void 0 ? void 0 : options.truncateThreshold) ?? 0), 0); | |
| let aLength = a.length; | |
| let bLength = b.length; | |
| if (truncate) { | |
| const aMultipleLines = a.includes("\n"); | |
| const bMultipleLines = b.includes("\n"); | |
| const aNewLineSymbol = getNewLineSymbol(a); | |
| const bNewLineSymbol = getNewLineSymbol(b); | |
| // multiple-lines string expects a newline to be appended at the end | |
| const _a = aMultipleLines ? `${a.split(aNewLineSymbol, truncateThreshold).join(aNewLineSymbol)}\n` : a; | |
| const _b = bMultipleLines ? `${b.split(bNewLineSymbol, truncateThreshold).join(bNewLineSymbol)}\n` : b; | |
| aLength = _a.length; | |
| bLength = _b.length; | |
| } | |
| const truncated = aLength !== a.length || bLength !== b.length; | |
| const isCommon = (aIndex, bIndex) => a[aIndex] === b[bIndex]; | |
| let aIndex = 0; | |
| let bIndex = 0; | |
| const diffs = []; | |
| const foundSubsequence = (nCommon, aCommon, bCommon) => { | |
| if (aIndex !== aCommon) { | |
| diffs.push(new Diff(DIFF_DELETE, a.slice(aIndex, aCommon))); | |
| } | |
| if (bIndex !== bCommon) { | |
| diffs.push(new Diff(DIFF_INSERT, b.slice(bIndex, bCommon))); | |
| } | |
| aIndex = aCommon + nCommon; | |
| bIndex = bCommon + nCommon; | |
| diffs.push(new Diff(DIFF_EQUAL, b.slice(bCommon, bIndex))); | |
| }; | |
| diffSequences(aLength, bLength, isCommon, foundSubsequence); | |
| // After the last common subsequence, push remaining change items. | |
| if (aIndex !== aLength) { | |
| diffs.push(new Diff(DIFF_DELETE, a.slice(aIndex))); | |
| } | |
| if (bIndex !== bLength) { | |
| diffs.push(new Diff(DIFF_INSERT, b.slice(bIndex))); | |
| } | |
| return [diffs, truncated]; | |
| } | |
| // Given change op and array of diffs, return concatenated string: | |
| // * include common strings | |
| // * include change strings which have argument op with changeColor | |
| // * exclude change strings which have opposite op | |
| function concatenateRelevantDiffs(op, diffs, changeColor) { | |
| return diffs.reduce((reduced, diff) => reduced + (diff[0] === DIFF_EQUAL ? diff[1] : diff[0] === op && diff[1].length !== 0 ? changeColor(diff[1]) : ""), ""); | |
| } | |
| // Encapsulate change lines until either a common newline or the end. | |
| class ChangeBuffer { | |
| op; | |
| line; | |
| lines; | |
| changeColor; | |
| constructor(op, changeColor) { | |
| this.op = op; | |
| this.line = []; | |
| this.lines = []; | |
| this.changeColor = changeColor; | |
| } | |
| pushSubstring(substring) { | |
| this.pushDiff(new Diff(this.op, substring)); | |
| } | |
| pushLine() { | |
| // Assume call only if line has at least one diff, | |
| // therefore an empty line must have a diff which has an empty string. | |
| // If line has multiple diffs, then assume it has a common diff, | |
| // therefore change diffs have change color; | |
| // otherwise then it has line color only. | |
| this.lines.push(this.line.length !== 1 ? new Diff(this.op, concatenateRelevantDiffs(this.op, this.line, this.changeColor)) : this.line[0][0] === this.op ? this.line[0] : new Diff(this.op, this.line[0][1])); | |
| this.line.length = 0; | |
| } | |
| isLineEmpty() { | |
| return this.line.length === 0; | |
| } | |
| // Minor input to buffer. | |
| pushDiff(diff) { | |
| this.line.push(diff); | |
| } | |
| // Main input to buffer. | |
| align(diff) { | |
| const string = diff[1]; | |
| if (string.includes("\n")) { | |
| const substrings = string.split("\n"); | |
| const iLast = substrings.length - 1; | |
| substrings.forEach((substring, i) => { | |
| if (i < iLast) { | |
| // The first substring completes the current change line. | |
| // A middle substring is a change line. | |
| this.pushSubstring(substring); | |
| this.pushLine(); | |
| } else if (substring.length !== 0) { | |
| // The last substring starts a change line, if it is not empty. | |
| // Important: This non-empty condition also automatically omits | |
| // the newline appended to the end of expected and received strings. | |
| this.pushSubstring(substring); | |
| } | |
| }); | |
| } else { | |
| // Append non-multiline string to current change line. | |
| this.pushDiff(diff); | |
| } | |
| } | |
| // Output from buffer. | |
| moveLinesTo(lines) { | |
| if (!this.isLineEmpty()) { | |
| this.pushLine(); | |
| } | |
| lines.push(...this.lines); | |
| this.lines.length = 0; | |
| } | |
| } | |
| // Encapsulate common and change lines. | |
| class CommonBuffer { | |
| deleteBuffer; | |
| insertBuffer; | |
| lines; | |
| constructor(deleteBuffer, insertBuffer) { | |
| this.deleteBuffer = deleteBuffer; | |
| this.insertBuffer = insertBuffer; | |
| this.lines = []; | |
| } | |
| pushDiffCommonLine(diff) { | |
| this.lines.push(diff); | |
| } | |
| pushDiffChangeLines(diff) { | |
| const isDiffEmpty = diff[1].length === 0; | |
| // An empty diff string is redundant, unless a change line is empty. | |
| if (!isDiffEmpty || this.deleteBuffer.isLineEmpty()) { | |
| this.deleteBuffer.pushDiff(diff); | |
| } | |
| if (!isDiffEmpty || this.insertBuffer.isLineEmpty()) { | |
| this.insertBuffer.pushDiff(diff); | |
| } | |
| } | |
| flushChangeLines() { | |
| this.deleteBuffer.moveLinesTo(this.lines); | |
| this.insertBuffer.moveLinesTo(this.lines); | |
| } | |
| // Input to buffer. | |
| align(diff) { | |
| const op = diff[0]; | |
| const string = diff[1]; | |
| if (string.includes("\n")) { | |
| const substrings = string.split("\n"); | |
| const iLast = substrings.length - 1; | |
| substrings.forEach((substring, i) => { | |
| if (i === 0) { | |
| const subdiff = new Diff(op, substring); | |
| if (this.deleteBuffer.isLineEmpty() && this.insertBuffer.isLineEmpty()) { | |
| // If both current change lines are empty, | |
| // then the first substring is a common line. | |
| this.flushChangeLines(); | |
| this.pushDiffCommonLine(subdiff); | |
| } else { | |
| // If either current change line is non-empty, | |
| // then the first substring completes the change lines. | |
| this.pushDiffChangeLines(subdiff); | |
| this.flushChangeLines(); | |
| } | |
| } else if (i < iLast) { | |
| // A middle substring is a common line. | |
| this.pushDiffCommonLine(new Diff(op, substring)); | |
| } else if (substring.length !== 0) { | |
| // The last substring starts a change line, if it is not empty. | |
| // Important: This non-empty condition also automatically omits | |
| // the newline appended to the end of expected and received strings. | |
| this.pushDiffChangeLines(new Diff(op, substring)); | |
| } | |
| }); | |
| } else { | |
| // Append non-multiline string to current change lines. | |
| // Important: It cannot be at the end following empty change lines, | |
| // because newline appended to the end of expected and received strings. | |
| this.pushDiffChangeLines(diff); | |
| } | |
| } | |
| // Output from buffer. | |
| getLines() { | |
| this.flushChangeLines(); | |
| return this.lines; | |
| } | |
| } | |
| // Given diffs from expected and received strings, | |
| // return new array of diffs split or joined into lines. | |
| // | |
| // To correctly align a change line at the end, the algorithm: | |
| // * assumes that a newline was appended to the strings | |
| // * omits the last newline from the output array | |
| // | |
| // Assume the function is not called: | |
| // * if either expected or received is empty string | |
| // * if neither expected nor received is multiline string | |
| function getAlignedDiffs(diffs, changeColor) { | |
| const deleteBuffer = new ChangeBuffer(DIFF_DELETE, changeColor); | |
| const insertBuffer = new ChangeBuffer(DIFF_INSERT, changeColor); | |
| const commonBuffer = new CommonBuffer(deleteBuffer, insertBuffer); | |
| diffs.forEach((diff) => { | |
| switch (diff[0]) { | |
| case DIFF_DELETE: | |
| deleteBuffer.align(diff); | |
| break; | |
| case DIFF_INSERT: | |
| insertBuffer.align(diff); | |
| break; | |
| default: commonBuffer.align(diff); | |
| } | |
| }); | |
| return commonBuffer.getLines(); | |
| } | |
| function hasCommonDiff(diffs, isMultiline) { | |
| if (isMultiline) { | |
| // Important: Ignore common newline that was appended to multiline strings! | |
| const iLast = diffs.length - 1; | |
| return diffs.some((diff, i) => diff[0] === DIFF_EQUAL && (i !== iLast || diff[1] !== "\n")); | |
| } | |
| return diffs.some((diff) => diff[0] === DIFF_EQUAL); | |
| } | |
| // Compare two strings character-by-character. | |
| // Format as comparison lines in which changed substrings have inverse colors. | |
| function diffStringsUnified(a, b, options) { | |
| if (a !== b && a.length !== 0 && b.length !== 0) { | |
| const isMultiline = a.includes("\n") || b.includes("\n"); | |
| // getAlignedDiffs assumes that a newline was appended to the strings. | |
| const [diffs, truncated] = diffStringsRaw(isMultiline ? `${a}\n` : a, isMultiline ? `${b}\n` : b, true, options); | |
| if (hasCommonDiff(diffs, isMultiline)) { | |
| const optionsNormalized = normalizeDiffOptions(options); | |
| const lines = getAlignedDiffs(diffs, optionsNormalized.changeColor); | |
| return printDiffLines(lines, truncated, optionsNormalized); | |
| } | |
| } | |
| // Fall back to line-by-line diff. | |
| return diffLinesUnified(a.split("\n"), b.split("\n"), options); | |
| } | |
| // Compare two strings character-by-character. | |
| // Optionally clean up small common substrings, also known as chaff. | |
| function diffStringsRaw(a, b, cleanup, options) { | |
| const [diffs, truncated] = diffStrings(a, b, options); | |
| if (cleanup) { | |
| diff_cleanupSemantic(diffs); | |
| } | |
| return [diffs, truncated]; | |
| } | |
| function getCommonMessage(message, options) { | |
| const { commonColor } = normalizeDiffOptions(options); | |
| return commonColor(message); | |
| } | |
| const { AsymmetricMatcher, DOMCollection, DOMElement, Immutable, ReactElement, ReactTestComponent } = plugins; | |
| const PLUGINS = [ | |
| ReactTestComponent, | |
| ReactElement, | |
| DOMElement, | |
| DOMCollection, | |
| Immutable, | |
| AsymmetricMatcher, | |
| plugins.Error | |
| ]; | |
| const FORMAT_OPTIONS = { | |
| maxDepth: 20, | |
| plugins: PLUGINS | |
| }; | |
| const FALLBACK_FORMAT_OPTIONS = { | |
| callToJSON: false, | |
| maxDepth: 8, | |
| plugins: PLUGINS | |
| }; | |
| // Generate a string that will highlight the difference between two values | |
| // with green and red. (similar to how github does code diffing) | |
| /** | |
| * @param a Expected value | |
| * @param b Received value | |
| * @param options Diff options | |
| * @returns {string | null} a string diff | |
| */ | |
| function diff(a, b, options) { | |
| if (Object.is(a, b)) { | |
| return ""; | |
| } | |
| const aType = getType(a); | |
| let expectedType = aType; | |
| let omitDifference = false; | |
| if (aType === "object" && typeof a.asymmetricMatch === "function") { | |
| if (a.$$typeof !== Symbol.for("jest.asymmetricMatcher")) { | |
| // Do not know expected type of user-defined asymmetric matcher. | |
| return undefined; | |
| } | |
| if (typeof a.getExpectedType !== "function") { | |
| // For example, expect.anything() matches either null or undefined | |
| return undefined; | |
| } | |
| expectedType = a.getExpectedType(); | |
| // Primitive types boolean and number omit difference below. | |
| // For example, omit difference for expect.stringMatching(regexp) | |
| omitDifference = expectedType === "string"; | |
| } | |
| if (expectedType !== getType(b)) { | |
| const { aAnnotation, aColor, aIndicator, bAnnotation, bColor, bIndicator } = normalizeDiffOptions(options); | |
| const formatOptions = getFormatOptions(FALLBACK_FORMAT_OPTIONS, options); | |
| let aDisplay = format(a, formatOptions); | |
| let bDisplay = format(b, formatOptions); | |
| // even if prettyFormat prints successfully big objects, | |
| // large string can choke later on (concatenation? RPC?), | |
| // so truncate it to a reasonable length here. | |
| // (For example, playwright's ElementHandle can become about 200_000_000 length string) | |
| const MAX_LENGTH = 1e5; | |
| function truncate(s) { | |
| return s.length <= MAX_LENGTH ? s : `${s.slice(0, MAX_LENGTH)}...`; | |
| } | |
| aDisplay = truncate(aDisplay); | |
| bDisplay = truncate(bDisplay); | |
| const aDiff = `${aColor(`${aIndicator} ${aAnnotation}:`)} \n${aDisplay}`; | |
| const bDiff = `${bColor(`${bIndicator} ${bAnnotation}:`)} \n${bDisplay}`; | |
| return `${aDiff}\n\n${bDiff}`; | |
| } | |
| if (omitDifference) { | |
| return undefined; | |
| } | |
| switch (aType) { | |
| case "string": return diffLinesUnified(a.split("\n"), b.split("\n"), options); | |
| case "boolean": | |
| case "number": return comparePrimitive(a, b, options); | |
| case "map": return compareObjects(sortMap(a), sortMap(b), options); | |
| case "set": return compareObjects(sortSet(a), sortSet(b), options); | |
| default: return compareObjects(a, b, options); | |
| } | |
| } | |
| function comparePrimitive(a, b, options) { | |
| const aFormat = format(a, FORMAT_OPTIONS); | |
| const bFormat = format(b, FORMAT_OPTIONS); | |
| return aFormat === bFormat ? "" : diffLinesUnified(aFormat.split("\n"), bFormat.split("\n"), options); | |
| } | |
| function sortMap(map) { | |
| return new Map(Array.from(map.entries()).sort()); | |
| } | |
| function sortSet(set) { | |
| return new Set(Array.from(set.values()).sort()); | |
| } | |
| function compareObjects(a, b, options) { | |
| let difference; | |
| let hasThrown = false; | |
| try { | |
| const formatOptions = getFormatOptions(FORMAT_OPTIONS, options); | |
| difference = getObjectsDifference(a, b, formatOptions, options); | |
| } catch { | |
| hasThrown = true; | |
| } | |
| const noDiffMessage = getCommonMessage(NO_DIFF_MESSAGE, options); | |
| // If the comparison yields no results, compare again but this time | |
| // without calling `toJSON`. It's also possible that toJSON might throw. | |
| if (difference === undefined || difference === noDiffMessage) { | |
| const formatOptions = getFormatOptions(FALLBACK_FORMAT_OPTIONS, options); | |
| difference = getObjectsDifference(a, b, formatOptions, options); | |
| if (difference !== noDiffMessage && !hasThrown) { | |
| difference = `${getCommonMessage(SIMILAR_MESSAGE, options)}\n\n${difference}`; | |
| } | |
| } | |
| return difference; | |
| } | |
| function getFormatOptions(formatOptions, options) { | |
| const { compareKeys, printBasicPrototype, maxDepth } = normalizeDiffOptions(options); | |
| return { | |
| ...formatOptions, | |
| compareKeys, | |
| printBasicPrototype, | |
| maxDepth: maxDepth ?? formatOptions.maxDepth | |
| }; | |
| } | |
| function getObjectsDifference(a, b, formatOptions, options) { | |
| const formatOptionsZeroIndent = { | |
| ...formatOptions, | |
| indent: 0 | |
| }; | |
| const aCompare = format(a, formatOptionsZeroIndent); | |
| const bCompare = format(b, formatOptionsZeroIndent); | |
| if (aCompare === bCompare) { | |
| return getCommonMessage(NO_DIFF_MESSAGE, options); | |
| } else { | |
| const aDisplay = format(a, formatOptions); | |
| const bDisplay = format(b, formatOptions); | |
| return diffLinesUnified2(aDisplay.split("\n"), bDisplay.split("\n"), aCompare.split("\n"), bCompare.split("\n"), options); | |
| } | |
| } | |
| const MAX_DIFF_STRING_LENGTH = 2e4; | |
| function isAsymmetricMatcher(data) { | |
| const type = getType$1(data); | |
| return type === "Object" && typeof data.asymmetricMatch === "function"; | |
| } | |
| function isReplaceable(obj1, obj2) { | |
| const obj1Type = getType$1(obj1); | |
| const obj2Type = getType$1(obj2); | |
| return obj1Type === obj2Type && (obj1Type === "Object" || obj1Type === "Array"); | |
| } | |
| function printDiffOrStringify(received, expected, options) { | |
| const { aAnnotation, bAnnotation } = normalizeDiffOptions(options); | |
| if (typeof expected === "string" && typeof received === "string" && expected.length > 0 && received.length > 0 && expected.length <= MAX_DIFF_STRING_LENGTH && received.length <= MAX_DIFF_STRING_LENGTH && expected !== received) { | |
| if (expected.includes("\n") || received.includes("\n")) { | |
| return diffStringsUnified(expected, received, options); | |
| } | |
| const [diffs] = diffStringsRaw(expected, received, true); | |
| const hasCommonDiff = diffs.some((diff) => diff[0] === DIFF_EQUAL); | |
| const printLabel = getLabelPrinter(aAnnotation, bAnnotation); | |
| const expectedLine = printLabel(aAnnotation) + printExpected(getCommonAndChangedSubstrings(diffs, DIFF_DELETE, hasCommonDiff)); | |
| const receivedLine = printLabel(bAnnotation) + printReceived(getCommonAndChangedSubstrings(diffs, DIFF_INSERT, hasCommonDiff)); | |
| return `${expectedLine}\n${receivedLine}`; | |
| } | |
| // if (isLineDiffable(expected, received)) { | |
| const clonedExpected = deepClone(expected, { forceWritable: true }); | |
| const clonedReceived = deepClone(received, { forceWritable: true }); | |
| const { replacedExpected, replacedActual } = replaceAsymmetricMatcher(clonedReceived, clonedExpected); | |
| const difference = diff(replacedExpected, replacedActual, options); | |
| return difference; | |
| // } | |
| // const printLabel = getLabelPrinter(aAnnotation, bAnnotation) | |
| // const expectedLine = printLabel(aAnnotation) + printExpected(expected) | |
| // const receivedLine | |
| // = printLabel(bAnnotation) | |
| // + (stringify(expected) === stringify(received) | |
| // ? 'serializes to the same string' | |
| // : printReceived(received)) | |
| // return `${expectedLine}\n${receivedLine}` | |
| } | |
| function replaceAsymmetricMatcher(actual, expected, actualReplaced = new WeakSet(), expectedReplaced = new WeakSet()) { | |
| // handle asymmetric Error.cause diff | |
| if (actual instanceof Error && expected instanceof Error && typeof actual.cause !== "undefined" && typeof expected.cause === "undefined") { | |
| delete actual.cause; | |
| return { | |
| replacedActual: actual, | |
| replacedExpected: expected | |
| }; | |
| } | |
| if (!isReplaceable(actual, expected)) { | |
| return { | |
| replacedActual: actual, | |
| replacedExpected: expected | |
| }; | |
| } | |
| if (actualReplaced.has(actual) || expectedReplaced.has(expected)) { | |
| return { | |
| replacedActual: actual, | |
| replacedExpected: expected | |
| }; | |
| } | |
| actualReplaced.add(actual); | |
| expectedReplaced.add(expected); | |
| getOwnProperties(expected).forEach((key) => { | |
| const expectedValue = expected[key]; | |
| const actualValue = actual[key]; | |
| if (isAsymmetricMatcher(expectedValue)) { | |
| if (expectedValue.asymmetricMatch(actualValue)) { | |
| actual[key] = expectedValue; | |
| } | |
| } else if (isAsymmetricMatcher(actualValue)) { | |
| if (actualValue.asymmetricMatch(expectedValue)) { | |
| expected[key] = actualValue; | |
| } | |
| } else if (isReplaceable(actualValue, expectedValue)) { | |
| const replaced = replaceAsymmetricMatcher(actualValue, expectedValue, actualReplaced, expectedReplaced); | |
| actual[key] = replaced.replacedActual; | |
| expected[key] = replaced.replacedExpected; | |
| } | |
| }); | |
| return { | |
| replacedActual: actual, | |
| replacedExpected: expected | |
| }; | |
| } | |
| function getLabelPrinter(...strings) { | |
| const maxLength = strings.reduce((max, string) => string.length > max ? string.length : max, 0); | |
| return (string) => `${string}: ${" ".repeat(maxLength - string.length)}`; | |
| } | |
| const SPACE_SYMBOL = "·"; | |
| function replaceTrailingSpaces(text) { | |
| return text.replace(/\s+$/gm, (spaces) => SPACE_SYMBOL.repeat(spaces.length)); | |
| } | |
| function printReceived(object) { | |
| return c.red(replaceTrailingSpaces(stringify(object))); | |
| } | |
| function printExpected(value) { | |
| return c.green(replaceTrailingSpaces(stringify(value))); | |
| } | |
| function getCommonAndChangedSubstrings(diffs, op, hasCommonDiff) { | |
| return diffs.reduce((reduced, diff) => reduced + (diff[0] === DIFF_EQUAL ? diff[1] : diff[0] === op ? hasCommonDiff ? c.inverse(diff[1]) : diff[1] : ""), ""); | |
| } | |
| export { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff, diff, diffLinesRaw, diffLinesUnified, diffLinesUnified2, diffStringsRaw, diffStringsUnified, getLabelPrinter, printDiffOrStringify, replaceAsymmetricMatcher }; | |