rapier-markdown-kit 1.1.102 → 1.2.3

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.
Files changed (82) hide show
  1. package/LICENSE +5 -2
  2. package/README.md +245 -54
  3. package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
  4. package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
  5. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
  6. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
  7. package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
  8. package/dist/agent/vendor/pretext/LICENSE +208 -0
  9. package/dist/agent/vendor/pretext/NOTICE +26 -13
  10. package/dist/agent/vendor/pretext/SOURCE.json +213 -45
  11. package/dist/agent/vendor/pretext/analysis.js +675 -1224
  12. package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
  13. package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
  14. package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
  15. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
  16. package/dist/agent/vendor/pretext/graphemes.js +98 -0
  17. package/dist/agent/vendor/pretext/han-kerning.js +253 -0
  18. package/dist/agent/vendor/pretext/layout.js +352 -290
  19. package/dist/agent/vendor/pretext/line-break.js +1154 -588
  20. package/dist/agent/vendor/pretext/line-breaks.js +892 -0
  21. package/dist/agent/vendor/pretext/line-text.js +81 -39
  22. package/dist/agent/vendor/pretext/measurement.js +1105 -176
  23. package/dist/agent/vendor/pretext/prepare.js +807 -0
  24. package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
  25. package/dist/agent/will.mjs +2 -0
  26. package/dist/images/gif.mjs +47 -0
  27. package/dist/images/playback.mjs +101 -0
  28. package/dist/kit/assets.mjs +5 -1
  29. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  30. package/dist/kit/conformance/04-wrap-box.md +1 -1
  31. package/dist/kit/conformance/05-behind.md +1 -1
  32. package/dist/kit/conformance/06-front.md +1 -1
  33. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  34. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  35. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  36. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  37. package/dist/kit/conformance/10-both-sides.md +1 -1
  38. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  39. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  40. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  41. package/dist/kit/conformance/README.md +19 -28
  42. package/dist/kit/conformance/run.mjs +13 -11
  43. package/dist/kit/export-fonts.mjs +26 -11
  44. package/dist/kit/index.mjs +1 -0
  45. package/dist/kit/ledger/carried.mjs +17 -10
  46. package/dist/kit/ledger/data.mjs +73 -13
  47. package/dist/kit/ledger/format.mjs +176 -54
  48. package/dist/kit/ledger/hash.mjs +91 -18
  49. package/dist/kit/ledger/history.mjs +156 -116
  50. package/dist/kit/ledger/inverse-source.mjs +158 -19
  51. package/dist/kit/ledger/journal-records.mjs +292 -67
  52. package/dist/kit/ledger/merge.mjs +4 -3
  53. package/dist/kit/model.mjs +1 -1
  54. package/dist/kit/pdf-glyphs.mjs +4 -4
  55. package/dist/kit/pdf-vectors.mjs +4 -1
  56. package/dist/kit/rapier-ledger.mjs +2 -2
  57. package/dist/kit/render-ink.mjs +1 -1
  58. package/dist/kit/render-markdown.mjs +400 -148
  59. package/dist/kit/render-print.mjs +131 -55
  60. package/dist/kit/render-sanitize.mjs +30 -7
  61. package/dist/kit/render-work.mjs +99 -20
  62. package/dist/kit/render.mjs +133 -140
  63. package/dist/kit/typography.mjs +2 -0
  64. package/dist/layout/hyphenation.mjs +127 -0
  65. package/dist/layout/line-plan.mjs +88 -96
  66. package/dist/layout/model.mjs +71 -88
  67. package/dist/layout/paragraph-fit.mjs +191 -0
  68. package/dist/layout/preparation.mjs +373 -0
  69. package/dist/layout/prepared-paragraph.mjs +223 -0
  70. package/dist/layout/typography.mjs +345 -111
  71. package/dist/packages/rapier-diff/index.mjs +359 -224
  72. package/dist/spec/document-fonts.mjs +14 -5
  73. package/dist/spec/document-settings.mjs +11 -164
  74. package/dist/spec/html-blocks.mjs +618 -79
  75. package/dist/spec/md-assets.mjs +178 -36
  76. package/dist/spec/md-layout.mjs +13 -2
  77. package/dist/spec/md-marks.mjs +220 -127
  78. package/dist/spec/md-source.mjs +157 -0
  79. package/package.json +2 -1
  80. package/style.css +15 -50
  81. package/dist/agent/vendor/pretext/bidi.js +0 -151
  82. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,59 +1,119 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ // Capture only the reader contract before another observation can reuse its objects.
3
+ function observe(text, reader, spend, safe, references = true, lenient = false) {
4
+ let value;
5
+ try { value = reader(text); } catch { return null; }
6
+ try {
7
+ const flat = [], links = [], meaning = references && value?.meaning;
8
+ const span = (at, from, end) => {
9
+ if (!Array.isArray(at) || at.length !== 2 || !at.every(Number.isSafeInteger) ||
10
+ at[0] < from || at[1] < at[0] || at[1] > end) throw null;
11
+ if (safe(text, at[0]) && safe(text, at[1])) return [at[0], at[1]];
12
+ if (!lenient) throw null;
13
+ };
14
+ const scalar = (value, key = false) => {
15
+ if (value != null && typeof value !== 'string' && !(key && (typeof value === 'boolean' || Number.isFinite(value)))) throw false;
16
+ return value;
17
+ };
18
+ let count = 0;
19
+ function copy(rows, from, end, depth, retained = true) {
20
+ if (!Array.isArray(rows) || rows.length > 131072 || depth > 128) throw null;
21
+ const parts = [];
22
+ let at = from;
23
+ for (const row of rows) {
24
+ if (!spend() || count++ >= 131072) throw null;
25
+ const {start, end: stop, kind, name, key, nameAt, children} = row;
26
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(stop) ||
27
+ start < at || stop < start || stop > end) throw null;
28
+ at = stop;
29
+ const usable = safe(text, start) && safe(text, stop);
30
+ if (!usable && !lenient) throw null;
31
+ const part = {start, end: stop, kind: scalar(kind), name: scalar(name), key: scalar(key, true)};
32
+ if (nameAt != null) part.nameAt = span(nameAt, start, stop);
33
+ // Discard unsafe evidence only after validating its complete subtree.
34
+ if (retained && usable) { parts.push(part); flat.push(part); }
35
+ part.children = children == null ? [] : copy(children, start, stop, depth + 1, retained && usable);
36
+ }
37
+ return parts;
38
+ }
39
+ const parts = copy(value?.parts, 0, text.length, 0), rows = references && value.links || [];
40
+ if (!Array.isArray(rows) || rows.length > 131072) throw null;
41
+ for (const row of rows) {
42
+ if (!spend()) throw null;
43
+ const {at, to, key, labelAt} = row;
44
+ if (typeof to !== 'string') throw null;
45
+ const place = span(at, 0, text.length), label = labelAt == null ? null : span(labelAt, 0, text.length), binding = scalar(key, true);
46
+ if (place) links.push({at: place, to, key: binding, ...(label && {labelAt: label})});
47
+ }
48
+ return {parts, flat, links, meaning: typeof meaning === 'string' ? meaning : undefined};
49
+ } catch (error) { return error === false ? {invalid: true} : null; }
50
+ }
51
+
52
+ // Segment only requested non-ASCII boundaries; observations never share a global document cache.
53
+ function boundaries() {
54
+ const segments = Object.create(null);
55
+ let segmenter;
56
+ return function safe(text, at) {
57
+ if (!at || at === text.length) return true;
58
+ const a = text.charCodeAt(at - 1), b = text.charCodeAt(at);
59
+ if ((a & 0xfc00) === 0xd800 && (b & 0xfc00) === 0xdc00 || a === 13 && b === 10) return false;
60
+ if (a < 0x300 && b < 0x300) return true;
61
+ return (segments[text] ||= (segmenter ||= new Intl.Segmenter('en', {granularity: 'grapheme'})).segment(text)).containing(at).index === at;
62
+ }
63
+ }
64
+
2
65
  export function diff(before, after, options = {}) {
3
66
  if (before === after) return {edits: [], changes: []};
4
67
  const budget = options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7};
