@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.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");
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
- 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;
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]);
40
64
  }
65
+ length2 = length2 - length1;
66
+ length1 = 0;
41
67
  }
42
- if (needsLength) {
43
- parts.push(retainIndex);
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
- 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);
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
- * 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);
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;
@@ -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
- factor() {
148
- const insertSizes = [];
149
- const deleteSizes = [];
150
- let inserted = "";
151
- const operations = this.operations();
152
- for (let i = 0; i < operations.length; i++) {
153
- const op = operations[i];
154
- switch (op.type) {
155
- case "retain": {
156
- const length = op.end - op.start;
157
- Subseq.pushSegment(insertSizes, length, false);
158
- Subseq.pushSegment(deleteSizes, length, false);
159
- break;
160
- }
161
- case "delete": {
162
- const length = op.end - op.start;
163
- Subseq.pushSegment(insertSizes, length, false);
164
- Subseq.pushSegment(deleteSizes, length, true);
165
- break;
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 = new Subseq(insertSizes);
176
- const deleteSeq = new Subseq(deleteSizes);
177
- 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);
178
299
  }
179
300
  normalize() {
180
301
  if (typeof this.deleted === "undefined") {
181
- throw new Error("Missing deleted property");
302
+ throw new Error("Edit is not normalizable");
182
303
  }
183
- const insertSizes = [];
184
- const deleteSizes = [];
304
+ const insertSeq = [];
305
+ const deleteSeq = [];
185
306
  let inserted = "";
186
307
  let deleted = "";
187
- let prevInserted;
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
- prevInserted = op.value;
314
+ insertion = op.value;
194
315
  break;
195
316
  }
196
317
  case "retain": {
197
- if (prevInserted !== undefined) {
198
- Subseq.pushSegment(insertSizes, prevInserted.length, true);
199
- inserted += prevInserted;
200
- prevInserted = undefined;
318
+ if (insertion !== undefined) {
319
+ pushSegment(insertSeq, insertion.length, true);
320
+ inserted += insertion;
321
+ insertion = undefined;
201
322
  }
202
- Subseq.pushSegment(insertSizes, op.end - op.start, false);
203
- Subseq.pushSegment(deleteSizes, op.end - op.start, false);
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
- if (prevInserted !== undefined) {
210
- prefix = commonPrefixLength(prevInserted, op.value);
211
- Subseq.pushSegment(insertSizes, prefix, false);
212
- Subseq.pushSegment(insertSizes, prevInserted.length - prefix, true);
213
- inserted += prevInserted.slice(prefix);
214
- 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);
215
343
  }
216
- deleted += op.value.slice(prefix);
217
- Subseq.pushSegment(deleteSizes, prefix, false);
218
- Subseq.pushSegment(deleteSizes, length - prefix, true);
219
- 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;
220
352
  break;
221
353
  }
222
- default: {
223
- throw new TypeError("Invalid operation type");
224
- }
225
354
  }
226
355
  }
227
- if (prevInserted !== undefined) {
228
- Subseq.pushSegment(insertSizes, prevInserted.length, true);
229
- 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
+ }
230
380
  }
231
- const insertSeq = new Subseq(insertSizes);
232
- const deleteSeq = new Subseq(deleteSizes);
233
- 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
+ };
234
445
  }
235
446
  /**
236
- * 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.
237
455
  */
238
- compose(that) {
239
- let [insertSeq1, inserted1, deleteSeq1, deleted1] = this.factor();
240
- let [insertSeq2, inserted2, deleteSeq2, deleted2] = that.factor();
241
- // Expand all subseqs so that they share the same coordinate space.
242
- deleteSeq1 = deleteSeq1.expand(insertSeq1);
243
- deleteSeq2 = deleteSeq2.expand(deleteSeq1);
244
- [deleteSeq1, insertSeq2] = deleteSeq1.interleave(insertSeq2);
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
- const insertSeq = insertSeq1.union(insertSeq2);
259
- const inserted = consolidate(insertSeq1, inserted1, insertSeq2, inserted2);
260
- const deleteSeq = deleteSeq1.union(deleteSeq2).shrink(insertSeq);
261
- const deleted = deleted1 != null && deleted2 != null
262
- ? consolidate(deleteSeq1, deleted1, deleteSeq2, deleted2)
263
- : undefined;
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
- invert() {
267
- if (typeof this.deleted === "undefined") {
268
- 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;
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 size, and the included segments of these
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 [size, flag1, flag2] of subseq1.align(subseq2)) {
289
- if (flag1 && flag2) {
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 (flag1) {
293
- result += text1.slice(i1, i1 + size);
294
- i1 += size;
556
+ else if (included1) {
557
+ result += text1.slice(i1, i1 + length);
558
+ i1 += length;
295
559
  }
296
- else if (flag2) {
297
- result += text2.slice(i2, i2 + size);
298
- i2 += size;
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 size, and the included segments of the second
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 [size, flag1, flag2] of subseq1.align(subseq2)) {
315
- if (flag1) {
316
- if (!flag2) {
317
- 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);
318
582
  }
319
- i += size;
583
+ i += length;
320
584
  }
321
- else if (flag2) {
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;