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