198 lines
5.3 KiB
JavaScript
198 lines
5.3 KiB
JavaScript
// Myers' Diff Algorithm
|
|
// Modified from https://github.com/kpdecker/jsdiff/blob/master/src/diff/base.js
|
|
function Diff() {}
|
|
|
|
Diff.prototype = {
|
|
diff: function (oldArr, newArr, equals) {
|
|
if (!equals) {
|
|
equals = function (a, b) {
|
|
return a === b;
|
|
};
|
|
}
|
|
|
|
this.equals = equals;
|
|
var self = this;
|
|
oldArr = oldArr.slice();
|
|
newArr = newArr.slice(); // Allow subclasses to massage the input prior to running
|
|
|
|
var newLen = newArr.length;
|
|
var oldLen = oldArr.length;
|
|
var editLength = 1;
|
|
var maxEditLength = newLen + oldLen;
|
|
var bestPath = [{
|
|
newPos: -1,
|
|
components: []
|
|
}]; // Seed editLength = 0, i.e. the content starts with the same values
|
|
|
|
var oldPos = this.extractCommon(bestPath[0], newArr, oldArr, 0);
|
|
|
|
if (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen) {
|
|
var indices = [];
|
|
|
|
for (var i = 0; i < newArr.length; i++) {
|
|
indices.push(i);
|
|
} // Identity per the equality and tokenizer
|
|
|
|
|
|
return [{
|
|
indices: indices,
|
|
count: newArr.length
|
|
}];
|
|
} // Main worker method. checks all permutations of a given edit length for acceptance.
|
|
|
|
|
|
function execEditLength() {
|
|
for (var diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) {
|
|
var basePath;
|
|
var addPath = bestPath[diagonalPath - 1];
|
|
var removePath = bestPath[diagonalPath + 1];
|
|
var oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
|
|
|
|
if (addPath) {
|
|
// No one else is going to attempt to use this value, clear it
|
|
bestPath[diagonalPath - 1] = undefined;
|
|
}
|
|
|
|
var canAdd = addPath && addPath.newPos + 1 < newLen;
|
|
var canRemove = removePath && 0 <= oldPos && oldPos < oldLen;
|
|
|
|
if (!canAdd && !canRemove) {
|
|
// If this path is a terminal then prune
|
|
bestPath[diagonalPath] = undefined;
|
|
continue;
|
|
} // Select the diagonal that we want to branch from. We select the prior
|
|
// path whose position in the new string is the farthest from the origin
|
|
// and does not pass the bounds of the diff graph
|
|
|
|
|
|
if (!canAdd || canRemove && addPath.newPos < removePath.newPos) {
|
|
basePath = clonePath(removePath);
|
|
self.pushComponent(basePath.components, undefined, true);
|
|
} else {
|
|
basePath = addPath; // No need to clone, we've pulled it from the list
|
|
|
|
basePath.newPos++;
|
|
self.pushComponent(basePath.components, true, undefined);
|
|
}
|
|
|
|
oldPos = self.extractCommon(basePath, newArr, oldArr, diagonalPath); // If we have hit the end of both strings, then we are done
|
|
|
|
if (basePath.newPos + 1 >= newLen && oldPos + 1 >= oldLen) {
|
|
return buildValues(self, basePath.components, newArr, oldArr);
|
|
} else {
|
|
// Otherwise track this path as a potential candidate and continue.
|
|
bestPath[diagonalPath] = basePath;
|
|
}
|
|
}
|
|
|
|
editLength++;
|
|
}
|
|
|
|
while (editLength <= maxEditLength) {
|
|
var ret = execEditLength();
|
|
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
}
|
|
},
|
|
pushComponent: function (components, added, removed) {
|
|
var last = components[components.length - 1];
|
|
|
|
if (last && last.added === added && last.removed === removed) {
|
|
// We need to clone here as the component clone operation is just
|
|
// as shallow array clone
|
|
components[components.length - 1] = {
|
|
count: last.count + 1,
|
|
added: added,
|
|
removed: removed
|
|
};
|
|
} else {
|
|
components.push({
|
|
count: 1,
|
|
added: added,
|
|
removed: removed
|
|
});
|
|
}
|
|
},
|
|
extractCommon: function (basePath, newArr, oldArr, diagonalPath) {
|
|
var newLen = newArr.length;
|
|
var oldLen = oldArr.length;
|
|
var newPos = basePath.newPos;
|
|
var oldPos = newPos - diagonalPath;
|
|
var commonCount = 0;
|
|
|
|
while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newArr[newPos + 1], oldArr[oldPos + 1])) {
|
|
newPos++;
|
|
oldPos++;
|
|
commonCount++;
|
|
}
|
|
|
|
if (commonCount) {
|
|
basePath.components.push({
|
|
count: commonCount
|
|
});
|
|
}
|
|
|
|
basePath.newPos = newPos;
|
|
return oldPos;
|
|
},
|
|
tokenize: function (value) {
|
|
return value.slice();
|
|
},
|
|
join: function (value) {
|
|
return value.slice();
|
|
}
|
|
};
|
|
|
|
function buildValues(diff, components, newArr, oldArr) {
|
|
var componentPos = 0;
|
|
var componentLen = components.length;
|
|
var newPos = 0;
|
|
var oldPos = 0;
|
|
|
|
for (; componentPos < componentLen; componentPos++) {
|
|
var component = components[componentPos];
|
|
|
|
if (!component.removed) {
|
|
var indices = [];
|
|
|
|
for (var i = newPos; i < newPos + component.count; i++) {
|
|
indices.push(i);
|
|
}
|
|
|
|
component.indices = indices;
|
|
newPos += component.count; // Common case
|
|
|
|
if (!component.added) {
|
|
oldPos += component.count;
|
|
}
|
|
} else {
|
|
var indices = [];
|
|
|
|
for (var i = oldPos; i < oldPos + component.count; i++) {
|
|
indices.push(i);
|
|
}
|
|
|
|
component.indices = indices;
|
|
oldPos += component.count;
|
|
}
|
|
}
|
|
|
|
return components;
|
|
}
|
|
|
|
function clonePath(path) {
|
|
return {
|
|
newPos: path.newPos,
|
|
components: path.components.slice(0)
|
|
};
|
|
}
|
|
|
|
var arrayDiff = new Diff();
|
|
|
|
function _default(oldArr, newArr, callback) {
|
|
return arrayDiff.diff(oldArr, newArr, callback);
|
|
}
|
|
|
|
module.exports = _default; |