@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/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");
22
+ }
23
+ else if (length === 0) {
24
+ return;
25
+ }
26
+ else if (!subseq.length) {
27
+ if (included) {
28
+ subseq.push(0, length);
29
+ }
30
+ else {
31
+ subseq.push(length);
32
+ }
33
+ }
34
+ else {
35
+ const included1 = subseq.length % 2 === 0;
36
+ if (included === included1) {
37
+ subseq[subseq.length - 1] += length;
38
+ }
39
+ else {
40
+ subseq.push(length);
41
+ }
42
+ }
43
+ }
44
+ function align(subseq1, subseq2) {
45
+ if (measure(subseq1).length !== measure(subseq2).length) {
46
+ throw new Error("Length mismatch");
14
47
  }
15
- 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;
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]);
43
67
  }
68
+ length2 = length2 - length1;
69
+ length1 = 0;
44
70
  }
45
- if (needsLength) {
46
- parts.push(retainIndex);
71
+ else if (length1 > length2) {
72
+ if (length2) {
73
+ result.push([length2, included1, included2]);
74
+ }
75
+ length1 = length1 - length2;
76
+ length2 = 0;
77
+ }
78
+ else {
79
+ if (length1) {
80
+ result.push([length1, included1, included2]);
81
+ }
82
+ length1 = length2 = 0;
47
83
  }
48
- return new Edit(parts, deleted);
49
84
  }
50
- 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);
91
+ }
92
+ return result;
93
+ }
94
+ function intersection(subseq1, subseq2) {
95
+ const result = [];
96
+ for (const [length, included1, included2] of align(subseq1, subseq2)) {
97
+ pushSegment(result, length, included1 && included2);
62
98
  }
63
- /**
64
- * 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);
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;
@@ -129,8 +246,6 @@ class Edit {
129
246
  }
130
247
  return operations;
131
248
  }
132
- // TODO: I’m not too happy about the name of this method, insofar as it might
133
- // imply that this object is callable.
134
249
  apply(text) {
135
250
  let text1 = "";
136
251
  const operations = this.operations();
@@ -147,158 +262,307 @@ class Edit {
147
262
  }
148
263
  return text1;
149
264
  }
150
- factor() {
151
- const insertSizes = [];
152
- const deleteSizes = [];
153
- let inserted = "";
154
- const operations = this.operations();
155
- for (let i = 0; i < operations.length; i++) {
156
- const op = operations[i];
157
- switch (op.type) {
158
- case "retain": {
159
- const length = op.end - op.start;
160
- subseq.Subseq.pushSegment(insertSizes, length, false);
161
- subseq.Subseq.pushSegment(deleteSizes, length, false);
162
- break;
163
- }
164
- case "delete": {
165
- const length = op.end - op.start;
166
- subseq.Subseq.pushSegment(insertSizes, length, false);
167
- subseq.Subseq.pushSegment(deleteSizes, length, true);
168
- break;
169
- }
170
- case "insert":
171
- subseq.Subseq.pushSegment(insertSizes, op.value.length, true);
172
- inserted += op.value;
173
- break;
174
- default:
175
- throw new TypeError("Invalid operation type");
265
+ /** Composes two consecutive edits. */
266
+ compose(that) {
267
+ let [insertSeq1, inserted1, deleteSeq1, deleted1] = factor(this);
268
+ let [insertSeq2, inserted2, deleteSeq2, deleted2] = factor(that);
269
+ // Expand all subseqs so that they share the same coordinate space.
270
+ deleteSeq1 = expand(deleteSeq1, insertSeq1);
271
+ deleteSeq2 = expand(deleteSeq2, deleteSeq1);
272
+ [deleteSeq1, insertSeq2] = interleave(deleteSeq1, insertSeq2);
273
+ deleteSeq2 = expand(deleteSeq2, insertSeq2);
274
+ insertSeq1 = expand(insertSeq1, insertSeq2);
275
+ {
276
+ // Find insertions which have been deleted and remove them.
277
+ const toggleSeq = intersection(insertSeq1, deleteSeq2);
278
+ if (measure(toggleSeq).includedLength) {
279
+ deleteSeq1 = shrink(deleteSeq1, toggleSeq);
280
+ inserted1 = erase(insertSeq1, inserted1, toggleSeq);
281
+ insertSeq1 = shrink(insertSeq1, toggleSeq);
282
+ deleteSeq2 = shrink(deleteSeq2, toggleSeq);
283
+ insertSeq2 = shrink(insertSeq2, toggleSeq);
176
284
  }
177
285
  }
178
- const insertSeq = new subseq.Subseq(insertSizes);
179
- const deleteSeq = new subseq.Subseq(deleteSizes);
180
- 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);
181
302
  }
