@b9g/revise 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -2
- package/_subseq.d.ts +18 -0
- package/contentarea.cjs +327 -408
- package/contentarea.cjs.map +1 -1
- package/contentarea.d.ts +29 -47
- package/contentarea.js +327 -408
- package/contentarea.js.map +1 -1
- package/edit.cjs +451 -186
- package/edit.cjs.map +1 -1
- package/edit.d.ts +69 -26
- package/edit.js +451 -186
- package/edit.js.map +1 -1
- package/history.cjs +3 -0
- package/history.cjs.map +1 -1
- package/history.d.ts +1 -1
- package/history.js +3 -0
- package/history.js.map +1 -1
- package/keyer.cjs +9 -9
- package/keyer.cjs.map +1 -1
- package/keyer.d.ts +1 -1
- package/keyer.js +9 -9
- package/keyer.js.map +1 -1
- package/package.json +16 -42
- package/subseq.cjs +0 -251
- package/subseq.cjs.map +0 -1
- package/subseq.d.ts +0 -62
- package/subseq.js +0 -248
- package/subseq.js.map +0 -1
package/edit.cjs
CHANGED
|
@@ -2,87 +2,190 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
function measure(subseq) {
|
|
6
|
+
let length = 0, includedLength = 0, excludedLength = 0;
|
|
7
|
+
for (let i = 0; i < subseq.length; i++) {
|
|
8
|
+
const s = subseq[i];
|
|
9
|
+
length += s;
|
|
10
|
+
if (i % 2 === 0) {
|
|
11
|
+
excludedLength += s;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
includedLength += s;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { length, includedLength, excludedLength };
|
|
18
|
+
}
|
|
19
|
+
function pushSegment(subseq, length, included) {
|
|
20
|
+
if (length < 0) {
|
|
21
|
+
throw new RangeError("Negative length");
|
|
14
22
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
else if (length === 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
else if (!subseq.length) {
|
|
27
|
+
if (included) {
|
|
28
|
+
subseq.push(0, length);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
subseq.push(length);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const included1 = subseq.length % 2 === 0;
|
|
36
|
+
if (included === included1) {
|
|
37
|
+
subseq[subseq.length - 1] += length;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
subseq.push(length);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function align(subseq1, subseq2) {
|
|
45
|
+
if (measure(subseq1).length !== measure(subseq2).length) {
|
|
46
|
+
throw new Error("Length mismatch");
|
|
47
|
+
}
|
|
48
|
+
const result = [];
|
|
49
|
+
for (let i1 = 0, i2 = 0, length1 = 0, length2 = 0, included1 = true, included2 = true; i1 < subseq1.length || i2 < subseq2.length;) {
|
|
50
|
+
if (length1 === 0) {
|
|
51
|
+
if (i1 >= subseq1.length) {
|
|
52
|
+
throw new Error("Length mismatch");
|
|
36
53
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
length1 = subseq1[i1++];
|
|
55
|
+
included1 = !included1;
|
|
56
|
+
}
|
|
57
|
+
if (length2 === 0) {
|
|
58
|
+
if (i2 >= subseq2.length) {
|
|
59
|
+
throw new Error("Size mismatch");
|
|
60
|
+
}
|
|
61
|
+
length2 = subseq2[i2++];
|
|
62
|
+
included2 = !included2;
|
|
63
|
+
}
|
|
64
|
+
if (length1 < length2) {
|
|
65
|
+
if (length1) {
|
|
66
|
+
result.push([length1, included1, included2]);
|
|
67
|
+
}
|
|
68
|
+
length2 = length2 - length1;
|
|
69
|
+
length1 = 0;
|
|
70
|
+
}
|
|
71
|
+
else if (length1 > length2) {
|
|
72
|
+
if (length2) {
|
|
73
|
+
result.push([length2, included1, included2]);
|
|
43
74
|
}
|
|
75
|
+
length1 = length1 - length2;
|
|
76
|
+
length2 = 0;
|
|
44
77
|
}
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
else {
|
|
79
|
+
if (length1) {
|
|
80
|
+
result.push([length1, included1, included2]);
|
|
81
|
+
}
|
|
82
|
+
length1 = length2 = 0;
|
|
47
83
|
}
|
|
48
|
-
return new Edit(parts, deleted);
|
|
49
84
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const deleteSizes = [];
|
|
57
|
-
subseq.Subseq.pushSegment(deleteSizes, from, false);
|
|
58
|
-
subseq.Subseq.pushSegment(deleteSizes, to - from, true);
|
|
59
|
-
subseq.Subseq.pushSegment(deleteSizes, text.length - to, false);
|
|
60
|
-
const deleted = text.slice(from, to);
|
|
61
|
-
return Edit.synthesize(new subseq.Subseq(insertSizes), inserted, new subseq.Subseq(deleteSizes), deleted);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
function union(subseq1, subseq2) {
|
|
88
|
+
const result = [];
|
|
89
|
+
for (const [length, included1, included2] of align(subseq1, subseq2)) {
|
|
90
|
+
pushSegment(result, length, included1 || included2);
|
|
62
91
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
function intersection(subseq1, subseq2) {
|
|
95
|
+
const result = [];
|
|
96
|
+
for (const [length, included1, included2] of align(subseq1, subseq2)) {
|
|
97
|
+
pushSegment(result, length, included1 && included2);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
function shrink(subseq1, subseq2) {
|
|
102
|
+
if (measure(subseq1).length !== measure(subseq2).length) {
|
|
103
|
+
throw new Error("Length mismatch");
|
|
104
|
+
}
|
|
105
|
+
const result = [];
|
|
106
|
+
for (const [length, included1, included2] of align(subseq1, subseq2)) {
|
|
107
|
+
if (!included2) {
|
|
108
|
+
pushSegment(result, length, included1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
function expand(subseq1, subseq2) {
|
|
114
|
+
if (measure(subseq1).length !== measure(subseq2).excludedLength) {
|
|
115
|
+
throw new Error("Length mismatch");
|
|
116
|
+
}
|
|
117
|
+
const result = [];
|
|
118
|
+
for (let i1 = 0, i2 = 0, length1 = 0, included1 = true, included2 = true; i2 < subseq2.length; i2++) {
|
|
119
|
+
let length2 = subseq2[i2];
|
|
120
|
+
included2 = !included2;
|
|
121
|
+
if (included2) {
|
|
122
|
+
pushSegment(result, length2, false);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
while (length2) {
|
|
126
|
+
if (length1 === 0) {
|
|
127
|
+
length1 = subseq1[i1++];
|
|
128
|
+
included1 = !included1;
|
|
129
|
+
}
|
|
130
|
+
const minLength = Math.min(length1, length2);
|
|
131
|
+
pushSegment(result, minLength, included1);
|
|
132
|
+
length1 -= minLength;
|
|
133
|
+
length2 -= minLength;
|
|
76
134
|
}
|
|
77
|
-
// TODO: We can probably avoid the commonSuffixLength() call here in
|
|
78
|
-
// favor of arithmetic but I’m too dumb to figure it out.
|
|
79
|
-
suffix = commonSuffixLength(text1.slice(prefix), text2.slice(prefix));
|
|
80
135
|
}
|
|
81
|
-
return Edit.build(text1, text2.slice(prefix, text2.length - suffix), prefix, text1.length - suffix);
|
|
82
136
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
function interleave(subseq1, subseq2) {
|
|
140
|
+
if (measure(subseq1).excludedLength !== measure(subseq2).excludedLength) {
|
|
141
|
+
throw new Error("Length mismatch");
|
|
142
|
+
}
|
|
143
|
+
const result1 = [];
|
|
144
|
+
const result2 = [];
|
|
145
|
+
for (let i1 = 0, i2 = 0, length1 = 0, length2 = 0, included1 = true, included2 = true; i1 < subseq1.length || i2 < subseq2.length;) {
|
|
146
|
+
if (length1 === 0 && i1 < subseq1.length) {
|
|
147
|
+
length1 = subseq1[i1++];
|
|
148
|
+
included1 = !included1;
|
|
149
|
+
}
|
|
150
|
+
if (length2 === 0 && i2 < subseq2.length) {
|
|
151
|
+
length2 = subseq2[i2++];
|
|
152
|
+
included2 = !included2;
|
|
153
|
+
}
|
|
154
|
+
if (included1 && included2) {
|
|
155
|
+
pushSegment(result1, length1, true);
|
|
156
|
+
pushSegment(result1, length2, false);
|
|
157
|
+
pushSegment(result2, length1, false);
|
|
158
|
+
pushSegment(result2, length2, true);
|
|
159
|
+
length1 = length2 = 0;
|
|
160
|
+
}
|
|
161
|
+
else if (included1) {
|
|
162
|
+
pushSegment(result1, length1, true);
|
|
163
|
+
pushSegment(result2, length1, false);
|
|
164
|
+
length1 = 0;
|
|
165
|
+
}
|
|
166
|
+
else if (included2) {
|
|
167
|
+
pushSegment(result1, length2, false);
|
|
168
|
+
pushSegment(result2, length2, true);
|
|
169
|
+
length2 = 0;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const minLength = Math.min(length1, length2);
|
|
173
|
+
pushSegment(result1, minLength, false);
|
|
174
|
+
pushSegment(result2, minLength, false);
|
|
175
|
+
length1 -= minLength;
|
|
176
|
+
length2 -= minLength;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [result1, result2];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A compact data structure for representing changes to strings. */
|
|
183
|
+
class Edit {
|
|
184
|
+
constructor(parts, deleted) {
|
|
185
|
+
this.parts = parts;
|
|
186
|
+
this.deleted = deleted;
|
|
187
|
+
}
|
|
188
|
+
/** A string which represents a concatenation of all insertions. */
|
|
86
189
|
get inserted() {
|
|
87
190
|
let text = "";
|
|
88
191
|
for (let i = 0; i < this.parts.length; i++) {
|
|
@@ -92,6 +195,20 @@ class Edit {
|
|
|
92
195
|
}
|
|
93
196
|
return text;
|
|
94
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Returns an array of operations, which is more readable than the parts
|
|
200
|
+
* array.
|
|
201
|
+
*
|
|
202
|
+
* new Edit([0, 1, " ", 2], "x").operations();
|
|
203
|
+
* [
|
|
204
|
+
* {type: "retain", start: 0, end: 1},
|
|
205
|
+
* {type: "insert", start: 1, value: " "},
|
|
206
|
+
* {type: "delete", start: 1, end: 2, value: "x"},
|
|
207
|
+
* ]
|
|
208
|
+
*
|
|
209
|
+
* When insertions and deletions happen at the same index, insertions will
|
|
210
|
+
* always appear before deletions in the operations array.
|
|
211
|
+
*/
|
|
95
212
|
operations() {
|
|
96
213
|
const operations = [];
|
|
97
214
|
let retaining = false;
|
|
@@ -145,155 +262,307 @@ class Edit {
|
|
|
145
262
|
}
|
|
146
263
|
return text1;
|
|
147
264
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
case "insert":
|
|
169
|
-
subseq.Subseq.pushSegment(insertSizes, op.value.length, true);
|
|
170
|
-
inserted += op.value;
|
|
171
|
-
break;
|
|
172
|
-
default:
|
|
173
|
-
throw new TypeError("Invalid operation type");
|
|
265
|
+
/** Composes two consecutive edits. */
|
|
266
|
+
compose(that) {
|
|
267
|
+
let [insertSeq1, inserted1, deleteSeq1, deleted1] = factor(this);
|
|
268
|
+
let [insertSeq2, inserted2, deleteSeq2, deleted2] = factor(that);
|
|
269
|
+
// Expand all subseqs so that they share the same coordinate space.
|
|
270
|
+
deleteSeq1 = expand(deleteSeq1, insertSeq1);
|
|
271
|
+
deleteSeq2 = expand(deleteSeq2, deleteSeq1);
|
|
272
|
+
[deleteSeq1, insertSeq2] = interleave(deleteSeq1, insertSeq2);
|
|
273
|
+
deleteSeq2 = expand(deleteSeq2, insertSeq2);
|
|
274
|
+
insertSeq1 = expand(insertSeq1, insertSeq2);
|
|
275
|
+
{
|
|
276
|
+
// Find insertions which have been deleted and remove them.
|
|
277
|
+
const toggleSeq = intersection(insertSeq1, deleteSeq2);
|
|
278
|
+
if (measure(toggleSeq).includedLength) {
|
|
279
|
+
deleteSeq1 = shrink(deleteSeq1, toggleSeq);
|
|
280
|
+
inserted1 = erase(insertSeq1, inserted1, toggleSeq);
|
|
281
|
+
insertSeq1 = shrink(insertSeq1, toggleSeq);
|
|
282
|
+
deleteSeq2 = shrink(deleteSeq2, toggleSeq);
|
|
283
|
+
insertSeq2 = shrink(insertSeq2, toggleSeq);
|
|
174
284
|
}
|
|
175
285
|
}
|
|
176
|
-
const insertSeq =
|
|
177
|
-
const
|
|
178
|
-
|
|
286
|
+
const insertSeq = union(insertSeq1, insertSeq2);
|
|
287
|
+
const inserted = consolidate(insertSeq1, inserted1, insertSeq2, inserted2);
|
|
288
|
+
const deleteSeq = shrink(union(deleteSeq1, deleteSeq2), insertSeq);
|
|
289
|
+
const deleted = deleted1 != null && deleted2 != null
|
|
290
|
+
? consolidate(deleteSeq1, deleted1, deleteSeq2, deleted2)
|
|
291
|
+
: undefined;
|
|
292
|
+
return synthesize(insertSeq, inserted, deleteSeq, deleted).normalize();
|
|
293
|
+
}
|
|
294
|
+
invert() {
|
|
295
|
+
if (typeof this.deleted === "undefined") {
|
|
296
|
+
throw new Error("Edit is not invertible");
|
|
297
|
+
}
|
|
298
|
+
let [insertSeq, inserted, deleteSeq, deleted] = factor(this);
|
|
299
|
+
deleteSeq = expand(deleteSeq, insertSeq);
|
|
300
|
+
insertSeq = shrink(insertSeq, deleteSeq);
|
|
301
|
+
return synthesize(deleteSeq, deleted, insertSeq, inserted);
|
|
179
302
|
}
|
|
180
303
|
normalize() {
|
|
181
304
|
if (typeof this.deleted === "undefined") {
|
|
182
|
-
throw new Error("
|
|
305
|
+
throw new Error("Edit is not normalizable");
|
|
183
306
|
}
|
|
184
|
-
const
|
|
185
|
-
const
|
|
307
|
+
const insertSeq = [];
|
|
308
|
+
const deleteSeq = [];
|
|
186
309
|
let inserted = "";
|
|
187
310
|
let deleted = "";
|
|
188
|
-
let
|
|
311
|
+
let insertion;
|
|
189
312
|
const operations = this.operations();
|
|
190
313
|
for (let i = 0; i < operations.length; i++) {
|
|
191
314
|
const op = operations[i];
|
|
192
315
|
switch (op.type) {
|
|
193
|
-
case "insert":
|
|
194
|
-
|
|
316
|
+
case "insert": {
|
|
317
|
+
insertion = op.value;
|
|
195
318
|
break;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
319
|
+
}
|
|
320
|
+
case "retain": {
|
|
321
|
+
if (insertion !== undefined) {
|
|
322
|
+
pushSegment(insertSeq, insertion.length, true);
|
|
323
|
+
inserted += insertion;
|
|
324
|
+
insertion = undefined;
|
|
201
325
|
}
|
|
202
|
-
|
|
203
|
-
|
|
326
|
+
pushSegment(insertSeq, op.end - op.start, false);
|
|
327
|
+
pushSegment(deleteSeq, op.end - op.start, false);
|
|
204
328
|
break;
|
|
329
|
+
}
|
|
205
330
|
case "delete": {
|
|
206
331
|
const length = op.end - op.start;
|
|
332
|
+
const deletion = op.value;
|
|
207
333
|
let prefix = 0;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
334
|
+
let suffix = 0;
|
|
335
|
+
if (insertion !== undefined) {
|
|
336
|
+
if (insertion === deletion) {
|
|
337
|
+
prefix = deletion.length;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
prefix = commonPrefixLength(insertion, deletion);
|
|
341
|
+
suffix = commonSuffixLength(insertion.slice(prefix), deletion.slice(prefix));
|
|
342
|
+
}
|
|
343
|
+
pushSegment(insertSeq, prefix, false);
|
|
344
|
+
pushSegment(insertSeq, insertion.length - prefix - suffix, true);
|
|
345
|
+
inserted += insertion.slice(prefix, insertion.length - suffix);
|
|
214
346
|
}
|
|
215
|
-
deleted +=
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
347
|
+
deleted += deletion.slice(prefix, deletion.length - suffix);
|
|
348
|
+
pushSegment(deleteSeq, prefix, false);
|
|
349
|
+
// TODO: This line is throwing for some reason
|
|
350
|
+
pushSegment(deleteSeq, length - prefix - suffix, true);
|
|
351
|
+
pushSegment(deleteSeq, suffix, false);
|
|
352
|
+
pushSegment(insertSeq, length - prefix - suffix, false);
|
|
353
|
+
pushSegment(insertSeq, suffix, false);
|
|
354
|
+
insertion = undefined;
|
|
219
355
|
break;
|
|
220
356
|
}
|
|
221
|
-
default:
|
|
222
|
-
throw new TypeError("Invalid operation type");
|
|
223
357
|
}
|
|
224
358
|
}
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
inserted +=
|
|
359
|
+
if (insertion !== undefined) {
|
|
360
|
+
pushSegment(insertSeq, insertion.length, true);
|
|
361
|
+
inserted += insertion;
|
|
362
|
+
}
|
|
363
|
+
return synthesize(insertSeq, inserted, deleteSeq, deleted);
|
|
364
|
+
}
|
|
365
|
+
hasChangesBetween(start, end) {
|
|
366
|
+
const ops = this.operations();
|
|
367
|
+
for (const op of ops) {
|
|
368
|
+
switch (op.type) {
|
|
369
|
+
case "delete": {
|
|
370
|
+
if ((start <= op.start && op.start <= end) ||
|
|
371
|
+
(start <= op.end && op.end <= end)) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case "insert": {
|
|
377
|
+
if (start <= op.start && op.start <= end) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
228
383
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
static builder(value) {
|
|
387
|
+
let index = 0;
|
|
388
|
+
let inserted = "";
|
|
389
|
+
let deleted = undefined;
|
|
390
|
+
const insertSeq = [];
|
|
391
|
+
const deleteSeq = [];
|
|
392
|
+
return {
|
|
393
|
+
retain(length) {
|
|
394
|
+
if (value != null) {
|
|
395
|
+
length = Math.min(value.length - index, length);
|
|
396
|
+
}
|
|
397
|
+
index += length;
|
|
398
|
+
pushSegment(insertSeq, length, false);
|
|
399
|
+
pushSegment(deleteSeq, length, false);
|
|
400
|
+
return this;
|
|
401
|
+
},
|
|
402
|
+
delete(length) {
|
|
403
|
+
if (value != null) {
|
|
404
|
+
length = Math.min(value.length - index, length);
|
|
405
|
+
deleted = (deleted || "") + value.slice(index, index + length);
|
|
406
|
+
}
|
|
407
|
+
index += length;
|
|
408
|
+
pushSegment(insertSeq, length, false);
|
|
409
|
+
pushSegment(deleteSeq, length, true);
|
|
410
|
+
return this;
|
|
411
|
+
},
|
|
412
|
+
insert(value) {
|
|
413
|
+
pushSegment(insertSeq, value.length, true);
|
|
414
|
+
inserted += value;
|
|
415
|
+
return this;
|
|
416
|
+
},
|
|
417
|
+
concat(edit) {
|
|
418
|
+
const ops = edit.operations();
|
|
419
|
+
for (const op of ops) {
|
|
420
|
+
switch (op.type) {
|
|
421
|
+
case "delete":
|
|
422
|
+
this.delete(op.end - op.start);
|
|
423
|
+
break;
|
|
424
|
+
case "insert":
|
|
425
|
+
this.insert(op.value);
|
|
426
|
+
break;
|
|
427
|
+
case "retain":
|
|
428
|
+
this.retain(op.end - op.start);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (value != null && index > value.length) {
|
|
433
|
+
throw new RangeError("Edit is longer than original value");
|
|
434
|
+
}
|
|
435
|
+
return this;
|
|
436
|
+
},
|
|
437
|
+
build() {
|
|
438
|
+
if (value != null) {
|
|
439
|
+
deleted = deleted || "";
|
|
440
|
+
if (index < value.length) {
|
|
441
|
+
pushSegment(insertSeq, value.length - index, false);
|
|
442
|
+
pushSegment(deleteSeq, value.length - index, false);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return synthesize(insertSeq, inserted, deleteSeq, deleted);
|
|
446
|
+
},
|
|
447
|
+
};
|
|
232
448
|
}
|
|
233
449
|
/**
|
|
234
|
-
*
|
|
450
|
+
* Given two strings, this method finds an edit which can be applied to the
|
|
451
|
+
* first string to result in the second.
|
|
452
|
+
*
|
|
453
|
+
* @param startHint - An optional hint can be provided to disambiguate edits
|
|
454
|
+
* which cannot be inferred by comparing the text alone. For example,
|
|
455
|
+
* inserting "a" into the string "aaaa" to make it "aaaaa" could be an
|
|
456
|
+
* insertion at any index in the string. This value should be the smaller of
|
|
457
|
+
* the start indices of the selection from before and after the edit.
|
|
235
458
|
*/
|
|
236
|
-
|
|
237
|
-
let
|
|
238
|
-
let
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
deleteSeq2 = deleteSeq2.expand(insertSeq2);
|
|
244
|
-
insertSeq1 = insertSeq1.expand(insertSeq2);
|
|
245
|
-
// Find insertions which have been deleted and remove them.
|
|
246
|
-
{
|
|
247
|
-
const toggleSeq = insertSeq1.intersection(deleteSeq2);
|
|
248
|
-
if (toggleSeq.includedSize) {
|
|
249
|
-
deleteSeq1 = deleteSeq1.shrink(toggleSeq);
|
|
250
|
-
inserted1 = erase(insertSeq1, inserted1, toggleSeq);
|
|
251
|
-
insertSeq1 = insertSeq1.shrink(toggleSeq);
|
|
252
|
-
deleteSeq2 = deleteSeq2.shrink(toggleSeq);
|
|
253
|
-
insertSeq2 = insertSeq2.shrink(toggleSeq);
|
|
459
|
+
static diff(text1, text2, startHint) {
|
|
460
|
+
let prefix = commonPrefixLength(text1, text2);
|
|
461
|
+
let suffix = commonSuffixLength(text1, text2);
|
|
462
|
+
// prefix and suffix overlap when edits are in runs of the same character.
|
|
463
|
+
if (prefix + suffix > Math.min(text1.length, text2.length)) {
|
|
464
|
+
if (startHint != null && startHint >= 0) {
|
|
465
|
+
prefix = Math.min(prefix, startHint);
|
|
254
466
|
}
|
|
467
|
+
// TODO: We can probably avoid the commonSuffixLength() call here in
|
|
468
|
+
// favor of arithmetic but I’m too dumb to figure it out.
|
|
469
|
+
suffix = commonSuffixLength(text1.slice(prefix), text2.slice(prefix));
|
|
255
470
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return Edit.synthesize(insertSeq, inserted, deleteSeq, deleted).normalize();
|
|
471
|
+
return Edit.builder(text1)
|
|
472
|
+
.retain(prefix)
|
|
473
|
+
.insert(text2.slice(prefix, text2.length - suffix))
|
|
474
|
+
.delete(text1.length - prefix - suffix)
|
|
475
|
+
.retain(suffix)
|
|
476
|
+
.build();
|
|
263
477
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
478
|
+
}
|
|
479
|
+
function synthesize(insertSeq, inserted, deleteSeq, deleted) {
|
|
480
|
+
if (measure(insertSeq).includedLength !== inserted.length) {
|
|
481
|
+
throw new Error("insertSeq and inserted string do not match in length");
|
|
482
|
+
}
|
|
483
|
+
else if (deleted !== undefined &&
|
|
484
|
+
measure(deleteSeq).includedLength !== deleted.length) {
|
|
485
|
+
throw new Error("deleteSeq and deleted string do not match in length");
|
|
486
|
+
}
|
|
487
|
+
const parts = [];
|
|
488
|
+
let insertIndex = 0;
|
|
489
|
+
let retainIndex = 0;
|
|
490
|
+
let needsLength = true;
|
|
491
|
+
for (const [length, deleting, inserting] of align(expand(deleteSeq, insertSeq), insertSeq)) {
|
|
492
|
+
if (inserting) {
|
|
493
|
+
const insertion = inserted.slice(insertIndex, insertIndex + length);
|
|
494
|
+
if (parts.length && typeof parts[parts.length - 1] === "string") {
|
|
495
|
+
parts[parts.length - 1] += insertion;
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
parts.push(insertion);
|
|
499
|
+
}
|
|
500
|
+
insertIndex += length;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
if (!deleting) {
|
|
504
|
+
parts.push(retainIndex, retainIndex + length);
|
|
505
|
+
}
|
|
506
|
+
retainIndex += length;
|
|
507
|
+
needsLength = deleting;
|
|
267
508
|
}
|
|
268
|
-
let [insertSeq, inserted, deleteSeq, deleted] = this.factor();
|
|
269
|
-
deleteSeq = deleteSeq.expand(insertSeq);
|
|
270
|
-
insertSeq = insertSeq.shrink(deleteSeq);
|
|
271
|
-
return Edit.synthesize(deleteSeq, deleted, insertSeq, inserted);
|
|
272
509
|
}
|
|
510
|
+
if (needsLength) {
|
|
511
|
+
parts.push(retainIndex);
|
|
512
|
+
}
|
|
513
|
+
return new Edit(parts, deleted);
|
|
514
|
+
}
|
|
515
|
+
function factor(edit) {
|
|
516
|
+
const insertSeq = [];
|
|
517
|
+
const deleteSeq = [];
|
|
518
|
+
let inserted = "";
|
|
519
|
+
const operations = edit.operations();
|
|
520
|
+
for (let i = 0; i < operations.length; i++) {
|
|
521
|
+
const op = operations[i];
|
|
522
|
+
switch (op.type) {
|
|
523
|
+
case "retain": {
|
|
524
|
+
const length = op.end - op.start;
|
|
525
|
+
pushSegment(insertSeq, length, false);
|
|
526
|
+
pushSegment(deleteSeq, length, false);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case "delete": {
|
|
530
|
+
const length = op.end - op.start;
|
|
531
|
+
pushSegment(insertSeq, length, false);
|
|
532
|
+
pushSegment(deleteSeq, length, true);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case "insert":
|
|
536
|
+
pushSegment(insertSeq, op.value.length, true);
|
|
537
|
+
inserted += op.value;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return [insertSeq, inserted, deleteSeq, edit.deleted];
|
|
273
542
|
}
|
|
274
543
|
/**
|
|
275
544
|
* Given two subseqs and strings which are represented by the included segments
|
|
276
545
|
* of the subseqs, this function combines the two strings so that they overlap
|
|
277
546
|
* according to the positions of the included segments of subseqs.
|
|
278
547
|
*
|
|
279
|
-
* The subseqs must have the same
|
|
548
|
+
* The subseqs must have the same length, and the included segments of these
|
|
280
549
|
* subseqs may not overlap.
|
|
281
550
|
*/
|
|
282
551
|
function consolidate(subseq1, text1, subseq2, text2) {
|
|
283
552
|
let i1 = 0;
|
|
284
553
|
let i2 = 0;
|
|
285
554
|
let result = "";
|
|
286
|
-
for (const [
|
|
287
|
-
if (
|
|
555
|
+
for (const [length, included1, included2] of align(subseq1, subseq2)) {
|
|
556
|
+
if (included1 && included2) {
|
|
288
557
|
throw new Error("Overlapping subseqs");
|
|
289
558
|
}
|
|
290
|
-
else if (
|
|
291
|
-
result += text1.slice(i1, i1 +
|
|
292
|
-
i1 +=
|
|
559
|
+
else if (included1) {
|
|
560
|
+
result += text1.slice(i1, i1 + length);
|
|
561
|
+
i1 += length;
|
|
293
562
|
}
|
|
294
|
-
else if (
|
|
295
|
-
result += text2.slice(i2, i2 +
|
|
296
|
-
i2 +=
|
|
563
|
+
else if (included2) {
|
|
564
|
+
result += text2.slice(i2, i2 + length);
|
|
565
|
+
i2 += length;
|
|
297
566
|
}
|
|
298
567
|
}
|
|
299
568
|
return result;
|
|
@@ -303,28 +572,26 @@ function consolidate(subseq1, text1, subseq2, text2) {
|
|
|
303
572
|
* segments of the first subseq, this function returns the result of removing
|
|
304
573
|
* the included segments of the second subseq from the first subseq.
|
|
305
574
|
*
|
|
306
|
-
* The subseqs must have the same
|
|
575
|
+
* The subseqs must have the same length, and the included segments of the second
|
|
307
576
|
* subseq must overlap with the first subseq’s included segments.
|
|
308
577
|
*/
|
|
309
578
|
function erase(subseq1, str, subseq2) {
|
|
310
579
|
let i = 0;
|
|
311
580
|
let result = "";
|
|
312
|
-
for (const [
|
|
313
|
-
if (
|
|
314
|
-
if (!
|
|
315
|
-
result += str.slice(i, i +
|
|
581
|
+
for (const [length, included1, included2] of align(subseq1, subseq2)) {
|
|
582
|
+
if (included1) {
|
|
583
|
+
if (!included2) {
|
|
584
|
+
result += str.slice(i, i + length);
|
|
316
585
|
}
|
|
317
|
-
i +=
|
|
586
|
+
i += length;
|
|
318
587
|
}
|
|
319
|
-
else if (
|
|
588
|
+
else if (included2) {
|
|
320
589
|
throw new Error("Non-overlapping subseqs");
|
|
321
590
|
}
|
|
322
591
|
}
|
|
323
592
|
return result;
|
|
324
593
|
}
|
|
325
|
-
/**
|
|
326
|
-
* @returns The length of the common prefix between two strings.
|
|
327
|
-
*/
|
|
594
|
+
/** @returns The length of the common prefix between two strings. */
|
|
328
595
|
function commonPrefixLength(text1, text2) {
|
|
329
596
|
const length = Math.min(text1.length, text2.length);
|
|
330
597
|
for (let i = 0; i < length; i++) {
|
|
@@ -334,9 +601,7 @@ function commonPrefixLength(text1, text2) {
|
|
|
334
601
|
}
|
|
335
602
|
return length;
|
|
336
603
|
}
|
|
337
|
-
/**
|
|
338
|
-
* @returns The length of the common suffix between two strings.
|
|
339
|
-
*/
|
|
604
|
+
/** @returns The length of the common suffix between two strings. */
|
|
340
605
|
function commonSuffixLength(text1, text2) {
|
|
341
606
|
const length1 = text1.length;
|
|
342
607
|
const length2 = text2.length;
|