jsondiffpatch
Version:
JSON diff & patch (object and array diff, text diff, multiple output formats)
428 lines (427 loc) • 15.9 kB
JavaScript
import DiffContext from "../contexts/diff.js";
import PatchContext from "../contexts/patch.js";
import ReverseContext from "../contexts/reverse.js";
import lcs from "./lcs.js";
const ARRAY_MOVE = 3;
function arraysHaveMatchByRef(array1, array2, len1, len2) {
for (let index1 = 0; index1 < len1; index1++) {
const val1 = array1[index1];
for (let index2 = 0; index2 < len2; index2++) {
const val2 = array2[index2];
if (index1 !== index2 && val1 === val2) {
return true;
}
}
}
return false;
}
function matchItems(array1, array2, index1, index2, context) {
const value1 = array1[index1];
const value2 = array2[index2];
if (value1 === value2) {
return true;
}
if (typeof value1 !== "object" || typeof value2 !== "object") {
return false;
}
const objectHash = context.objectHash;
if (!objectHash) {
// no way to match objects was provided, try match by position
return context.matchByPosition && index1 === index2;
}
context.hashCache1 = context.hashCache1 || [];
let hash1 = context.hashCache1[index1];
if (typeof hash1 === "undefined") {
context.hashCache1[index1] = hash1 = objectHash(value1, index1);
}
if (typeof hash1 === "undefined") {
return false;
}
context.hashCache2 = context.hashCache2 || [];
let hash2 = context.hashCache2[index2];
if (typeof hash2 === "undefined") {
context.hashCache2[index2] = hash2 = objectHash(value2, index2);
}
if (typeof hash2 === "undefined") {
return false;
}
return hash1 === hash2;
}
export const diffFilter = function arraysDiffFilter(context) {
var _a, _b, _c, _d, _e;
if (!context.leftIsArray) {
return;
}
const matchContext = {
objectHash: (_a = context.options) === null || _a === void 0 ? void 0 : _a.objectHash,
matchByPosition: (_b = context.options) === null || _b === void 0 ? void 0 : _b.matchByPosition,
};
let commonHead = 0;
let commonTail = 0;
let index;
let index1;
let index2;
const array1 = context.left;
const array2 = context.right;
const len1 = array1.length;
const len2 = array2.length;
let child;
if (len1 > 0 &&
len2 > 0 &&
!matchContext.objectHash &&
typeof matchContext.matchByPosition !== "boolean") {
matchContext.matchByPosition = !arraysHaveMatchByRef(array1, array2, len1, len2);
}
// separate common head
while (commonHead < len1 &&
commonHead < len2 &&
matchItems(array1, array2, commonHead, commonHead, matchContext)) {
index = commonHead;
child = new DiffContext(array1[index], array2[index]);
context.push(child, index);
commonHead++;
}
// separate common tail
while (commonTail + commonHead < len1 &&
commonTail + commonHead < len2 &&
matchItems(array1, array2, len1 - 1 - commonTail, len2 - 1 - commonTail, matchContext)) {
index1 = len1 - 1 - commonTail;
index2 = len2 - 1 - commonTail;
child = new DiffContext(array1[index1], array2[index2]);
context.push(child, index2);
commonTail++;
}
let result;
if (commonHead + commonTail === len1) {
if (len1 === len2) {
// arrays are identical
context.setResult(undefined).exit();
return;
}
// trivial case, a block (1 or more consecutive items) was added
result = result || {
_t: "a",
};
for (index = commonHead; index < len2 - commonTail; index++) {
result[index] = [array2[index]];
context.prepareDeltaResult(result[index]);
}
context.setResult(result).exit();
return;
}
if (commonHead + commonTail === len2) {
// trivial case, a block (1 or more consecutive items) was removed
result = result || {
_t: "a",
};
for (index = commonHead; index < len1 - commonTail; index++) {
const key = `_${index}`;
result[key] = [array1[index], 0, 0];
context.prepareDeltaResult(result[key]);
}
context.setResult(result).exit();
return;
}
// reset hash cache
matchContext.hashCache1 = undefined;
matchContext.hashCache2 = undefined;
// diff is not trivial, find the LCS (Longest Common Subsequence)
const trimmed1 = array1.slice(commonHead, len1 - commonTail);
const trimmed2 = array2.slice(commonHead, len2 - commonTail);
const seq = lcs.get(trimmed1, trimmed2, matchItems, matchContext);
const removedItems = [];
result = result || {
_t: "a",
};
for (index = commonHead; index < len1 - commonTail; index++) {
if (seq.indices1.indexOf(index - commonHead) < 0) {
// removed
const key = `_${index}`;
result[key] = [array1[index], 0, 0];
context.prepareDeltaResult(result[key]);
removedItems.push(index);
}
}
let detectMove = true;
if (((_c = context.options) === null || _c === void 0 ? void 0 : _c.arrays) && context.options.arrays.detectMove === false) {
detectMove = false;
}
let includeValueOnMove = false;
if ((_e = (_d = context.options) === null || _d === void 0 ? void 0 : _d.arrays) === null || _e === void 0 ? void 0 : _e.includeValueOnMove) {
includeValueOnMove = true;
}
const removedItemsLength = removedItems.length;
for (index = commonHead; index < len2 - commonTail; index++) {
const indexOnArray2 = seq.indices2.indexOf(index - commonHead);
if (indexOnArray2 < 0) {
// added, try to match with a removed item and register as position move
let isMove = false;
if (detectMove && removedItemsLength > 0) {
for (let removeItemIndex1 = 0; removeItemIndex1 < removedItemsLength; removeItemIndex1++) {
index1 = removedItems[removeItemIndex1];
const resultItem = index1 === undefined ? undefined : result[`_${index1}`];
if (index1 !== undefined &&
resultItem &&
matchItems(trimmed1, trimmed2, index1 - commonHead, index - commonHead, matchContext)) {
// store position move as: [originalValue, newPosition, ARRAY_MOVE]
resultItem.splice(1, 2, index, ARRAY_MOVE);
resultItem.splice(1, 2, index, ARRAY_MOVE);
if (!includeValueOnMove) {
// don't include moved value on diff, to save bytes
resultItem[0] = "";
}
index2 = index;
child = new DiffContext(array1[index1], array2[index2]);
context.push(child, index2);
removedItems.splice(removeItemIndex1, 1);
isMove = true;
break;
}
}
}
if (!isMove) {
// added
result[index] = [array2[index]];
context.prepareDeltaResult(result[index]);
}
}
else {
// match, do inner diff
if (seq.indices1[indexOnArray2] === undefined) {
throw new Error(`Invalid indexOnArray2: ${indexOnArray2}, seq.indices1: ${seq.indices1}`);
}
index1 = seq.indices1[indexOnArray2] + commonHead;
if (seq.indices2[indexOnArray2] === undefined) {
throw new Error(`Invalid indexOnArray2: ${indexOnArray2}, seq.indices2: ${seq.indices2}`);
}
index2 = seq.indices2[indexOnArray2] + commonHead;
child = new DiffContext(array1[index1], array2[index2]);
context.push(child, index2);
}
}
context.setResult(result).exit();
};
diffFilter.filterName = "arrays";
const compare = {
numerically(a, b) {
return a - b;
},
numericallyBy(name) {
return (a, b) => a[name] - b[name];
},
};
export const patchFilter = function nestedPatchFilter(context) {
var _a;
if (!context.nested) {
return;
}
const nestedDelta = context.delta;
if (nestedDelta._t !== "a") {
return;
}
let index;
let index1;
const delta = nestedDelta;
const array = context.left;
// first, separate removals, insertions and modifications
let toRemove = [];
let toInsert = [];
const toModify = [];
for (index in delta) {
if (index !== "_t") {
if (index[0] === "_") {
const removedOrMovedIndex = index;
// removed item from original array
if (delta[removedOrMovedIndex] !== undefined &&
(delta[removedOrMovedIndex][2] === 0 ||
delta[removedOrMovedIndex][2] === ARRAY_MOVE)) {
toRemove.push(Number.parseInt(index.slice(1), 10));
}
else {
throw new Error(`only removal or move can be applied at original array indices, invalid diff type: ${(_a = delta[removedOrMovedIndex]) === null || _a === void 0 ? void 0 : _a[2]}`);
}
}
else {
const numberIndex = index;
if (delta[numberIndex].length === 1) {
// added item at new array
toInsert.push({
index: Number.parseInt(numberIndex, 10),
value: delta[numberIndex][0],
});
}
else {
// modified item at new array
toModify.push({
index: Number.parseInt(numberIndex, 10),
delta: delta[numberIndex],
});
}
}
}
}
// remove items, in reverse order to avoid sawing our own floor
toRemove = toRemove.sort(compare.numerically);
for (index = toRemove.length - 1; index >= 0; index--) {
index1 = toRemove[index];
if (index1 === undefined)
continue;
const indexDiff = delta[`_${index1}`];
const removedValue = array.splice(index1, 1)[0];
if ((indexDiff === null || indexDiff === void 0 ? void 0 : indexDiff[2]) === ARRAY_MOVE) {
// reinsert later
toInsert.push({
index: indexDiff[1],
value: removedValue,
});
}
}
// insert items, in reverse order to avoid moving our own floor
toInsert = toInsert.sort(compare.numericallyBy("index"));
const toInsertLength = toInsert.length;
for (index = 0; index < toInsertLength; index++) {
const insertion = toInsert[index];
if (insertion === undefined)
continue;
array.splice(insertion.index, 0, insertion.value);
}
// apply modifications
const toModifyLength = toModify.length;
if (toModifyLength > 0) {
for (index = 0; index < toModifyLength; index++) {
const modification = toModify[index];
if (modification === undefined)
continue;
const child = new PatchContext(array[modification.index], modification.delta);
context.push(child, modification.index);
}
}
if (!context.children) {
context.setResult(array).exit();
return;
}
context.exit();
};
patchFilter.filterName = "arrays";
export const collectChildrenPatchFilter = function collectChildrenPatchFilter(context) {
if (!context || !context.children) {
return;
}
const deltaWithChildren = context.delta;
if (deltaWithChildren._t !== "a") {
return;
}
const array = context.left;
const length = context.children.length;
for (let index = 0; index < length; index++) {
const child = context.children[index];
if (child === undefined)
continue;
const arrayIndex = child.childName;
array[arrayIndex] = child.result;
}
context.setResult(array).exit();
};
collectChildrenPatchFilter.filterName = "arraysCollectChildren";
export const reverseFilter = function arraysReverseFilter(context) {
if (!context.nested) {
const nonNestedDelta = context.delta;
if (nonNestedDelta[2] === ARRAY_MOVE) {
const arrayMoveDelta = nonNestedDelta;
context.newName = `_${arrayMoveDelta[1]}`;
context
.setResult([
arrayMoveDelta[0],
Number.parseInt(context.childName.substring(1), 10),
ARRAY_MOVE,
])
.exit();
}
return;
}
const nestedDelta = context.delta;
if (nestedDelta._t !== "a") {
return;
}
const arrayDelta = nestedDelta;
for (const name in arrayDelta) {
if (name === "_t") {
continue;
}
const child = new ReverseContext(arrayDelta[name]);
context.push(child, name);
}
context.exit();
};
reverseFilter.filterName = "arrays";
const reverseArrayDeltaIndex = (delta, index, itemDelta) => {
if (typeof index === "string" && index[0] === "_") {
return Number.parseInt(index.substring(1), 10);
}
if (Array.isArray(itemDelta) && itemDelta[2] === 0) {
return `_${index}`;
}
let reverseIndex = +index;
for (const deltaIndex in delta) {
const deltaItem = delta[deltaIndex];
if (Array.isArray(deltaItem)) {
if (deltaItem[2] === ARRAY_MOVE) {
const moveFromIndex = Number.parseInt(deltaIndex.substring(1), 10);
const moveToIndex = deltaItem[1];
if (moveToIndex === +index) {
return moveFromIndex;
}
if (moveFromIndex <= reverseIndex && moveToIndex > reverseIndex) {
reverseIndex++;
}
else if (moveFromIndex >= reverseIndex &&
moveToIndex < reverseIndex) {
reverseIndex--;
}
}
else if (deltaItem[2] === 0) {
const deleteIndex = Number.parseInt(deltaIndex.substring(1), 10);
if (deleteIndex <= reverseIndex) {
reverseIndex++;
}
}
else if (deltaItem.length === 1 &&
Number.parseInt(deltaIndex, 10) <= reverseIndex) {
reverseIndex--;
}
}
}
return reverseIndex;
};
export const collectChildrenReverseFilter = (context) => {
if (!context || !context.children) {
return;
}
const deltaWithChildren = context.delta;
if (deltaWithChildren._t !== "a") {
return;
}
const arrayDelta = deltaWithChildren;
const length = context.children.length;
const delta = {
_t: "a",
};
for (let index = 0; index < length; index++) {
const child = context.children[index];
if (child === undefined)
continue;
let name = child.newName;
if (typeof name === "undefined") {
if (child.childName === undefined) {
throw new Error("child.childName is undefined");
}
name = reverseArrayDeltaIndex(arrayDelta, child.childName, child.result);
}
if (delta[name] !== child.result) {
// There's no way to type this well.
delta[name] = child.result;
}
}
context.setResult(delta).exit();
};
collectChildrenReverseFilter.filterName = "arraysCollectChildren";