UNPKG

@clipchamp/json-delta

Version:

Json object diff / patching with configurable short-circuit tolerance. Forked from: github.com/corps

269 lines (268 loc) 8.47 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const isArray_1 = require("./util/isArray"); const isInsert_1 = require("./util/isInsert"); function isObject(o) { return o instanceof Object && !(o instanceof Array); } exports.isObject = isObject; function shallowCopy(o) { if (isObject(o)) { return Object.assign({}, o); } if (isArray_1.isArray(o)) { return o.slice(); } return o; } exports.shallowCopy = shallowCopy; function getContainer(orig, result, path) { let len = path.length; if (!len) return undefined; let origContainer = orig; let container = result; if (container === origContainer) container = shallowCopy(origContainer); for (let i = 0; i < len - 1; ++i) { let seg = path[i]; if (typeof seg === 'number' && isArray_1.isArray(origContainer) && isArray_1.isArray(container)) { origContainer = origContainer[seg]; if (container[seg] === origContainer) { container = container[seg] = shallowCopy(origContainer); } else { container = container[seg]; } } if (typeof seg === 'string' && isObject(origContainer) && isObject(container)) { origContainer = origContainer[seg]; if (container[seg] === origContainer) { container = container[seg] = shallowCopy(origContainer); } else { container = container[seg]; } } } return container; } exports.getContainer = getContainer; function getVal(container, path) { let len = path.length; for (let i = 0; i < len; ++i) { let seg = path[i]; if (typeof seg === 'number' && isArray_1.isArray(container)) { container = container[seg]; } if (typeof seg === 'string' && isObject(container)) { container = container[seg]; } } return container; } exports.getVal = getVal; function applyDiff(o, d) { if (!d) return o; let result = shallowCopy(o); d.forEach(p => { if (isInsert_1.isInsert(p)) result = applyInsert(o, result, p); else result = applyDelete(o, result, p); }); return result; } exports.applyDiff = applyDiff; function applyInsert(orig, result, insert) { let [path, val] = insert; let container = getContainer(orig, result, path); if (!container) return val; let key = path[path.length - 1]; if (typeof key === 'number' && isArray_1.isArray(container)) { container.splice(key, 0, val); } if (typeof key === 'string' && isObject(container)) { container[key] = val; } return result; } exports.applyInsert = applyInsert; function applyDelete(orig, result, path) { let container = getContainer(orig, result, path); if (!container) return null; let key = path[path.length - 1]; if (typeof key === 'number' && isArray_1.isArray(container)) { container.splice(key, 1); return result; } if (typeof key === 'string' && isObject(container)) { delete container[key]; return result; } return null; } exports.applyDelete = applyDelete; function diff(a, b, tolerance = Infinity) { let result = []; if (gatherDiff(a, b, tolerance, [], result) || result.length > tolerance) return [[[], b]]; if (result.length === 0) return null; return result; } exports.diff = diff; function gatherDiff(a, b, tolerance = 3, path, result) { if (a === undefined) a = null; if (b === undefined) b = null; if (typeof a === 'number' && isNaN(a)) a = null; if (typeof b === 'number' && isNaN(b)) b = null; if (a === b) return false; if (typeof a !== typeof b) { result.push([path, b]); return false; } if (a instanceof Array) { if (!(b instanceof Array)) { result.push([path, b]); return false; } let offset = 0; const thunks = []; if (!arrDiff(a, b, tolerance - result.length, () => thunks.push(() => ++offset), (aIdx, bIdx) => thunks.push(() => result.push(path.concat([offset]))), (aIdx, bIdx) => thunks.push(() => { result.push([path.concat([offset++]), b[bIdx]]); }))) return true; for (let i = thunks.length - 1; i >= 0; --i) { thunks[i](); } return false; } if (b instanceof Array) { result.push([path, b]); return false; } if (a instanceof Object) { if (!(b instanceof Object)) { result.push([path, b]); return false; } for (var k in a) { if (!(k in (b))) { result.push(path.concat([k])); if (result.length > tolerance) { return true; } continue; } if (gatherDiff(a[k], b[k], tolerance, path.concat([k]), result)) { return true; } if (result.length > tolerance) { return true; } } for (var k in b) { if (!(k in a)) { result.push([path.concat([k]), b[k]]); if (result.length > tolerance) { return true; } } } return false; } result.push([path, b]); return false; } function deepEqual(a, b) { return a === b || diff(a, b, 0) == null; } exports.deepEqual = deepEqual; /** * Finds the longest common subsequence between a and b, * optionally shortcutting any search whose removed elements * would exceed the provided tolerance value. * If there is no match within the provided tolerance, this function * returns null. */ function lcs(a, b, tolerance = a.length + b.length) { let result = []; return arrDiff(a, b, tolerance, aIdx => result.push(a[aIdx])) ? result.reverse() : null; } exports.lcs = lcs; function arrDiff(a, b, tolerance = a.length + b.length, onEq, onPickA = () => null, onPickB = () => null) { tolerance = Math.min(tolerance, a.length + b.length); let aLen = a.length; let bLen = b.length; let aOfDiagonal = new Uint32Array(tolerance * 2 + 2); let aOfDiagonalForEditSize = new Array(tolerance + 1); let shortestEdit = (function () { for (var d = 0; d <= tolerance; ++d) { for (var k = -d; k <= d; k += 2) { let aIdx; let takeB = aOfDiagonal[k + 1 + tolerance]; let takeA = aOfDiagonal[k - 1 + tolerance]; if (k === -d || (k !== d && takeA < takeB)) { aIdx = takeB; } else { aIdx = takeA + 1; } let bIdx = aIdx - k; while (aIdx < aLen && bIdx < bLen && deepEqual(a[aIdx], b[bIdx])) { aIdx++; bIdx++; } aOfDiagonal[k + tolerance] = aIdx; if (aIdx >= aLen && bIdx >= bLen) { aOfDiagonalForEditSize[d] = aOfDiagonal.slice(); return [d, k]; } } aOfDiagonalForEditSize[d] = aOfDiagonal.slice(); } return null; })(); if (shortestEdit) { let [d, k] = shortestEdit; let aIdx = aOfDiagonalForEditSize[d][k + tolerance]; let bIdx = aIdx - k; while (d > 0) { let k = aIdx - bIdx; let v = aOfDiagonalForEditSize[d - 1]; let prevK; if (k === -d || (k !== d && v[k - 1 + tolerance] < v[k + 1 + tolerance])) { prevK = k + 1; } else { prevK = k - 1; } let prevAIdx = v[prevK + tolerance]; let prevBIdx = prevAIdx - prevK; while (aIdx > prevAIdx && bIdx > prevBIdx) { onEq(--aIdx, --bIdx); } if (aIdx > prevAIdx) { onPickA(--aIdx, bIdx); } else if (bIdx > prevBIdx) { onPickB(aIdx, --bIdx); } --d; } while (aIdx > 0 && bIdx > 0) { onEq(--aIdx, --bIdx); } return true; } return false; }