rapier-markdown-kit 1.1.98 → 1.1.101

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.
@@ -0,0 +1,610 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export function diff(before, after, options = {}) {
3
+ if (before === after) return {edits: [], changes: []};
4
+ const budget = options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7};
5
+ const initial = budget.left, raw = options._unit;
6
+ let result = [], code = options.as === 'code', structures, pa, pb, la, lb, aliases;
7
+ const spend = (n = 1) => (budget.left -= n) >= 0;
8
+ const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c);
9
+ const overlaps = (a, b) => a[0] < b[1] && a[1] > b[0];
10
+ const inside = (c, a, b) => [a, b].every((range, side) => c[side][0] === c[side][1] ||
11
+ c[side][0] >= range[0] && c[side][1] <= range[1]);
12
+ 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
+
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
+ }
24
+
25
+ function record(a0, a1, b0, b1, unit, part, coarse = false) {
26
+ if (before.slice(a0, a1) === after.slice(b0, b1)) return;
27
+ // Internal records: before, after, coarse, concern, transported fragments.
28
+ result.push([[a0, a1], [b0, b1], coarse, [a0 === a1 ? 2 : b0 === b1 ? 0 : 1, part?.kind || unit, part?.name]]);
29
+ }
30
+
31
+ function tokens(text, start, end, level, parts) {
32
+ const out = [];
33
+ const add = (a, b, part) => {
34
+ 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]);
37
+ };
38
+ if (parts?.length && level === 0) {
39
+ let at = start;
40
+ for (const part of parts) {
41
+ if (!spend() || out.length > 131072) return null;
42
+ if (part.start < at || part.end <= part.start || part.end > end) continue;
43
+ if (at < part.start) add(at, part.start);
44
+ add(part.start, part.end, part); at = part.end;
45
+ }
46
+ add(at, end); return out;
47
+ }
48
+ 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
+ pattern.lastIndex = start;
50
+ let match;
51
+ while (pattern.lastIndex < end && (match = pattern.exec(text))) {
52
+ if (!spend() || out.length >= 131072) return null;
53
+ 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);
57
+ if (b === end) break;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ // Unique anchors and fallback equal pairs share one bounded subsequence search.
63
+ function matches(a, b) {
64
+ const found = [], pending = [[0, a.length, 0, b.length]];
65
+ let nodes = 0;
66
+ while (pending.length) {
67
+ 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]);
70
+ if (a0 === a1 || b0 === b1 || !spend(a1 - a0 + b1 - b0)) continue;
71
+ const positions = Object.create(null), tails = [];
72
+ for (let j = b0; j < b1; j++) {
73
+ const key = b[j][2];
74
+ (positions[key] ||= [0]).push(j);
75
+ }
76
+ for (let i = a0; i < a1; i++) {
77
+ const list = positions[a[i][2]]; if (list) list[0]++;
78
+ }
79
+ const unique = a1 - a0 + b1 - b0 > 32 && Object.values(positions).some(list => list[0] === 1 && list.length === 2);
80
+ search: for (let i = a0; i < a1; i++) {
81
+ const list = positions[a[i][2]];
82
+ if (!list || unique && (list[0] !== 1 || list.length !== 2)) continue;
83
+ for (let p = list.length - 1; p > 0; p--) {
84
+ if (!spend() || ++nodes > 131072) { budget.left = -1; break search; }
85
+ const j = list[p];
86
+ let lo = 0, hi = tails.length;
87
+ while (lo < hi) {
88
+ if (!spend()) break search;
89
+ const middle = lo + hi >>> 1;
90
+ if (tails[middle][1] < j) lo = middle + 1; else hi = middle;
91
+ }
92
+ tails[lo] = [i, j, tails[lo - 1]];
93
+ }
94
+ }
95
+ if (budget.left < 0) continue;
96
+ const chain = [];
97
+ for (let node = tails.at(-1); node; node = node[2]) chain.push(node);
98
+ chain.reverse();
99
+ let x = a0, y = b0;
100
+ for (const [i, j] of chain) {
101
+ found.push([i, j]);
102
+ if (unique) pending.push([x, i, y, j]);
103
+ x = i + 1; y = j + 1;
104
+ }
105
+ if (unique) pending.push([x, a1, y, b1]);
106
+ }
107
+ return found.sort((x, y) => x[0] - y[0]);
108
+ }
109
+
110
+ function walk(a0, a1, b0, b1, level, ap, bp, context) {
111
+ if (before.slice(a0, a1) === after.slice(b0, b1)) return;
112
+ if (!spend()) { record(a0, a1, b0, b1, 'block', context, true); return; }
113
+ const structural = level <= 0 && level > -128 && (ap?.length || bp?.length);
114
+ 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--;
123
+ a1 -= n; b1 -= n;
124
+ }
125
+ let unit = level >= 3 ? 'char' : level === 2 ? 'word' : 'line';
126
+ if (!raw && level === 2 && a1 - a0 + b1 - b0 <= 2 && letter(before[a0 - 1] || after[b0 - 1]) && letter(before[a1] || after[b1])) unit = 'char';
127
+ 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;
133
+ 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);
136
+ if (ai < i || bi < j) {
137
+ if (budget.left < 0) record(x0, x1, y0, y1, unit, context, true);
138
+ 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);
141
+ } else if (structural) {
142
+ const p = a[ai]?.[3], q = b[bi]?.[3];
143
+ 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
+ else walk(x0, x1, y0, y1, 1, null, null, context);
145
+ } else if (level < 2 && raw !== 'line') walk(x0, x1, y0, y1, raw === 'char' ? 3 : 2, null, null, context);
146
+ else if (level === 2 && !raw && i - ai <= 1 && j - bi <= 1 && x1 - x0 + y1 - y0 < 100) {
147
+ const start = result.length;
148
+ walk(x0, x1, y0, y1, 3, null, null, context);
149
+ const edits = result.slice(start);
150
+ 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
+ }
156
+ }
157
+ } else record(x0, x1, y0, y1, unit, context);
158
+ }
159
+ 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);
163
+ }
164
+ ai = i + 1; bi = j + 1;
165
+ }
166
+ }
167
+
168
+ // Surviving source pieces keep the occurrence witnessed by the host.
169
+ function operations(rows) {
170
+ if (!Array.isArray(rows)) return null;
171
+ const pieces = [[0, before.length]];
172
+ let length = before.length, steps = 1e7;
173
+ const verify = n => {
174
+ if ((steps -= n) >= 0) return true;
175
+ budget.left = -1; return false;
176
+ };
177
+ const split = at => {
178
+ let position = 0;
179
+ for (let i = 0; i < pieces.length; i++) {
180
+ const [start, end, text] = pieces[i], size = end - start;
181
+ if (position === at) return i;
182
+ if (position + size > at) {
183
+ const n = at - position;
184
+ pieces.splice(i, 1, [start, start + n, text], [start + n, end, text]);
185
+ return i + 1;
186
+ }
187
+ position += size;
188
+ }
189
+ return pieces.length;
190
+ };
191
+ for (const row of rows) {
192
+ if (!row || !Number.isSafeInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
193
+ row.pos < 0 || row.pos + row.removed.length > length || !verify(pieces.length * 2 + 1)) return null;
194
+ 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;
196
+ if (row.removed === row.inserted) continue;
197
+ pieces.splice(left, right - left, [0, row.inserted.length, row.inserted]);
198
+ length += row.inserted.length - row.removed.length;
199
+ }
200
+ if (length !== after.length) return null;
201
+ const spans = [];
202
+ let a = 0, b = 0, at = 0;
203
+ for (const [start, end, text] of pieces) {
204
+ const size = end - start;
205
+ if (!verify(1) || (text ?? before).slice(start, end) !== after.slice(at, at + size)) return null;
206
+ if (text === undefined) {
207
+ let x = start, y = at, tail = end;
208
+ while (x < tail && (!safe(before, x) || !safe(after, y))) { x++; y++; if (!verify(1)) return null; }
209
+ while (tail > x && (!safe(before, tail) || !safe(after, at + tail - start))) { tail--; if (!verify(1)) return null; }
210
+ if (tail > x) {
211
+ if (a !== x || b !== y) spans.push([a, x, b, y]);
212
+ a = tail; b = at + tail - start;
213
+ }
214
+ }
215
+ at += size;
216
+ }
217
+ if (a !== before.length || b !== after.length) spans.push([a, before.length, b, after.length]);
218
+ return spans;
219
+ }
220
+
221
+ if (options.structure && (!raw || options._structure)) {
222
+ 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 || []);
237
+ aliases = new Map();
238
+ const oldByKey = new Map(pa.filter(p => p.key).map(p => [p.key, p]));
239
+ const oldNames = new Set(pa.map(p => p.name)), newNames = new Set(pb.map(p => p.name));
240
+ for (const p of pb) {
241
+ const old = oldByKey.get(p.key);
242
+ if (old?.nameAt && p.nameAt && old.kind === p.kind && !oldNames.has(p.name) && !newNames.has(old.name)) aliases.set(p, old);
243
+ }
244
+ } catch { structures = undefined; code = true; }
245
+ }
246
+ const evidence = options.from && operations(options.from);
247
+ if (evidence) for (const span of evidence) walk(...span, structures ? 0 : raw === 'char' ? 3 : 1, pa, pb);
248
+ else walk(0, before.length, 0, after.length, structures ? 0 : raw === 'char' ? 3 : raw === 'word' ? 2 : 1,
249
+ structures?.[0]?.parts, structures?.[1]?.parts);
250
+
251
+ try { if (!raw && budget.left >= 0) {
252
+ const check = n => { if (!spend(n)) throw null; };
253
+ // Split transported text at wording boundaries; positions and words own distinct edits.
254
+ const deleted = new Map();
255
+ const moveKey = (item, side, text, parts = []) => {
256
+ check(parts.length);
257
+ return parts.some(p => p.start === item[side][0] && p.end <= item[side][1] && p.name === item[3][2] && !/\S/.test(text.slice(p.end, item[side][1])))
258
+ ? item[3][1] + '\0' + item[3][2] : text.slice(...item[side]).trim();
259
+ };
260
+ for (const item of result) if (item[3][0] === 0 && !item[2]) {
261
+ const value = before.slice(...item[0]), key = moveKey(item, 0, before, pa);
262
+ if (item[3][2] || value.trim().length >= 4 && /[\n ]/.test(value)) deleted.set(key, deleted.has(key) ? null : item);
263
+ }
264
+ for (const item of result) if (item[3][0] === 2 && !item[2]) {
265
+ const value = after.slice(...item[1]), key = moveKey(item, 1, after, pb);
266
+ let old = deleted.get(key), score = 0;
267
+ if (!old && !structures && key.length >= 24) for (const [prior, candidate] of deleted) {
268
+ if (!candidate || candidate[4]) continue;
269
+ const a = tokens(prior, 0, prior.length, 2), b = tokens(key, 0, key.length, 2);
270
+ if (!a || !b) throw null;
271
+ const n = matches(a, b).reduce((n, [i]) => n + a[i][1] - a[i][0], 0);
272
+ check(0);
273
+ if (n * 5 > Math.max(prior.length, key.length) * 3 && n >= score) { old = n === score ? null : candidate; score = n; }
274
+ }
275
+ 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;
281
+ }
282
+ const outer = result; result = [];
283
+ walk(...x, ...y, 1);
284
+ const inner = result; result = outer;
285
+ // Equal boundary fragments make the exact extent recoverable without display context.
286
+ if (inner.length && (inner[0][0][0] === x[0] || inner[0][1][0] === y[0] ||
287
+ inner.at(-1)[0][1] === x[1] || inner.at(-1)[1][1] === y[1] || inner.some(c => c[2]))) continue;
288
+ const move = [3, item[3][2] ? item[3][1] : code ? 'line' : 'block', item[3][2], [x, y]];
289
+ old[4] = []; item[4] = [];
290
+ const fragment = (a0, a1, b0, b1, group) => {
291
+ if (a0 < a1) old[4].push([[a0, a1], [old[1][0], old[1][0]], false, group]);
292
+ if (b0 < b1) item[4].push([[item[0][0], item[0][0]], [b0, b1], false, group]);
293
+ };
294
+ let a = old[0][0], b = item[1][0], wording;
295
+ for (const edit of inner) {
296
+ if (edit[3][3]) wording = edit[3];
297
+ else if (a !== edit[0][0] || b !== edit[1][0]) wording = [edit[3][0], edit[3][1]];
298
+ wording[2] = item[3][2]; wording[3] = move[3];
299
+ if (wording[0] !== edit[3][0]) wording[0] = 1;
300
+ fragment(a, edit[0][0], b, edit[1][0], move);
301
+ fragment(...edit[0], ...edit[1], wording);
302
+ a = edit[0][1]; b = edit[1][1];
303
+ }
304
+ fragment(a, x[1], b, y[1], move);
305
+ fragment(x[1], old[0][1], 0, 0, [0, 'char']);
306
+ fragment(0, 0, y[1], item[1][1], [2, 'char']);
307
+ }
308
+ result = result.flatMap(item => item[4] || [item]);
309
+
310
+ if (structures) {
311
+ check(la.length * Math.ceil(Math.log2(la.length + 1)) + lb.length + pa.length + pb.length);
312
+ const mapped = {}; let cursor = 0, delta = 0;
313
+ for (const link of [...la].sort((a, b) => a.at[0] - b.at[0])) {
314
+ while (cursor < result.length && result[cursor][0][1] <= link.at[0]) { delta = result[cursor][1][1] - result[cursor][0][1]; cursor++; }
315
+ if (cursor === result.length || result[cursor][0][0] >= link.at[1]) mapped[[link.at[0] + delta, link.at[1] + delta]] = link.to;
316
+ }
317
+ lb = lb.map(l => l.key ? l : {...l, to: mapped[l.at] || l.to});
318
+ for (const [p, old] of aliases) {
319
+ if (before.slice(...old.nameAt) === after.slice(...p.nameAt)) continue;
320
+ check(la.length + lb.length);
321
+ const left = la.filter(l => l.key === old.key), right = lb.filter(l => l.key === p.key);
322
+ if (left.length !== right.length) continue;
323
+ const x = [old.nameAt], y = [p.nameAt], owned = [];
324
+ for (let i = 0; i < left.length; i++) {
325
+ x.push(left[i].at); y.push(right[i].at);
326
+ if (left[i].labelAt && right[i].labelAt && before.slice(...left[i].labelAt) === before.slice(...old.nameAt) &&
327
+ after.slice(...right[i].labelAt) === after.slice(...p.nameAt)) { x.push(left[i].labelAt); y.push(right[i].labelAt); }
328
+ }
329
+ x.sort((a, b) => a[0] - b[0]); y.sort((a, b) => a[0] - b[0]);
330
+ let complete = true;
331
+ for (let i = 0; i < x.length; i++) {
332
+ check(result.length * 2);
333
+ if (result.some(c => c[3]?.[0] !== 3 && (overlaps(c[0], x[i]) || overlaps(c[1], y[i])) && !inside(c, x[i], y[i]))) { complete = false; break; }
334
+ const found = result.filter(c => c[3][0] < 3 && inside(c, x[i], y[i]));
335
+ if (!found.length) { complete = false; break; }
336
+ owned.push(...found);
337
+ }
338
+ if (!complete) continue;
339
+ check(result.length + owned.length * 2);
340
+ const owners = new Set(owned.map(c => c[3]).filter(Boolean)), members = new Set(owned);
341
+ if (result.some(c => owners.has(c[3]) && !members.has(c))) continue;
342
+ const group = [4, p.kind, old.name, [[...old.nameAt], [...p.nameAt]]];
343
+ for (const item of owned) item[3] = group;
344
+ }
345
+ }
346
+ } } catch (error) {
347
+ if (error !== null) throw error;
348
+ result = result.flatMap(item => item[4] || [item]);
349
+ }
350
+
351
+ const changes = [], edits = [];
352
+ let normalized;
353
+ const meaning = structures?.[0]?.meaning;
354
+ const format = typeof meaning === 'string' && meaning === structures[1].meaning;
355
+ for (let i = 0; i < result.length; i++) {
356
+ const item = result[i];
357
+ if (raw === 'char' && !evidence) {
358
+ const a = item[0], b = item[1], next = result[i + 1];
359
+ if (item[3][0] !== 1) {
360
+ const room = Math.min((next?.[0][0] ?? before.length) - a[1], (next?.[1][0] ?? after.length) - b[1]);
361
+ let n = 0;
362
+ while (n < room && before[a[0] + n] === after[b[0] + n] && spend()) n++;
363
+ while (n && (!safe(before, a[0] + n) || !safe(before, a[1] + n) || !safe(after, b[0] + n) || !safe(after, b[1] + n))) n--;
364
+ a[0] += n; a[1] += n; b[0] += n; b[1] += n;
365
+ }
366
+ const p = normalized;
367
+ if (p && p[0][0] === p[0][1] && item[3][0] === 2 && p[0][0] === a[0] && p[1][1] === b[0]) {
368
+ p[1][1] = b[1]; if (item[2]) edits.at(-1).coarse = true; continue;
369
+ }
370
+ normalized = item;
371
+ }
372
+ const group = item[3];
373
+ if (!raw && !group[3]) group[3] = [[...item[0]], [...item[1]]];
374
+ if (!group[4]) {
375
+ const context = group[3];
376
+ 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]++;
379
+ }
380
+ group[4] = []; changes.push(group);
381
+ if (format) group[0] = 5;
382
+ }
383
+ group[4].push(edits.length);
384
+ edits.push({before: item[0], after: item[1], ...(item[2] ? {coarse: true} : {})});
385
+ }
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
+ if (structures && !raw && budget.left >= 0) {
401
+ 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
+ for (const change of changes) {
403
+ if (change[0] === 5) continue;
404
+ if (!spend(pa.length + pb.length + lb.length)) break;
405
+ const range = change[3];
406
+ const older = pa.filter(p => p.start <= range[0][0] && p.end >= range[0][1]), newer = pb.filter(p => p.start <= range[1][0] && p.end >= range[1][1]);
407
+ const part = (change[0] === 0 ? older : newer).at(-1) || older.at(-1);
408
+ if (!change[2] && part?.name) { change[2] = part.name; change[1] = part.kind; }
409
+ const names = new Set([change[2], ...older.map(p => p.name), ...newer.map(p => p.name)]), breaks = [], affects = [];
410
+ for (const {at, to} of lb) {
411
+ if (!names.has(to)) continue;
412
+ if (!newNames.has(to)) { if (oldNames.has(to)) breaks.push({at, to}); continue; }
413
+ if (!spend(edits.length + pb.length) || edits.some(c => overlaps(at, c.after))) continue;
414
+ const p = pb.filter(p => p.start <= at[0] && p.end >= at[1] && /^(paragraph|call|item|task|cell|declaration)$/.test(p.kind)).at(-1);
415
+ affects.push(p && !edits.some(c => p.start < c.after[1] && p.end > c.after[0]) ? [p.start, p.end] : at);
416
+ }
417
+ if (breaks.length) change[6] = breaks;
418
+ if (affects.length) change[5] = affects;
419
+ }
420
+ }
421
+ if (options._stats) Object.assign(options._stats, {work: Math.max(0, initial - Math.max(0, budget.left)), coarse: edits.some(c => c.coarse)});
422
+ // Concern tuples: kind, unit, name, context, edits, affects, breaks.
423
+ return {edits, changes: changes.map(([kind, unit, name, context, owned, affects, breaks], i) => ({
424
+ id: i + 1, kind: ['delete', 'replace', 'insert', 'move', 'rename', 'format'][kind], unit, edits: owned,
425
+ ...(name ? {name} : {}), ...(context ? {context: {before: context[0], after: context[1]}} : {}),
426
+ ...(affects ? {affects} : {}), ...(breaks ? {breaks} : {})
427
+ }))};
428
+ }
429
+
430
+ export function merge(base, mine, theirs, options = {}) {
431
+ if (mine === theirs || base === theirs || base === mine) return {text: base === mine ? theirs : mine, conflicts: []};
432
+ const budget = {...options, _unit: 'char', _structure: true,
433
+ _budget: options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7}};
434
+ const sources = [base, mine, theirs];
435
+ 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
+ }) : [];
460
+ 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)]}));
464
+ if (trees.some(Boolean) || repeatedBase) {
465
+ 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);
496
+ }
497
+ }
498
+ return map;
499
+ });
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;
504
+ }
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;
525
+ }
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;
554
+ }
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.
561
+ for (const side of [0, 1]) {
562
+ const destinations = new Map();
563
+ spend(added[side].size + removed[side].size);
564
+ for (const [kind, parent] of added[side].values())
565
+ destinations.set(kind, destinations.has(kind) && destinations.get(kind) !== parent ? null : parent);
566
+ for (const [kind, parent] of removed[side].values())
567
+ if (destinations.has(kind) && destinations.get(kind) !== parent) throw null;
568
+ }
569
+ // A shared disappearance with different additions may be competing destinations.
570
+ if ([...removed[0].keys()].some(name => removed[1].has(name)) &&
571
+ (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;
575
+ }
576
+ } catch (error) { if (error !== null) throw error; return complete(); }
577
+ }
578
+ const changes = options._edits || [mine, theirs].map(text => diff(base, text, budget).edits);
579
+ if (trees.some(Boolean) && budget._budget.left < 0) return complete();
580
+ const edits = [mine, theirs].flatMap((text, side) => changes[side].map(change =>
581
+ [...change.before, text.slice(...change.after), side]));
582
+ edits.sort((a, b) => a[0] - b[0] || a[3] - b[3] || a[1] - b[1]);
583
+ const conflicts = [];
584
+ let at = 0, text = '';
585
+ for (let index = 0; index < edits.length;) {
586
+ const start = edits[index][0], sides = [['', start], ['', start]];
587
+ let end = start, changed = 0;
588
+ for (;;) {
589
+ const edit = edits[index++], side = edit[3], row = sides[side];
590
+ if (edit[1] >= end) {
591
+ if (edit[1] > end) sides[1 - side][2] = null;
592
+ end = edit[1]; row[2] = edit;
593
+ }
594
+ row[0] += base.slice(row[1], edit[0]) + edit[2]; changed |= 1 << side;
595
+ row[1] = edit[1];
596
+ if (index === edits.length) break;
597
+ const [a, b, t, side2] = edits[index], peer = sides[1 - side2][2];
598
+ if (!(a < end || a === start || peer &&
599
+ (a === end && peer[0] === end || a === b && peer[0] < end &&
600
+ a - end < t.length && (peer[2] + base.slice(end, a)).endsWith(t)))) break;
601
+ }
602
+ for (const row of sides) row[0] += base.slice(row[1], end);
603
+ text += base.slice(at, start);
604
+ const from = text.length;
605
+ text += sides[changed & 1 ? 0 : 1][0];
606
+ if (changed === 3 && sides[0][0] !== sides[1][0]) conflicts.push({at: [from, text.length], theirs: sides[1][0]});
607
+ at = end;
608
+ }
609
+ return {text: text + base.slice(at), conflicts};
610
+ }