@b9g/revise 0.1.1 → 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 +325 -412
- package/contentarea.cjs.map +1 -1
- package/contentarea.d.ts +29 -47
- package/contentarea.js +325 -412
- package/contentarea.js.map +1 -1
- package/edit.cjs +447 -187
- package/edit.cjs.map +1 -1
- package/edit.d.ts +71 -26
- package/edit.js +447 -187
- 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 +5 -7
- package/keyer.cjs.map +1 -1
- package/keyer.d.ts +1 -1
- package/keyer.js +5 -7
- 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");
|
|
22
|
+
}
|
|
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");
|
|
14
47
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
deleteSeq.includedSize !== deleted.length) {
|
|
21
|
-
throw new Error("deleteSeq and deleted string do not match in length");
|
|
22
|
-
}
|
|
23
|
-
else if (deleteSeq.size !== insertSeq.excludedSize) {
|
|
24
|
-
throw new Error("deleteSeq and insertSeq do not match in length");
|
|
25
|
-
}
|
|
26
|
-
const parts = [];
|
|
27
|
-
let insertIndex = 0;
|
|
28
|
-
let retainIndex = 0;
|
|
29
|
-
let needsLength = true;
|
|
30
|
-
for (const [length, deleting, inserting] of deleteSeq
|
|
31
|
-
.expand(insertSeq)
|
|
32
|
-
.align(insertSeq)) {
|
|
33
|
-
if (inserting) {
|
|
34
|
-
parts.push(inserted.slice(insertIndex, insertIndex + length));
|
|
35
|
-
insertIndex += length;
|
|
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]);
|
|
43
67
|
}
|
|
68
|
+
length2 = length2 - length1;
|
|
69
|
+
length1 = 0;
|
|
44
70
|
}
|
|
45
|
-
if (
|
|
46
|
-
|
|
71
|
+
else if (length1 > length2) {
|
|
72
|
+
if (length2) {
|
|
73
|
+
result.push([length2, included1, included2]);
|
|
74
|
+
}
|
|
75
|
+
length1 = length1 - length2;
|
|
76
|
+
length2 = 0;
|
|
77
|
+
}
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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);
|
|
91
|
+
}
|
|
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);
|
|
62
98
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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;
|
|
@@ -129,8 +246,6 @@ class Edit {
|
|
|
129
246
|
}
|
|
130
247
|
return operations;
|
|
131
248
|
}
|
|
132
|
-
// TODO: I’m not too happy about the name of this method, insofar as it might
|
|
133
|
-
// imply that this object is callable.
|
|
134
249
|
apply(text) {
|
|
135
250
|
let text1 = "";
|
|
136
251
|
const operations = this.operations();
|
|
@@ -147,158 +262,307 @@ class Edit {
|
|
|
147
262
|
}
|
|
148
263
|
return text1;
|
|
149
264
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
let
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
case "insert":
|
|
171
|
-
subseq.Subseq.pushSegment(insertSizes, op.value.length, true);
|
|
172
|
-
inserted += op.value;
|
|
173
|
-
break;
|
|
174
|
-
default:
|
|
175
|
-
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);
|
|
176
284
|
}
|
|
177
285
|
}
|
|
178
|
-
const insertSeq =
|
|
179
|
-
const
|
|
180
|
-
|
|
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);
|
|
181
302
|
}
|
|
182
303
|
normalize() {
|
|
183
304
|
if (typeof this.deleted === "undefined") {
|
|
184
|
-
throw new Error("
|
|
305
|
+
throw new Error("Edit is not normalizable");
|
|
185
306
|
}
|
|
186
|
-
const
|
|
187
|
-
const
|
|
307
|
+
const insertSeq = [];
|
|
308
|
+
const deleteSeq = [];
|
|
188
309
|
let inserted = "";
|
|
189
310
|
let deleted = "";
|
|
190
|
-
let
|
|
311
|
+
let insertion;
|
|
191
312
|
const operations = this.operations();
|
|
192
313
|
for (let i = 0; i < operations.length; i++) {
|
|
193
314
|
const op = operations[i];
|
|
194
315
|
switch (op.type) {
|
|
195
316
|
case "insert": {
|
|
196
|
-
|
|
317
|
+
insertion = op.value;
|
|
197
318
|
break;
|
|
198
319
|
}
|
|
199
320
|
case "retain": {
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
inserted +=
|
|
203
|
-
|
|
321
|
+
if (insertion !== undefined) {
|
|
322
|
+
pushSegment(insertSeq, insertion.length, true);
|
|
323
|
+
inserted += insertion;
|
|
324
|
+
insertion = undefined;
|
|
204
325
|
}
|
|
205
|
-
|
|
206
|
-
|
|
326
|
+
pushSegment(insertSeq, op.end - op.start, false);
|
|
327
|
+
pushSegment(deleteSeq, op.end - op.start, false);
|
|
207
328
|
break;
|
|
208
329
|
}
|
|
209
330
|
case "delete": {
|
|
210
331
|
const length = op.end - op.start;
|
|
332
|
+
const deletion = op.value;
|
|
211
333
|
let prefix = 0;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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);
|
|
218
346
|
}
|
|
219
|
-
deleted +=
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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;
|
|
223
355
|
break;
|
|
224
356
|
}
|
|
225
|
-
default: {
|
|
226
|
-
throw new TypeError("Invalid operation type");
|
|
227
|
-
}
|
|
228
357
|
}
|
|
229
358
|
}
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
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
|
+
}
|
|
233
383
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
};
|
|
237
448
|
}
|
|
238
449
|
/**
|
|
239
|
-
*
|
|
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.
|
|
240
458
|
*/
|
|
241
|
-
|
|
242
|
-
let
|
|
243
|
-
let
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
deleteSeq2 = deleteSeq2.expand(insertSeq2);
|
|
249
|
-
insertSeq1 = insertSeq1.expand(insertSeq2);
|
|
250
|
-
// Find insertions which have been deleted and remove them.
|
|
251
|
-
{
|
|
252
|
-
const toggleSeq = insertSeq1.intersection(deleteSeq2);
|
|
253
|
-
if (toggleSeq.includedSize) {
|
|
254
|
-
deleteSeq1 = deleteSeq1.shrink(toggleSeq);
|
|
255
|
-
inserted1 = erase(insertSeq1, inserted1, toggleSeq);
|
|
256
|
-
insertSeq1 = insertSeq1.shrink(toggleSeq);
|
|
257
|
-
deleteSeq2 = deleteSeq2.shrink(toggleSeq);
|
|
258
|
-
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);
|
|
259
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));
|
|
260
470
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
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();
|
|
268
477
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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;
|
|
272
508
|
}
|
|
273
|
-
let [insertSeq, inserted, deleteSeq, deleted] = this.factor();
|
|
274
|
-
deleteSeq = deleteSeq.expand(insertSeq);
|
|
275
|
-
insertSeq = insertSeq.shrink(deleteSeq);
|
|
276
|
-
return Edit.synthesize(deleteSeq, deleted, insertSeq, inserted);
|
|
277
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];
|
|
278
542
|
}
|
|
279
543
|
/**
|
|
280
544
|
* Given two subseqs and strings which are represented by the included segments
|
|
281
545
|
* of the subseqs, this function combines the two strings so that they overlap
|
|
282
546
|
* according to the positions of the included segments of subseqs.
|
|
283
547
|
*
|
|
284
|
-
* The subseqs must have the same
|
|
548
|
+
* The subseqs must have the same length, and the included segments of these
|
|
285
549
|
* subseqs may not overlap.
|
|
286
550
|
*/
|
|
287
551
|
function consolidate(subseq1, text1, subseq2, text2) {
|
|
288
552
|
let i1 = 0;
|
|
289
553
|
let i2 = 0;
|
|
290
554
|
let result = "";
|
|
291
|
-
for (const [
|
|
292
|
-
if (
|
|
555
|
+
for (const [length, included1, included2] of align(subseq1, subseq2)) {
|
|
556
|
+
if (included1 && included2) {
|
|
293
557
|
throw new Error("Overlapping subseqs");
|
|
294
558
|
}
|
|
295
|
-
else if (
|
|
296
|
-
result += text1.slice(i1, i1 +
|
|
297
|
-
i1 +=
|
|
559
|
+
else if (included1) {
|
|
560
|
+
result += text1.slice(i1, i1 + length);
|
|
561
|
+
i1 += length;
|
|
298
562
|
}
|
|
299
|
-
else if (
|
|
300
|
-
result += text2.slice(i2, i2 +
|
|
301
|
-
i2 +=
|
|
563
|
+
else if (included2) {
|
|
564
|
+
result += text2.slice(i2, i2 + length);
|
|
565
|
+
i2 += length;
|
|
302
566
|
}
|
|
303
567
|
}
|
|
304
568
|
return result;
|
|
@@ -308,28 +572,26 @@ function consolidate(subseq1, text1, subseq2, text2) {
|
|
|
308
572
|
* segments of the first subseq, this function returns the result of removing
|
|
309
573
|
* the included segments of the second subseq from the first subseq.
|
|
310
574
|
*
|
|
311
|
-
* The subseqs must have the same
|
|
575
|
+
* The subseqs must have the same length, and the included segments of the second
|
|
312
576
|
* subseq must overlap with the first subseq’s included segments.
|
|
313
577
|
*/
|
|
314
578
|
function erase(subseq1, str, subseq2) {
|
|
315
579
|
let i = 0;
|
|
316
580
|
let result = "";
|
|
317
|
-
for (const [
|
|
318
|
-
if (
|
|
319
|
-
if (!
|
|
320
|
-
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);
|
|
321
585
|
}
|
|
322
|
-
i +=
|
|
586
|
+
i += length;
|
|
323
587
|
}
|
|
324
|
-
else if (
|
|
588
|
+
else if (included2) {
|
|
325
589
|
throw new Error("Non-overlapping subseqs");
|
|
326
590
|
}
|
|
327
591
|
}
|
|
328
592
|
return result;
|
|
329
593
|
}
|
|
330
|
-
/**
|
|
331
|
-
* @returns The length of the common prefix between two strings.
|
|
332
|
-
*/
|
|
594
|
+
/** @returns The length of the common prefix between two strings. */
|
|
333
595
|
function commonPrefixLength(text1, text2) {
|
|
334
596
|
const length = Math.min(text1.length, text2.length);
|
|
335
597
|
for (let i = 0; i < length; i++) {
|
|
@@ -339,9 +601,7 @@ function commonPrefixLength(text1, text2) {
|
|
|
339
601
|
}
|
|
340
602
|
return length;
|
|
341
603
|
}
|
|
342
|
-
/**
|
|
343
|
-
* @returns The length of the common suffix between two strings.
|
|
344
|
-
*/
|
|
604
|
+
/** @returns The length of the common suffix between two strings. */
|
|
345
605
|
function commonSuffixLength(text1, text2) {
|
|
346
606
|
const length1 = text1.length;
|
|
347
607
|
const length2 = text2.length;
|