@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/edit.cjs CHANGED
@@ -2,87 +2,190 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var subseq = require('./subseq.cjs');
6
-
7
- /**
8
- * A data structure which represents edits to strings.
9
- */
10
- class Edit {
11
- constructor(parts, deleted) {
12
- this.parts = parts;
13
- this.deleted = deleted;
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
- static synthesize(insertSeq, inserted, deleteSeq, deleted) {
16
- if (insertSeq.includedSize !== inserted.length) {
17
- throw new Error("insertSeq and inserted string do not match in length");
18
- }
19
- else if (deleted !== undefined &&
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;
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
- else {
38
- if (!deleting) {
39
- parts.push(retainIndex, retainIndex + length);
40
- }
41
- retainIndex += length;
42
- needsLength = deleting;
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
- if (needsLength) {
46
- parts.push(retainIndex);
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
- static build(text, inserted, from, to = from) {
51
- const insertSizes = [];
52
- subseq.Subseq.pushSegment(insertSizes, from, false);
53
- subseq.Subseq.pushSegment(insertSizes, inserted.length, true);
54
- subseq.Subseq.pushSegment(insertSizes, to - from, false);
55
- subseq.Subseq.pushSegment(insertSizes, text.length - to, false);
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
- * Given two strings, this method finds an edit which can be applied to the
65
- * first string to result in the second.
66
- * An optional hint can be provided to disambiguate edits when they are made
67
- * to overlapping characters.
68
- */
69
- static diff(text1, text2, hint) {
70
- let prefix = commonPrefixLength(text1, text2);
71
- let suffix = commonSuffixLength(text1, text2);
72
- // prefix and suffix overlap when edits are runs of the same character.
73
- if (prefix + suffix > Math.min(text1.length, text2.length)) {
74
- if (hint !== undefined && hint > -1) {
75
- prefix = Math.min(prefix, hint);
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
- * A string which represents a concatenation of all insertions.
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
- factor() {
149
- const insertSizes = [];
150
- const deleteSizes = [];
151
- let inserted = "";
152
- const operations = this.operations();
153
- for (let i = 0; i < operations.length; i++) {
154
- const op = operations[i];
155
- switch (op.type) {
156
- case "retain": {
157
- const length = op.end - op.start;
158
- subseq.Subseq.pushSegment(insertSizes, length, false);
159
- subseq.Subseq.pushSegment(deleteSizes, length, false);
160
- break;
161
- }
162
- case "delete": {
163
- const length = op.end - op.start;
164
- subseq.Subseq.pushSegment(insertSizes, length, false);
165
- subseq.Subseq.pushSegment(deleteSizes, length, true);
166
- break;
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 = new subseq.Subseq(insertSizes);
177
- const deleteSeq = new subseq.Subseq(deleteSizes);
178
- return [insertSeq, inserted, deleteSeq, this.deleted];
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("Missing deleted property");
305
+ throw new Error("Edit is not normalizable");
183
306
  }
184
- const insertSizes = [];
185
- const deleteSizes = [];
307
+ const insertSeq = [];
308
+ const deleteSeq = [];
186
309
  let inserted = "";
187
310
  let deleted = "";
188
- let prevInserted;
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
- prevInserted = op.value;
316
+ case "insert": {
317
+ insertion = op.value;
195
318
  break;
196
- case "retain":
197
- if (prevInserted !== undefined) {
198
- subseq.Subseq.pushSegment(insertSizes, prevInserted.length, true);
199
- inserted += prevInserted;
200
- prevInserted = undefined;
319
+ }
320
+ case "retain": {
321
+ if (insertion !== undefined) {
322
+ pushSegment(insertSeq, insertion.length, true);
323
+ inserted += insertion;
324
+ insertion = undefined;
201
325
  }
202
- subseq.Subseq.pushSegment(insertSizes, op.end - op.start, false);
203
- subseq.Subseq.pushSegment(deleteSizes, op.end - op.start, false);
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
- if (prevInserted !== undefined) {
209
- prefix = commonPrefixLength(prevInserted, op.value);
210
- subseq.Subseq.pushSegment(insertSizes, prefix, false);
211
- subseq.Subseq.pushSegment(insertSizes, prevInserted.length - prefix, true);
212
- inserted += prevInserted.slice(prefix);
213
- prevInserted = undefined;
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 += op.value.slice(prefix);
216
- subseq.Subseq.pushSegment(deleteSizes, prefix, false);
217
- subseq.Subseq.pushSegment(deleteSizes, length - prefix, true);
218
- subseq.Subseq.pushSegment(insertSizes, length - prefix, false);
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 (prevInserted !== undefined) {
226
- subseq.Subseq.pushSegment(insertSizes, prevInserted.length, true);
227
- inserted += prevInserted;
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
- const insertSeq = new subseq.Subseq(insertSizes);
230
- const deleteSeq = new subseq.Subseq(deleteSizes);
231
- return Edit.synthesize(insertSeq, inserted, deleteSeq, deleted);
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
- * Composes two consecutive edits.
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
- compose(that) {
237
- let [insertSeq1, inserted1, deleteSeq1, deleted1] = this.factor();
238
- let [insertSeq2, inserted2, deleteSeq2, deleted2] = that.factor();
239
- // Expand all subseqs so that they share the same coordinate space.
240
- deleteSeq1 = deleteSeq1.expand(insertSeq1);
241
- deleteSeq2 = deleteSeq2.expand(deleteSeq1);
242
- [deleteSeq1, insertSeq2] = deleteSeq1.interleave(insertSeq2);
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
- const insertSeq = insertSeq1.union(insertSeq2);
257
- const inserted = consolidate(insertSeq1, inserted1, insertSeq2, inserted2);
258
- const deleteSeq = deleteSeq1.union(deleteSeq2).shrink(insertSeq);
259
- const deleted = deleted1 != null && deleted2 != null
260
- ? consolidate(deleteSeq1, deleted1, deleteSeq2, deleted2)
261
- : undefined;
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
- invert() {
265
- if (typeof this.deleted === "undefined") {
266
- throw new Error("Missing deleted property");
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 size, and the included segments of these
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 [size, flag1, flag2] of subseq1.align(subseq2)) {
287
- if (flag1 && flag2) {
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 (flag1) {
291
- result += text1.slice(i1, i1 + size);
292
- i1 += size;
559
+ else if (included1) {
560
+ result += text1.slice(i1, i1 + length);
561
+ i1 += length;
293
562
  }
294
- else if (flag2) {
295
- result += text2.slice(i2, i2 + size);
296
- i2 += size;
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 size, and the included segments of the second
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 [size, flag1, flag2] of subseq1.align(subseq2)) {
313
- if (flag1) {
314
- if (!flag2) {
315
- result += str.slice(i, i + size);
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 += size;
586
+ i += length;
318
587
  }
319
- else if (flag2) {
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;