@plugjs/plug
Version:
PlugJS Build System ===================
278 lines • 10.6 kB
JavaScript
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