rapier-markdown-kit 1.1.102 → 1.1.104

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 (60) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +213 -53
  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/kit/assets.mjs +5 -1
  27. package/dist/kit/conformance/run.mjs +4 -4
  28. package/dist/kit/export-fonts.mjs +26 -11
  29. package/dist/kit/index.mjs +1 -0
  30. package/dist/kit/ledger/carried.mjs +9 -7
  31. package/dist/kit/ledger/data.mjs +73 -13
  32. package/dist/kit/ledger/format.mjs +166 -53
  33. package/dist/kit/ledger/hash.mjs +75 -18
  34. package/dist/kit/ledger/history.mjs +143 -115
  35. package/dist/kit/ledger/inverse-source.mjs +158 -19
  36. package/dist/kit/ledger/journal-records.mjs +292 -67
  37. package/dist/kit/model.mjs +1 -1
  38. package/dist/kit/pdf-glyphs.mjs +4 -4
  39. package/dist/kit/render-markdown.mjs +259 -122
  40. package/dist/kit/render-sanitize.mjs +24 -4
  41. package/dist/kit/render-work.mjs +99 -20
  42. package/dist/kit/render.mjs +101 -65
  43. package/dist/kit/typography.mjs +2 -0
  44. package/dist/layout/hyphenation.mjs +127 -0
  45. package/dist/layout/line-plan.mjs +88 -96
  46. package/dist/layout/model.mjs +61 -80
  47. package/dist/layout/paragraph-fit.mjs +191 -0
  48. package/dist/layout/preparation.mjs +373 -0
  49. package/dist/layout/prepared-paragraph.mjs +223 -0
  50. package/dist/layout/typography.mjs +345 -111
  51. package/dist/packages/rapier-diff/index.mjs +288 -196
  52. package/dist/spec/document-fonts.mjs +13 -4
  53. package/dist/spec/md-assets.mjs +149 -30
  54. package/dist/spec/md-layout.mjs +5 -2
  55. package/dist/spec/md-marks.mjs +218 -3
  56. package/dist/spec/md-source.mjs +122 -0
  57. package/package.json +2 -1
  58. package/style.css +9 -0
  59. package/dist/agent/vendor/pretext/bidi.js +0 -151
  60. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,4 +1,67 @@
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};
@@ -11,49 +74,46 @@ export function diff(before, after, options = {}) {
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,35 @@ 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)) { budget.left = -1; continue; }
78
150
  }
