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