Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80635 views
1
/* See LICENSE file for terms of use */
2
3
/*
4
* Text diff implementation.
5
*
6
* This library supports the following APIS:
7
* JsDiff.diffChars: Character by character diff
8
* JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace
9
* JsDiff.diffLines: Line based diff
10
*
11
* JsDiff.diffCss: Diff targeted at CSS content
12
*
13
* These methods are based on the implementation proposed in
14
* "An O(ND) Difference Algorithm and its Variations" (Myers, 1986).
15
* http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927
16
*/
17
var JsDiff = (function() {
18
/*jshint maxparams: 5*/
19
function clonePath(path) {
20
return { newPos: path.newPos, components: path.components.slice(0) };
21
}
22
function removeEmpty(array) {
23
var ret = [];
24
for (var i = 0; i < array.length; i++) {
25
if (array[i]) {
26
ret.push(array[i]);
27
}
28
}
29
return ret;
30
}
31
function escapeHTML(s) {
32
var n = s;
33
n = n.replace(/&/g, '&amp;');
34
n = n.replace(/</g, '&lt;');
35
n = n.replace(/>/g, '&gt;');
36
n = n.replace(/"/g, '&quot;');
37
38
return n;
39
}
40
41
var Diff = function(ignoreWhitespace) {
42
this.ignoreWhitespace = ignoreWhitespace;
43
};
44
Diff.prototype = {
45
diff: function(oldString, newString) {
46
// Handle the identity case (this is due to unrolling editLength == 0
47
if (newString === oldString) {
48
return [{ value: newString }];
49
}
50
if (!newString) {
51
return [{ value: oldString, removed: true }];
52
}
53
if (!oldString) {
54
return [{ value: newString, added: true }];
55
}
56
57
newString = this.tokenize(newString);
58
oldString = this.tokenize(oldString);
59
60
var newLen = newString.length, oldLen = oldString.length;
61
var maxEditLength = newLen + oldLen;
62
var bestPath = [{ newPos: -1, components: [] }];
63
64
// Seed editLength = 0
65
var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0);
66
if (bestPath[0].newPos+1 >= newLen && oldPos+1 >= oldLen) {
67
return bestPath[0].components;
68
}
69
70
for (var editLength = 1; editLength <= maxEditLength; editLength++) {
71
for (var diagonalPath = -1*editLength; diagonalPath <= editLength; diagonalPath+=2) {
72
var basePath;
73
var addPath = bestPath[diagonalPath-1],
74
removePath = bestPath[diagonalPath+1];
75
oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
76
if (addPath) {
77
// No one else is going to attempt to use this value, clear it
78
bestPath[diagonalPath-1] = undefined;
79
}
80
81
var canAdd = addPath && addPath.newPos+1 < newLen;
82
var canRemove = removePath && 0 <= oldPos && oldPos < oldLen;
83
if (!canAdd && !canRemove) {
84
bestPath[diagonalPath] = undefined;
85
continue;
86
}
87
88
// Select the diagonal that we want to branch from. We select the prior
89
// path whose position in the new string is the farthest from the origin
90
// and does not pass the bounds of the diff graph
91
if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) {
92
basePath = clonePath(removePath);
93
this.pushComponent(basePath.components, oldString[oldPos], undefined, true);
94
} else {
95
basePath = clonePath(addPath);
96
basePath.newPos++;
97
this.pushComponent(basePath.components, newString[basePath.newPos], true, undefined);
98
}
99
100
var oldPos = this.extractCommon(basePath, newString, oldString, diagonalPath);
101
102
if (basePath.newPos+1 >= newLen && oldPos+1 >= oldLen) {
103
return basePath.components;
104
} else {
105
bestPath[diagonalPath] = basePath;
106
}
107
}
108
}
109
},
110
111
pushComponent: function(components, value, added, removed) {
112
var last = components[components.length-1];
113
if (last && last.added === added && last.removed === removed) {
114
// We need to clone here as the component clone operation is just
115
// as shallow array clone
116
components[components.length-1] =
117
{value: this.join(last.value, value), added: added, removed: removed };
118
} else {
119
components.push({value: value, added: added, removed: removed });
120
}
121
},
122
extractCommon: function(basePath, newString, oldString, diagonalPath) {
123
var newLen = newString.length,
124
oldLen = oldString.length,
125
newPos = basePath.newPos,
126
oldPos = newPos - diagonalPath;
127
while (newPos+1 < newLen && oldPos+1 < oldLen && this.equals(newString[newPos+1], oldString[oldPos+1])) {
128
newPos++;
129
oldPos++;
130
131
this.pushComponent(basePath.components, newString[newPos], undefined, undefined);
132
}
133
basePath.newPos = newPos;
134
return oldPos;
135
},
136
137
equals: function(left, right) {
138
var reWhitespace = /\S/;
139
if (this.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right)) {
140
return true;
141
} else {
142
return left === right;
143
}
144
},
145
join: function(left, right) {
146
return left + right;
147
},
148
tokenize: function(value) {
149
return value;
150
}
151
};
152
153
var CharDiff = new Diff();
154
155
var WordDiff = new Diff(true);
156
var WordWithSpaceDiff = new Diff();
157
WordDiff.tokenize = WordWithSpaceDiff.tokenize = function(value) {
158
return removeEmpty(value.split(/(\s+|\b)/));
159
};
160
161
var CssDiff = new Diff(true);
162
CssDiff.tokenize = function(value) {
163
return removeEmpty(value.split(/([{}:;,]|\s+)/));
164
};
165
166
var LineDiff = new Diff();
167
LineDiff.tokenize = function(value) {
168
var retLines = [],
169
lines = value.split(/^/m);
170
171
for(var i = 0; i < lines.length; i++) {
172
var line = lines[i],
173
lastLine = lines[i - 1];
174
175
// Merge lines that may contain windows new lines
176
if (line == '\n' && lastLine && lastLine[lastLine.length - 1] === '\r') {
177
retLines[retLines.length - 1] += '\n';
178
} else if (line) {
179
retLines.push(line);
180
}
181
}
182
183
return retLines;
184
};
185
186
return {
187
Diff: Diff,
188
189
diffChars: function(oldStr, newStr) { return CharDiff.diff(oldStr, newStr); },
190
diffWords: function(oldStr, newStr) { return WordDiff.diff(oldStr, newStr); },
191
diffWordsWithSpace: function(oldStr, newStr) { return WordWithSpaceDiff.diff(oldStr, newStr); },
192
diffLines: function(oldStr, newStr) { return LineDiff.diff(oldStr, newStr); },
193
194
diffCss: function(oldStr, newStr) { return CssDiff.diff(oldStr, newStr); },
195
196
createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader) {
197
var ret = [];
198
199
ret.push('Index: ' + fileName);
200
ret.push('===================================================================');
201
ret.push('--- ' + fileName + (typeof oldHeader === 'undefined' ? '' : '\t' + oldHeader));
202
ret.push('+++ ' + fileName + (typeof newHeader === 'undefined' ? '' : '\t' + newHeader));
203
204
var diff = LineDiff.diff(oldStr, newStr);
205
if (!diff[diff.length-1].value) {
206
diff.pop(); // Remove trailing newline add
207
}
208
diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier
209
210
function contextLines(lines) {
211
return lines.map(function(entry) { return ' ' + entry; });
212
}
213
function eofNL(curRange, i, current) {
214
var last = diff[diff.length-2],
215
isLast = i === diff.length-2,
216
isLastOfType = i === diff.length-3 && (current.added !== last.added || current.removed !== last.removed);
217
218
// Figure out if this is the last line for the given file and missing NL
219
if (!/\n$/.test(current.value) && (isLast || isLastOfType)) {
220
curRange.push('\\ No newline at end of file');
221
}
222
}
223
224
var oldRangeStart = 0, newRangeStart = 0, curRange = [],
225
oldLine = 1, newLine = 1;
226
for (var i = 0; i < diff.length; i++) {
227
var current = diff[i],
228
lines = current.lines || current.value.replace(/\n$/, '').split('\n');
229
current.lines = lines;
230
231
if (current.added || current.removed) {
232
if (!oldRangeStart) {
233
var prev = diff[i-1];
234
oldRangeStart = oldLine;
235
newRangeStart = newLine;
236
237
if (prev) {
238
curRange = contextLines(prev.lines.slice(-4));
239
oldRangeStart -= curRange.length;
240
newRangeStart -= curRange.length;
241
}
242
}
243
curRange.push.apply(curRange, lines.map(function(entry) { return (current.added?'+':'-') + entry; }));
244
eofNL(curRange, i, current);
245
246
if (current.added) {
247
newLine += lines.length;
248
} else {
249
oldLine += lines.length;
250
}
251
} else {
252
if (oldRangeStart) {
253
// Close out any changes that have been output (or join overlapping)
254
if (lines.length <= 8 && i < diff.length-2) {
255
// Overlapping
256
curRange.push.apply(curRange, contextLines(lines));
257
} else {
258
// end the range and output
259
var contextSize = Math.min(lines.length, 4);
260
ret.push(
261
'@@ -' + oldRangeStart + ',' + (oldLine-oldRangeStart+contextSize)
262
+ ' +' + newRangeStart + ',' + (newLine-newRangeStart+contextSize)
263
+ ' @@');
264
ret.push.apply(ret, curRange);
265
ret.push.apply(ret, contextLines(lines.slice(0, contextSize)));
266
if (lines.length <= 4) {
267
eofNL(ret, i, current);
268
}
269
270
oldRangeStart = 0; newRangeStart = 0; curRange = [];
271
}
272
}
273
oldLine += lines.length;
274
newLine += lines.length;
275
}
276
}
277
278
return ret.join('\n') + '\n';
279
},
280
281
applyPatch: function(oldStr, uniDiff) {
282
var diffstr = uniDiff.split('\n');
283
var diff = [];
284
var remEOFNL = false,
285
addEOFNL = false;
286
287
for (var i = (diffstr[0][0]==='I'?4:0); i < diffstr.length; i++) {
288
if(diffstr[i][0] === '@') {
289
var meh = diffstr[i].split(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/);
290
diff.unshift({
291
start:meh[3],
292
oldlength:meh[2],
293
oldlines:[],
294
newlength:meh[4],
295
newlines:[]
296
});
297
} else if(diffstr[i][0] === '+') {
298
diff[0].newlines.push(diffstr[i].substr(1));
299
} else if(diffstr[i][0] === '-') {
300
diff[0].oldlines.push(diffstr[i].substr(1));
301
} else if(diffstr[i][0] === ' ') {
302
diff[0].newlines.push(diffstr[i].substr(1));
303
diff[0].oldlines.push(diffstr[i].substr(1));
304
} else if(diffstr[i][0] === '\\') {
305
if (diffstr[i-1][0] === '+') {
306
remEOFNL = true;
307
} else if(diffstr[i-1][0] === '-') {
308
addEOFNL = true;
309
}
310
}
311
}
312
313
var str = oldStr.split('\n');
314
for (var i = diff.length - 1; i >= 0; i--) {
315
var d = diff[i];
316
for (var j = 0; j < d.oldlength; j++) {
317
if(str[d.start-1+j] !== d.oldlines[j]) {
318
return false;
319
}
320
}
321
Array.prototype.splice.apply(str,[d.start-1,+d.oldlength].concat(d.newlines));
322
}
323
324
if (remEOFNL) {
325
while (!str[str.length-1]) {
326
str.pop();
327
}
328
} else if (addEOFNL) {
329
str.push('');
330
}
331
return str.join('\n');
332
},
333
334
convertChangesToXML: function(changes){
335
var ret = [];
336
for ( var i = 0; i < changes.length; i++) {
337
var change = changes[i];
338
if (change.added) {
339
ret.push('<ins>');
340
} else if (change.removed) {
341
ret.push('<del>');
342
}
343
344
ret.push(escapeHTML(change.value));
345
346
if (change.added) {
347
ret.push('</ins>');
348
} else if (change.removed) {
349
ret.push('</del>');
350
}
351
}
352
return ret.join('');
353
},
354
355
// See: http://code.google.com/p/google-diff-match-patch/wiki/API
356
convertChangesToDMP: function(changes){
357
var ret = [], change;
358
for ( var i = 0; i < changes.length; i++) {
359
change = changes[i];
360
ret.push([(change.added ? 1 : change.removed ? -1 : 0), change.value]);
361
}
362
return ret;
363
}
364
};
365
})();
366
367
if (typeof module !== 'undefined') {
368
module.exports = JsDiff;
369
}
370
371