79
- const unique = a1 - a0 + b1 - b0 > 32 && Object.values(positions).some(list => list[0] === 1 && list.length === 2);
80
151
  search: for (let i = a0; i < a1; i++) {
81
- const list = positions[a[i][2]];
152
+ const list = positions[a[i]];
82
153
  if (!list || unique && (list[0] !== 1 || list.length !== 2)) continue;
83
154
  for (let p = list.length - 1; p > 0; p--) {
84
155
  if (!spend() || ++nodes > 131072) { budget.left = -1; break search; }
@@ -98,48 +169,63 @@ export function diff(before, after, options = {}) {
98
169
  chain.reverse();
99
170
  let x = a0, y = b0;
100
171
  for (const [i, j] of chain) {
101
- found.push([i, j]);
102
- if (unique) pending.push([x, i, y, j]);
172
+ found[i] = j;
173
+ if (unique && x < i && y < j) pending.push([x, i, y, j]);
103
174
  x = i + 1; y = j + 1;
104
175
  }
105
- if (unique) pending.push([x, a1, y, b1]);
176
+ if (unique && x < a1 && y < b1) pending.push([x, a1, y, b1]);
177
+ }
178
+ return found;
179
+ }
180
+
181
+ // A search step scans at most one fixed-size source chunk.
182
+ function common(a, b, end, step) {
183
+ let n = 0;
184
+ while (n < end && spend()) {
185
+ const stop = Math.min(end, n + 256);
186
+ while (n < stop && before.charCodeAt(a) === after.charCodeAt(b)) { n++; a += step; b += step; }
187
+ if (n < stop) break;
106
188
  }
107
- return found.sort((x, y) => x[0] - y[0]);
189
+ return n;
108
190
  }
109
191
 
110
192
  function walk(a0, a1, b0, b1, level, ap, bp, context) {
111
- if (before.slice(a0, a1) === after.slice(b0, b1)) return;
193
+ if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
112
194
  if (!spend()) { record(a0, a1, b0, b1, 'block', context, true); return; }
113
195
  const structural = level <= 0 && level > -128 && (ap?.length || bp?.length);
114
196
  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--;
197
+ let n = common(a0, b0, Math.min(a1 - a0, b1 - b0), 1);
198
+ while (n && (!safe(before, a0 + n) || !safe(after, b0 + n))) { if (!spend()) { n = 0; break; } n--; }
199
+ if (level === 1 && n) {
200
+ const next = Math.max(0, before.lastIndexOf('\n', a0 + n - 1) + 1 - a0);
201
+ spend(Math.ceil((n - next) / 256)); n = next;
202
+ }
203
+ a0 += n; b0 += n;
204
+ n = common(a1 - 1, b1 - 1, Math.min(a1 - a0, b1 - b0), -1);
205
+ while (n && (!safe(before, a1 - n) || !safe(after, b1 - n))) { if (!spend()) { n = 0; break; } n--; }
206
+ 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
207
  a1 -= n; b1 -= n;
208
+ if (budget.left < 0) { record(a0, a1, b0, b1, 'block', context, true); return; }
124
209
  }
125
210
  let unit = level >= 3 ? 'char' : level === 2 ? 'word' : 'line';
126
211
  if (!raw && level === 2 && a1 - a0 + b1 - b0 <= 2 && letter(before[a0 - 1] || after[b0 - 1]) && letter(before[a1] || after[b1])) unit = 'char';
127
212
  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;
213
+ const left = tokens(before, a0, a1, structural ? 0 : Math.max(1, level), ap);
214
+ const right = tokens(after, b0, b1, structural ? 0 : Math.max(1, level), bp);
215
+ if (!left || !right) { record(a0, a1, b0, b1, unit, context, true); return; }
216
+ const [a, x, ownersA] = left, [b, y, ownersB] = right, pairs = matches(a, b);
133
217
  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);
218
+ for (let i = 0; i <= a.length; i++) {
219
+ const j = i === a.length ? b.length : pairs[i];
220
+ if (j === undefined) continue;
221
+ const x0 = x[ai], x1 = x[i], y0 = y[bi], y1 = y[j];
136
222
  if (ai < i || bi < j) {
137
223
  if (budget.left < 0) record(x0, x1, y0, y1, unit, context, true);
138
224
  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);
225
+ for (let k = ai; k < i; k++) record(x[k], x[k + 1], y0, y0, unit, ownersA[k] || context);
226
+ for (let k = bi; k < j; k++) record(x1, x1, y[k], y[k + 1], unit, ownersB[k] || context);
141
227
  } else if (structural) {
142
- const p = a[ai]?.[3], q = b[bi]?.[3];
228
+ const p = ownersA[ai], q = ownersB[bi];
143
229
  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
230
  else walk(x0, x1, y0, y1, 1, null, null, context);
145
231
  } else if (level < 2 && raw !== 'line') walk(x0, x1, y0, y1, raw === 'char' ? 3 : 2, null, null, context);
@@ -157,9 +243,9 @@ export function diff(before, after, options = {}) {
157
243
  } else record(x0, x1, y0, y1, unit, context);
158
244
  }
159
245
  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);
246
+ const p = ownersA[i], q = ownersB[j];
247
+ if ((p || q || level <= 1 && !raw) && before.slice(x[i], x[i + 1]) !== after.slice(y[j], y[j + 1]))
248
+ walk(x[i], x[i + 1], y[j], y[j + 1], p || q ? level - 1 : 3, p?.children, q?.children, q || p || context);
163
249
  }
164
250
  ai = i + 1; bi = j + 1;
165
251
  }
