UNPKG

@plugjs/plug

Version:
278 lines 10.6 kB
import { fail } from 'node:assert'; import { inspect, isDeepStrictEqual } from 'node:util'; import { assert } from "../asserts.js"; import { $grn, $red, logOptions } from "../logging.js"; /* ========================================================================== * * MYERS IMPLEMENTATION * * Lifted from https://github.com/wickedest/myers-diff/ (Apache 2.0) * * ========================================================================== */ function compareLongestCommonSubsequence(lhsCtx, rhsCtx) { let lhsStart; let rhsStart; let lhsItem = 0; let rhsItem = 0; const changes = []; while (lhsItem < lhsCtx.length || rhsItem < rhsCtx.length) { if ((lhsItem < lhsCtx.length) && (!lhsCtx.modified[lhsItem]) && (rhsItem < rhsCtx.length) && (!rhsCtx.modified[rhsItem])) { // equal lines lhsItem++; rhsItem++; continue; } // maybe deleted and/or inserted lines lhsStart = lhsItem; rhsStart = rhsItem; while ((lhsItem < lhsCtx.length) && (rhsItem >= rhsCtx.length || lhsCtx.modified[lhsItem])) { lhsItem++; } while ((rhsItem < rhsCtx.length) && (lhsItem >= lhsCtx.length || rhsCtx.modified[rhsItem])) { rhsItem++; } if ((lhsStart < lhsItem) || (rhsStart < rhsItem)) { const lat = Math.min(lhsStart, (lhsCtx.length) ? lhsCtx.length - 1 : 0); const rat = Math.min(rhsStart, (rhsCtx.length) ? rhsCtx.length - 1 : 0); changes.push({ lhsPos: lat, lhsDel: lhsItem - lhsStart, rhsPos: rat, rhsAdd: rhsItem - rhsStart, }); } } return changes; } function getShortestMiddleSnake(lhsCtx, lhsLower, lhsUpper, rhsCtx, rhsLower, rhsUpper, vectorU, vectorD) { const max = lhsCtx.length + rhsCtx.length + 1; const kdown = lhsLower - rhsLower; const kup = lhsUpper - rhsUpper; const delta = (lhsUpper - lhsLower) - (rhsUpper - rhsLower); const odd = (delta & 1) != 0; const offsetDown = max - kdown; const offsetUp = max - kup; const maxd = ((lhsUpper - lhsLower + rhsUpper - rhsLower) / 2) + 1; const ret = { x: 0, y: 0 }; let d; let k; let x; let y; vectorD[offsetDown + kdown + 1] = lhsLower; vectorU[offsetUp + kup - 1] = lhsUpper; for (d = 0; d <= maxd; ++d) { for (k = kdown - d; k <= kdown + d; k += 2) { if (k === kdown - d) { x = vectorD[offsetDown + k + 1]; // down } else { x = vectorD[offsetDown + k - 1] + 1; // right if ((k < (kdown + d)) && (vectorD[offsetDown + k + 1] >= x)) { x = vectorD[offsetDown + k + 1]; // down } } y = x - k; // find the end of the furthest reaching forward D-path in diagonal k. while ((x < lhsUpper) && (y < rhsUpper) && (lhsCtx.codes[x] === rhsCtx.codes[y])) { x++; y++; } vectorD[offsetDown + k] = x; // overlap ? if (odd && (kup - d < k) && (k < kup + d)) { if (vectorU[offsetUp + k] <= vectorD[offsetDown + k]) { ret.x = vectorD[offsetDown + k]; ret.y = vectorD[offsetDown + k] - k; return (ret); } } } // Extend the reverse path. for (k = kup - d; k <= kup + d; k += 2) { // find the only or better starting point if (k === kup + d) { x = vectorU[offsetUp + k - 1]; // up } else { x = vectorU[offsetUp + k + 1] - 1; // left if ((k > kup - d) && (vectorU[offsetUp + k - 1] < x)) { x = vectorU[offsetUp + k - 1]; } // up } y = x - k; while ((x > lhsLower) && (y > rhsLower) && (lhsCtx.codes[x - 1] === rhsCtx.codes[y - 1])) { // diagonal x--; y--; } vectorU[offsetUp + k] = x; // overlap ? if (!odd && (kdown - d <= k) && (k <= kdown + d)) { if (vectorU[offsetUp + k] <= vectorD[offsetDown + k]) { ret.x = vectorD[offsetDown + k]; ret.y = vectorD[offsetDown + k] - k; return (ret); } } } } // coverage ignore next // we should never get here fail('Unexpected state computing diff'); } function getLongestCommonSubsequence(lhsCtx, lhsLower, lhsUpper, rhsCtx, rhsLower, rhsUpper, vectorU = [], vectorD = []) { // trim off the matching items at the beginning while ((lhsLower < lhsUpper) && (rhsLower < rhsUpper) && (lhsCtx.codes[lhsLower] === rhsCtx.codes[rhsLower])) { ++lhsLower; ++rhsLower; } // trim off the matching items at the end while ((lhsLower < lhsUpper) && (rhsLower < rhsUpper) && (lhsCtx.codes[lhsUpper - 1] === rhsCtx.codes[rhsUpper - 1])) { --lhsUpper; --rhsUpper; } if (lhsLower === lhsUpper) { while (rhsLower < rhsUpper) { rhsCtx.modified[rhsLower++] = true; } } else if (rhsLower === rhsUpper) { while (lhsLower < lhsUpper) { lhsCtx.modified[lhsLower++] = true; } } else { const { x, y } = getShortestMiddleSnake(lhsCtx, lhsLower, lhsUpper, rhsCtx, rhsLower, rhsUpper, vectorU, vectorD); getLongestCommonSubsequence(lhsCtx, lhsLower, x, rhsCtx, rhsLower, y, vectorU, vectorD); getLongestCommonSubsequence(lhsCtx, x, lhsUpper, rhsCtx, y, rhsUpper, vectorU, vectorD); } } /* ========================================================================== * * INTERNAL CLASSES * * ========================================================================== */ /** The context to use while executing the diff */ class Context { /** Keep a tab on modified items */ modified; /** A _code table_ for all the items in this context */ codes; /** The number of item held by this context */ length; /** Construct with a _code table_ */ constructor(codes) { const length = this.length = codes.length; this.modified = new Array(length); this.codes = codes; } } /** A codec producing _code tables_ */ class Coder { _primitives = new Map(); _objects = []; _index = 1; _getObjectCode(item) { for (const [object, code] of this._objects) { if (isDeepStrictEqual(item, object)) return code; } const code = ++this._index; this._objects.push([item, code]); return code; } _getPrimitiveCode(item) { let code = this._primitives.get(item); if (code) return code; code = ++this._index; this._primitives.set(item, code); return code; } /** Get the code table for an {@link Iterable} */ getCodes(iterable) { const codes = []; for (const item of iterable) { const type = item === null ? 'null' : typeof item; const code = type === 'object' ? this._getObjectCode(item) : this._getPrimitiveCode(item); codes.push(code); } return codes; } } /** Shared constant for inspect options */ const inspectOptions = { showHidden: false, depth: 10, colors: false, maxArrayLength: 100, maxStringLength: 250, breakLength: Infinity, compact: false, sorted: true, getters: true, }; /* ========================================================================== * * EXPORTED FUNCTIONS * * ========================================================================== */ /** * Compare the _Left-Hand side_ {@link Iterable} to _Right-Hand side_ one, * producing an array of {@link Change | Changes} identifying the differences. */ export function diff(lhs, rhs) { assert(lhs !== undefined, 'Left-Hand side undefined'); assert(rhs !== undefined, 'Right-Hand side undefined'); const codec = new Coder(); const lhsCtx = new Context(codec.getCodes(lhs)); const rhsCtx = new Context(codec.getCodes(rhs)); getLongestCommonSubsequence(lhsCtx, 0, lhsCtx.length, rhsCtx, 0, rhsCtx.length); return compareLongestCommonSubsequence(lhsCtx, rhsCtx); } /** Produce a textual diff between two values. */ export function textDiff(lhs, rhs, add, del, not) { // Defaults for our "add" "del" and "not" functions (depending on colorization) const _add = add || (logOptions.colors ? $grn : (s) => `+ ${s}`); const _del = del || (logOptions.colors ? $red : (s) => `- ${s}`); const _not = not || (logOptions.colors ? (s) => s : (s) => ` ${s}`); // We'll need two array of string lines to compare... let lhsLines; let rhsLines; // If _both_ arguments are strings, then just split and compare, otherwise // we nuse NodeJS' inspect to prep their string version if ((typeof lhs === 'string') && (typeof rhs === 'string')) { lhsLines = lhs.split('\n'); rhsLines = rhs.split('\n'); } else { lhsLines = inspect(lhs, inspectOptions).split('\n'); rhsLines = inspect(rhs, inspectOptions).split('\n'); } // Calculate the difference between the two arrays of strings const changes = diff(lhsLines, rhsLines); if (changes.length === 0) return ''; // Go through changes and highlight let offset = 0; const result = []; changes.forEach(({ lhsPos, lhsDel, rhsPos, rhsAdd }) => { if (offset != lhsPos) result.push(...lhsLines.slice(offset, lhsPos).map(_not)); if (lhsDel) result.push(...lhsLines.slice(lhsPos, lhsPos + lhsDel).map(_del)); if (rhsAdd) result.push(...rhsLines.slice(rhsPos, rhsPos + rhsAdd).map(_add)); offset = lhsPos + lhsDel; }); if (offset < lhsLines.length) result.push(...lhsLines.slice(offset).map(_not)); // Join our results and return return result.join('\n'); } //# sourceMappingURL=diff.js.map