5
68
  const initial = budget.left, raw = options._unit;
6
69
  let result = [], code = options.as === 'code', structures, pa, pb, la, lb, aliases;
7
70
  const spend = (n = 1) => (budget.left -= n) >= 0;
8
- const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c);
71
+ const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c), within = (text, at) => letter(text[at - 1]) && letter(text[at]);
9
72
  const overlaps = (a, b) => a[0] < b[1] && a[1] > b[0];
10
73
  const inside = (c, a, b) => [a, b].every((range, side) => c[side][0] === c[side][1] ||
11
74
  c[side][0] >= range[0] && c[side][1] <= range[1]);
12
75
  if (options.as !== 'prose' && options.as !== 'code') code = /(?:^|\n)\s*(?:[{}]|(?:function|class|const|let|var|import|export|def|fn|func|package)\s+\w[^]{0,80}[({=:;])/.test(before.slice(0, 4096) + after.slice(0, 4096));
13
76
 
14
- // Segment only a requested non-ASCII boundary; cache each input locally.
15
- const segments = Object.create(null);
16
- let segmenter;
17
- function safe(text, at) {
18
- if (!at || at === text.length) return true;
19
- const a = text.charCodeAt(at - 1), b = text.charCodeAt(at);
20
- if ((a & 0xfc00) === 0xd800 && (b & 0xfc00) === 0xdc00 || a === 13 && b === 10) return false;
21
- if (a < 0x300 && b < 0x300) return true;
22
- return (segments[text] ||= (segmenter ||= new Intl.Segmenter('en', {granularity: 'grapheme'})).segment(text)).containing(at).index === at;
23
- }
77
+ const safe = boundaries();
24
78
 
25
79
  function record(a0, a1, b0, b1, unit, part, coarse = false) {
26
- if (before.slice(a0, a1) === after.slice(b0, b1)) return;
80
+ if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
27
81
  // Internal records: before, after, coarse, concern, transported fragments.
28
82
  result.push([[a0, a1], [b0, b1], coarse, [a0 === a1 ? 2 : b0 === b1 ? 0 : 1, part?.kind || unit, part?.name]]);
29
83
  }
30
84
 
31
85
  function tokens(text, start, end, level, parts) {
32
- const out = [];
33
- const add = (a, b, part) => {
86
+ // Each source boundary is stored once, alongside comparison keys and optional owners.
87
+ const keys = [], at = [start], owners = [];
88
+ const out = [keys, at, owners];
89
+ const add = (a, b, part, value) => {
34
90
  if (b <= a) return;
35
- if (!part && out.at(-1)?.[3]?.name && !/\S/.test(text.slice(a, b))) out.at(-1)[1] = b;
36
- else out.push([a, b, part?.name ? '\0' + part.kind + '\0' + (aliases?.get(part)?.name || part.name) : level === 1 && !raw ? text.slice(a, b).trimEnd() : text.slice(a, b), part]);
91
+ if (!part && owners[keys.length - 1]?.name && !/\S/.test(text.slice(a, b))) at[at.length - 1] = b;
92
+ else {
93
+ at.push(b); if (part) owners[keys.length] = part;
94
+ const key = part?.name ? '\0' + part.kind + '\0' + (aliases?.get(part)?.name || part.name) : value ?? text.slice(a, b);
95
+ keys.push(level === 1 && !raw ? key.trimEnd() : key);
96
+ }
37
97
  };
38
98
  if (parts?.length && level === 0) {
39
99
  let at = start;
40
100
  for (const part of parts) {
41
- if (!spend() || out.length > 131072) return null;
101
+ if (!spend() || keys.length >= 131072) return null;
42
102
  if (part.start < at || part.end <= part.start || part.end > end) continue;
43
103
  if (at < part.start) add(at, part.start);
44
104
  add(part.start, part.end, part); at = part.end;
45
105
  }
46
- add(at, end); return out;
106
+ add(at, end); return keys.length > 131072 ? null : out;
47
107
  }
48
108
  const pattern = level === 1 ? /[^\n]*\n(?:\r?\n)*|[^\n]+$/g : level === 2 ? /[\p{L}\p{M}\p{N}_]+|[^\S\r\n]+|\r\n|[^]/gu : /[^]/gu;
49
109
  pattern.lastIndex = start;
50
110
  let match;
51
111
  while (pattern.lastIndex < end && (match = pattern.exec(text))) {
52
- if (!spend() || out.length >= 131072) return null;
112
+ if (!spend() || keys.length >= 131072) return null;
53
113
  const a = match.index, b = Math.min(end, pattern.lastIndex);
54
- if (out.length && !safe(text, a)) {
55
- out.at(-1)[1] = b; out.at(-1)[2] = text.slice(out.at(-1)[0], b);
56
- } else add(a, b);
114
+ if (keys.length && !safe(text, a)) {
115
+ at[at.length - 1] = b; keys[keys.length - 1] = text.slice(at[at.length - 2], b);
116
+ } else add(a, b, undefined, b === pattern.lastIndex ? match[0] : undefined);
57
117
  if (b === end) break;
58
118
  }
59
119
  return out;
@@ -61,24 +121,39 @@ export function diff(before, after, options = {}) {
61
121
 
62
122
  // Unique anchors and fallback equal pairs share one bounded subsequence search.
63
123
  function matches(a, b) {
64
- const found = [], pending = [[0, a.length, 0, b.length]];
124
+ const found = new Array(a.length), pending = [[0, a.length, 0, b.length]];
65
125
  let nodes = 0;
66
126
  while (pending.length) {
67
127
  let [a0, a1, b0, b1] = pending.pop();
68
- while (a0 < a1 && b0 < b1 && a[a0][2] === b[b0][2]) found.push([a0++, b0++]);
69
- while (a0 < a1 && b0 < b1 && a[a1 - 1][2] === b[b1 - 1][2]) found.push([--a1, --b1]);
128
+ while (a0 < a1 && b0 < b1 && a[a0] === b[b0]) found[a0++] = b0++;
129
+ while (a0 < a1 && b0 < b1 && a[a1 - 1] === b[b1 - 1]) found[--a1] = --b1;
70
130
  if (a0 === a1 || b0 === b1 || !spend(a1 - a0 + b1 - b0)) continue;
71
131
  const positions = Object.create(null), tails = [];
72
132
  for (let j = b0; j < b1; j++) {
73
- const key = b[j][2];
133
+ const key = b[j];
74
134
  (positions[key] ||= [0]).push(j);
75
135
  }
76
136
  for (let i = a0; i < a1; i++) {
77
- const list = positions[a[i][2]]; if (list) list[0]++;
137
+ const list = positions[a[i]]; if (list) list[0]++;
138
+ }
139
+ let unique = 0;
140
+ if (a1 - a0 + b1 - b0 > 32) {
141
+ let count = 0;
142
+ for (const list of Object.values(positions)) {
143
+ count += list[0] * (list.length - 1);
144
+ if (list[0] === 1 && list.length === 2) unique++;
145
+ }
146
+ if (!count) continue;
147
+ // A chain is admitted only after visiting every candidate. Reject a
148
+ // search whose mandatory visits cannot fit before allocating its nodes.
149
+ if ((unique || count) > Math.min(131072 - nodes, budget.left)) {
150
+ // Repetitive text has too many equal pairs; a shortest script needs no anchors.
151
+ if (!shortest(a, b, a0, a1, b0, b1, found) && budget.left >= 0) (found.coarse ||= new Set()).add(a0);
152
+ continue;
153
+ }
78
154
  }
79
- const unique = a1 - a0 + b1 - b0 > 32 && Object.values(positions).some(list => list[0] === 1 && list.length === 2);
80
155
  search: for (let i = a0; i < a1; i++) {
81
- const list = positions[a[i][2]];
156
+ const list = positions[a[i]];
82
157
  if (!list || unique && (list[0] !== 1 || list.length !== 2)) continue;
83
158
  for (let p = list.length - 1; p > 0; p--) {
84
159
  if (!spend() || ++nodes > 131072) { budget.left = -1; break search; }
@@ -98,48 +173,88 @@ export function diff(before, after, options = {}) {
98
173
  chain.reverse();
99
174
  let x = a0, y = b0;
100
175
  for (const [i, j] of chain) {
101
- found.push([i, j]);
102
- if (unique) pending.push([x, i, y, j]);
176
+ found[i] = j;
177
+ if (unique && x < i && y < j) pending.push([x, i, y, j]);
103
178
  x = i + 1; y = j + 1;
104
179
  }
105
- if (unique) pending.push([x, a1, y, b1]);
180
+ if (unique && x < a1 && y < b1) pending.push([x, a1, y, b1]);
181
+ }
182
+ return found;
183
+ }
184
+
185
+ // Myers' greedy search: O((N + M) D) steps; each frontier is kept for the backward pass.
186
+ function shortest(a, b, a0, a1, b0, b1, found) {
187
+ const n = a1 - a0, m = b1 - b0, v = new Int32Array(2 * (n + m) + 3), o = n + m + 1, trace = [];
188
+ for (let d = 0; d <= 2048 && spend(2 * d + 1); d++) {
189
+ trace.push(v.slice(o - d, o + d + 1));
190
+ for (let k = -d; k <= d; k += 2) {
191
+ let x = k === -d || k !== d && v[o + k - 1] < v[o + k + 1] ? v[o + k + 1] : v[o + k - 1] + 1, y = x - k;
192
+ while (x < n && y < m && a[a0 + x] === b[b0 + y] && spend()) { x++; y++; }
193
+ v[o + k] = x;
194
+ if (x < n || y < m) continue;
195
+ for (x = n, y = m; d >= 0; d--) {
196
+ const w = trace[d], k = x - y, prev = k === -d || k !== d && w[d + k - 1] < w[d + k + 1] ? k + 1 : k - 1;
197
+ const px = d ? w[d + prev] : 0, py = px - prev;
198
+ while (x > px && y > py) if (a[a0 + --x] === b[b0 + --y]) found[a0 + x] = b0 + y;
199
+ x = px; y = py;
200
+ }
201
+ return true;
202
+ }
106
203
  }
107
- return found.sort((x, y) => x[0] - y[0]);
204
+ }
205
+
206
+ // A search step scans at most one fixed-size source chunk.
207
+ function common(a, b, end, step) {
208
+ let n = 0;
209
+ while (n < end && spend()) {
210
+ const stop = Math.min(end, n + 256);
211
+ while (n < stop && before.charCodeAt(a) === after.charCodeAt(b)) { n++; a += step; b += step; }
212
+ if (n < stop) break;
213
+ }
214
+ return n;
108
215
  }
109
216
 
110
217
  function walk(a0, a1, b0, b1, level, ap, bp, context) {
111
- if (before.slice(a0, a1) === after.slice(b0, b1)) return;
218
+ if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
112
219
  if (!spend()) { record(a0, a1, b0, b1, 'block', context, true); return; }
113
220
  const structural = level <= 0 && level > -128 && (ap?.length || bp?.length);
114
221
  if (!structural && raw !== 'word') {
115
- let n = 0, end = Math.min(a1 - a0, b1 - b0);
116
- while (n < end && before.charCodeAt(a0 + n) === after.charCodeAt(b0 + n)) n++;
117
- while (n && (!safe(before, a0 + n) || !safe(after, b0 + n))) n--;
118
- if (level === 1) while (n && before[a0 + n - 1] !== '\n') n--;
119
- a0 += n; b0 += n; n = 0; end = Math.min(a1 - a0, b1 - b0);
120
- while (n < end && before.charCodeAt(a1 - n - 1) === after.charCodeAt(b1 - n - 1)) n++;
121
- while (n && (!safe(before, a1 - n) || !safe(after, b1 - n))) n--;
122
- if (level === 1) while (n && (before[a1 - n - 1] !== '\n' || after[b1 - n - 1] !== '\n' || before[a1 - n] === '\n' || after[b1 - n] === '\n')) n--;
222
+ let n = common(a0, b0, Math.min(a1 - a0, b1 - b0), 1);
223
+ while (n && (!safe(before, a0 + n) || !safe(after, b0 + n) || !raw && level < 3 && (within(before, a0 + n) || within(after, b0 + n)))) {
224
+ if (!spend()) { n = 0; break; } n--;
225
+ }
226
+ if (level === 1 && n) {
227
+ const next = Math.max(0, before.lastIndexOf('\n', a0 + n - 1) + 1 - a0);
228
+ spend(Math.ceil((n - next) / 256)); n = next;
229
+ }
230
+ a0 += n; b0 += n;
231
+ n = common(a1 - 1, b1 - 1, Math.min(a1 - a0, b1 - b0), -1);
232
+ while (n && (!safe(before, a1 - n) || !safe(after, b1 - n) || !raw && level < 3 && (within(before, a1 - n) || within(after, b1 - n)))) {
233
+ if (!spend()) { n = 0; break; } n--;
234
+ }
235
+ if (level === 1) while (n && (before[a1 - n - 1] !== '\n' || after[b1 - n - 1] !== '\n' || before[a1 - n] === '\n' || after[b1 - n] === '\n')) { if (!spend()) { n = 0; break; } n--; }
123
236
  a1 -= n; b1 -= n;
237
+ if (budget.left < 0) { record(a0, a1, b0, b1, 'block', context, true); return; }
124
238
  }
125
239
  let unit = level >= 3 ? 'char' : level === 2 ? 'word' : 'line';
126
240
  if (!raw && level === 2 && a1 - a0 + b1 - b0 <= 2 && letter(before[a0 - 1] || after[b0 - 1]) && letter(before[a1] || after[b1])) unit = 'char';
127
241
  if (!structural && (a0 === a1 || b0 === b1)) { record(a0, a1, b0, b1, unit, context); return; }
128
- const a = tokens(before, a0, a1, structural ? 0 : Math.max(1, level), ap);
129
- const b = tokens(after, b0, b1, structural ? 0 : Math.max(1, level), bp);
130
- if (!a || !b) { record(a0, a1, b0, b1, unit, context, true); return; }
131
- const pairs = matches(a, b);
132
- const offset = (ts, i, end) => ts[i]?.[0] ?? end;
242
+ const left = tokens(before, a0, a1, structural ? 0 : Math.max(1, level), ap);
243
+ const right = tokens(after, b0, b1, structural ? 0 : Math.max(1, level), bp);
244
+ if (!left || !right) { record(a0, a1, b0, b1, unit, context, true); return; }
245
+ const [a, x, ownersA] = left, [b, y, ownersB] = right, pairs = matches(a, b);
133
246
  let ai = 0, bi = 0;
134
- for (const [i, j] of [...pairs, [a.length, b.length]]) {
135
- const x0 = offset(a, ai, a1), x1 = offset(a, i, a1), y0 = offset(b, bi, b1), y1 = offset(b, j, b1);
247
+ for (let i = 0; i <= a.length; i++) {
248
+ const j = i === a.length ? b.length : pairs[i];
249
+ if (j === undefined) continue;
250
+ const x0 = x[ai], x1 = x[i], y0 = y[bi], y1 = y[j];
136
251
  if (ai < i || bi < j) {
137
- if (budget.left < 0) record(x0, x1, y0, y1, unit, context, true);
252
+ if (budget.left < 0 || pairs.coarse?.has(ai)) record(x0, x1, y0, y1, unit, context, true);
138
253
  else if (structural && (ai === i || bi === j)) {
139
- for (let k = ai; k < i; k++) record(a[k][0], a[k][1], y0, y0, unit, a[k][3] || context);
140
- for (let k = bi; k < j; k++) record(x1, x1, b[k][0], b[k][1], unit, b[k][3] || context);
254
+ for (let k = ai; k < i; k++) record(x[k], x[k + 1], y0, y0, unit, ownersA[k] || context);
255
+ for (let k = bi; k < j; k++) record(x1, x1, y[k], y[k + 1], unit, ownersB[k] || context);
141
256
  } else if (structural) {
142
- const p = a[ai]?.[3], q = b[bi]?.[3];
257
+ const p = ownersA[ai], q = ownersB[bi];
143
258
  if (i - ai === 1 && j - bi === 1 && p && q && p.kind === q.kind) walk(x0, x1, y0, y1, level - 1, p.children, q.children, q);
144
259
  else walk(x0, x1, y0, y1, 1, null, null, context);
145
260
  } else if (level < 2 && raw !== 'line') walk(x0, x1, y0, y1, raw === 'char' ? 3 : 2, null, null, context);
@@ -147,19 +262,16 @@ export function diff(before, after, options = {}) {
147
262
  const start = result.length;
148
263
  walk(x0, x1, y0, y1, 3, null, null, context);
149
264
  const edits = result.slice(start);
265
+ // A letter-sized correction stays a letter; any other word change marks the whole word.
150
266
  if (edits.length !== 1 || edits[0][0][1] - edits[0][0][0] + edits[0][1][1] - edits[0][1][0] > Math.max(2, (x1 - x0 + y1 - y0) / 3)) {
151
- const reading = [[x0, x1], [y0, y1]], group = [edits[0][3][0], context?.kind || 'word', edits[0][3][2], reading];
152
- for (const edit of edits) {
153
- if (group[0] !== edit[3][0]) group[0] = 1;
154
- edit[3] = group;
155
- }
267
+ result.length = start; record(x0, x1, y0, y1, unit, context);
156
268
  }
157
269
  } else record(x0, x1, y0, y1, unit, context);
158
270
  }
159
271
  if (i < a.length) {
160
- const p = a[i][3], q = b[j][3];
161
- if (before.slice(a[i][0], a[i][1]) !== after.slice(b[j][0], b[j][1]))
162
- walk(a[i][0], a[i][1], b[j][0], b[j][1], p || q ? level - 1 : 3, p?.children, q?.children, q || p || context);
272
+ const p = ownersA[i], q = ownersB[j];
273
+ if ((p || q || level <= 1 && !raw) && before.slice(x[i], x[i + 1]) !== after.slice(y[j], y[j + 1]))
274
+ walk(x[i], x[i + 1], y[j], y[j + 1], p || q ? level - 1 : 3, p?.children, q?.children, q || p || context);
163
275
  }
164
276
  ai = i + 1; bi = j + 1;
165
277
  }
@@ -192,7 +304,12 @@ export function diff(before, after, options = {}) {
192
304
  if (!row || !Number.isSafeInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
193
305
  row.pos < 0 || row.pos + row.removed.length > length || !verify(pieces.length * 2 + 1)) return null;
194
306
  const left = split(row.pos), right = split(row.pos + row.removed.length);
195
- if (pieces.slice(left, right).map(([start, end, text = before]) => text.slice(start, end)).join('') !== row.removed) return null;
307
+ let at = 0;
308
+ for (let i = left; i < right; i++) {
309
+ const [start, end, text = before] = pieces[i];
310
+ if (!row.removed.startsWith(text.slice(start, end), at)) return null;
311
+ at += end - start;
312
+ }
196
313
  if (row.removed === row.inserted) continue;
197
314
  pieces.splice(left, right - left, [0, row.inserted.length, row.inserted]);
198
315
  length += row.inserted.length - row.removed.length;
@@ -202,7 +319,7 @@ export function diff(before, after, options = {}) {
202
319
  let a = 0, b = 0, at = 0;
203
320
  for (const [start, end, text] of pieces) {
204
321
  const size = end - start;
205
- if (!verify(1) || (text ?? before).slice(start, end) !== after.slice(at, at + size)) return null;
322
+ if (!verify(1) || !after.startsWith((text ?? before).slice(start, end), at)) return null;
206
323
  if (text === undefined) {
207
324
  let x = start, y = at, tail = end;
208
325
  while (x < tail && (!safe(before, x) || !safe(after, y))) { x++; y++; if (!verify(1)) return null; }
@@ -220,22 +337,11 @@ export function diff(before, after, options = {}) {
220
337
 
221
338
  if (options.structure && (!raw || options._structure)) {
222
339
  try {
223
- structures = [options.structure(before), options.structure(after)];
224
- const flatten = parts => {
225
- if (parts.length > 131072) throw null;
226
- const list = [], pending = [...parts].reverse();
227
- while (pending.length && spend()) {
228
- const p = pending.pop(); list.push(p);
229
- if (p.children) {
230
- if (list.length + pending.length + p.children.length > 131072) throw null;
231
- pending.push(...[...p.children].reverse());
232
- }
233
- }
234
- return list;
235
- };
236
- [pa, pb] = structures.map(s => flatten(s.parts)); [la, lb] = structures.map(s => s.links || []);
340
+ structures = options._observed || [observe(before, options.structure, spend, safe), observe(after, options.structure, spend, safe)];
341
+ if (structures.some(value => !value || value.invalid)) throw null;
342
+ [pa, pb] = structures.map(value => value.flat); [la, lb] = structures.map(value => value.links);
237
343
  aliases = new Map();
238
- const oldByKey = new Map(pa.filter(p => p.key).map(p => [p.key, p]));
344
+ const oldByKey = new Map(pa.filter(p => p.key != null).map(p => [p.key, p]));
239
345
  const oldNames = new Set(pa.map(p => p.name)), newNames = new Set(pb.map(p => p.name));
240
346
  for (const p of pb) {
241
347
  const old = oldByKey.get(p.key);
@@ -248,6 +354,23 @@ export function diff(before, after, options = {}) {
248
354
  else walk(0, before.length, 0, after.length, structures ? 0 : raw === 'char' ? 3 : raw === 'word' ? 2 : 1,
249
355
  structures?.[0]?.parts, structures?.[1]?.parts);
250
356
 
357
+ // A separator-only piece beside another edit joins it; equal separators at its ends stay unchanged.
358
+ if (!raw) for (let i = result.length - 1; i > 0; i--) {
359
+ const p = result[i - 1], q = result[i], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1, text = side ? after : before;
360
+ if (side >= 0 && !/\S/.test(text.slice(...p[side]))) {
361
+ let n = 0;
362
+ while (p[0][1] + n < q[0][0] && text[p[side][0] + n] === text[p[side][1] + n]) n++;
363
+ if (p[0][1] + n === q[0][0] && p.slice(0, 2).every((span, k) => safe(k ? after : before, span[0] + n) && safe(k ? after : before, span[1] + n)))
364
+ for (const span of p.slice(0, 2)) { span[0] += n; span[1] += n; }
365
+ }
366
+ if (p[2] || q[2] || p[0][1] !== q[0][0] || p[1][1] !== q[1][0] ||
367
+ ![p, q].some(r => !/\S/.test(before.slice(...r[0]) + after.slice(...r[1])))) continue;
368
+ const x = [p[0][0], q[0][1]], y = [p[1][0], q[1][1]], keep = /\S/.test(before.slice(...q[0]) + after.slice(...q[1])) ? q : p;
369
+ while (x[0] < x[1] && y[0] < y[1] && /\s/.test(before[x[1] - 1]) && before[x[1] - 1] === after[y[1] - 1] &&
370
+ safe(before, x[1] - 1) && safe(after, y[1] - 1)) { x[1]--; y[1]--; }
371
+ keep[3][0] = x[0] === x[1] ? 2 : y[0] === y[1] ? 0 : 1;
372
+ result.splice(i - 1, 2, [x, y, false, keep[3]]);
373
+ }
251
374
  try { if (!raw && budget.left >= 0) {
252
375
  const check = n => { if (!spend(n)) throw null; };
253
376
  // Split transported text at wording boundaries; positions and words own distinct edits.
@@ -268,16 +391,19 @@ export function diff(before, after, options = {}) {
268
391
  if (!candidate || candidate[4]) continue;
269
392
  const a = tokens(prior, 0, prior.length, 2), b = tokens(key, 0, key.length, 2);
270
393
  if (!a || !b) throw null;
271
- const n = matches(a, b).reduce((n, [i]) => n + a[i][1] - a[i][0], 0);
394
+ const n = matches(a[0], b[0]).reduce((n, j, i) => n + a[1][i + 1] - a[1][i], 0);
272
395
  check(0);
273
396
  if (n * 5 > Math.max(prior.length, key.length) * 3 && n >= score) { old = n === score ? null : candidate; score = n; }
274
397
  }
275
398
  if (!old || old[4]) continue;
276
- const x = [...old[0]], y = [...item[1]], left = before.slice(...x).match(/\s*$/)[0], right = after.slice(...y).match(/\s*$/)[0];
277
- let common = 0;
278
- while (common < Math.min(left.length, right.length) && left[common] === right[common]) common++;
279
- if (common && safe(before, x[1] - left.length + common) && safe(after, y[1] - right.length + common)) {
280
- x[1] -= left.length - common; y[1] -= right.length - common;
399
+ // Separators that only one end carries stay outside the transported text.
400
+ const x = [...old[0]], y = [...item[1]];
401
+ for (const end of [0, 1]) {
402
+ const pattern = end ? /\s*$/ : /^\s*/, left = before.slice(...x).match(pattern)[0], right = after.slice(...y).match(pattern)[0];
403
+ let common = 0;
404
+ while (common < Math.min(left.length, right.length) && left.at(end ? common : -1 - common) === right.at(end ? common : -1 - common)) common++;
405
+ const dx = (left.length - common) * (end ? -1 : 1), dy = (right.length - common) * (end ? -1 : 1);
406
+ if (safe(before, x[end] + dx) && safe(after, y[end] + dy)) { x[end] += dx; y[end] += dy; }
281
407
  }
282
408
  const outer = result; result = [];
283
409
  walk(...x, ...y, 1);
@@ -291,7 +417,8 @@ export function diff(before, after, options = {}) {
291
417
  if (a0 < a1) old[4].push([[a0, a1], [old[1][0], old[1][0]], false, group]);
292
418
  if (b0 < b1) item[4].push([[item[0][0], item[0][0]], [b0, b1], false, group]);
293
419
  };
294
- let a = old[0][0], b = item[1][0], wording;
420
+ fragment(old[0][0], x[0], 0, 0, [0, 'char']); fragment(0, 0, item[1][0], y[0], [2, 'char']);
421
+ let a = x[0], b = y[0], wording;
295
422
  for (const edit of inner) {
296
423
  if (edit[3][3]) wording = edit[3];
297
424
  else if (a !== edit[0][0] || b !== edit[1][0]) wording = [edit[3][0], edit[3][1]];
@@ -314,7 +441,7 @@ export function diff(before, after, options = {}) {
314
441
  while (cursor < result.length && result[cursor][0][1] <= link.at[0]) { delta = result[cursor][1][1] - result[cursor][0][1]; cursor++; }
315
442
  if (cursor === result.length || result[cursor][0][0] >= link.at[1]) mapped[[link.at[0] + delta, link.at[1] + delta]] = link.to;
316
443
  }
317
- lb = lb.map(l => l.key ? l : {...l, to: mapped[l.at] || l.to});
444
+ lb = lb.map(l => l.key != null ? l : {...l, to: mapped[l.at] || l.to});
318
445
  for (const [p, old] of aliases) {
319
446
  if (before.slice(...old.nameAt) === after.slice(...p.nameAt)) continue;
320
447
  check(la.length + lb.length);
@@ -348,6 +475,14 @@ export function diff(before, after, options = {}) {
348
475
  result = result.flatMap(item => item[4] || [item]);
349
476
  }
350
477
 
478
+ // Whole-word replacements separated only by spaces read and restore as one phrase.
479
+ if (!raw && !code) for (let i = result.length - 1; i > 0; i--) {
480
+ const p = result[i - 1], q = result[i], gap = before.slice(p[0][1], q[0][0]);
481
+ if (p[3][0] !== 1 || q[3][0] !== 1 || p[3][3] || q[3][3] || p[2] || q[2] || !/^[^\S\r\n]+$/.test(gap) ||
482
+ gap !== after.slice(p[1][1], q[1][0]) || within(before, p[0][0]) || within(after, p[1][0]) ||
483
+ within(before, q[0][1]) || within(after, q[1][1])) continue;
484
+ p[0][1] = q[0][1]; p[1][1] = q[1][1]; result.splice(i, 1);
485
+ }
351
486
  const changes = [], edits = [];
352
487
  let normalized;
353
488
  const meaning = structures?.[0]?.meaning;
@@ -373,9 +508,11 @@ export function diff(before, after, options = {}) {
373
508
  if (!raw && !group[3]) group[3] = [[...item[0]], [...item[1]]];
374
509
  if (!group[4]) {
375
510
  const context = group[3];
511
+ // Context completes a word only where an edge falls inside it.
376
512
  if (!raw) for (const [text, span] of [[before, context[0]], [after, context[1]]]) {
377
- while (span[0] && letter(text[span[0] - 1]) && safe(text, span[0] - 1)) span[0]--;
378
- while (span[1] < text.length && letter(text[span[1]]) && safe(text, span[1] + 1)) span[1]++;
513
+ const start = within(text, span[0]), end = within(text, span[1]);
514
+ while (start && span[0] && spend() && letter(text[span[0] - 1]) && safe(text, span[0] - 1)) span[0]--;
515
+ while (end && span[1] < text.length && spend() && letter(text[span[1]]) && safe(text, span[1] + 1)) span[1]++;
379
516
  }
380
517
  group[4] = []; changes.push(group);
381
518
  if (format) group[0] = 5;
@@ -383,20 +520,6 @@ export function diff(before, after, options = {}) {
383
520
  group[4].push(edits.length);
384
521
  edits.push({before: item[0], after: item[1], ...(item[2] ? {coarse: true} : {})});
385
522
  }
386
- if (!raw && !structures && !code && !evidence) for (let i = 0; i < changes.length; i++) {
387
- let end = i + 1;
388
- for (; end < changes.length; end++) {
389
- const a = changes[end - 1], b = changes[end];
390
- const gap = before.slice(a[3][0][1], b[3][0][0]);
391
- if (a[0] !== 1 || b[0] !== 1 || a[1] !== 'word' || b[1] !== 'word' ||
392
- /\S|\r|\n/.test(gap) || gap !== after.slice(a[3][1][1], b[3][1][0])) break;
393
- }
394
- if (end - i < 4) continue;
395
- const group = changes[i]; group[1] = 'sentence';
396
- for (const next of changes.splice(i + 1, end - i - 1)) {
397
- group[4].push(...next[4]); group[3][0][1] = next[3][0][1]; group[3][1][1] = next[3][1][1];
398
- }
399
- }
400
523
  if (structures && !raw && budget.left >= 0) {
401
524
  const oldNames = new Set(pa.filter(p => p.nameAt).map(p => p.name)), newNames = new Set(pb.filter(p => p.nameAt).map(p => p.name));
402
525
  for (const change of changes) {
@@ -433,149 +556,161 @@ export function merge(base, mine, theirs, options = {}) {
433
556
  _budget: options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7}};
434
557
  const sources = [base, mine, theirs];
435
558
  const complete = () => ({text: mine, conflicts: [{at: [0, mine.length], theirs}]});
436
- const spend = n => { if ((budget._budget.left -= n) < 0) throw null; };
437
- const trees = options.structure && !options._edits ? [base, mine, base, theirs].map(text => {
438
- // Retain source spans, not live objects a caller may reuse on the next parse.
439
- try {
440
- const value = options.structure(text), parts = [], pending = [[value?.parts, parts, 0, text.length, 0]];
441
- let count = 0;
442
- while (pending.length) {
443
- const [rows, into, from, end, depth] = pending.pop();
444
- if (!Array.isArray(rows) || rows.length > 131072 || depth > 128) throw null;
445
- let at = from;
446
- for (const part of rows) {
447
- spend(1);
448
- if (++count > 131072 || !Number.isSafeInteger(part?.start) || !Number.isSafeInteger(part.end) ||
449
- part.start < at || part.end < part.start || part.end > end) throw null;
450
- const copy = {start: part.start, end: part.end, kind: part.kind, name: part.name, key: part.key,
451
- id: typeof part.name === 'string' && part.name && typeof part.kind === 'string' ? JSON.stringify([part.kind, part.name]) : null,
452
- ...(Array.isArray(part.nameAt) ? {nameAt: [...part.nameAt]} : {}), children: []};
453
- into.push(copy); at = part.end;
454
- if (part.children) pending.push([part.children, copy.children, part.start, part.end, depth + 1]);
455
- }
456
- }
457
- return parts;
458
- } catch { return null; }
459
- }) : [];
559
+ const spend = (n = 1) => (budget._budget.left -= n) >= 0;
560
+ const check = n => { if (!spend(n)) throw null; };
561
+ const safe = options.structure && boundaries();
562
+ const trees = options.structure && !options._edits
563
+ ? [base, mine, base, theirs].map(text => observe(text, options.structure, spend, safe, false, true)) : [];
460
564
  const repeatedBase = trees.splice(2, 1)[0];
461
- // Admit transport before comparing coordinates that a move would discard.
462
- // The base is still observed twice; later diffs consume only owned snapshots.
463
- budget.structure = options.structure && (text => ({parts: trees[sources.indexOf(text)]}));
565
+ const equal = (a, b) => a.length === b.length && a.every((item, i) => item === b[i]);
464
566
  if (trees.some(Boolean) || repeatedBase) {
465
567
  try {
466
- if (JSON.stringify(trees[0]) !== JSON.stringify(repeatedBase) || !trees.every(Boolean) || budget._budget.left < 0) return complete();
467
- } catch { return complete(); }
468
- const removed = [new Map(), new Map()], added = [new Map(), new Map()];
469
- let container = 0;
470
- const equal = (a, b) => a.length === b.length && a.every((item, i) => item === b[i]);
471
- const content = (part, side) => sources[side].slice(part.start, part.end);
472
- function align(nodes, depth = 0) {
473
- const original = content(nodes[0], 0);
474
- if (original === content(nodes[1], 1) && original === content(nodes[2], 2)) return;
475
- if (depth > 128) throw null;
476
- const hidden = nodes.flatMap(node => node.children.filter(part => !part.id));
477
- while (hidden.length) {
478
- const part = hidden.pop(); spend(1);
479
- if (part.id) throw null;
480
- for (const child of part.children) hidden.push(child);
481
- }
482
- const rows = nodes.map(node => node.children.filter(part => part.id));
483
- const width = rows.reduce((n, list) => n + list.length, 0);
484
- spend(1 + width);
485
- let repeated = false;
486
- // Display identity and definition spelling obey the same occurrence rule.
487
- const index = nameOf => {
488
- const duplicates = new Set();
489
- const maps = rows.map((list, side) => {
490
- const map = new Map();
491
- for (const part of list) {
492
- const name = nameOf(part, side);
493
- if (name) {
494
- if (map.has(name)) duplicates.add(name);
495
- map.set(name, map.has(name) ? null : part);
568
+ if (!trees.every(tree => tree && !tree.invalid) || !repeatedBase || repeatedBase.invalid) return complete();
569
+ const a = trees[0].flat, b = repeatedBase.flat;
570
+ check(a.length + b.length);
571
+ if (a.length !== b.length) return complete();
572
+ for (let i = 0; i < a.length; i++) {
573
+ const p = a[i], q = b[i];
574
+ if (p.start !== q.start || p.end !== q.end || p.kind !== q.kind || p.name !== q.name || p.key !== q.key ||
575
+ p.children.length !== q.children.length || !equal(p.nameAt || [], q.nameAt || [])) return complete();
576
+ }
577
+ for (const tree of trees) for (const part of tree.flat)
578
+ part.id = part.name && part.kind ? part.kind.length + ':' + part.kind + part.name : null;
579
+ const removed = [new Map(), new Map()], added = [new Map(), new Map()];
580
+ let container = 0;
581
+ const content = (part, side) => sources[side].slice(part.start, part.end);
582
+ function align(nodes) {
583
+ const original = content(nodes[0], 0);
584
+ if (original === content(nodes[1], 1) && original === content(nodes[2], 2)) {
585
+ nodes[0].common = original; return;
586
+ }
587
+ const hidden = nodes.flatMap(node => node.children.filter(part => !part.id));
588
+ while (hidden.length) {
589
+ const part = hidden.pop(); check(1);
590
+ if (part.id) throw null;
591
+ for (const child of part.children) hidden.push(child);
592
+ }
593
+ const rows = nodes.map(node => node.children.filter(part => part.id));
594
+ const width = rows.reduce((n, list) => n + list.length, 0);
595
+ check(1 + width);
596
+ let repeated = false;
597
+ const index = nameOf => {
598
+ const duplicates = new Set();
599
+ const maps = rows.map((list, side) => {
600
+ const map = new Map();
601
+ for (const part of list) {
602
+ const name = nameOf(part, side);
603
+ if (!name) continue;
604
+ const old = map.get(name);
605
+ if (!old) map.set(name, part);
606
+ else {
607
+ duplicates.add(name);
608
+ if (Array.isArray(old)) old.push(part); else map.set(name, [old, part]);
609
+ }
496
610
  }
611
+ return map;
612
+ });
613
+ for (const name of duplicates) {
614
+ repeated = true;
615
+ const values = maps.map((map, side) => {
616
+ const part = map.get(name), list = Array.isArray(part) ? part : part ? [part] : [];
617
+ check(list.length); map.set(name, null);
618
+ return list.map(part => content(part, side));
619
+ });
620
+ if (!equal(values[0], values[1]) || !equal(values[0], values[2])) throw null;
497
621
  }
498
- return map;
622
+ return maps;
623
+ };
624
+ const maps = index(part => part.id), orders = rows.map(list => list.map(part => part.id));
625
+ const reordered = [1, 2].map(side => !equal(orders[0].filter(name => maps[side].has(name)),
626
+ orders[side].filter(name => maps[0].has(name))));
627
+ index((part, side) => {
628
+ check(1);
629
+ if (part.nameAt) return part.kind.length + ':' + part.kind + sources[side].slice(...part.nameAt);
499
630
  });
500
- for (const name of duplicates) {
501
- spend(width); repeated = true;
502
- const values = rows.map((list, side) => list.filter(part => nameOf(part, side) === name).map(part => content(part, side)));
503
- if (!equal(values[0], values[1]) || !equal(values[0], values[2])) throw null;
631
+ const parent = container++;
632
+ for (const side of [1, 2]) {
633
+ const peer = 3 - side;
634
+ for (const [name, part] of maps[0]) if (!maps[side].has(name)) {
635
+ removed[side - 1].set(parent + ':' + name, [part.kind, parent]);
636
+ const other = maps[peer].get(name);
637
+ if (part?.nameAt && other?.nameAt && content(part, 0) !== content(other, peer)) throw null;
638
+ }
639
+ for (const [name, part] of maps[side]) if (!maps[0].has(name))
640
+ added[side - 1].set(parent + ':' + name, [part.kind, parent]);
504
641
  }
505
- return maps;
506
- };
507
- const maps = index(part => part.id), orders = rows.map(list => list.map(part => part.id));
508
- const reordered = [1, 2].map(side => !equal(orders[0].filter(name => maps[side].has(name)),
509
- orders[side].filter(name => maps[0].has(name))));
510
- index((part, side) => {
511
- spend(1);
512
- const at = part.nameAt;
513
- if (at?.length === 2 && at.every(Number.isSafeInteger) &&
514
- at[0] >= part.start && at[1] > at[0] && at[1] <= part.end)
515
- return JSON.stringify([part.kind, sources[side].slice(...at)]);
516
- });
517
- // This number identifies only this already-corresponding container.
518
- const parent = container++;
519
- for (const side of [1, 2]) {
520
- const peer = 3 - side;
521
- for (const [name, part] of maps[0]) if (!maps[side].has(name)) {
522
- removed[side - 1].set(parent + name, [part.kind, parent]);
523
- const other = maps[peer].get(name);
524
- if (part?.nameAt && other?.nameAt && content(part, 0) !== content(other, peer)) throw null;
642
+ let frame = -1;
643
+ if (reordered.some(Boolean)) {
644
+ if (repeated || !maps.every(map => map.size === maps[0].size && [...maps[0].keys()].every(name => map.has(name)))) throw null;
645
+ if (reordered.every(Boolean) && !equal(orders[1], orders[2])) throw null;
646
+ frame = reordered[0] ? 1 : 2;
647
+ const gaps = rows.map((list, side) => {
648
+ let at = nodes[side].start;
649
+ return [...list.map(part => { const gap = sources[side].slice(at, part.start); at = part.end; return gap; }),
650
+ sources[side].slice(at, nodes[side].end)];
651
+ });
652
+ if (!equal(gaps[3 - frame], gaps[reordered.every(Boolean) ? frame : 0])) throw null;
525
653
  }
526
- for (const [name, part] of maps[side]) if (!maps[0].has(name))
527
- added[side - 1].set(parent + name, [part.kind, parent]);
528
- }
529
- let frame = -1;
530
- if (reordered.some(Boolean)) {
531
- if (repeated || !maps.every(map => map.size === maps[0].size && [...maps[0].keys()].every(name => map.has(name)))) throw null;
532
- if (reordered.every(Boolean) && !equal(orders[1], orders[2])) throw null;
533
- frame = reordered[0] ? 1 : 2;
534
- const gaps = rows.map((list, side) => {
535
- let at = nodes[side].start;
536
- return [...list.map(part => { const gap = sources[side].slice(at, part.start); at = part.end; return gap; }),
537
- sources[side].slice(at, nodes[side].end)];
538
- });
539
- if (!equal(gaps[3 - frame], gaps[reordered.every(Boolean) ? frame : 0])) throw null;
540
- }
541
- const children = new Map();
542
- for (const [name, part] of maps[0]) if (part && maps[1].get(name) && maps[2].get(name)) {
543
- const aligned = align([part, maps[1].get(name), maps[2].get(name)], depth + 1);
544
- if (aligned) children.set(name, aligned);
545
- }
546
- if (frame < 0 && !children.size) return;
547
- // Each slice still uses its original source coordinates, even after child growth.
548
- return sources.map((source, side) => {
549
- const layout = frame < 0 ? side : frame;
550
- let at = nodes[layout].start, text = '';
551
- for (const part of rows[layout]) {
552
- text += sources[layout].slice(at, part.start) + (children.get(part.id)?.[side] ?? content(maps[side].get(part.id) || part, side));
553
- at = part.end;
654
+ const children = new Map();
655
+ for (const [name, part] of maps[0]) if (part && maps[1].get(name) && maps[2].get(name)) {
656
+ const child = align([part, maps[1].get(name), maps[2].get(name)]);
657
+ if (child) children.set(name, child);
554
658
  }
555
- return text + sources[layout].slice(at, nodes[layout].end);
556
- });
557
- }
558
- try {
559
- const aligned = align(trees.map((children, side) => ({start: 0, end: sources[side].length, children})));
560
- // A possible relocation between containers stays one complete source choice.
659
+ if (frame < 0 && !children.size) return;
660
+ // A plan is a tree of range triples; a string is an already-admitted gap.
661
+ const plan = [];
662
+ if (frame >= 0) {
663
+ let at = nodes[frame].start;
664
+ for (const part of rows[frame]) {
665
+ plan.push(sources[frame].slice(at, part.start), children.get(part.id) || (maps[0].get(part.id).common ?? {ranges: maps.map(map => map.get(part.id))}));
666
+ at = part.end;
667
+ }
668
+ plan.push(sources[frame].slice(at, nodes[frame].end));
669
+ } else {
670
+ let at = nodes.map(node => node.start);
671
+ for (const child of children.values()) {
672
+ const ranges = child.nodes;
673
+ plan.push({ranges: ranges.map((node, side) => ({start: at[side], end: node.start}))}, child);
674
+ at = ranges.map(node => node.end);
675
+ }
676
+ plan.push({ranges: nodes.map((node, side) => ({start: at[side], end: node.end}))});
677
+ }
678
+ return {nodes, plan};
679
+ }
680
+ const plan = align(trees.map((tree, side) => ({start: 0, end: sources[side].length, children: tree.parts})));
561
681
  for (const side of [0, 1]) {
562
682
  const destinations = new Map();
563
- spend(added[side].size + removed[side].size);
683
+ check(added[side].size + removed[side].size);
564
684
  for (const [kind, parent] of added[side].values())
565
685
  destinations.set(kind, destinations.has(kind) && destinations.get(kind) !== parent ? null : parent);
566
686
  for (const [kind, parent] of removed[side].values())
567
687
  if (destinations.has(kind) && destinations.get(kind) !== parent) throw null;
568
688
  }
569
- // A shared disappearance with different additions may be competing destinations.
570
689
  if ([...removed[0].keys()].some(name => removed[1].has(name)) &&
571
690
  (added[0].size !== added[1].size || [...added[0].keys()].some(name => !added[1].has(name)))) throw null;
572
- if (aligned) {
573
- const result = merge(...aligned, {...budget, structure: undefined, from: undefined});
574
- return budget._budget.left < 0 ? complete() : result;
691
+ if (plan) {
692
+ // Project original ranges once. Nested moves never rebuild aligned documents.
693
+ const chunks = [], conflicts = [];
694
+ let length = 0;
695
+ function project(item) {
696
+ if (item.plan) { for (const child of item.plan) project(child); return; }
697
+ check(1);
698
+ let text = item;
699
+ if (typeof item !== 'string') {
700
+ const result = merge(...item.ranges.map(content), {...budget, structure: undefined, from: undefined});
701
+ check(0); text = result.text;
702
+ for (const conflict of result.conflicts)
703
+ conflicts.push({at: conflict.at.map(at => at + length), theirs: conflict.theirs});
704
+ }
705
+ chunks.push(text); length += text.length;
706
+ }
707
+ project(plan);
708
+ return {text: chunks.join(''), conflicts};
575
709
  }
576
710
  } catch (error) { if (error !== null) throw error; return complete(); }
577
711
  }
578
- const changes = options._edits || [mine, theirs].map(text => diff(base, text, budget).edits);
712
+ const changes = options._edits || [mine, theirs].map((text, side) => diff(base, text,
713
+ {...budget, structure: trees[0] && options.structure, _observed: trees[0] ? [trees[0], trees[side + 1]] : undefined}).edits);
579
714
  if (trees.some(Boolean) && budget._budget.left < 0) return complete();
580
715
  const edits = [mine, theirs].flatMap((text, side) => changes[side].map(change =>
581
716
  [...change.before, text.slice(...change.after), side]));