@@ -192,7 +278,12 @@ export function diff(before, after, options = {}) {
192
278
  if (!row || !Number.isSafeInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
193
279
  row.pos < 0 || row.pos + row.removed.length > length || !verify(pieces.length * 2 + 1)) return null;
194
280
  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;
281
+ let at = 0;
282
+ for (let i = left; i < right; i++) {
283
+ const [start, end, text = before] = pieces[i];
284
+ if (!row.removed.startsWith(text.slice(start, end), at)) return null;
285
+ at += end - start;
286
+ }
196
287
  if (row.removed === row.inserted) continue;
197
288
  pieces.splice(left, right - left, [0, row.inserted.length, row.inserted]);
198
289
  length += row.inserted.length - row.removed.length;
@@ -202,7 +293,7 @@ export function diff(before, after, options = {}) {
202
293
  let a = 0, b = 0, at = 0;
203
294
  for (const [start, end, text] of pieces) {
204
295
  const size = end - start;
205
- if (!verify(1) || (text ?? before).slice(start, end) !== after.slice(at, at + size)) return null;
296
+ if (!verify(1) || !after.startsWith((text ?? before).slice(start, end), at)) return null;
206
297
  if (text === undefined) {
207
298
  let x = start, y = at, tail = end;
208
299
  while (x < tail && (!safe(before, x) || !safe(after, y))) { x++; y++; if (!verify(1)) return null; }
@@ -220,22 +311,11 @@ export function diff(before, after, options = {}) {
220
311
 
221
312
  if (options.structure && (!raw || options._structure)) {
222
313
  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 || []);
314
+ structures = options._observed || [observe(before, options.structure, spend, safe), observe(after, options.structure, spend, safe)];
315
+ if (structures.some(value => !value || value.invalid)) throw null;
316
+ [pa, pb] = structures.map(value => value.flat); [la, lb] = structures.map(value => value.links);
237
317
  aliases = new Map();
238
- const oldByKey = new Map(pa.filter(p => p.key).map(p => [p.key, p]));
318
+ const oldByKey = new Map(pa.filter(p => p.key != null).map(p => [p.key, p]));
239
319
  const oldNames = new Set(pa.map(p => p.name)), newNames = new Set(pb.map(p => p.name));
240
320
  for (const p of pb) {
241
321
  const old = oldByKey.get(p.key);
@@ -268,7 +348,7 @@ export function diff(before, after, options = {}) {
268
348
  if (!candidate || candidate[4]) continue;
269
349
  const a = tokens(prior, 0, prior.length, 2), b = tokens(key, 0, key.length, 2);
270
350
  if (!a || !b) throw null;
271
- const n = matches(a, b).reduce((n, [i]) => n + a[i][1] - a[i][0], 0);
351
+ const n = matches(a[0], b[0]).reduce((n, j, i) => n + a[1][i + 1] - a[1][i], 0);
272
352
  check(0);
273
353
  if (n * 5 > Math.max(prior.length, key.length) * 3 && n >= score) { old = n === score ? null : candidate; score = n; }
274
354
  }
@@ -314,7 +394,7 @@ export function diff(before, after, options = {}) {
314
394
  while (cursor < result.length && result[cursor][0][1] <= link.at[0]) { delta = result[cursor][1][1] - result[cursor][0][1]; cursor++; }
315
395
  if (cursor === result.length || result[cursor][0][0] >= link.at[1]) mapped[[link.at[0] + delta, link.at[1] + delta]] = link.to;
316
396
  }
317
- lb = lb.map(l => l.key ? l : {...l, to: mapped[l.at] || l.to});
397
+ lb = lb.map(l => l.key != null ? l : {...l, to: mapped[l.at] || l.to});
318
398
  for (const [p, old] of aliases) {
319
399
  if (before.slice(...old.nameAt) === after.slice(...p.nameAt)) continue;
320
400
  check(la.length + lb.length);
@@ -374,8 +454,8 @@ export function diff(before, after, options = {}) {
374
454
  if (!group[4]) {
375
455
  const context = group[3];
376
456
  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]++;
457
+ while (span[0] && spend() && letter(text[span[0] - 1]) && safe(text, span[0] - 1)) span[0]--;
458
+ while (span[1] < text.length && spend() && letter(text[span[1]]) && safe(text, span[1] + 1)) span[1]++;
379
459
  }
380
460
  group[4] = []; changes.push(group);
381
461
  if (format) group[0] = 5;
@@ -433,149 +513,161 @@ export function merge(base, mine, theirs, options = {}) {
433
513
  _budget: options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7}};
434
514
  const sources = [base, mine, theirs];
435
515
  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
- }) : [];
516
+ const spend = (n = 1) => (budget._budget.left -= n) >= 0;
517
+ const check = n => { if (!spend(n)) throw null; };
518
+ const safe = options.structure && boundaries();
519
+ const trees = options.structure && !options._edits
520
+ ? [base, mine, base, theirs].map(text => observe(text, options.structure, spend, safe, false, true)) : [];
460
521
  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)]}));