182
303
  normalize() {
183
304
  if (typeof this.deleted === "undefined") {
184
- throw new Error("Missing deleted property");
305
+ throw new Error("Edit is not normalizable");
185
306
  }
186
- const insertSizes = [];
187
- const deleteSizes = [];
307
+ const insertSeq = [];
308
+ const deleteSeq = [];
188
309
  let inserted = "";
189
310
  let deleted = "";
190
- let prevInserted;
311
+ let insertion;
191
312
  const operations = this.operations();
192
313
  for (let i = 0; i < operations.length; i++) {
193
314
  const op = operations[i];
194
315
  switch (op.type) {
195
316
  case "insert": {
196
- prevInserted = op.value;
317
+ insertion = op.value;
197
318
  break;
198
319
  }
199
320
  case "retain": {
200
- if (prevInserted !== undefined) {
201
- subseq.Subseq.pushSegment(insertSizes, prevInserted.length, true);
202
- inserted += prevInserted;
203
- prevInserted = undefined;
321
+ if (insertion !== undefined) {
322
+ pushSegment(insertSeq, insertion.length, true);
323
+ inserted += insertion;
324
+ insertion = undefined;
204
325
  }
205
- subseq.Subseq.pushSegment(insertSizes, op.end - op.start, false);
206
- 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);
207
328
  break;
208
329
  }
209
330
  case "delete": {
210
331
  const length = op.end - op.start;
332
+ const deletion = op.value;
211
333
  let prefix = 0;
212
- if (prevInserted !== undefined) {
213
- prefix = commonPrefixLength(prevInserted, op.value);
214
- subseq.Subseq.pushSegment(insertSizes, prefix, false);
215
- subseq.Subseq.pushSegment(insertSizes, prevInserted.length - prefix, true);
216
- inserted += prevInserted.slice(prefix);
217
- 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);
218
346
  }
219
- deleted += op.value.slice(prefix);
220
- subseq.Subseq.pushSegment(deleteSizes, prefix, false);
221
- subseq.Subseq.pushSegment(deleteSizes, length - prefix, true);
222
- 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;
223
355
  break;
224
356
  }
225
- default: {
226
- throw new TypeError("Invalid operation type");
227
- }
228
357
  }
229
358
  }
230
- if (prevInserted !== undefined) {
231
- subseq.Subseq.pushSegment(insertSizes, prevInserted.length, true);
232
- 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
+ }
233
383
  }
234
- const insertSeq = new subseq.Subseq(insertSizes);
235
- const deleteSeq = new subseq.Subseq(deleteSizes);
236
- 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
+ };
237
448
  }
238
449
  /**
239
- * 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.
240
458
  */
