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.
- package/LICENSE +2 -2
- package/README.md +213 -53
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/kit/assets.mjs +5 -1
- package/dist/kit/conformance/run.mjs +4 -4
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +9 -7
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +166 -53
- package/dist/kit/ledger/hash.mjs +75 -18
- package/dist/kit/ledger/history.mjs +143 -115
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/render-markdown.mjs +259 -122
- package/dist/kit/render-sanitize.mjs +24 -4
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +101 -65
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +61 -80
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +288 -196
- package/dist/spec/document-fonts.mjs +13 -4
- package/dist/spec/md-assets.mjs +149 -30
- package/dist/spec/md-layout.mjs +5 -2
- package/dist/spec/md-marks.mjs +218 -3
- package/dist/spec/md-source.mjs +122 -0
- package/package.json +2 -1
- package/style.css +9 -0
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- 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
|
-
|
|
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
|
-
|
|
33
|
-
const
|
|
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 &&
|
|
36
|
-
else
|
|
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() ||
|
|
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() ||
|
|
112
|
+
if (!spend() || keys.length >= 131072) return null;
|
|
53
113
|
const a = match.index, b = Math.min(end, pattern.lastIndex);
|
|
54
|
-
if (
|
|
55
|
-
|
|
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 =
|
|
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]
|
|
69
|
-
while (a0 < a1 && b0 < b1 && a[a1 - 1]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
|
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
|
|
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 =
|
|
116
|
-
while (n
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
129
|
-
const
|
|
130
|
-
if (!
|
|
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 (
|
|
135
|
-
const
|
|
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(
|
|
140
|
-
for (let k = bi; k < j; k++) record(x1, x1,
|
|
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 =
|
|
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 =
|
|
161
|
-
if (before.slice(
|
|
162
|
-
walk(
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
224
|
-
|
|
225
|
-
|
|
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,
|
|
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
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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 (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
let
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
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 (
|
|
573
|
-
|
|
574
|
-
|
|
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,
|
|
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]));
|