jsondiffpatch
Version:
Diff & Patch for Javascript objects
439 lines (405 loc) • 12.9 kB
JavaScript
var DiffContext = require('../contexts/diff').DiffContext;
var PatchContext = require('../contexts/patch').PatchContext;
var ReverseContext = require('../contexts/reverse').ReverseContext;
var lcs = require('./lcs');
var ARRAY_MOVE = 3;
var isArray = (typeof Array.isArray === 'function') ?
// use native function
Array.isArray :
// use instanceof operator
function(a) {
return a instanceof Array;
};
var arrayIndexOf = typeof Array.prototype.indexOf === 'function' ?
function(array, item) {
return array.indexOf(item);
} : function(array, item) {
var length = array.length;
for (var i = 0; i < length; i++) {
if (array[i] === item) {
return i;
}
}
return -1;
};
function arraysHaveMatchByRef(array1, array2, len1, len2) {
for (var index1 = 0; index1 < len1; index1++) {
var val1 = array1[index1];
for (var index2 = 0; index2 < len2; index2++) {
var val2 = array2[index2];
if (val1 === val2) {
return true;
}
}
}
}
function matchItems(array1, array2, index1, index2, context) {
var value1 = array1[index1];
var value2 = array2[index2];
if (value1 === value2) {
return true;
}
if (typeof value1 !== 'object' || typeof value2 !== 'object') {
return false;
}
var objectHash = context.objectHash;
if (!objectHash) {
// no way to match objects was provided, try match by position
return context.matchByPosition && index1 === index2;
}
var hash1;
var 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;
}
var diffFilter = function arraysDiffFilter(context) {
if (!context.leftIsArray) {
return;
}
var matchContext = {
objectHash: context.options && context.options.objectHash,
matchByPosition: context.options && context.options.matchByPosition
};
var commonHead = 0;
var commonTail = 0;
var index;
var index1;
var index2;
var array1 = context.left;
var array2 = context.right;
var len1 = array1.length;
var len2 = array2.length;
var 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(context.left[index], context.right[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(context.left[index1], context.right[index2]);
context.push(child, index2);
commonTail++;
}
var 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.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++) {
result['_' + index] = [array1[index], 0, 0];
}
context.setResult(result).exit();
return;
}
// reset hash cache
delete matchContext.hashCache1;
delete matchContext.hashCache2;
// diff is not trivial, find the LCS (Longest Common Subsequence)
var trimmed1 = array1.slice(commonHead, len1 - commonTail);
var trimmed2 = array2.slice(commonHead, len2 - commonTail);
var seq = lcs.get(
trimmed1, trimmed2,
matchItems,
matchContext
);
var removedItems = [];
result = result || {
_t: 'a'
};
for (index = commonHead; index < len1 - commonTail; index++) {
if (arrayIndexOf(seq.indices1, index - commonHead) < 0) {
// removed
result['_' + index] = [array1[index], 0, 0];
removedItems.push(index);
}
}
var detectMove = true;
if (context.options && context.options.arrays && context.options.arrays.detectMove === false) {
detectMove = false;
}
var includeValueOnMove = false;
if (context.options && context.options.arrays && context.options.arrays.includeValueOnMove) {
includeValueOnMove = true;
}
var removedItemsLength = removedItems.length;
for (index = commonHead; index < len2 - commonTail; index++) {
var indexOnArray2 = arrayIndexOf(seq.indices2, index - commonHead);
if (indexOnArray2 < 0) {
// added, try to match with a removed item and register as position move
var isMove = false;
if (detectMove && removedItemsLength > 0) {
for (var 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;
child = new DiffContext(context.left[index1], context.right[index2]);
context.push(child, index2);
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;
child = new DiffContext(context.left[index1], context.right[index2]);
context.push(child, index2);
}
}
context.setResult(result).exit();
};
diffFilter.filterName = 'arrays';
var compare = {
numerically: function(a, b) {
return a - b;
},
numericallyBy: function(name) {
return function(a, b) {
return a[name] - b[name];
};
}
};
var patchFilter = function nestedPatchFilter(context) {
if (!context.nested) {
return;
}
if (context.delta._t !== 'a') {
return;
}
var index, index1;
var delta = context.delta;
var array = context.left;
// first, separate removals, insertions and modifications
var toRemove = [];
var toInsert = [];
var 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) {
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];
var indexDiff = delta['_' + index1];
var removedValue = array.splice(index1, 1)[0];
if (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'));
var toInsertLength = toInsert.length;
for (index = 0; index < toInsertLength; index++) {
var insertion = toInsert[index];
array.splice(insertion.index, 0, insertion.value);
}
// apply modifications
var toModifyLength = toModify.length;
var child;
if (toModifyLength > 0) {
for (index = 0; index < toModifyLength; index++) {
var modification = toModify[index];
child = new PatchContext(context.left[modification.index], modification.delta);
context.push(child, modification.index);
}
}
if (!context.children) {
context.setResult(context.left).exit();
return;
}
context.exit();
};
patchFilter.filterName = 'arrays';
var collectChildrenPatchFilter = function collectChildrenPatchFilter(context) {
if (!context || !context.children) {
return;
}
if (context.delta._t !== 'a') {
return;
}
var length = context.children.length;
var child;
for (var index = 0; index < length; index++) {
child = context.children[index];
context.left[child.childName] = child.result;
}
context.setResult(context.left).exit();
};
collectChildrenPatchFilter.filterName = 'arraysCollectChildren';
var reverseFilter = function arraysReverseFilter(context) {
if (!context.nested) {
if (context.delta[2] === ARRAY_MOVE) {
context.newName = '_' + context.delta[1];
context.setResult([context.delta[0], parseInt(context.childName.substr(1), 10), ARRAY_MOVE]).exit();
}
return;
}
if (context.delta._t !== 'a') {
return;
}
var name, child;
for (name in context.delta) {
if (name === '_t') {
continue;
}
child = new ReverseContext(context.delta[name]);
context.push(child, name);
}
context.exit();
};
reverseFilter.filterName = 'arrays';
var reverseArrayDeltaIndex = function(delta, index, itemDelta) {
if (typeof index === 'string' && index[0] === '_') {
return parseInt(index.substr(1), 10);
} else if (isArray(itemDelta) && itemDelta[2] === 0) {
return '_' + index;
}
var reverseIndex = +index;
for (var deltaIndex in delta) {
var deltaItem = delta[deltaIndex];
if (isArray(deltaItem)) {
if (deltaItem[2] === ARRAY_MOVE) {
var moveFromIndex = parseInt(deltaIndex.substr(1), 10);
var 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) {
var deleteIndex = parseInt(deltaIndex.substr(1), 10);
if (deleteIndex <= reverseIndex) {
reverseIndex++;
}
} else if (deltaItem.length === 1 && deltaIndex <= reverseIndex) {
reverseIndex--;
}
}
}
return reverseIndex;
};
var collectChildrenReverseFilter = function collectChildrenReverseFilter(context) {
if (!context || !context.children) {
return;
}
if (context.delta._t !== 'a') {
return;
}
var length = context.children.length;
var child;
var delta = {
_t: 'a'
};
for (var index = 0; index < length; index++) {
child = context.children[index];
var name = child.newName;
if (typeof name === 'undefined') {
name = reverseArrayDeltaIndex(context.delta, child.childName, child.result);
}
if (delta[name] !== child.result) {
delta[name] = child.result;
}
}
context.setResult(delta).exit();
};
collectChildrenReverseFilter.filterName = 'arraysCollectChildren';
exports.diffFilter = diffFilter;
exports.patchFilter = patchFilter;
exports.collectChildrenPatchFilter = collectChildrenPatchFilter;
exports.reverseFilter = reverseFilter;
exports.collectChildrenReverseFilter = collectChildrenReverseFilter;