@n1ru4l/json-patch-plus
Version:
This is a slimmed version of [jsondiffpatch](https://github.com/benjamine/jsondiffpatch). All the code is taken from the [jsondiffpatch](https://github.com/benjamine/jsondiffpatch) repository, slimmed down, slightly altered and converted to TypeScript.
424 lines (423 loc) • 14.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.diff = void 0;
const tslib_1 = require("tslib");
const lcs = tslib_1.__importStar(require("./lcs.js"));
function diff(input, options) {
var _a;
const includePreviousValue = (_a = options === null || options === void 0 ? void 0 : options.includePreviousValue) !== null && _a !== void 0 ? _a : false;
const objectHash = options === null || options === void 0 ? void 0 : options.objectHash;
const matchByPosition = options === null || options === void 0 ? void 0 : options.matchByPosition;
const context = {
result: undefined,
left: input.left,
right: input.right,
includePreviousValue,
objectHash,
matchByPosition,
stopped: false,
};
function process(context) {
var _a, _b;
const steps = [
nested_collectChildrenDiffFilter,
trivialDiffFilter,
nested_objectsDiffFilter,
array_diffFilter,
];
for (const step of steps) {
step(context);
if (context.stopped) {
context.stopped = false;
break;
}
}
if ((_a = context.children) === null || _a === void 0 ? void 0 : _a.length) {
for (const childrenContext of context.children) {
process(childrenContext);
if (childrenContext.result !== undefined) {
context.result = (_b = context.result) !== null && _b !== void 0 ? _b : {};
context.result[childrenContext.name] =
childrenContext.result;
}
}
if (context.result && context.leftIsArray) {
context.result._t = "a";
}
}
}
process(context);
return context.result;
}
exports.diff = diff;
// diff primitive values and non arrays
function trivialDiffFilter(context) {
if (context.left === context.right) {
context.result = undefined;
context.stopped = true;
return;
}
// Item was added
if (typeof context.left === "undefined") {
context.result = [context.right];
context.stopped = true;
return;
}
// Item was removed
if (typeof context.right === "undefined") {
const previousValue = context.includePreviousValue ? context.left : null;
context.result = [previousValue, 0, 0];
context.stopped = true;
return;
}
context.leftType = context.left === null ? "null" : typeof context.left;
context.rightType = context.right === null ? "null" : typeof context.right;
if (context.leftType !== context.rightType) {
const previousValue = context.includePreviousValue ? context.left : null;
context.result = [previousValue, context.right];
context.stopped = true;
return;
}
if (context.leftType === "boolean" ||
context.leftType === "number" ||
context.leftType === "string") {
const previousValue = context.includePreviousValue ? context.left : null;
context.result = [previousValue, context.right];
context.stopped = true;
return;
}
if (context.leftType === "object") {
context.leftIsArray = Array.isArray(context.left);
}
if (context.rightType === "object") {
context.rightIsArray = Array.isArray(context.right);
}
if (context.leftIsArray !== context.rightIsArray) {
const previousValue = context.includePreviousValue ? context.left : null;
context.result = [previousValue, context.right];
context.stopped = true;
return;
}
}
function nested_collectChildrenDiffFilter(context) {
if (!context || !context.children) {
return;
}
const length = context.children.length;
let child;
let result = context.result;
for (let index = 0; index < length; index++) {
child = context.children[index];
if (typeof child.result === "undefined") {
continue;
}
result = result !== null && result !== void 0 ? result : {};
result[child.name] = child.result;
}
if (result && context.leftIsArray) {
result["_t"] = "a";
}
context.result = result;
context.stopped = true;
}
function nested_objectsDiffFilter(context) {
if (context.leftIsArray || context.leftType !== "object") {
return;
}
const left = context.left;
const right = context.right;
for (const name in left) {
if (!Object.prototype.hasOwnProperty.call(left, name)) {
continue;
}
if (context.children === undefined) {
context.children = [];
}
context.children.push({
left: left[name],
right: right[name],
result: undefined,
name,
includePreviousValue: context.includePreviousValue,
objectHash: context.objectHash,
matchByPosition: context.matchByPosition,
stopped: false,
});
}
for (const name in right) {
if (!Object.prototype.hasOwnProperty.call(right, name)) {
continue;
}
if (typeof left[name] === "undefined") {
if (context.children === undefined) {
context.children = [];
}
context.children.push({
left: undefined,
right: right[name],
result: undefined,
name,
includePreviousValue: context.includePreviousValue,
objectHash: context.objectHash,
matchByPosition: context.matchByPosition,
stopped: false,
});
}
}
if (!context.children || context.children.length === 0) {
context.result = undefined;
context.stopped = true;
return;
}
context.stopped = true;
}
const ARRAY_MOVE = 3;
function array_diffFilter(context) {
if (!context.leftIsArray) {
return;
}
let matchContext = {
objectHash: context.objectHash,
matchByPosition: context.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;
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;
const left = context.left;
const right = context.right;
if (context.children === undefined) {
context.children = [];
}
context.children.push({
left: left[index],
right: right[index],
result: undefined,
name: index,
includePreviousValue: context.includePreviousValue,
objectHash: context.objectHash,
matchByPosition: context.matchByPosition,
stopped: false,
});
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;
const left = context.left;
const right = context.right;
if (context.children === undefined) {
context.children = [];
}
context.children.push({
left: left[index1],
right: right[index2],
result: undefined,
name: index2,
includePreviousValue: context.includePreviousValue,
objectHash: context.objectHash,
matchByPosition: context.matchByPosition,
stopped: false,
});
commonTail++;
}
if (commonHead + commonTail === len1) {
if (len1 === len2) {
// arrays are identical
context.result = undefined;
context.stopped = true;
return;
}
// trivial case, a block (1 or more consecutive items) was added
const result = {
_t: "a",
};
for (index = commonHead; index < len2 - commonTail; index++) {
result[index] = [array2[index]];
}
context.result = result;
context.stopped = true;
return;
}
if (commonHead + commonTail === len2) {
// trivial case, a block (1 or more consecutive items) was removed
const result = {
_t: "a",
};
for (index = commonHead; index < len1 - commonTail; index++) {
result[`_${index}`] = [
context.includePreviousValue ? array1[index] : null,
0,
0,
];
}
context.result = result;
context.stopped = true;
return;
}
// reset hash cache
delete matchContext.hashCache1;
delete matchContext.hashCache2;
// diff is not trivial, find the LCS (Longest Common Subsequence)
let trimmed1 = array1.slice(commonHead, len1 - commonTail);
let trimmed2 = array2.slice(commonHead, len2 - commonTail);
let seq = lcs.get(trimmed1, trimmed2, matchItems, matchContext);
let removedItems = [];
const result = {
_t: "a",
};
for (index = commonHead; index < len1 - commonTail; index++) {
if (seq.indices1.indexOf(index - commonHead) < 0) {
// removed
result[`_${index}`] = [
context.includePreviousValue ? array1[index] : null,
0,
0,
];
removedItems.push(index);
}
}
const detectMove = true;
let includeValueOnMove = true;
let removedItemsLength = removedItems.length;
for (index = commonHead; index < len2 - commonTail; index++) {
let 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];
if (matchItems(trimmed1, trimmed2, index1 - commonHead, index - commonHead, matchContext)) {
// store position move as: [originalValue, newPosition, ARRAY_MOVE]
result[`_${index1}`].splice(1, 2, index, ARRAY_MOVE);
if (!includeValueOnMove) {
// don't include moved value on diff, to save bytes
result[`_${index1}`][0] = "";
}
index2 = index;
if (context.children === undefined) {
context.children = [];
}
const left = context.left;
const right = context.right;
context.children.push({
left: left[index1],
right: right[index2],
result: undefined,
name: index2,
includePreviousValue: context.includePreviousValue,
objectHash: context.objectHash,
matchByPosition: context.matchByPosition,
stopped: false,
});
removedItems.splice(removeItemIndex1, 1);
isMove = true;
break;
}
}
}
if (!isMove) {
// added
result[index] = [array2[index]];
}
}
else {
// match, do inner diff
index1 = seq.indices1[indexOnArray2] + commonHead;
index2 = seq.indices2[indexOnArray2] + commonHead;
if (context.children === undefined) {
context.children = [];
}
const left = context.left;
const right = context.right;
context.children.push({
left: left[index1],
right: right[index2],
result: undefined,
name: index2,
includePreviousValue: context.includePreviousValue,
objectHash: context.objectHash,
matchByPosition: context.matchByPosition,
stopped: false,
});
}
}
context.result = result;
context.stopped = true;
}
function arraysHaveMatchByRef(array1, array2, len1, len2) {
for (let index1 = 0; index1 < len1; index1++) {
let val1 = array1[index1];
for (let index2 = 0; index2 < len2; index2++) {
let val2 = array2[index2];
if (index1 !== index2 && val1 === val2) {
return true;
}
}
}
return false;
}
function matchItems(array1, array2, index1, index2, context) {
let value1 = array1[index1];
let value2 = array2[index2];
if (value1 === value2) {
return true;
}
if (typeof value1 !== "object" || typeof value2 !== "object") {
return false;
}
let objectHash = context.objectHash;
if (!objectHash) {
// no way to match objects was provided, try match by position
return context.matchByPosition && index1 === index2;
}
let hash1;
let hash2;
if (typeof index1 === "number") {
context.hashCache1 = context.hashCache1 || [];
hash1 = context.hashCache1[index1];
if (typeof hash1 === "undefined") {
context.hashCache1[index1] = hash1 = objectHash(value1, index1);
}
}
else {
hash1 = objectHash(value1);
}
if (typeof hash1 === "undefined") {
return false;
}
if (typeof index2 === "number") {
context.hashCache2 = context.hashCache2 || [];
hash2 = context.hashCache2[index2];
if (typeof hash2 === "undefined") {
context.hashCache2[index2] = hash2 = objectHash(value2, index2);
}
}
else {
hash2 = objectHash(value2);
}
if (typeof hash2 === "undefined") {
return false;
}
return hash1 === hash2;
}