universal-diff
Version:
universal diff & merge algorithm realized with Javascript (between arbitrary scequences)
280 lines (214 loc) • 6.79 kB
JavaScript
/*! universal-diff v2.0.2 | nighca(nighca@live.cn) | Apache License(2.0) */
(function(global, undefined){
// steps
var STEP_NOCHANGE = 0,
STEP_REPLACE = 1,
STEP_REMOVE = 2,
STEP_INSERT = 3;
// script marks
var MARK_EMPTY = -1,
MARK_SAME = 0;
var defaultEqual = function(a, b){
return a === b;
};
// caculate min-edit-script (naive)
var naiveCompare = function(seq1, seq2, eq){
var l1 = seq1.length,
l2 = seq2.length,
distMap = Array.apply(null, {length: l1 + 1}).map(function(){return [];}),
stepMap = Array.apply(null, {length: l1 + 1}).map(function(){return [];}),
i, j;
eq = eq || defaultEqual;
for(i = 0; i <= l1; i++){
for(j = 0; j <= l2; j++){
if(i === 0 || j === 0){
distMap[i][j] = i || j;
stepMap[i][j] = i > 0 ? STEP_REMOVE : STEP_INSERT;
}else{
var equal = eq(seq1[i-1], seq2[j-1]),
removeDist = distMap[i-1][j] + 1,
insertDist = distMap[i][j-1] + 1,
replaceDist = distMap[i-1][j-1] + (equal ? 0 : 2),
dist = Math.min(replaceDist, removeDist, insertDist);
distMap[i][j] = dist;
switch(dist){
case replaceDist:
stepMap[i][j] = equal ? STEP_NOCHANGE : STEP_REPLACE;
break;
case removeDist:
stepMap[i][j] = STEP_REMOVE;
break;
case insertDist:
stepMap[i][j] = STEP_INSERT;
}
}
}
}
return stepMap;
};
// caculate min-edit-script (myers)
var myersCompare = function(seq1, seq2, eq){
var N = seq1.length,
M = seq2.length,
MAX = N + M,
stepMap = Array.apply(null, {length: M+N+1}).map(function(){return [];}),
furthestReaching = [],
dist = -1;
eq = eq || defaultEqual;
furthestReaching[MAX + 1] = 0;
// caculate min distance & log each step
for(var D = 0; D <= MAX && dist === -1; D++){
for(var k = -D, x, y, step; k <= D && dist === -1; k+=2){
if(k === -D || (k !== D && furthestReaching[k - 1 + MAX] < furthestReaching[k + 1 + MAX])){
x = furthestReaching[k + 1 + MAX];
step = STEP_INSERT;
}else{
x = furthestReaching[k - 1 + MAX] + 1;
step = STEP_REMOVE;
}
y = x - k;
stepMap[x][y] = step;
while(x < N && y < M && eq(seq1[x], seq2[y])){
x++;
y++;
stepMap[x][y] = STEP_NOCHANGE;
}
furthestReaching[k + MAX] = x;
if(x >= N && y >= M){
dist = D;
}
}
}
return stepMap;
};
// use myers as default
var coreCompare = myersCompare;
// stepMap to contrast array
var transformStepMap = function(seq1, seq2, stepMap){
// get contrast arrays (src & target) by analyze step by step
var l1 = seq1.length,
l2 = seq2.length,
src = [], target = [];
for(var i = l1,j = l2; i > 0 || j > 0;){
switch(stepMap[i][j]){
case STEP_NOCHANGE:
src.unshift(seq1[i-1]);
target.unshift(MARK_SAME);
i -= 1;
j -= 1;
break;
case STEP_REPLACE:
src.unshift(seq1[i-1]);
target.unshift(seq2[j-1]);
i -= 1;
j -= 1;
break;
case STEP_REMOVE:
src.unshift(seq1[i-1]);
target.unshift(MARK_EMPTY);
i -= 1;
j -= 0;
break;
case STEP_INSERT:
src.unshift(MARK_EMPTY);
target.unshift(seq2[j-1]);
i -= 0;
j -= 1;
break;
}
}
return {
src: src,
target: target
};
};
// get edit script
var compare = function(seq1, seq2, eq){
// do compare
var stepMap = coreCompare(seq1, seq2, eq);
// transform stepMap
var contrast = transformStepMap(seq1, seq2, stepMap),
src = contrast.src,
target = contrast.target;
// convert contrast arrays to edit script
var l = target.length,
start, len, to,
notEmpty = function(s){return s !== MARK_EMPTY;},
script = [],
i, j;
for(i = l - 1; i >= 0;){
// join continuous diffs
for(j = i; target[j] !== MARK_SAME && j >= 0; j--){}
if(j < i){
start = src.slice(0, j + 1).filter(notEmpty).length; // start pos of diffs (on src)
len = src.slice(j + 1, i + 1).filter(notEmpty).length; // length should be replaced (on src)
to = target.slice(j + 1, i + 1).filter(notEmpty); // new content
script.unshift(
to.length ?
[start, len, to] : // replace
[start, len] // remove
);
}
i = j - 1;
}
return script;
};
// merge
var merge = function(seq, script){
var result = seq.slice();
for(var i = script.length - 1, modify; i >= 0; i--){
modify = script[i];
var to = modify[2];
if(to){
modify = modify.slice(0, 2).concat(to);
}
result.splice.apply(result, modify);
}
return result;
};
// compare string (use splitter)
var compareStr = function(str1, str2, splitter){
splitter = typeof splitter === 'string' ? splitter : '';
var seq1 = str1.split(splitter),
seq2 = str2.split(splitter),
script = compare(seq1, seq2);
script.forEach(function(change){
if(change[2]){
change[2] = change[2].join(splitter);
}
});
return {
splitter: splitter,
diff: script
};
};
// merge string (add spliter back)
var mergeStr = function(cnt, compareResult){
var splitter = compareResult.splitter,
diff = compareResult.diff,
result = cnt.split(splitter);
for(var i = diff.length - 1, item; i >= 0; i--){
item = diff[i];
result.splice.apply(result, item);
}
return result.join(splitter);
};
var diff = {
coreCompare: coreCompare,
compare: compare,
merge: merge,
compareStr: compareStr,
mergeStr: mergeStr
};
// RequireJS && SeaJS
if(typeof define === 'function'){
define(function(){
return diff;
});
// NodeJS
}else if(typeof exports !== 'undefined'){
module.exports = diff;
}else{
global.diff = diff;
}
})(this);