new major change
This commit is contained in:
+1
-5
@@ -1,6 +1,6 @@
|
||||
# Fast Diff [](https://travis-ci.org/jhchen/fast-diff)
|
||||
|
||||
This is a simplified import of the excellent [diff-match-patch](https://code.google.com/p/google-diff-match-patch/) library by [Neil Fraser](https://neil.fraser.name/) into the Node.js environment. The match and patch parts are removed, as well as all the extra diff options. What remains is incredibly fast diffing between two strings.
|
||||
This is a simplified import of the excellent [diff-match-patch](https://code.google.com/p/google-diff-match-patch/) library by [Neil Fraser](https://neil.fraser.name/) into the Node.js environment. The match and patch parts are removed, as well as all the extra diff options. What is incredibly fast diffing between two strings.
|
||||
|
||||
The diff function is an implementation of ["An O(ND) Difference Algorithm and its Variations" (Myers, 1986)](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.4.6927&rep=rep1&type=pdf) with the suggested divide and conquer strategy along with several [optimizations](http://neil.fraser.name/news/2007/10/09/) Neil added.
|
||||
|
||||
@@ -13,10 +13,6 @@ var bad = 'Bad dog';
|
||||
var result = diff(good, bad);
|
||||
// [[-1, "Goo"], [1, "Ba"], [0, "d dog"]]
|
||||
|
||||
// Respect suggested edit location (cursor position), added in v1.1
|
||||
diff('aaa', 'aaaa', 1)
|
||||
// [[0, "a"], [1, "a"], [0, "aa"]]
|
||||
|
||||
// For convenience
|
||||
diff.INSERT === 1;
|
||||
diff.EQUAL === 0;
|
||||
|
||||
+2
-170
@@ -39,10 +39,9 @@ var DIFF_EQUAL = 0;
|
||||
* any common prefix or suffix off the texts before diffing.
|
||||
* @param {string} text1 Old string to be diffed.
|
||||
* @param {string} text2 New string to be diffed.
|
||||
* @param {Int} cursor_pos Expected edit position in text1 (optional)
|
||||
* @return {Array} Array of diff tuples.
|
||||
*/
|
||||
function diff_main(text1, text2, cursor_pos) {
|
||||
function diff_main(text1, text2) {
|
||||
// Check for equality (speedup).
|
||||
if (text1 == text2) {
|
||||
if (text1) {
|
||||
@@ -51,11 +50,6 @@ function diff_main(text1, text2, cursor_pos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check cursor_pos within bounds
|
||||
if (cursor_pos < 0 || text1.length < cursor_pos) {
|
||||
cursor_pos = null;
|
||||
}
|
||||
|
||||
// Trim off common prefix (speedup).
|
||||
var commonlength = diff_commonPrefix(text1, text2);
|
||||
var commonprefix = text1.substring(0, commonlength);
|
||||
@@ -79,10 +73,6 @@ function diff_main(text1, text2, cursor_pos) {
|
||||
diffs.push([DIFF_EQUAL, commonsuffix]);
|
||||
}
|
||||
diff_cleanupMerge(diffs);
|
||||
if (cursor_pos != null) {
|
||||
diffs = fix_cursor(diffs, cursor_pos);
|
||||
}
|
||||
diffs = fix_emoji(diffs);
|
||||
return diffs;
|
||||
};
|
||||
|
||||
@@ -576,163 +566,5 @@ diff.INSERT = DIFF_INSERT;
|
||||
diff.DELETE = DIFF_DELETE;
|
||||
diff.EQUAL = DIFF_EQUAL;
|
||||
|
||||
|
||||
module.exports = diff;
|
||||
|
||||
/*
|
||||
* Modify a diff such that the cursor position points to the start of a change:
|
||||
* E.g.
|
||||
* cursor_normalize_diff([[DIFF_EQUAL, 'abc']], 1)
|
||||
* => [1, [[DIFF_EQUAL, 'a'], [DIFF_EQUAL, 'bc']]]
|
||||
* cursor_normalize_diff([[DIFF_INSERT, 'new'], [DIFF_DELETE, 'xyz']], 2)
|
||||
* => [2, [[DIFF_INSERT, 'new'], [DIFF_DELETE, 'xy'], [DIFF_DELETE, 'z']]]
|
||||
*
|
||||
* @param {Array} diffs Array of diff tuples
|
||||
* @param {Int} cursor_pos Suggested edit position. Must not be out of bounds!
|
||||
* @return {Array} A tuple [cursor location in the modified diff, modified diff]
|
||||
*/
|
||||
function cursor_normalize_diff (diffs, cursor_pos) {
|
||||
if (cursor_pos === 0) {
|
||||
return [DIFF_EQUAL, diffs];
|
||||
}
|
||||
for (var current_pos = 0, i = 0; i < diffs.length; i++) {
|
||||
var d = diffs[i];
|
||||
if (d[0] === DIFF_DELETE || d[0] === DIFF_EQUAL) {
|
||||
var next_pos = current_pos + d[1].length;
|
||||
if (cursor_pos === next_pos) {
|
||||
return [i + 1, diffs];
|
||||
} else if (cursor_pos < next_pos) {
|
||||
// copy to prevent side effects
|
||||
diffs = diffs.slice();
|
||||
// split d into two diff changes
|
||||
var split_pos = cursor_pos - current_pos;
|
||||
var d_left = [d[0], d[1].slice(0, split_pos)];
|
||||
var d_right = [d[0], d[1].slice(split_pos)];
|
||||
diffs.splice(i, 1, d_left, d_right);
|
||||
return [i + 1, diffs];
|
||||
} else {
|
||||
current_pos = next_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('cursor_pos is out of bounds!')
|
||||
}
|
||||
|
||||
/*
|
||||
* Modify a diff such that the edit position is "shifted" to the proposed edit location (cursor_position).
|
||||
*
|
||||
* Case 1)
|
||||
* Check if a naive shift is possible:
|
||||
* [0, X], [ 1, Y] -> [ 1, Y], [0, X] (if X + Y === Y + X)
|
||||
* [0, X], [-1, Y] -> [-1, Y], [0, X] (if X + Y === Y + X) - holds same result
|
||||
* Case 2)
|
||||
* Check if the following shifts are possible:
|
||||
* [0, 'pre'], [ 1, 'prefix'] -> [ 1, 'pre'], [0, 'pre'], [ 1, 'fix']
|
||||
* [0, 'pre'], [-1, 'prefix'] -> [-1, 'pre'], [0, 'pre'], [-1, 'fix']
|
||||
* ^ ^
|
||||
* d d_next
|
||||
*
|
||||
* @param {Array} diffs Array of diff tuples
|
||||
* @param {Int} cursor_pos Suggested edit position. Must not be out of bounds!
|
||||
* @return {Array} Array of diff tuples
|
||||
*/
|
||||
function fix_cursor (diffs, cursor_pos) {
|
||||
var norm = cursor_normalize_diff(diffs, cursor_pos);
|
||||
var ndiffs = norm[1];
|
||||
var cursor_pointer = norm[0];
|
||||
var d = ndiffs[cursor_pointer];
|
||||
var d_next = ndiffs[cursor_pointer + 1];
|
||||
|
||||
if (d == null) {
|
||||
// Text was deleted from end of original string,
|
||||
// cursor is now out of bounds in new string
|
||||
return diffs;
|
||||
} else if (d[0] !== DIFF_EQUAL) {
|
||||
// A modification happened at the cursor location.
|
||||
// This is the expected outcome, so we can return the original diff.
|
||||
return diffs;
|
||||
} else {
|
||||
if (d_next != null && d[1] + d_next[1] === d_next[1] + d[1]) {
|
||||
// Case 1)
|
||||
// It is possible to perform a naive shift
|
||||
ndiffs.splice(cursor_pointer, 2, d_next, d)
|
||||
return merge_tuples(ndiffs, cursor_pointer, 2)
|
||||
} else if (d_next != null && d_next[1].indexOf(d[1]) === 0) {
|
||||
// Case 2)
|
||||
// d[1] is a prefix of d_next[1]
|
||||
// We can assume that d_next[0] !== 0, since d[0] === 0
|
||||
// Shift edit locations..
|
||||
ndiffs.splice(cursor_pointer, 2, [d_next[0], d[1]], [0, d[1]]);
|
||||
var suffix = d_next[1].slice(d[1].length);
|
||||
if (suffix.length > 0) {
|
||||
ndiffs.splice(cursor_pointer + 2, 0, [d_next[0], suffix]);
|
||||
}
|
||||
return merge_tuples(ndiffs, cursor_pointer, 3)
|
||||
} else {
|
||||
// Not possible to perform any modification
|
||||
return diffs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Check diff did not split surrogate pairs.
|
||||
* Ex. [0, '\uD83D'], [-1, '\uDC36'], [1, '\uDC2F'] -> [-1, '\uD83D\uDC36'], [1, '\uD83D\uDC2F']
|
||||
* '\uD83D\uDC36' === '🐶', '\uD83D\uDC2F' === '🐯'
|
||||
*
|
||||
* @param {Array} diffs Array of diff tuples
|
||||
* @return {Array} Array of diff tuples
|
||||
*/
|
||||
function fix_emoji (diffs) {
|
||||
var compact = false;
|
||||
var starts_with_pair_end = function(str) {
|
||||
return str.charCodeAt(0) >= 0xDC00 && str.charCodeAt(0) <= 0xDFFF;
|
||||
}
|
||||
var ends_with_pair_start = function(str) {
|
||||
return str.charCodeAt(str.length-1) >= 0xD800 && str.charCodeAt(str.length-1) <= 0xDBFF;
|
||||
}
|
||||
for (var i = 2; i < diffs.length; i += 1) {
|
||||
if (diffs[i-2][0] === DIFF_EQUAL && ends_with_pair_start(diffs[i-2][1]) &&
|
||||
diffs[i-1][0] === DIFF_DELETE && starts_with_pair_end(diffs[i-1][1]) &&
|
||||
diffs[i][0] === DIFF_INSERT && starts_with_pair_end(diffs[i][1])) {
|
||||
compact = true;
|
||||
|
||||
diffs[i-1][1] = diffs[i-2][1].slice(-1) + diffs[i-1][1];
|
||||
diffs[i][1] = diffs[i-2][1].slice(-1) + diffs[i][1];
|
||||
|
||||
diffs[i-2][1] = diffs[i-2][1].slice(0, -1);
|
||||
}
|
||||
}
|
||||
if (!compact) {
|
||||
return diffs;
|
||||
}
|
||||
var fixed_diffs = [];
|
||||
for (var i = 0; i < diffs.length; i += 1) {
|
||||
if (diffs[i][1].length > 0) {
|
||||
fixed_diffs.push(diffs[i]);
|
||||
}
|
||||
}
|
||||
return fixed_diffs;
|
||||
}
|
||||
|
||||
/*
|
||||
* Try to merge tuples with their neigbors in a given range.
|
||||
* E.g. [0, 'a'], [0, 'b'] -> [0, 'ab']
|
||||
*
|
||||
* @param {Array} diffs Array of diff tuples.
|
||||
* @param {Int} start Position of the first element to merge (diffs[start] is also merged with diffs[start - 1]).
|
||||
* @param {Int} length Number of consecutive elements to check.
|
||||
* @return {Array} Array of merged diff tuples.
|
||||
*/
|
||||
function merge_tuples (diffs, start, length) {
|
||||
// Check from (start-1) to (start+length).
|
||||
for (var i = start + length - 1; i >= 0 && i >= start - 1; i--) {
|
||||
if (i + 1 < diffs.length) {
|
||||
var left_d = diffs[i];
|
||||
var right_d = diffs[i+1];
|
||||
if (left_d[0] === right_d[1]) {
|
||||
diffs.splice(i, 2, [left_d[0], left_d[1] + right_d[1]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fast-diff",
|
||||
"version": "1.1.2",
|
||||
"version": "1.0.1",
|
||||
"description": "Fast Javascript text diff",
|
||||
"author": "Jason Chen <jhchen7@gmail.com>",
|
||||
"main": "diff.js",
|
||||
@@ -19,6 +19,6 @@
|
||||
"scripts": {
|
||||
"test": "node test.js"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"license": "Apache 2.0",
|
||||
"keywords": ["diff"]
|
||||
}
|
||||
|
||||
+1
-53
@@ -25,7 +25,7 @@ for(var i = 0; i <= ITERATIONS; ++i) {
|
||||
strings.push(chars.join(''));
|
||||
}
|
||||
|
||||
console.log('Running tests *without* cursor information...');
|
||||
console.log('Running tests...');
|
||||
for(var i = 0; i < ITERATIONS; ++i) {
|
||||
var result = diff(strings[i], strings[i+1]);
|
||||
var expected = googlediff.diff_main(strings[i], strings[i+1]);
|
||||
@@ -36,56 +36,4 @@ for(var i = 0; i < ITERATIONS; ++i) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Running tests *with* cursor information');
|
||||
for(var i = 0; i < ITERATIONS; ++i) {
|
||||
var cursor_pos = Math.floor(random() * strings[i].length + 1);
|
||||
var diffs = diff(strings[i], strings[i+1], cursor_pos);
|
||||
var patch = googlediff.patch_make(strings[i], strings[i+1], diffs);
|
||||
var expected = googlediff.patch_apply(patch, strings[i])[0];
|
||||
if (expected !== strings[i+1]) {
|
||||
console.log('Expected', expected);
|
||||
console.log('Result', strings[i+1]);
|
||||
throw new Error('Diff produced difference results.');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Running emoji tests');
|
||||
(function() {
|
||||
var result = diff('🐶', '🐯');
|
||||
var expected = [
|
||||
[diff.DELETE, '🐶'],
|
||||
[diff.INSERT, '🐯'],
|
||||
];
|
||||
if (!_.isEqual(result, expected)) {
|
||||
console.log(result, '!==', expected);
|
||||
throw new Error('Emoji simple case test failed');
|
||||
}
|
||||
})();
|
||||
|
||||
(function() {
|
||||
var result = diff('👨🏽', '👩🏽');
|
||||
var expected = [
|
||||
[diff.DELETE, '👨'],
|
||||
[diff.INSERT, '👩'],
|
||||
[diff.EQUAL, '🏽']
|
||||
];
|
||||
if (!_.isEqual(result, expected)) {
|
||||
console.log(result, '!==', expected);
|
||||
throw new Error('Emoji before case test failed');
|
||||
}
|
||||
})();
|
||||
|
||||
(function() {
|
||||
var result = diff('👩🏼', '👩🏽');
|
||||
var expected = [
|
||||
[diff.EQUAL, '👩'],
|
||||
[diff.DELETE, '🏼'],
|
||||
[diff.INSERT, '🏽'],
|
||||
];
|
||||
if (!_.isEqual(result, expected)) {
|
||||
console.log(result, '!==', expected);
|
||||
throw new Error('Emoji after case test failed');
|
||||
}
|
||||
})();
|
||||
|
||||
console.log("Success!");
|
||||
|
||||
Reference in New Issue
Block a user