241
- compose(that) {
242
- let [insertSeq1, inserted1, deleteSeq1, deleted1] = this.factor();
243
- let [insertSeq2, inserted2, deleteSeq2, deleted2] = that.factor();
244
- // Expand all subseqs so that they share the same coordinate space.
245
- deleteSeq1 = deleteSeq1.expand(insertSeq1);
246
- deleteSeq2 = deleteSeq2.expand(deleteSeq1);
247
- [deleteSeq1, insertSeq2] = deleteSeq1.interleave(insertSeq2);
248
- deleteSeq2 = deleteSeq2.expand(insertSeq2);
249
- insertSeq1 = insertSeq1.expand(insertSeq2);
250
- // Find insertions which have been deleted and remove them.
251
- {
252
- const toggleSeq = insertSeq1.intersection(deleteSeq2);
253
- if (toggleSeq.includedSize) {
254
- deleteSeq1 = deleteSeq1.shrink(toggleSeq);
255
- inserted1 = erase(insertSeq1, inserted1, toggleSeq);
256
- insertSeq1 = insertSeq1.shrink(toggleSeq);
257
- deleteSeq2 = deleteSeq2.shrink(toggleSeq);
258
- insertSeq2 = insertSeq2.shrink(toggleSeq);
459
+ static diff(text1, text2, startHint) {
460
+ let prefix = commonPrefixLength(text1, text2);
461
+ let suffix = commonSuffixLength(text1, text2);
462
+ // prefix and suffix overlap when edits are in runs of the same character.
463
+ if (prefix + suffix > Math.min(text1.length, text2.length)) {
464
+ if (startHint != null && startHint >= 0) {
465
+ prefix = Math.min(prefix, startHint);
259
466
  }
467
+ // TODO: We can probably avoid the commonSuffixLength() call here in
468
+ // favor of arithmetic but I’m too dumb to figure it out.
469
+ suffix = commonSuffixLength(text1.slice(prefix), text2.slice(prefix));
260
470
  }
261
- const insertSeq = insertSeq1.union(insertSeq2);
262
- const inserted = consolidate(insertSeq1, inserted1, insertSeq2, inserted2);
263
- const deleteSeq = deleteSeq1.union(deleteSeq2).shrink(insertSeq);
264
- const deleted = deleted1 != null && deleted2 != null
265
- ? consolidate(deleteSeq1, deleted1, deleteSeq2, deleted2)
266
- : undefined;
267
- return Edit.synthesize(insertSeq, inserted, deleteSeq, deleted).normalize();
471
+ return Edit.builder(text1)
472
+ .retain(prefix)
473
+ .insert(text2.slice(prefix, text2.length - suffix))
474
+ .delete(text1.length - prefix - suffix)
475
+ .retain(suffix)
476
+ .build();
268
477
  }
269
- invert() {
270
- if (typeof this.deleted === "undefined") {
271
- 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;
272
508
  }
273
- let [insertSeq, inserted, deleteSeq, deleted] = this.factor();
274
- deleteSeq = deleteSeq.expand(insertSeq);
275
- insertSeq = insertSeq.shrink(deleteSeq);
276
- return Edit.synthesize(deleteSeq, deleted, insertSeq, inserted);
277
509
  }
510
+ if (needsLength) {
511
+ parts.push(retainIndex);
512
+ }
513
+ return new Edit(parts, deleted);
514
+ }
515
+ function factor(edit) {
516
+ const insertSeq = [];
517
+ const deleteSeq = [];
518
+ let inserted = "";
519
+ const operations = edit.operations();
520
+ for (let i = 0; i < operations.length; i++) {
521
+ const op = operations[i];
522
+ switch (op.type) {
523
+ case "retain": {
524
+ const length = op.end - op.start;
525
+ pushSegment(insertSeq, length, false);
526
+ pushSegment(deleteSeq, length, false);
527
+ break;
528
+ }
529
+ case "delete": {
530
+ const length = op.end - op.start;
531
+ pushSegment(insertSeq, length, false);
532
+ pushSegment(deleteSeq, length, true);
533
+ break;
534
+ }
535
+ case "insert":
536
+ pushSegment(insertSeq, op.value.length, true);
537
+ inserted += op.value;
538
+ break;
539
+ }
540
+ }
541
+ return [insertSeq, inserted, deleteSeq, edit.deleted];
278
542
  }
279
543
  /**
280
544
  * Given two subseqs and strings which are represented by the included segments
281
545
  * of the subseqs, this function combines the two strings so that they overlap
282
546
  * according to the positions of the included segments of subseqs.
283
547
  *
284
- * The subseqs must have the same size, and the included segments of these
548
+ * The subseqs must have the same length, and the included segments of these
285
549
  * subseqs may not overlap.
286
550
  */
287
551
  function consolidate(subseq1, text1, subseq2, text2) {
288
552
  let i1 = 0;
289
553
  let i2 = 0;
290
554
  let result = "";
291
- for (const [size, flag1, flag2] of subseq1.align(subseq2)) {
292
- if (flag1 && flag2) {
555
+ for (const [length, included1, included2] of align(subseq1, subseq2)) {
556
+ if (included1 && included2) {
293
557
  throw new Error("Overlapping subseqs");
294
558
  }
295
- else if (flag1) {
296
- result += text1.slice(i1, i1 + size);
297
- i1 += size;
559
+ else if (included1) {
560
+ result += text1.slice(i1, i1 + length);
561
+ i1 += length;
298
562
  }
299
- else if (flag2) {
300
- result += text2.slice(i2, i2 + size);
301
- i2 += size;
563
+ else if (included2) {
564
+ result += text2.slice(i2, i2 + length);
565
+ i2 += length;
302
566
  }
303
567
  }
304
568
  return result;
@@ -308,28 +572,26 @@ function consolidate(subseq1, text1, subseq2, text2) {
308
572
  * segments of the first subseq, this function returns the result of removing
309
573
  * the included segments of the second subseq from the first subseq.
310
574
  *
311
- * The subseqs must have the same size, and the included segments of the second
575
+ * The subseqs must have the same length, and the included segments of the second
312
576
  * subseq must overlap with the first subseq’s included segments.
313
577
  */
314
578
  function erase(subseq1, str, subseq2) {
315
579
  let i = 0;
316
580
  let result = "";
317
- for (const [size, flag1, flag2] of subseq1.align(subseq2)) {
318
- if (flag1) {
319
- if (!flag2) {
320
- 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);
321
585
  }
322
- i += size;
586
+ i += length;
323
587
  }
324
- else if (flag2) {
588
+ else if (included2) {
325
589
  throw new Error("Non-overlapping subseqs");
326
590
  }
327
591
  }
328
592
  return result;
329
593
  }
330
- /**
331
- * @returns The length of the common prefix between two strings.
332
- */
594
+ /** @returns The length of the common prefix between two strings. */
333
595
  function commonPrefixLength(text1, text2) {
334
596
  const length = Math.min(text1.length, text2.length);
335
597
  for (let i = 0; i < length; i++) {
@@ -339,9 +601,7 @@ function commonPrefixLength(text1, text2) {
339
601
  }
340
602
  return length;
341
603
  }
342
- /**
343
- * @returns The length of the common suffix between two strings.
344
- */
604
+ /** @returns The length of the common suffix between two strings. */
345
605
  function commonSuffixLength(text1, text2) {
346
606
  const length1 = text1.length;
347
607
  const length2 = text2.length;