522
+ const equal = (a, b) => a.length === b.length && a.every((item, i) => item === b[i]);
464
523
  if (trees.some(Boolean) || repeatedBase) {
465
524
  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);
525
+ if (!trees.every(tree => tree && !tree.invalid) || !repeatedBase || repeatedBase.invalid) return complete();
526
+ const a = trees[0].flat, b = repeatedBase.flat;
527
+ check(a.length + b.length);
528
+ if (a.length !== b.length) return complete();
529
+ for (let i = 0; i < a.length; i++) {
530
+ const p = a[i], q = b[i];
531
+ if (p.start !== q.start || p.end !== q.end || p.kind !== q.kind || p.name !== q.name || p.key !== q.key ||
532
+ p.children.length !== q.children.length || !equal(p.nameAt || [], q.nameAt || [])) return complete();
481
533
  }
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);
534
+ for (const tree of trees) for (const part of tree.flat)
535
+ part.id = part.name && part.kind ? part.kind.length + ':' + part.kind + part.name : null;
536
+ const removed = [new Map(), new Map()], added = [new Map(), new Map()];
537
+ let container = 0;
538
+ const content = (part, side) => sources[side].slice(part.start, part.end);
539
+ function align(nodes) {
540
+ const original = content(nodes[0], 0);
541
+ if (original === content(nodes[1], 1) && original === content(nodes[2], 2)) {
542
+ nodes[0].common = original; return;
543
+ }
544
+ const hidden = nodes.flatMap(node => node.children.filter(part => !part.id));
545
+ while (hidden.length) {
546
+ const part = hidden.pop(); check(1);
547
+ if (part.id) throw null;
548
+ for (const child of part.children) hidden.push(child);
549
+ }
550
+ const rows = nodes.map(node => node.children.filter(part => part.id));
551
+ const width = rows.reduce((n, list) => n + list.length, 0);
552
+ check(1 + width);
553
+ let repeated = false;
554
+ const index = nameOf => {
555
+ const duplicates = new Set();
556
+ const maps = rows.map((list, side) => {
557
+ const map = new Map();
558
+ for (const part of list) {
559
+ const name = nameOf(part, side);
560
+ if (!name) continue;
561
+ const old = map.get(name);
562
+ if (!old) map.set(name, part);
563
+ else {
564
+ duplicates.add(name);
565
+ if (Array.isArray(old)) old.push(part); else map.set(name, [old, part]);
566
+ }
496
567
  }
568
+ return map;
569
+ });
570
+ for (const name of duplicates) {
571
+ repeated = true;
572
+ const values = maps.map((map, side) => {
573
+ const part = map.get(name), list = Array.isArray(part) ? part : part ? [part] : [];
574
+ check(list.length); map.set(name, null);
575
+ return list.map(part => content(part, side));
576
+ });
577
+ if (!equal(values[0], values[1]) || !equal(values[0], values[2])) throw null;
497
578
  }
498
- return map;
579
+ return maps;
580
+ };
581
+ const maps = index(part => part.id), orders = rows.map(list => list.map(part => part.id));
582
+ const reordered = [1, 2].map(side => !equal(orders[0].filter(name => maps[side].has(name)),
583
+ orders[side].filter(name => maps[0].has(name))));
584
+ index((part, side) => {
585
+ check(1);
586
+ if (part.nameAt) return part.kind.length + ':' + part.kind + sources[side].slice(...part.nameAt);
499
587
  });
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;
588
+ const parent = container++;
589
+ for (const side of [1, 2]) {
590
+ const peer = 3 - side;
591
+ for (const [name, part] of maps[0]) if (!maps[side].has(name)) {
592
+ removed[side - 1].set(parent + ':' + name, [part.kind, parent]);
593
+ const other = maps[peer].get(name);
594
+ if (part?.nameAt && other?.nameAt && content(part, 0) !== content(other, peer)) throw null;
595
+ }
596
+ for (const [name, part] of maps[side]) if (!maps[0].has(name))
597
+ added[side - 1].set(parent + ':' + name, [part.kind, parent]);
504
598
  }
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;
599
+ let frame = -1;
600
+ if (reordered.some(Boolean)) {
601
+ if (repeated || !maps.every(map => map.size === maps[0].size && [...maps[0].keys()].every(name => map.has(name)))) throw null;
602
+ if (reordered.every(Boolean) && !equal(orders[1], orders[2])) throw null;
603
+ frame = reordered[0] ? 1 : 2;
604
+ const gaps = rows.map((list, side) => {
605
+ let at = nodes[side].start;
606
+ return [...list.map(part => { const gap = sources[side].slice(at, part.start); at = part.end; return gap; }),
607
+ sources[side].slice(at, nodes[side].end)];
608
+ });
609
+ if (!equal(gaps[3 - frame], gaps[reordered.every(Boolean) ? frame : 0])) throw null;
525
610
  }
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;
611
+ const children = new Map();
612
+ for (const [name, part] of maps[0]) if (part && maps[1].get(name) && maps[2].get(name)) {
613
+ const child = align([part, maps[1].get(name), maps[2].get(name)]);
614
+ if (child) children.set(name, child);
554
615
  }
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.
616
+ if (frame < 0 && !children.size) return;
617
+ // A plan is a tree of range triples; a string is an already-admitted gap.
618
+ const plan = [];
619
+ if (frame >= 0) {
620
+ let at = nodes[frame].start;
621
+ for (const part of rows[frame]) {
622
+ 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))}));
623
+ at = part.end;
624
+ }
625
+ plan.push(sources[frame].slice(at, nodes[frame].end));
626
+ } else {
627
+ let at = nodes.map(node => node.start);
628
+ for (const child of children.values()) {
629
+ const ranges = child.nodes;
630
+ plan.push({ranges: ranges.map((node, side) => ({start: at[side], end: node.start}))}, child);
631
+ at = ranges.map(node => node.end);
632
+ }
633
+ plan.push({ranges: nodes.map((node, side) => ({start: at[side], end: node.end}))});
634
+ }
635
+ return {nodes, plan};
636
+ }
637
+ const plan = align(trees.map((tree, side) => ({start: 0, end: sources[side].length, children: tree.parts})));
561
638
  for (const side of [0, 1]) {
562
639
  const destinations = new Map();
563
- spend(added[side].size + removed[side].size);
640
+ check(added[side].size + removed[side].size);
564
641
  for (const [kind, parent] of added[side].values())
565
642
  destinations.set(kind, destinations.has(kind) && destinations.get(kind) !== parent ? null : parent);
566
643
  for (const [kind, parent] of removed[side].values())
567
644
  if (destinations.has(kind) && destinations.get(kind) !== parent) throw null;
568
645
  }
