Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fast-diff

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fast-diff - npm Package Compare versions

Comparing version 1.1.2 to 1.2.0

diff.d.ts

506

diff.js

@@ -42,8 +42,8 @@ /**

* @param {string} text2 New string to be diffed.
* @param {Int} cursor_pos Expected edit position in text1 (optional)
* @param {Int|Object} [cursor_pos] Edit position in text1 or object with more info
* @return {Array} Array of diff tuples.
*/
function diff_main(text1, text2, cursor_pos) {
// Check for equality (speedup).
if (text1 == text2) {
function diff_main(text1, text2, cursor_pos, _fix_unicode) {
// Check for equality
if (text1 === text2) {
if (text1) {

@@ -55,5 +55,7 @@ return [[DIFF_EQUAL, text1]];

// Check cursor_pos within bounds
if (cursor_pos < 0 || text1.length < cursor_pos) {
cursor_pos = null;
if (cursor_pos != null) {
var editdiff = find_cursor_edit_diff(text1, text2, cursor_pos);
if (editdiff) {
return editdiff;
}
}

@@ -83,7 +85,3 @@

}
diff_cleanupMerge(diffs);
if (cursor_pos != null) {
diffs = fix_cursor(diffs, cursor_pos);
}
diffs = fix_emoji(diffs);
diff_cleanupMerge(diffs, _fix_unicode);
return diffs;

@@ -116,7 +114,9 @@ };

var i = longtext.indexOf(shorttext);
if (i != -1) {
if (i !== -1) {
// Shorter text is inside the longer text (speedup).
diffs = [[DIFF_INSERT, longtext.substring(0, i)],
[DIFF_EQUAL, shorttext],
[DIFF_INSERT, longtext.substring(i + shorttext.length)]];
diffs = [
[DIFF_INSERT, longtext.substring(0, i)],
[DIFF_EQUAL, shorttext],
[DIFF_INSERT, longtext.substring(i + shorttext.length)]
];
// Swap insertions for deletions if diff is reversed.

@@ -129,3 +129,3 @@ if (text1.length > text2.length) {

if (shorttext.length == 1) {
if (shorttext.length === 1) {
// Single character string.

@@ -185,3 +185,3 @@ // After the previous speedup, the character can't be an equality.

// with the reverse path.
var front = (delta % 2 != 0);
var front = (delta % 2 !== 0);
// Offsets for start and end of k loop.

@@ -198,3 +198,3 @@ // Prevents mapping of space beyond the grid.

var x1;
if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
if (k1 === -d || (k1 !== d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
x1 = v1[k1_offset + 1];

@@ -205,4 +205,6 @@ } else {

var y1 = x1 - k1;
while (x1 < text1_length && y1 < text2_length &&
text1.charAt(x1) == text2.charAt(y1)) {
while (
x1 < text1_length && y1 < text2_length &&
text1.charAt(x1) === text2.charAt(y1)
) {
x1++;

@@ -220,3 +222,3 @@ y1++;

var k2_offset = v_offset + delta - k1;
if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) {
if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] !== -1) {
// Mirror x2 onto top-left coordinate system.

@@ -236,3 +238,3 @@ var x2 = text1_length - v2[k2_offset];

var x2;
if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
if (k2 === -d || (k2 !== d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
x2 = v2[k2_offset + 1];

@@ -243,5 +245,6 @@ } else {

var y2 = x2 - k2;
while (x2 < text1_length && y2 < text2_length &&
text1.charAt(text1_length - x2 - 1) ==
text2.charAt(text2_length - y2 - 1)) {
while (
x2 < text1_length && y2 < text2_length &&
text1.charAt(text1_length - x2 - 1) === text2.charAt(text2_length - y2 - 1)
) {
x2++;

@@ -259,3 +262,3 @@ y2++;

var k1_offset = v_offset + delta - k2;
if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) {
if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] !== -1) {
var x1 = v1[k1_offset];

@@ -311,3 +314,3 @@ var y1 = v_offset + x1 - k1_offset;

// Quick check for common null cases.
if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) {
if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) {
return 0;

@@ -322,4 +325,6 @@ }

while (pointermin < pointermid) {
if (text1.substring(pointerstart, pointermid) ==
text2.substring(pointerstart, pointermid)) {
if (
text1.substring(pointerstart, pointermid) ==
text2.substring(pointerstart, pointermid)
) {
pointermin = pointermid;

@@ -332,2 +337,7 @@ pointerstart = pointermin;

}
if (is_surrogate_pair_start(text1.charCodeAt(pointermid - 1))) {
pointermid--;
}
return pointermid;

@@ -345,4 +355,3 @@ };

// Quick check for common null cases.
if (!text1 || !text2 ||
text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) {
if (!text1 || !text2 || text1.slice(-1) !== text2.slice(-1)) {
return 0;

@@ -357,4 +366,6 @@ }

while (pointermin < pointermid) {
if (text1.substring(text1.length - pointermid, text1.length - pointerend) ==
text2.substring(text2.length - pointermid, text2.length - pointerend)) {
if (
text1.substring(text1.length - pointermid, text1.length - pointerend) ==
text2.substring(text2.length - pointermid, text2.length - pointerend)
) {
pointermin = pointermid;

@@ -367,2 +378,7 @@ pointerend = pointermin;

}
if (is_surrogate_pair_end(text1.charCodeAt(text1.length - pointermid))) {
pointermid--;
}
return pointermid;

@@ -407,10 +423,10 @@ };

var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
var prefixLength = diff_commonPrefix(longtext.substring(i),
shorttext.substring(j));
var suffixLength = diff_commonSuffix(longtext.substring(0, i),
shorttext.substring(0, j));
while ((j = shorttext.indexOf(seed, j + 1)) !== -1) {
var prefixLength = diff_commonPrefix(
longtext.substring(i), shorttext.substring(j));
var suffixLength = diff_commonSuffix(
longtext.substring(0, i), shorttext.substring(0, j));
if (best_common.length < suffixLength + prefixLength) {
best_common = shorttext.substring(j - suffixLength, j) +
shorttext.substring(j, j + prefixLength);
best_common = shorttext.substring(
j - suffixLength, j) + shorttext.substring(j, j + prefixLength);
best_longtext_a = longtext.substring(0, i - suffixLength);

@@ -423,4 +439,6 @@ best_longtext_b = longtext.substring(i + prefixLength);

if (best_common.length * 2 >= longtext.length) {
return [best_longtext_a, best_longtext_b,
best_shorttext_a, best_shorttext_b, best_common];
return [
best_longtext_a, best_longtext_b,
best_shorttext_a, best_shorttext_b, best_common
];
} else {

@@ -432,7 +450,5 @@ return null;

// First check if the second quarter is the seed for a half-match.
var hm1 = diff_halfMatchI_(longtext, shorttext,
Math.ceil(longtext.length / 4));
var hm1 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 4));
// Check again based on the third quarter.
var hm2 = diff_halfMatchI_(longtext, shorttext,
Math.ceil(longtext.length / 2));
var hm2 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 2));
var hm;

@@ -472,4 +488,5 @@ if (!hm1 && !hm2) {

* @param {Array} diffs Array of diff tuples.
* @param {boolean} fix_unicode Whether to normalize to a unicode-correct diff
*/
function diff_cleanupMerge(diffs) {
function diff_cleanupMerge(diffs, fix_unicode) {
diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end.

@@ -483,4 +500,9 @@ var pointer = 0;

while (pointer < diffs.length) {
if (pointer < diffs.length - 1 && !diffs[pointer][1]) {
diffs.splice(pointer, 1);
continue;
}
switch (diffs[pointer][0]) {
case DIFF_INSERT:
count_insert++;

@@ -496,16 +518,59 @@ text_insert += diffs[pointer][1];

case DIFF_EQUAL:
// Upon reaching an equality, check for prior redundancies.
if (count_delete + count_insert > 1) {
if (count_delete !== 0 && count_insert !== 0) {
// Factor out any common prefixies.
var previous_equality = pointer - count_insert - count_delete - 1;
if (fix_unicode) {
// prevent splitting of unicode surrogate pairs. when fix_unicode is true,
// we assume that the old and new text in the diff are complete and correct
// unicode-encoded JS strings, but the tuple boundaries may fall between
// surrogate pairs. we fix this by shaving off stray surrogates from the end
// of the previous equality and the beginning of this equality. this may create
// empty equalities or a common prefix or suffix. for example, if AB and AC are
// emojis, `[[0, 'A'], [-1, 'BA'], [0, 'C']]` would turn into deleting 'ABAC' and
// inserting 'AC', and then the common suffix 'AC' will be eliminated. in this
// particular case, both equalities go away, we absorb any previous inequalities,
// and we keep scanning for the next equality before rewriting the tuples.
if (previous_equality >= 0 && ends_with_pair_start(diffs[previous_equality][1])) {
var stray = diffs[previous_equality][1].slice(-1);
diffs[previous_equality][1] = diffs[previous_equality][1].slice(0, -1);
text_delete = stray + text_delete;
text_insert = stray + text_insert;
if (!diffs[previous_equality][1]) {
// emptied out previous equality, so delete it and include previous delete/insert
diffs.splice(previous_equality, 1);
pointer--;
var k = previous_equality - 1;
if (diffs[k] && diffs[k][0] === DIFF_INSERT) {
count_insert++;
text_insert = diffs[k][1] + text_insert;
k--;
}
if (diffs[k] && diffs[k][0] === DIFF_DELETE) {
count_delete++;
text_delete = diffs[k][1] + text_delete;
k--;
}
previous_equality = k;
}
}
if (starts_with_pair_end(diffs[pointer][1])) {
var stray = diffs[pointer][1].charAt(0);
diffs[pointer][1] = diffs[pointer][1].slice(1);
text_delete += stray;
text_insert += stray;
}
}
if (pointer < diffs.length - 1 && !diffs[pointer][1]) {
// for empty equality not at end, wait for next equality
diffs.splice(pointer, 1);
break;
}
if (text_delete.length > 0 || text_insert.length > 0) {
// note that diff_commonPrefix and diff_commonSuffix are unicode-aware
if (text_delete.length > 0 && text_insert.length > 0) {
// Factor out any common prefixes.
commonlength = diff_commonPrefix(text_insert, text_delete);
if (commonlength !== 0) {
if ((pointer - count_delete - count_insert) > 0 &&
diffs[pointer - count_delete - count_insert - 1][0] ==
DIFF_EQUAL) {
diffs[pointer - count_delete - count_insert - 1][1] +=
text_insert.substring(0, commonlength);
if (previous_equality >= 0) {
diffs[previous_equality][1] += text_insert.substring(0, commonlength);
} else {
diffs.splice(0, 0, [DIFF_EQUAL,
text_insert.substring(0, commonlength)]);
diffs.splice(0, 0, [DIFF_EQUAL, text_insert.substring(0, commonlength)]);
pointer++;

@@ -516,28 +581,28 @@ }

}
// Factor out any common suffixies.
// Factor out any common suffixes.
commonlength = diff_commonSuffix(text_insert, text_delete);
if (commonlength !== 0) {
diffs[pointer][1] = text_insert.substring(text_insert.length -
commonlength) + diffs[pointer][1];
text_insert = text_insert.substring(0, text_insert.length -
commonlength);
text_delete = text_delete.substring(0, text_delete.length -
commonlength);
diffs[pointer][1] =
text_insert.substring(text_insert.length - commonlength) + diffs[pointer][1];
text_insert = text_insert.substring(0, text_insert.length - commonlength);
text_delete = text_delete.substring(0, text_delete.length - commonlength);
}
}
// Delete the offending records and add the merged ones.
if (count_delete === 0) {
diffs.splice(pointer - count_insert,
count_delete + count_insert, [DIFF_INSERT, text_insert]);
} else if (count_insert === 0) {
diffs.splice(pointer - count_delete,
count_delete + count_insert, [DIFF_DELETE, text_delete]);
var n = count_insert + count_delete;
if (text_delete.length === 0 && text_insert.length === 0) {
diffs.splice(pointer - n, n);
pointer = pointer - n;
} else if (text_delete.length === 0) {
diffs.splice(pointer - n, n, [DIFF_INSERT, text_insert]);
pointer = pointer - n + 1;
} else if (text_insert.length === 0) {
diffs.splice(pointer - n, n, [DIFF_DELETE, text_delete]);
pointer = pointer - n + 1;
} else {
diffs.splice(pointer - count_delete - count_insert,
count_delete + count_insert, [DIFF_DELETE, text_delete],
[DIFF_INSERT, text_insert]);
diffs.splice(pointer - n, n, [DIFF_DELETE, text_delete], [DIFF_INSERT, text_insert]);
pointer = pointer - n + 2;
}
pointer = pointer - count_delete - count_insert +
(count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1;
} else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) {
}
if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {
// Merge this equality with the previous one.

@@ -567,11 +632,11 @@ diffs[pointer - 1][1] += diffs[pointer][1];

while (pointer < diffs.length - 1) {
if (diffs[pointer - 1][0] == DIFF_EQUAL &&
diffs[pointer + 1][0] == DIFF_EQUAL) {
if (diffs[pointer - 1][0] === DIFF_EQUAL &&
diffs[pointer + 1][0] === DIFF_EQUAL) {
// This is a single edit surrounded by equalities.
if (diffs[pointer][1].substring(diffs[pointer][1].length -
diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) {
diffs[pointer - 1][1].length) === diffs[pointer - 1][1]) {
// Shift the edit over the previous equality.
diffs[pointer][1] = diffs[pointer - 1][1] +
diffs[pointer][1].substring(0, diffs[pointer][1].length -
diffs[pointer - 1][1].length);
diffs[pointer][1].substring(0, diffs[pointer][1].length -
diffs[pointer - 1][1].length);
diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];

@@ -581,8 +646,8 @@ diffs.splice(pointer - 1, 1);

} else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) ==
diffs[pointer + 1][1]) {
diffs[pointer + 1][1]) {
// Shift the edit over the next equality.
diffs[pointer - 1][1] += diffs[pointer + 1][1];
diffs[pointer][1] =
diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
diffs[pointer + 1][1];
diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
diffs[pointer + 1][1];
diffs.splice(pointer + 1, 1);

@@ -596,171 +661,142 @@ changes = true;

if (changes) {
diff_cleanupMerge(diffs);
diff_cleanupMerge(diffs, fix_unicode);
}
};
function is_surrogate_pair_start(charCode) {
return charCode >= 0xD800 && charCode <= 0xDBFF;
}
var diff = diff_main;
diff.INSERT = DIFF_INSERT;
diff.DELETE = DIFF_DELETE;
diff.EQUAL = DIFF_EQUAL;
function is_surrogate_pair_end(charCode) {
return charCode >= 0xDC00 && charCode <= 0xDFFF;
}
module.exports = diff;
function starts_with_pair_end(str) {
return is_surrogate_pair_end(str.charCodeAt(0));
}
/*
* 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!')
function ends_with_pair_start(str) {
return is_surrogate_pair_start(str.charCodeAt(str.length - 1));
}
/*
* 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;
function remove_empty_tuples(tuples) {
var ret = [];
for (var i = 0; i < tuples.length; i++) {
if (tuples[i][1].length > 0) {
ret.push(tuples[i]);
}
}
return ret;
}
/*
* 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;
function make_edit_splice(before, oldMiddle, newMiddle, after) {
if (ends_with_pair_start(before) || starts_with_pair_end(after)) {
return null;
}
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;
return remove_empty_tuples([
[DIFF_EQUAL, before],
[DIFF_DELETE, oldMiddle],
[DIFF_INSERT, newMiddle],
[DIFF_EQUAL, after]
]);
}
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);
function find_cursor_edit_diff(oldText, newText, cursor_pos) {
// note: this runs after equality check has ruled out exact equality
var oldRange = typeof cursor_pos === 'number' ?
{ index: cursor_pos, length: 0 } : cursor_pos.oldRange;
var newRange = typeof cursor_pos === 'number' ?
null : cursor_pos.newRange;
// take into account the old and new selection to generate the best diff
// possible for a text edit. for example, a text change from "xxx" to "xx"
// could be a delete or forwards-delete of any one of the x's, or the
// result of selecting two of the x's and typing "x".
var oldLength = oldText.length;
var newLength = newText.length;
if (oldRange.length === 0 && (newRange === null || newRange.length === 0)) {
// see if we have an insert or delete before or after cursor
var oldCursor = oldRange.index;
var oldBefore = oldText.slice(0, oldCursor);
var oldAfter = oldText.slice(oldCursor);
var maybeNewCursor = newRange ? newRange.index : null;
editBefore: {
// is this an insert or delete right before oldCursor?
var newCursor = oldCursor + newLength - oldLength;
if (maybeNewCursor !== null && maybeNewCursor !== newCursor) {
break editBefore;
}
if (newCursor < 0 || newCursor > newLength) {
break editBefore;
}
var newBefore = newText.slice(0, newCursor);
var newAfter = newText.slice(newCursor);
if (newAfter !== oldAfter) {
break editBefore;
}
var prefixLength = Math.min(oldCursor, newCursor);
var oldPrefix = oldBefore.slice(0, prefixLength);
var newPrefix = newBefore.slice(0, prefixLength);
if (oldPrefix !== newPrefix) {
break editBefore;
}
var oldMiddle = oldBefore.slice(prefixLength);
var newMiddle = newBefore.slice(prefixLength);
return make_edit_splice(oldPrefix, oldMiddle, newMiddle, oldAfter);
}
}
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]);
editAfter: {
// is this an insert or delete right after oldCursor?
if (maybeNewCursor !== null && maybeNewCursor !== oldCursor) {
break editAfter;
}
var cursor = oldCursor;
var newBefore = newText.slice(0, cursor);
var newAfter = newText.slice(cursor);
if (newBefore !== oldBefore) {
break editAfter;
}
var suffixLength = Math.min(oldLength - cursor, newLength - cursor);
var oldSuffix = oldAfter.slice(oldAfter.length - suffixLength);
var newSuffix = newAfter.slice(newAfter.length - suffixLength);
if (oldSuffix !== newSuffix) {
break editAfter;
}
var oldMiddle = oldAfter.slice(0, oldAfter.length - suffixLength);
var newMiddle = newAfter.slice(0, newAfter.length - suffixLength);
return make_edit_splice(oldBefore, oldMiddle, newMiddle, oldSuffix);
}
}
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]]);
if (oldRange.length > 0 && newRange && newRange.length === 0) {
replaceRange: {
// see if diff could be a splice of the old selection range
var oldPrefix = oldText.slice(0, oldRange.index);
var oldSuffix = oldText.slice(oldRange.index + oldRange.length);
var prefixLength = oldPrefix.length;
var suffixLength = oldSuffix.length;
if (newLength < prefixLength + suffixLength) {
break replaceRange;
}
var newPrefix = newText.slice(0, prefixLength);
var newSuffix = newText.slice(newLength - suffixLength);
if (oldPrefix !== newPrefix || oldSuffix !== newSuffix) {
break replaceRange;
}
var oldMiddle = oldText.slice(prefixLength, oldLength - suffixLength);
var newMiddle = newText.slice(prefixLength, newLength - suffixLength);
return make_edit_splice(oldPrefix, oldMiddle, newMiddle, oldSuffix);
}
}
return diffs;
return null;
}
function diff(text1, text2, cursor_pos) {
// only pass fix_unicode=true at the top level, not when diff_main is
// recursively invoked
return diff_main(text1, text2, cursor_pos, true);
}
diff.INSERT = DIFF_INSERT;
diff.DELETE = DIFF_DELETE;
diff.EQUAL = DIFF_EQUAL;
module.exports = diff;
{
"name": "fast-diff",
"version": "1.1.2",
"version": "1.2.0",
"description": "Fast Javascript text diff",
"author": "Jason Chen <jhchen7@gmail.com>",
"main": "diff.js",
"types": "diff.d.ts",
"devDependencies": {
"googlediff": "~0.1.0",
"lodash": "~3.9.3",

@@ -23,3 +23,5 @@ "seedrandom": "~2.4.0"

"license": "Apache-2.0",
"keywords": ["diff"]
"keywords": [
"diff"
]
}
var _ = require('lodash');
var googlediff = require('googlediff');
var seedrandom = require('seedrandom');
var diff = require('./diff.js');
googlediff = new googlediff();
var ITERATIONS = 10000;
var ALPHABET = 'GATTACA';
var LENGTH = 100;
var EMOJI_MAX_LENGTH = 50;

@@ -15,2 +13,17 @@ var seed = Math.floor(Math.random() * 10000);

console.log('Running regression tests...');
[
['GAATAAAAAAAGATTAACAT', 'AAAAACTTGTAATTAACAAC'],
['🔘🤘🔗🔗', '🔗🤗🤗__🤗🤘🤘🤗🔗🤘🔗'],
['🔗🤗🤗__🤗🤘🤘🤗🔗🤘🔗', '🤗🤘🔘'],
['🤘🤘🔘🔘_🔘🔗🤘🤗🤗__🔗🤘', '🤘🔘🤘🔗🤘🤘🔗🤗🤘🔘🔘'],
['🤗🤘🤗🔘🤘🔘🤗_🤗🔗🤘🤗_🤘🔗🤗🤘🔗🤘🤘🤘🔗🤗🔗🔗🔗🤗_🤘🔗🤗🤗🔘🤗🤗🤘🤗',
'_🤗🤘_🤘🤘🔘🤗🔘🤘_🔘🤗🔗🔘🔗🤘🔗🤘🤗🔗🔗🔗🤘🔘_🤗🤘🤘🤘__🤘_🔘🤘🤘_🔗🤘🔘'],
['🔗🤘🤗🔘🔘🤗', '🤘🤘🤘🤗🔘🔗🔗'],
['🔘_🔗🔗🔗🤗🔗', '🤘🤗🔗🤗_🤘🔘_'],
].forEach(function (data) {
var result = diff(data[0], data[1]);
applyDiff(result, data[0], data[1]);
});
console.log('Running computing ' + ITERATIONS + ' diffs with seed ' + seed + '...');

@@ -20,5 +33,5 @@

var strings = [];
for(var i = 0; i <= ITERATIONS; ++i) {
for (var i = 0; i <= ITERATIONS; ++i) {
var chars = [];
for(var l = 0; l < LENGTH; ++l) {
for (var l = 0; l < LENGTH; ++l) {
var letter = ALPHABET.substr(Math.floor(random() * ALPHABET.length), 1);

@@ -30,65 +43,295 @@ chars.push(letter);

console.log('Running tests *without* cursor information...');
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]);
if (!_.isEqual(result, expected)) {
console.log('Expected', expected);
console.log('Result', result);
throw new Error('Diff produced difference results.');
}
console.log('Running fuzz tests *without* cursor information...');
for (var i = 0; i < ITERATIONS; ++i) {
var result = diff(strings[i], strings[i + 1]);
applyDiff(result, strings[i], strings[i + 1]);
}
console.log('Running tests *with* cursor information');
for(var i = 0; i < ITERATIONS; ++i) {
console.log('Running fuzz 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.');
var diffs = diff(strings[i], strings[i + 1], cursor_pos);
applyDiff(diffs, strings[i], strings[i + 1]);
}
function parseDiff(str) {
if (!str) {
return [];
}
return str.split(/(?=[+\-=])/).map(function (piece) {
var symbol = piece.charAt(0);
var text = piece.slice(1);
return [
symbol === '+' ? diff.INSERT : symbol === '-' ? diff.DELETE : diff.EQUAL,
text
]
});
}
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');
console.log('Running cursor tests');
[
['', 0, '', null, ''],
['', 0, 'a', null, '+a'],
['a', 0, 'aa', null, '+a=a'],
['a', 1, 'aa', null, '=a+a'],
['aa', 0, 'aaa', null, '+a=aa'],
['aa', 1, 'aaa', null, '=a+a=a'],
['aa', 2, 'aaa', null, '=aa+a'],
['aaa', 0, 'aaaa', null, '+a=aaa'],
['aaa', 1, 'aaaa', null, '=a+a=aa'],
['aaa', 2, 'aaaa', null, '=aa+a=a'],
['aaa', 3, 'aaaa', null, '=aaa+a'],
['a', 0, '', null, '-a'],
['a', 1, '', null, '-a'],
['aa', 0, 'a', null, '-a=a'],
['aa', 1, 'a', null, '-a=a'],
['aa', 2, 'a', null, '=a-a'],
['aaa', 0, 'aa', null, '-a=aa'],
['aaa', 1, 'aa', null, '-a=aa'],
['aaa', 2, 'aa', null, '=a-a=a'],
['aaa', 3, 'aa', null, '=aa-a'],
['', 0, '', 0, ''],
['', 0, 'a', 1, '+a'],
['a', 0, 'aa', 1, '+a=a'],
['a', 1, 'aa', 2, '=a+a'],
['aa', 0, 'aaa', 1, '+a=aa'],
['aa', 1, 'aaa', 2, '=a+a=a'],
['aa', 2, 'aaa', 3, '=aa+a'],
['aaa', 0, 'aaaa', 1, '+a=aaa'],
['aaa', 1, 'aaaa', 2, '=a+a=aa'],
['aaa', 2, 'aaaa', 3, '=aa+a=a'],
['aaa', 3, 'aaaa', 4, '=aaa+a'],
['a', 1, '', 0, '-a'],
['aa', 1, 'a', 0, '-a=a'],
['aa', 2, 'a', 1, '=a-a'],
['aaa', 1, 'aa', 0, '-a=aa'],
['aaa', 2, 'aa', 1, '=a-a=a'],
['aaa', 3, 'aa', 2, '=aa-a'],
['a', 1, '', 0, '-a'],
['aa', 1, 'a', 0, '-a=a'],
['aa', 2, 'a', 1, '=a-a'],
['aaa', 1, 'aa', 0, '-a=aa'],
['aaa', 2, 'aa', 1, '=a-a=a'],
['aaa', 3, 'aa', 2, '=aa-a'],
// forward-delete
['a', 0, '', 0, '-a'],
['aa', 0, 'a', 0, '-a=a'],
['aa', 1, 'a', 1, '=a-a'],
['aaa', 0, 'aa', 0, '-a=aa'],
['aaa', 1, 'aa', 1, '=a-a=a'],
['aaa', 2, 'aa', 2, '=aa-a'],
['bob', 0, 'bobob', null, '+bo=bob'],
['bob', 1, 'bobob', null, '=b+ob=ob'],
['bob', 2, 'bobob', null, '=bo+bo=b'],
['bob', 3, 'bobob', null, '=bob+ob'],
['bob', 0, 'bobob', 2, '+bo=bob'],
['bob', 1, 'bobob', 3, '=b+ob=ob'],
['bob', 2, 'bobob', 4, '=bo+bo=b'],
['bob', 3, 'bobob', 5, '=bob+ob'],
['bobob', 2, 'bob', null, '-bo=bob'],
['bobob', 3, 'bob', null, '=b-ob=ob'],
['bobob', 4, 'bob', null, '=bo-bo=b'],
['bobob', 5, 'bob', null, '=bob-ob'],
['bobob', 2, 'bob', 0, '-bo=bob'],
['bobob', 3, 'bob', 1, '=b-ob=ob'],
['bobob', 4, 'bob', 2, '=bo-bo=b'],
['bobob', 5, 'bob', 3, '=bob-ob'],
['bob', 1, 'b', null, '=b-ob'],
['hello', [0, 5], 'h', 1, '-hello+h'],
['yay', [0, 3], 'y', 1, '-yay+y'],
['bobob', [1, 4], 'bob', 2, '=b-obo+o=b'],
].forEach(function (data) {
var oldText = data[0];
var newText = data[2];
var oldRange = typeof data[1] === 'number' ?
{ index: data[1], length: 0 } :
{ index: data[1][0], length: data[1][1] - data[1][0] };
var newRange = typeof data[3] === 'number' ?
{ index: data[3], length: 0 } :
data[3] === null ? null : { index: data[3][0], length: data[3][1] - data[3][0] };
var expected = parseDiff(data[4]);
if (newRange === null && typeof data[1] !== 'number') {
throw new Error('invalid test case');
}
})();
var cursorInfo = newRange === null ? data[1] : {
oldRange: oldRange,
newRange: newRange,
};
doCursorTest(oldText, newText, cursorInfo, expected);
doCursorTest('x' + oldText, 'x' + newText, shiftCursorInfo(cursorInfo, 1), diffPrepend(expected, 'x'));
doCursorTest(oldText + 'x', newText + 'x', cursorInfo, diffAppend(expected, 'x'));
});
(function() {
var result = diff('👨🏽', '👩🏽');
var expected = [
[diff.DELETE, '👨'],
[diff.INSERT, '👩'],
[diff.EQUAL, '🏽']
];
function diffPrepend(tuples, text) {
if (tuples.length > 0 && tuples[0][0] === diff.EQUAL) {
return [[diff.EQUAL, text + tuples[0][1]]].concat(tuples.slice(1));
} else {
return [[diff.EQUAL, text]].concat(tuples);
}
}
function diffAppend(tuples, text) {
var lastTuple = tuples[tuples.length - 1];
if (lastTuple && lastTuple[0] === diff.EQUAL) {
return tuples.slice(0, -1).concat([[diff.EQUAL, lastTuple[1] + text]]);
} else {
return tuples.concat([[diff.EQUAL, text]]);
}
}
function shiftCursorInfo(cursorInfo, amount) {
if (typeof cursorInfo === 'number') {
return cursorInfo + amount;
} else {
return {
oldRange: {
index: cursorInfo.oldRange.index + amount,
length: cursorInfo.oldRange.length,
},
newRange: {
index: cursorInfo.newRange.index + amount,
length: cursorInfo.newRange.length,
},
}
}
}
function doCursorTest(oldText, newText, cursorInfo, expected) {
var result = diff(oldText, newText, cursorInfo);
if (!_.isEqual(result, expected)) {
console.log([oldText, newText, cursorInfo]);
console.log(result, '!==', expected);
throw new Error('Emoji before case test failed');
throw new Error('cursor test failed');
}
})();
}
(function() {
var result = diff('👩🏼', '👩🏽');
var expected = [
[diff.EQUAL, '👩'],
[diff.DELETE, '🏼'],
[diff.INSERT, '🏽'],
];
console.log('Running emoji tests');
[
['🐶', '🐯', '-🐶+🐯'],
['👨🏽', '👩🏽', '-👨+👩=🏽'],
['👩🏼', '👩🏽', '=👩-🏼+🏽'],
['🍏🍎', '🍎', '-🍏=🍎'],
['🍎', '🍏🍎', '+🍏=🍎'],
].forEach(function (data) {
var oldText = data[0];
var newText = data[1];
var expected = parseDiff(data[2]);
doEmojiTest(oldText, newText, expected);
doEmojiTest('x' + oldText, 'x' + newText, diffPrepend(expected, 'x'));
doEmojiTest(oldText + 'x', newText + 'x', diffAppend(expected, 'x'));
});
function doEmojiTest(oldText, newText, expected) {
var result = diff(oldText, newText);
if (!_.isEqual(result, expected)) {
console.log(oldText, newText, expected);
console.log(result, '!==', expected);
throw new Error('Emoji after case test failed');
throw new Error('Emoji simple test case failed');
}
})();
}
// emojis chosen to share high and low surrogates!
var EMOJI_ALPHABET = ['_', '🤗', '🔗', '🤘', '🔘'];
console.log('Generating emoji strings...');
var emoji_strings = [];
for (var i = 0; i <= ITERATIONS; ++i) {
var letters = [];
var len = Math.floor(random() * EMOJI_MAX_LENGTH);
for (var l = 0; l < len; ++l) {
var letter = EMOJI_ALPHABET[Math.floor(random() * EMOJI_ALPHABET.length)];
letters.push(letter);
}
emoji_strings.push(letters.join(''));
}
console.log('Running emoji fuzz tests...');
for (var i = 0; i < ITERATIONS; ++i) {
var oldText = emoji_strings[i];
var newText = emoji_strings[i + 1];
var result = diff(oldText, newText);
applyDiff(result, oldText, newText);
}
// Applies a diff to text, throwing an error if diff is invalid or incorrect
function applyDiff(diffs, text, expectedResult) {
var pos = 0;
function throwError(message) {
console.log(diffs, text, expectedResult);
throw new Error(message);
}
function expect(expected) {
var found = text.substr(pos, expected.length);
if (found !== expected) {
throwError('Expected "' + expected + '", found "' + found + '"');
}
}
var result = '';
var inserts_since_last_equality = 0;
var deletes_since_last_equality = 0;
for (var i = 0; i < diffs.length; i++) {
var d = diffs[i];
if (!d[1]) {
throwError('Empty tuple in diff')
}
var firstCharCode = d[1].charCodeAt(0);
var lastCharCode = d[1].slice(-1).charCodeAt(0);
if (firstCharCode >= 0xDC00 && firstCharCode <= 0xDFFF ||
lastCharCode >= 0xD800 && lastCharCode <= 0xDBFF) {
throwError('Bad unicode diff tuple')
}
switch (d[0]) {
case diff.EQUAL:
if (i !== 0 && !inserts_since_last_equality && !deletes_since_last_equality) {
throwError('two consecutive equalities in diff');
}
inserts_since_last_equality = 0;
deletes_since_last_equality = 0;
expect(d[1]);
result += d[1];
pos += d[1].length;
break;
case diff.DELETE:
if (deletes_since_last_equality) {
throwError('multiple deletes between equalities')
}
if (inserts_since_last_equality) {
throwError('delete following insert in diff')
}
deletes_since_last_equality++;
expect(d[1]);
pos += d[1].length;
break
case diff.INSERT:
if (inserts_since_last_equality) {
throwError('multiple inserts between equalities')
}
inserts_since_last_equality++;
result += d[1];
break;
}
}
if (pos !== text.length) {
throwError('Diff did not consume entire input text');
}
if (result !== expectedResult) {
console.log(diffs, text, expectedResult, result);
throw new Error('Diff not correct')
}
return result;
}
console.log("Success!");
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc