@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.js CHANGED
@@ -1,85 +1,188 @@
1
1
  /// <reference types="edit.d.ts" />
2
- import { Subseq } from './subseq.js';
3
-
4
- /**
5
- * A data structure which represents edits to strings.
6
- */
7
- class Edit {
8
- constructor(parts, deleted) {
9
- this.parts = parts;
10
- this.deleted = deleted;
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
- static synthesize(insertSeq, inserted, deleteSeq, deleted) {
13
- if (insertSeq.includedSize !== inserted.length) {
14
- throw new Error("insertSeq and inserted string do not match in length");
15
- }
16
- else if (deleted !== undefined &&
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;
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
- else {
35
- if (!deleting) {
36
- parts.push(retainIndex, retainIndex + length);
37
- }
38
- retainIndex += length;
39
- needsLength = deleting;
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
- if (needsLength) {
43
- parts.push(retainIndex);
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
- static build(text, inserted, from, to = from) {
48
- const insertSizes = [];
49
- Subseq.pushSegment(insertSizes, from, false);
50
- Subseq.pushSegment(insertSizes, inserted.length, true);
51
- Subseq.pushSegment(insertSizes, to - from, false);
52
- Subseq.pushSegment(insertSizes, text.length - to, false);
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
- * Given two strings, this method finds an edit which can be applied to the
62
- * first string to result in the second.
63
- * An optional hint can be provided to disambiguate edits when they are made
64
- * to overlapping characters.
65
- */
66
- static diff(text1, text2, hint) {
67
- let prefix = commonPrefixLength(text1, text2);
68
- let suffix = commonSuffixLength(text1, text2);
69
- // prefix and suffix overlap when edits are runs of the same character.
70
- if (prefix + suffix > Math.min(text1.length, text2.length)) {
71
- if (hint !== undefined && hint > -1) {
72
- prefix = Math.min(prefix, hint);
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
- * A string which represents a concatenation of all insertions.
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
- factor() {
146
- const insertSizes = [];
147
- const deleteSizes = [];
148
- let inserted = "";
149
- const operations = this.operations();
150
- for (let i = 0; i < operations.length; i++) {
151
- const op = operations[i];
152
- switch (op.type) {
153
- case "retain": {
154
- const length = op.end - op.start;
155
- Subseq.pushSegment(insertSizes, length, false);
156
- Subseq.pushSegment(deleteSizes, length, false);
157
- break;
158
- }
159
- case "delete": {
160
- const length = op.end - op.start;
161
- Subseq.pushSegment(insertSizes, length, false);
162
- Subseq.pushSegment(deleteSizes, length, true);
163
- break;
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 = new Subseq(insertSizes);
174
- const deleteSeq = new Subseq(deleteSizes);
175
- return [insertSeq, inserted, deleteSeq, this.deleted];
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("Missing deleted property");
302
+ throw new Error("Edit is not normalizable");
180
303
  }
181
- const insertSizes = [];
182
- const deleteSizes = [];
304
+ const insertSeq = [];
305
+ const deleteSeq = [];
183
306
  let inserted = "";
184
307
  let deleted = "";
185
- let prevInserted;
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
- prevInserted = op.value;
313
+ case "insert": {
314
+ insertion = op.value;
192
315
  break;
193
- case "retain":
194
- if (prevInserted !== undefined) {
195
- Subseq.pushSegment(insertSizes, prevInserted.length, true);
196
- inserted += prevInserted;
197
- prevInserted = undefined;
316
+ }
317
+ case "retain": {
318
+ if (insertion !== undefined) {
319
+ pushSegment(insertSeq, insertion.length, true);
320
+ inserted += insertion;
321
+ insertion = undefined;
198
322
  }
199
- Subseq.pushSegment(insertSizes, op.end - op.start, false);
200
- Subseq.pushSegment(deleteSizes, op.end - op.start, false);
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
- if (prevInserted !== undefined) {
206
- prefix = commonPrefixLength(prevInserted, op.value);
207
- Subseq.pushSegment(insertSizes, prefix, false);
208
- Subseq.pushSegment(insertSizes, prevInserted.length - prefix, true);
209
- inserted += prevInserted.slice(prefix);
210
- prevInserted = undefined;
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 += op.value.slice(prefix);
213
- Subseq.pushSegment(deleteSizes, prefix, false);
214
- Subseq.pushSegment(deleteSizes, length - prefix, true);
215
- Subseq.pushSegment(insertSizes, length - prefix, false);
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 (prevInserted !== undefined) {
223
- Subseq.pushSegment(insertSizes, prevInserted.length, true);
224
- inserted += prevInserted;
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
- const insertSeq = new Subseq(insertSizes);
227
- const deleteSeq = new Subseq(deleteSizes);
228
- return Edit.synthesize(insertSeq, inserted, deleteSeq, deleted);
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
- * Composes two consecutive edits.
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
- compose(that) {
234
- let [insertSeq1, inserted1, deleteSeq1, deleted1] = this.factor();
235
- let [insertSeq2, inserted2, deleteSeq2, deleted2] = that.factor();
236
- // Expand all subseqs so that they share the same coordinate space.
237
- deleteSeq1 = deleteSeq1.expand(insertSeq1);
238
- deleteSeq2 = deleteSeq2.expand(deleteSeq1);
239
- [deleteSeq1, insertSeq2] = deleteSeq1.interleave(insertSeq2);
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
- const insertSeq = insertSeq1.union(insertSeq2);
254
- const inserted = consolidate(insertSeq1, inserted1, insertSeq2, inserted2);
255
- const deleteSeq = deleteSeq1.union(deleteSeq2).shrink(insertSeq);
256
- const deleted = deleted1 != null && deleted2 != null
257
- ? consolidate(deleteSeq1, deleted1, deleteSeq2, deleted2)
258
- : undefined;
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
- invert() {
262
- if (typeof this.deleted === "undefined") {
263
- throw new Error("Missing deleted property");
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 size, and the included segments of these
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 [size, flag1, flag2] of subseq1.align(subseq2)) {
284
- if (flag1 && flag2) {
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 (flag1) {
288
- result += text1.slice(i1, i1 + size);
289
- i1 += size;
556
+ else if (included1) {
557
+ result += text1.slice(i1, i1 + length);
558
+ i1 += length;
290
559
  }
291
- else if (flag2) {
292
- result += text2.slice(i2, i2 + size);
293
- i2 += size;
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 size, and the included segments of the second
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 [size, flag1, flag2] of subseq1.align(subseq2)) {
310
- if (flag1) {
311
- if (!flag2) {
312
- result += str.slice(i, i + size);
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 += size;
583
+ i += length;
315
584
  }
316
- else if (flag2) {
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;