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