UNPKG

jiff

Version:

JSON diff and patch based on rfc6902

178 lines (151 loc) 4.39 kB
var jsonPointer = require('./jsonPointer'); /** * commute the patch sequence a,b to b,a * @param {object} a patch operation * @param {object} b patch operation */ module.exports = function commutePaths(a, b) { // TODO: cases for special paths: '' and '/' var left = jsonPointer.parse(a.path); var right = jsonPointer.parse(b.path); var prefix = getCommonPathPrefix(left, right); var isArray = isArrayPath(left, right, prefix.length); // Never mutate the originals var ac = copyPatch(a); var bc = copyPatch(b); if(prefix.length === 0 && !isArray) { // Paths share no common ancestor, simple swap return [bc, ac]; } if(isArray) { return commuteArrayPaths(ac, left, bc, right); } else { return commuteTreePaths(ac, left, bc, right); } }; function commuteTreePaths(a, left, b, right) { if(a.path === b.path) { throw new TypeError('cannot commute ' + a.op + ',' + b.op + ' with identical object paths'); } // FIXME: Implement tree path commutation return [b, a]; } /** * Commute two patches whose common ancestor (which may be the immediate parent) * is an array * @param a * @param left * @param b * @param right * @returns {*} */ function commuteArrayPaths(a, left, b, right) { if(left.length === right.length) { return commuteArraySiblings(a, left, b, right); } if (left.length > right.length) { // left is longer, commute by "moving" it to the right left = commuteArrayAncestor(b, right, a, left, -1); a.path = jsonPointer.absolute(jsonPointer.join(left)); } else { // right is longer, commute by "moving" it to the left right = commuteArrayAncestor(a, left, b, right, 1); b.path = jsonPointer.absolute(jsonPointer.join(right)); } return [b, a]; } function isArrayPath(left, right, index) { return jsonPointer.isValidArrayIndex(left[index]) && jsonPointer.isValidArrayIndex(right[index]); } /** * Commute two patches referring to items in the same array * @param l * @param lpath * @param r * @param rpath * @returns {*[]} */ function commuteArraySiblings(l, lpath, r, rpath) { var target = lpath.length-1; var lindex = +lpath[target]; var rindex = +rpath[target]; var commuted; if(lindex < rindex) { // Adjust right path if(l.op === 'add' || l.op === 'copy') { commuted = rpath.slice(); commuted[target] = Math.max(0, rindex - 1); r.path = jsonPointer.absolute(jsonPointer.join(commuted)); } else if(l.op === 'remove') { commuted = rpath.slice(); commuted[target] = rindex + 1; r.path = jsonPointer.absolute(jsonPointer.join(commuted)); } } else if(r.op === 'add' || r.op === 'copy') { // Adjust left path commuted = lpath.slice(); commuted[target] = lindex + 1; l.path = jsonPointer.absolute(jsonPointer.join(commuted)); } else if (lindex > rindex && r.op === 'remove') { // Adjust left path only if remove was at a (strictly) lower index commuted = lpath.slice(); commuted[target] = Math.max(0, lindex - 1); l.path = jsonPointer.absolute(jsonPointer.join(commuted)); } return [r, l]; } /** * Commute two patches with a common array ancestor * @param l * @param lpath * @param r * @param rpath * @param direction * @returns {*} */ function commuteArrayAncestor(l, lpath, r, rpath, direction) { // rpath is longer or same length var target = lpath.length-1; var lindex = +lpath[target]; var rindex = +rpath[target]; // Copy rpath, then adjust its array index var rc = rpath.slice(); if(lindex > rindex) { return rc; } if(l.op === 'add' || l.op === 'copy') { rc[target] = Math.max(0, rindex - direction); } else if(l.op === 'remove') { rc[target] = Math.max(0, rindex + direction); } return rc; } function getCommonPathPrefix(p1, p2) { var p1l = p1.length; var p2l = p2.length; if(p1l === 0 || p2l === 0 || (p1l < 2 && p2l < 2)) { return []; } // If paths are same length, the last segment cannot be part // of a common prefix. If not the same length, the prefix cannot // be longer than the shorter path. var l = p1l === p2l ? p1l - 1 : Math.min(p1l, p2l); var i = 0; while(i < l && p1[i] === p2[i]) { ++i } return p1.slice(0, i); } function copyPatch(p) { if(p.op === 'remove') { return { op: p.op, path: p.path }; } if(p.op === 'copy' || p.op === 'move') { return { op: p.op, path: p.path, from: p.from }; } // test, add, replace return { op: p.op, path: p.path, value: p.value }; }