569
- // A shared disappearance with different additions may be competing destinations.
570
646
  if ([...removed[0].keys()].some(name => removed[1].has(name)) &&
571
647
  (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;
648
+ if (plan) {
649
+ // Project original ranges once. Nested moves never rebuild aligned documents.
650
+ const chunks = [], conflicts = [];
651
+ let length = 0;
652
+ function project(item) {
653
+ if (item.plan) { for (const child of item.plan) project(child); return; }
654
+ check(1);
655
+ let text = item;
656
+ if (typeof item !== 'string') {
657
+ const result = merge(...item.ranges.map(content), {...budget, structure: undefined, from: undefined});
658
+ check(0); text = result.text;
659
+ for (const conflict of result.conflicts)
660
+ conflicts.push({at: conflict.at.map(at => at + length), theirs: conflict.theirs});
661
+ }
662
+ chunks.push(text); length += text.length;
663
+ }
664
+ project(plan);
665
+ return {text: chunks.join(''), conflicts};
575
666
  }
576
667
  } catch (error) { if (error !== null) throw error; return complete(); }
577
668
  }
578
- const changes = options._edits || [mine, theirs].map(text => diff(base, text, budget).edits);
669
+ const changes = options._edits || [mine, theirs].map((text, side) => diff(base, text,
670
+ {...budget, structure: trees[0] && options.structure, _observed: trees[0] ? [trees[0], trees[side + 1]] : undefined}).edits);
579
671
  if (trees.some(Boolean) && budget._budget.left < 0) return complete();
580
672
  const edits = [mine, theirs].flatMap((text, side) => changes[side].map(change =>
581
673
  [...change.before, text.slice(...change.after), side]));