UNPKG

@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.

705 lines (701 loc) 23.3 kB
/** * LCS implementation that supports arrays or strings * * reference: http://en.wikipedia.org/wiki/Longest_common_subsequence_problem */ function defaultMatch(array1, array2, index1, index2) { return array1[index1] === array2[index2]; } function lengthMatrix(array1, array2, match, context) { const len1 = array1.length; const len2 = array2.length; // initialize empty matrix of len1+1 x len2+1 let matrix = Object.assign([len1 + 1], { match, }); for (let x = 0; x < len1 + 1; x++) { matrix[x] = [len2 + 1]; for (let y = 0; y < len2 + 1; y++) { matrix[x][y] = 0; } } // save sequence lengths for each coordinate for (let x = 1; x < len1 + 1; x++) { for (let y = 1; y < len2 + 1; y++) { if (match(array1, array2, x - 1, y - 1, context)) { matrix[x][y] = matrix[x - 1][y - 1] + 1; } else { matrix[x][y] = Math.max(matrix[x - 1][y], matrix[x][y - 1]); } } } return matrix; } function backtrack(matrix, array1, array2, context) { let index1 = array1.length; let index2 = array2.length; const subsequence = { sequence: [], indices1: [], indices2: [], }; while (index1 !== 0 && index2 !== 0) { const sameLetter = matrix.match(array1, array2, index1 - 1, index2 - 1, context); if (sameLetter) { subsequence.sequence.unshift(array1[index1 - 1]); subsequence.indices1.unshift(index1 - 1); subsequence.indices2.unshift(index2 - 1); --index1; --index2; } else { const valueAtMatrixAbove = matrix[index1][index2 - 1]; const valueAtMatrixLeft = matrix[index1 - 1][index2]; if (valueAtMatrixAbove > valueAtMatrixLeft) { --index2; } else { --index1; } } } return subsequence; } function get(array1, array2, match, context) { const innerContext = context || {}; const matrix = lengthMatrix(array1, array2, match || defaultMatch, innerContext); return backtrack(matrix, array1, array2, innerContext); } 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; } // 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 = 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); } } 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 ( 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); 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; } function patch(params) { const context = { left: params.left, delta: params.delta, children: undefined, result: undefined, name: undefined, nested: false, stopped: false, }; function process(context) { var _a; const steps = [ nested_collectChildrenPatchFilter, array_collectChildrenPatchFilter, trivial_patchFilter, nested_patchFilter, array_patchFilter, ]; for (const step of steps) { step(context); if (context.stopped) { context.stopped = false; break; } } if (context.children) { for (const childrenContext of context.children) { process(childrenContext); context.result = (_a = context.result) !== null && _a !== void 0 ? _a : context.left; context.result[childrenContext.name] = childrenContext.result; } } } process(context); return context.result; } function nested_collectChildrenPatchFilter(context) { if (!context || !context.children) { return; } if (context.delta._t) { return; } let length = context.children.length; let child; for (let index = 0; index < length; index++) { child = context.children[index]; if (Object.prototype.hasOwnProperty.call(context.left, child.name) && child.result === undefined) { delete context.left[child.name]; } else if (context.left[child.name] !== child.result) { context.left[child.name] = child.result; } } context.result = context.left; context.stopped = true; } function array_collectChildrenPatchFilter(context) { if (!context || !context.children) { return; } if (context.delta._t !== "a") { return; } let length = context.children.length; let child; for (let index = 0; index < length; index++) { child = context.children[index]; context.left[child.name] = child.result; } context.result = context.left; context.stopped = true; } function trivial_patchFilter(context) { if (typeof context.delta === "undefined") { context.result = context.left; return; } context.nested = !Array.isArray(context.delta); if (context.nested) { return; } if (context.delta.length === 1) { context.result = context.delta[0]; context.stopped = true; return; } if (context.delta.length === 2) { context.result = context.delta[1]; context.stopped = true; return; } if (context.delta.length === 3 && context.delta[2] === 0) { context.result = undefined; context.stopped = true; } } function nested_patchFilter(context) { if (!context.nested) { return; } if (context.delta._t) { return; } let name; for (name in context.delta) { if (context.children === undefined) { context.children = []; } context.children.push({ left: context.left[name], delta: context.delta[name], result: undefined, name, stopped: false, }); } context.stopped = true; } const ARRAY_MOVE$1 = 3; let compare = { numerically(a, b) { return a - b; }, numericallyBy(name) { return (a, b) => a[name] - b[name]; }, }; function array_patchFilter(context) { if (!context.nested) { return; } if (context.delta._t !== "a") { return; } let index; let index1; let delta = context.delta; let array = context.left; // first, separate removals, insertions and modifications let toRemove = []; let toInsert = []; let toModify = []; for (index in delta) { if (index !== "_t") { if (index[0] === "_") { // removed item from original array if (delta[index][2] === 0 || delta[index][2] === ARRAY_MOVE$1) { toRemove.push(parseInt(index.slice(1), 10)); } else { throw new Error(`only removal or move can be applied at original array indices,` + ` invalid diff type: ${delta[index][2]}`); } } else { if (delta[index].length === 1) { // added item at new array toInsert.push({ index: parseInt(index, 10), value: delta[index][0], }); } else { // modified item at new array toModify.push({ index: parseInt(index, 10), delta: delta[index], }); } } } } // 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]; let indexDiff = delta[`_${index1}`]; let removedValue = array.splice(index1, 1)[0]; if (indexDiff[2] === ARRAY_MOVE$1) { // 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")); let toInsertLength = toInsert.length; for (index = 0; index < toInsertLength; index++) { let insertion = toInsert[index]; array.splice(insertion.index, 0, insertion.value); } // apply modifications let toModifyLength = toModify.length; if (toModifyLength > 0) { for (index = 0; index < toModifyLength; index++) { let modification = toModify[index]; if (context.children === undefined) { context.children = []; } context.children.push({ left: context.left[modification.index], delta: modification.delta, name: modification.index, result: undefined, stopped: false, }); } } if (!context.children) { context.result = context.left; context.stopped = true; return; } } export { diff, patch };