rapier-markdown-kit 1.1.104 → 1.2.5
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 +8 -4
- package/README.md +50 -7
- package/dist/images/gif.mjs +49 -0
- package/dist/images/playback.mjs +103 -0
- 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 +10 -8
- package/dist/kit/ledger/authored-origin.mjs +49 -0
- package/dist/kit/ledger/authorship.mjs +18 -9
- package/dist/kit/ledger/carried.mjs +10 -5
- package/dist/kit/ledger/data.mjs +7 -4
- package/dist/kit/ledger/format.mjs +37 -11
- package/dist/kit/ledger/hash.mjs +31 -15
- package/dist/kit/ledger/history.mjs +48 -10
- package/dist/kit/ledger/inverse-source.mjs +111 -49
- package/dist/kit/ledger/journal-records.mjs +24 -29
- package/dist/kit/ledger/merge.mjs +51 -15
- package/dist/kit/ledger/text-merge.mjs +75 -29
- package/dist/kit/ledger/transport.mjs +30 -45
- package/dist/kit/pdf-vectors.mjs +6 -2
- package/dist/kit/print-capitals.mjs +0 -1
- package/dist/kit/rapier-ledger.mjs +4 -2
- package/dist/kit/render-ink.mjs +1 -1
- package/dist/kit/render-markdown.mjs +155 -31
- package/dist/kit/render-print.mjs +173 -61
- package/dist/kit/render-sanitize.mjs +25 -15
- package/dist/kit/render-styles.mjs +3 -3
- package/dist/kit/render.mjs +37 -115
- package/dist/layout/model.mjs +10 -8
- package/dist/layout/paragraph-fit.mjs +1 -15
- package/dist/layout/typography.mjs +2 -1
- package/dist/packages/rapier-diff/concerns.mjs +163 -0
- package/dist/packages/rapier-diff/diff.mjs +68 -0
- package/dist/packages/rapier-diff/index.mjs +4 -701
- package/dist/packages/rapier-diff/matching.mjs +157 -0
- package/dist/packages/rapier-diff/merge.mjs +226 -0
- package/dist/packages/rapier-diff/moves.mjs +130 -0
- package/dist/packages/rapier-diff/structure.mjs +50 -0
- package/dist/packages/rapier-diff/tokens.mjs +72 -0
- package/dist/packages/rapier-diff/walk.mjs +219 -0
- package/dist/spec/document-fonts.mjs +1 -1
- package/dist/spec/document-settings.mjs +11 -164
- package/dist/spec/html-blocks.mjs +618 -79
- package/dist/spec/md-assets.mjs +29 -6
- package/dist/spec/md-layout.mjs +9 -1
- package/dist/spec/md-marks.mjs +25 -147
- package/dist/spec/md-source.mjs +69 -26
- package/package.json +1 -1
- package/style.css +7 -51
|
@@ -1,702 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
65
|
-
export function diff(before, after, options = {}) {
|
|
66
|
-
if (before === after) return {edits: [], changes: []};
|
|
67
|
-
const budget = options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7};
|
|
68
|
-
const initial = budget.left, raw = options._unit;
|
|
69
|
-
let result = [], code = options.as === 'code', structures, pa, pb, la, lb, aliases;
|
|
70
|
-
const spend = (n = 1) => (budget.left -= n) >= 0;
|
|
71
|
-
const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c);
|
|
72
|
-
const overlaps = (a, b) => a[0] < b[1] && a[1] > b[0];
|
|
73
|
-
const inside = (c, a, b) => [a, b].every((range, side) => c[side][0] === c[side][1] ||
|
|
74
|
-
c[side][0] >= range[0] && c[side][1] <= range[1]);
|
|
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));
|
|
76
|
-
|
|
77
|
-
const safe = boundaries();
|
|
78
|
-
|
|
79
|
-
function record(a0, a1, b0, b1, unit, part, coarse = false) {
|
|
80
|
-
if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
|
|
81
|
-
// Internal records: before, after, coarse, concern, transported fragments.
|
|
82
|
-
result.push([[a0, a1], [b0, b1], coarse, [a0 === a1 ? 2 : b0 === b1 ? 0 : 1, part?.kind || unit, part?.name]]);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function tokens(text, start, end, level, parts) {
|
|
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) => {
|
|
90
|
-
if (b <= a) return;
|
|
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
|
-
}
|
|
97
|
-
};
|
|
98
|
-
if (parts?.length && level === 0) {
|
|
99
|
-
let at = start;
|
|
100
|
-
for (const part of parts) {
|
|
101
|
-
if (!spend() || keys.length >= 131072) return null;
|
|
102
|
-
if (part.start < at || part.end <= part.start || part.end > end) continue;
|
|
103
|
-
if (at < part.start) add(at, part.start);
|
|
104
|
-
add(part.start, part.end, part); at = part.end;
|
|
105
|
-
}
|
|
106
|
-
add(at, end); return keys.length > 131072 ? null : out;
|
|
107
|
-
}
|
|
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;
|
|
109
|
-
pattern.lastIndex = start;
|
|
110
|
-
let match;
|
|
111
|
-
while (pattern.lastIndex < end && (match = pattern.exec(text))) {
|
|
112
|
-
if (!spend() || keys.length >= 131072) return null;
|
|
113
|
-
const a = match.index, b = Math.min(end, pattern.lastIndex);
|
|
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);
|
|
117
|
-
if (b === end) break;
|
|
118
|
-
}
|
|
119
|
-
return out;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Unique anchors and fallback equal pairs share one bounded subsequence search.
|
|
123
|
-
function matches(a, b) {
|
|
124
|
-
const found = new Array(a.length), pending = [[0, a.length, 0, b.length]];
|
|
125
|
-
let nodes = 0;
|
|
126
|
-
while (pending.length) {
|
|
127
|
-
let [a0, a1, b0, b1] = pending.pop();
|
|
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;
|
|
130
|
-
if (a0 === a1 || b0 === b1 || !spend(a1 - a0 + b1 - b0)) continue;
|
|
131
|
-
const positions = Object.create(null), tails = [];
|
|
132
|
-
for (let j = b0; j < b1; j++) {
|
|
133
|
-
const key = b[j];
|
|
134
|
-
(positions[key] ||= [0]).push(j);
|
|
135
|
-
}
|
|
136
|
-
for (let i = a0; i < a1; 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; }
|
|
150
|
-
}
|
|
151
|
-
search: for (let i = a0; i < a1; i++) {
|
|
152
|
-
const list = positions[a[i]];
|
|
153
|
-
if (!list || unique && (list[0] !== 1 || list.length !== 2)) continue;
|
|
154
|
-
for (let p = list.length - 1; p > 0; p--) {
|
|
155
|
-
if (!spend() || ++nodes > 131072) { budget.left = -1; break search; }
|
|
156
|
-
const j = list[p];
|
|
157
|
-
let lo = 0, hi = tails.length;
|
|
158
|
-
while (lo < hi) {
|
|
159
|
-
if (!spend()) break search;
|
|
160
|
-
const middle = lo + hi >>> 1;
|
|
161
|
-
if (tails[middle][1] < j) lo = middle + 1; else hi = middle;
|
|
162
|
-
}
|
|
163
|
-
tails[lo] = [i, j, tails[lo - 1]];
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (budget.left < 0) continue;
|
|
167
|
-
const chain = [];
|
|
168
|
-
for (let node = tails.at(-1); node; node = node[2]) chain.push(node);
|
|
169
|
-
chain.reverse();
|
|
170
|
-
let x = a0, y = b0;
|
|
171
|
-
for (const [i, j] of chain) {
|
|
172
|
-
found[i] = j;
|
|
173
|
-
if (unique && x < i && y < j) pending.push([x, i, y, j]);
|
|
174
|
-
x = i + 1; y = j + 1;
|
|
175
|
-
}
|
|
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;
|
|
188
|
-
}
|
|
189
|
-
return n;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function walk(a0, a1, b0, b1, level, ap, bp, context) {
|
|
193
|
-
if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
|
|
194
|
-
if (!spend()) { record(a0, a1, b0, b1, 'block', context, true); return; }
|
|
195
|
-
const structural = level <= 0 && level > -128 && (ap?.length || bp?.length);
|
|
196
|
-
if (!structural && raw !== 'word') {
|
|
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--; }
|
|
207
|
-
a1 -= n; b1 -= n;
|
|
208
|
-
if (budget.left < 0) { record(a0, a1, b0, b1, 'block', context, true); return; }
|
|
209
|
-
}
|
|
210
|
-
let unit = level >= 3 ? 'char' : level === 2 ? 'word' : 'line';
|
|
211
|
-
if (!raw && level === 2 && a1 - a0 + b1 - b0 <= 2 && letter(before[a0 - 1] || after[b0 - 1]) && letter(before[a1] || after[b1])) unit = 'char';
|
|
212
|
-
if (!structural && (a0 === a1 || b0 === b1)) { record(a0, a1, b0, b1, unit, context); return; }
|
|
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);
|
|
217
|
-
let ai = 0, bi = 0;
|
|
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];
|
|
222
|
-
if (ai < i || bi < j) {
|
|
223
|
-
if (budget.left < 0) record(x0, x1, y0, y1, unit, context, true);
|
|
224
|
-
else if (structural && (ai === i || bi === j)) {
|
|
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);
|
|
227
|
-
} else if (structural) {
|
|
228
|
-
const p = ownersA[ai], q = ownersB[bi];
|
|
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);
|
|
230
|
-
else walk(x0, x1, y0, y1, 1, null, null, context);
|
|
231
|
-
} else if (level < 2 && raw !== 'line') walk(x0, x1, y0, y1, raw === 'char' ? 3 : 2, null, null, context);
|
|
232
|
-
else if (level === 2 && !raw && i - ai <= 1 && j - bi <= 1 && x1 - x0 + y1 - y0 < 100) {
|
|
233
|
-
const start = result.length;
|
|
234
|
-
walk(x0, x1, y0, y1, 3, null, null, context);
|
|
235
|
-
const edits = result.slice(start);
|
|
236
|
-
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)) {
|
|
237
|
-
const reading = [[x0, x1], [y0, y1]], group = [edits[0][3][0], context?.kind || 'word', edits[0][3][2], reading];
|
|
238
|
-
for (const edit of edits) {
|
|
239
|
-
if (group[0] !== edit[3][0]) group[0] = 1;
|
|
240
|
-
edit[3] = group;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
} else record(x0, x1, y0, y1, unit, context);
|
|
244
|
-
}
|
|
245
|
-
if (i < a.length) {
|
|
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);
|
|
249
|
-
}
|
|
250
|
-
ai = i + 1; bi = j + 1;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Surviving source pieces keep the occurrence witnessed by the host.
|
|
255
|
-
function operations(rows) {
|
|
256
|
-
if (!Array.isArray(rows)) return null;
|
|
257
|
-
const pieces = [[0, before.length]];
|
|
258
|
-
let length = before.length, steps = 1e7;
|
|
259
|
-
const verify = n => {
|
|
260
|
-
if ((steps -= n) >= 0) return true;
|
|
261
|
-
budget.left = -1; return false;
|
|
262
|
-
};
|
|
263
|
-
const split = at => {
|
|
264
|
-
let position = 0;
|
|
265
|
-
for (let i = 0; i < pieces.length; i++) {
|
|
266
|
-
const [start, end, text] = pieces[i], size = end - start;
|
|
267
|
-
if (position === at) return i;
|
|
268
|
-
if (position + size > at) {
|
|
269
|
-
const n = at - position;
|
|
270
|
-
pieces.splice(i, 1, [start, start + n, text], [start + n, end, text]);
|
|
271
|
-
return i + 1;
|
|
272
|
-
}
|
|
273
|
-
position += size;
|
|
274
|
-
}
|
|
275
|
-
return pieces.length;
|
|
276
|
-
};
|
|
277
|
-
for (const row of rows) {
|
|
278
|
-
if (!row || !Number.isSafeInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
|
|
279
|
-
row.pos < 0 || row.pos + row.removed.length > length || !verify(pieces.length * 2 + 1)) return null;
|
|
280
|
-
const left = split(row.pos), right = split(row.pos + row.removed.length);
|
|
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
|
-
}
|
|
287
|
-
if (row.removed === row.inserted) continue;
|
|
288
|
-
pieces.splice(left, right - left, [0, row.inserted.length, row.inserted]);
|
|
289
|
-
length += row.inserted.length - row.removed.length;
|
|
290
|
-
}
|
|
291
|
-
if (length !== after.length) return null;
|
|
292
|
-
const spans = [];
|
|
293
|
-
let a = 0, b = 0, at = 0;
|
|
294
|
-
for (const [start, end, text] of pieces) {
|
|
295
|
-
const size = end - start;
|
|
296
|
-
if (!verify(1) || !after.startsWith((text ?? before).slice(start, end), at)) return null;
|
|
297
|
-
if (text === undefined) {
|
|
298
|
-
let x = start, y = at, tail = end;
|
|
299
|
-
while (x < tail && (!safe(before, x) || !safe(after, y))) { x++; y++; if (!verify(1)) return null; }
|
|
300
|
-
while (tail > x && (!safe(before, tail) || !safe(after, at + tail - start))) { tail--; if (!verify(1)) return null; }
|
|
301
|
-
if (tail > x) {
|
|
302
|
-
if (a !== x || b !== y) spans.push([a, x, b, y]);
|
|
303
|
-
a = tail; b = at + tail - start;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
at += size;
|
|
307
|
-
}
|
|
308
|
-
if (a !== before.length || b !== after.length) spans.push([a, before.length, b, after.length]);
|
|
309
|
-
return spans;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (options.structure && (!raw || options._structure)) {
|
|
313
|
-
try {
|
|
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);
|
|
317
|
-
aliases = new Map();
|
|
318
|
-
const oldByKey = new Map(pa.filter(p => p.key != null).map(p => [p.key, p]));
|
|
319
|
-
const oldNames = new Set(pa.map(p => p.name)), newNames = new Set(pb.map(p => p.name));
|
|
320
|
-
for (const p of pb) {
|
|
321
|
-
const old = oldByKey.get(p.key);
|
|
322
|
-
if (old?.nameAt && p.nameAt && old.kind === p.kind && !oldNames.has(p.name) && !newNames.has(old.name)) aliases.set(p, old);
|
|
323
|
-
}
|
|
324
|
-
} catch { structures = undefined; code = true; }
|
|
325
|
-
}
|
|
326
|
-
const evidence = options.from && operations(options.from);
|
|
327
|
-
if (evidence) for (const span of evidence) walk(...span, structures ? 0 : raw === 'char' ? 3 : 1, pa, pb);
|
|
328
|
-
else walk(0, before.length, 0, after.length, structures ? 0 : raw === 'char' ? 3 : raw === 'word' ? 2 : 1,
|
|
329
|
-
structures?.[0]?.parts, structures?.[1]?.parts);
|
|
330
|
-
|
|
331
|
-
try { if (!raw && budget.left >= 0) {
|
|
332
|
-
const check = n => { if (!spend(n)) throw null; };
|
|
333
|
-
// Split transported text at wording boundaries; positions and words own distinct edits.
|
|
334
|
-
const deleted = new Map();
|
|
335
|
-
const moveKey = (item, side, text, parts = []) => {
|
|
336
|
-
check(parts.length);
|
|
337
|
-
return parts.some(p => p.start === item[side][0] && p.end <= item[side][1] && p.name === item[3][2] && !/\S/.test(text.slice(p.end, item[side][1])))
|
|
338
|
-
? item[3][1] + '\0' + item[3][2] : text.slice(...item[side]).trim();
|
|
339
|
-
};
|
|
340
|
-
for (const item of result) if (item[3][0] === 0 && !item[2]) {
|
|
341
|
-
const value = before.slice(...item[0]), key = moveKey(item, 0, before, pa);
|
|
342
|
-
if (item[3][2] || value.trim().length >= 4 && /[\n ]/.test(value)) deleted.set(key, deleted.has(key) ? null : item);
|
|
343
|
-
}
|
|
344
|
-
for (const item of result) if (item[3][0] === 2 && !item[2]) {
|
|
345
|
-
const value = after.slice(...item[1]), key = moveKey(item, 1, after, pb);
|
|
346
|
-
let old = deleted.get(key), score = 0;
|
|
347
|
-
if (!old && !structures && key.length >= 24) for (const [prior, candidate] of deleted) {
|
|
348
|
-
if (!candidate || candidate[4]) continue;
|
|
349
|
-
const a = tokens(prior, 0, prior.length, 2), b = tokens(key, 0, key.length, 2);
|
|
350
|
-
if (!a || !b) throw null;
|
|
351
|
-
const n = matches(a[0], b[0]).reduce((n, j, i) => n + a[1][i + 1] - a[1][i], 0);
|
|
352
|
-
check(0);
|
|
353
|
-
if (n * 5 > Math.max(prior.length, key.length) * 3 && n >= score) { old = n === score ? null : candidate; score = n; }
|
|
354
|
-
}
|
|
355
|
-
if (!old || old[4]) continue;
|
|
356
|
-
const x = [...old[0]], y = [...item[1]], left = before.slice(...x).match(/\s*$/)[0], right = after.slice(...y).match(/\s*$/)[0];
|
|
357
|
-
let common = 0;
|
|
358
|
-
while (common < Math.min(left.length, right.length) && left[common] === right[common]) common++;
|
|
359
|
-
if (common && safe(before, x[1] - left.length + common) && safe(after, y[1] - right.length + common)) {
|
|
360
|
-
x[1] -= left.length - common; y[1] -= right.length - common;
|
|
361
|
-
}
|
|
362
|
-
const outer = result; result = [];
|
|
363
|
-
walk(...x, ...y, 1);
|
|
364
|
-
const inner = result; result = outer;
|
|
365
|
-
// Equal boundary fragments make the exact extent recoverable without display context.
|
|
366
|
-
if (inner.length && (inner[0][0][0] === x[0] || inner[0][1][0] === y[0] ||
|
|
367
|
-
inner.at(-1)[0][1] === x[1] || inner.at(-1)[1][1] === y[1] || inner.some(c => c[2]))) continue;
|
|
368
|
-
const move = [3, item[3][2] ? item[3][1] : code ? 'line' : 'block', item[3][2], [x, y]];
|
|
369
|
-
old[4] = []; item[4] = [];
|
|
370
|
-
const fragment = (a0, a1, b0, b1, group) => {
|
|
371
|
-
if (a0 < a1) old[4].push([[a0, a1], [old[1][0], old[1][0]], false, group]);
|
|
372
|
-
if (b0 < b1) item[4].push([[item[0][0], item[0][0]], [b0, b1], false, group]);
|
|
373
|
-
};
|
|
374
|
-
let a = old[0][0], b = item[1][0], wording;
|
|
375
|
-
for (const edit of inner) {
|
|
376
|
-
if (edit[3][3]) wording = edit[3];
|
|
377
|
-
else if (a !== edit[0][0] || b !== edit[1][0]) wording = [edit[3][0], edit[3][1]];
|
|
378
|
-
wording[2] = item[3][2]; wording[3] = move[3];
|
|
379
|
-
if (wording[0] !== edit[3][0]) wording[0] = 1;
|
|
380
|
-
fragment(a, edit[0][0], b, edit[1][0], move);
|
|
381
|
-
fragment(...edit[0], ...edit[1], wording);
|
|
382
|
-
a = edit[0][1]; b = edit[1][1];
|
|
383
|
-
}
|
|
384
|
-
fragment(a, x[1], b, y[1], move);
|
|
385
|
-
fragment(x[1], old[0][1], 0, 0, [0, 'char']);
|
|
386
|
-
fragment(0, 0, y[1], item[1][1], [2, 'char']);
|
|
387
|
-
}
|
|
388
|
-
result = result.flatMap(item => item[4] || [item]);
|
|
389
|
-
|
|
390
|
-
if (structures) {
|
|
391
|
-
check(la.length * Math.ceil(Math.log2(la.length + 1)) + lb.length + pa.length + pb.length);
|
|
392
|
-
const mapped = {}; let cursor = 0, delta = 0;
|
|
393
|
-
for (const link of [...la].sort((a, b) => a.at[0] - b.at[0])) {
|
|
394
|
-
while (cursor < result.length && result[cursor][0][1] <= link.at[0]) { delta = result[cursor][1][1] - result[cursor][0][1]; cursor++; }
|
|
395
|
-
if (cursor === result.length || result[cursor][0][0] >= link.at[1]) mapped[[link.at[0] + delta, link.at[1] + delta]] = link.to;
|
|
396
|
-
}
|
|
397
|
-
lb = lb.map(l => l.key != null ? l : {...l, to: mapped[l.at] || l.to});
|
|
398
|
-
for (const [p, old] of aliases) {
|
|
399
|
-
if (before.slice(...old.nameAt) === after.slice(...p.nameAt)) continue;
|
|
400
|
-
check(la.length + lb.length);
|
|
401
|
-
const left = la.filter(l => l.key === old.key), right = lb.filter(l => l.key === p.key);
|
|
402
|
-
if (left.length !== right.length) continue;
|
|
403
|
-
const x = [old.nameAt], y = [p.nameAt], owned = [];
|
|
404
|
-
for (let i = 0; i < left.length; i++) {
|
|
405
|
-
x.push(left[i].at); y.push(right[i].at);
|
|
406
|
-
if (left[i].labelAt && right[i].labelAt && before.slice(...left[i].labelAt) === before.slice(...old.nameAt) &&
|
|
407
|
-
after.slice(...right[i].labelAt) === after.slice(...p.nameAt)) { x.push(left[i].labelAt); y.push(right[i].labelAt); }
|
|
408
|
-
}
|
|
409
|
-
x.sort((a, b) => a[0] - b[0]); y.sort((a, b) => a[0] - b[0]);
|
|
410
|
-
let complete = true;
|
|
411
|
-
for (let i = 0; i < x.length; i++) {
|
|
412
|
-
check(result.length * 2);
|
|
413
|
-
if (result.some(c => c[3]?.[0] !== 3 && (overlaps(c[0], x[i]) || overlaps(c[1], y[i])) && !inside(c, x[i], y[i]))) { complete = false; break; }
|
|
414
|
-
const found = result.filter(c => c[3][0] < 3 && inside(c, x[i], y[i]));
|
|
415
|
-
if (!found.length) { complete = false; break; }
|
|
416
|
-
owned.push(...found);
|
|
417
|
-
}
|
|
418
|
-
if (!complete) continue;
|
|
419
|
-
check(result.length + owned.length * 2);
|
|
420
|
-
const owners = new Set(owned.map(c => c[3]).filter(Boolean)), members = new Set(owned);
|
|
421
|
-
if (result.some(c => owners.has(c[3]) && !members.has(c))) continue;
|
|
422
|
-
const group = [4, p.kind, old.name, [[...old.nameAt], [...p.nameAt]]];
|
|
423
|
-
for (const item of owned) item[3] = group;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
} } catch (error) {
|
|
427
|
-
if (error !== null) throw error;
|
|
428
|
-
result = result.flatMap(item => item[4] || [item]);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const changes = [], edits = [];
|
|
432
|
-
let normalized;
|
|
433
|
-
const meaning = structures?.[0]?.meaning;
|
|
434
|
-
const format = typeof meaning === 'string' && meaning === structures[1].meaning;
|
|
435
|
-
for (let i = 0; i < result.length; i++) {
|
|
436
|
-
const item = result[i];
|
|
437
|
-
if (raw === 'char' && !evidence) {
|
|
438
|
-
const a = item[0], b = item[1], next = result[i + 1];
|
|
439
|
-
if (item[3][0] !== 1) {
|
|
440
|
-
const room = Math.min((next?.[0][0] ?? before.length) - a[1], (next?.[1][0] ?? after.length) - b[1]);
|
|
441
|
-
let n = 0;
|
|
442
|
-
while (n < room && before[a[0] + n] === after[b[0] + n] && spend()) n++;
|
|
443
|
-
while (n && (!safe(before, a[0] + n) || !safe(before, a[1] + n) || !safe(after, b[0] + n) || !safe(after, b[1] + n))) n--;
|
|
444
|
-
a[0] += n; a[1] += n; b[0] += n; b[1] += n;
|
|
445
|
-
}
|
|
446
|
-
const p = normalized;
|
|
447
|
-
if (p && p[0][0] === p[0][1] && item[3][0] === 2 && p[0][0] === a[0] && p[1][1] === b[0]) {
|
|
448
|
-
p[1][1] = b[1]; if (item[2]) edits.at(-1).coarse = true; continue;
|
|
449
|
-
}
|
|
450
|
-
normalized = item;
|
|
451
|
-
}
|
|
452
|
-
const group = item[3];
|
|
453
|
-
if (!raw && !group[3]) group[3] = [[...item[0]], [...item[1]]];
|
|
454
|
-
if (!group[4]) {
|
|
455
|
-
const context = group[3];
|
|
456
|
-
if (!raw) for (const [text, span] of [[before, context[0]], [after, context[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]++;
|
|
459
|
-
}
|
|
460
|
-
group[4] = []; changes.push(group);
|
|
461
|
-
if (format) group[0] = 5;
|
|
462
|
-
}
|
|
463
|
-
group[4].push(edits.length);
|
|
464
|
-
edits.push({before: item[0], after: item[1], ...(item[2] ? {coarse: true} : {})});
|
|
465
|
-
}
|
|
466
|
-
if (!raw && !structures && !code && !evidence) for (let i = 0; i < changes.length; i++) {
|
|
467
|
-
let end = i + 1;
|
|
468
|
-
for (; end < changes.length; end++) {
|
|
469
|
-
const a = changes[end - 1], b = changes[end];
|
|
470
|
-
const gap = before.slice(a[3][0][1], b[3][0][0]);
|
|
471
|
-
if (a[0] !== 1 || b[0] !== 1 || a[1] !== 'word' || b[1] !== 'word' ||
|
|
472
|
-
/\S|\r|\n/.test(gap) || gap !== after.slice(a[3][1][1], b[3][1][0])) break;
|
|
473
|
-
}
|
|
474
|
-
if (end - i < 4) continue;
|
|
475
|
-
const group = changes[i]; group[1] = 'sentence';
|
|
476
|
-
for (const next of changes.splice(i + 1, end - i - 1)) {
|
|
477
|
-
group[4].push(...next[4]); group[3][0][1] = next[3][0][1]; group[3][1][1] = next[3][1][1];
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (structures && !raw && budget.left >= 0) {
|
|
481
|
-
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));
|
|
482
|
-
for (const change of changes) {
|
|
483
|
-
if (change[0] === 5) continue;
|
|
484
|
-
if (!spend(pa.length + pb.length + lb.length)) break;
|
|
485
|
-
const range = change[3];
|
|
486
|
-
const older = pa.filter(p => p.start <= range[0][0] && p.end >= range[0][1]), newer = pb.filter(p => p.start <= range[1][0] && p.end >= range[1][1]);
|
|
487
|
-
const part = (change[0] === 0 ? older : newer).at(-1) || older.at(-1);
|
|
488
|
-
if (!change[2] && part?.name) { change[2] = part.name; change[1] = part.kind; }
|
|
489
|
-
const names = new Set([change[2], ...older.map(p => p.name), ...newer.map(p => p.name)]), breaks = [], affects = [];
|
|
490
|
-
for (const {at, to} of lb) {
|
|
491
|
-
if (!names.has(to)) continue;
|
|
492
|
-
if (!newNames.has(to)) { if (oldNames.has(to)) breaks.push({at, to}); continue; }
|
|
493
|
-
if (!spend(edits.length + pb.length) || edits.some(c => overlaps(at, c.after))) continue;
|
|
494
|
-
const p = pb.filter(p => p.start <= at[0] && p.end >= at[1] && /^(paragraph|call|item|task|cell|declaration)$/.test(p.kind)).at(-1);
|
|
495
|
-
affects.push(p && !edits.some(c => p.start < c.after[1] && p.end > c.after[0]) ? [p.start, p.end] : at);
|
|
496
|
-
}
|
|
497
|
-
if (breaks.length) change[6] = breaks;
|
|
498
|
-
if (affects.length) change[5] = affects;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
if (options._stats) Object.assign(options._stats, {work: Math.max(0, initial - Math.max(0, budget.left)), coarse: edits.some(c => c.coarse)});
|
|
502
|
-
// Concern tuples: kind, unit, name, context, edits, affects, breaks.
|
|
503
|
-
return {edits, changes: changes.map(([kind, unit, name, context, owned, affects, breaks], i) => ({
|
|
504
|
-
id: i + 1, kind: ['delete', 'replace', 'insert', 'move', 'rename', 'format'][kind], unit, edits: owned,
|
|
505
|
-
...(name ? {name} : {}), ...(context ? {context: {before: context[0], after: context[1]}} : {}),
|
|
506
|
-
...(affects ? {affects} : {}), ...(breaks ? {breaks} : {})
|
|
507
|
-
}))};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
export function merge(base, mine, theirs, options = {}) {
|
|
511
|
-
if (mine === theirs || base === theirs || base === mine) return {text: base === mine ? theirs : mine, conflicts: []};
|
|
512
|
-
const budget = {...options, _unit: 'char', _structure: true,
|
|
513
|
-
_budget: options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7}};
|
|
514
|
-
const sources = [base, mine, theirs];
|
|
515
|
-
const complete = () => ({text: mine, conflicts: [{at: [0, mine.length], theirs}]});
|
|
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)) : [];
|
|
521
|
-
const repeatedBase = trees.splice(2, 1)[0];
|
|
522
|
-
const equal = (a, b) => a.length === b.length && a.every((item, i) => item === b[i]);
|
|
523
|
-
if (trees.some(Boolean) || repeatedBase) {
|
|
524
|
-
try {
|
|
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();
|
|
533
|
-
}
|
|
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
|
-
}
|
|
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;
|
|
578
|
-
}
|
|
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);
|
|
587
|
-
});
|
|
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]);
|
|
598
|
-
}
|
|
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;
|
|
610
|
-
}
|
|
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);
|
|
615
|
-
}
|
|
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})));
|
|
638
|
-
for (const side of [0, 1]) {
|
|
639
|
-
const destinations = new Map();
|
|
640
|
-
check(added[side].size + removed[side].size);
|
|
641
|
-
for (const [kind, parent] of added[side].values())
|
|
642
|
-
destinations.set(kind, destinations.has(kind) && destinations.get(kind) !== parent ? null : parent);
|
|
643
|
-
for (const [kind, parent] of removed[side].values())
|
|
644
|
-
if (destinations.has(kind) && destinations.get(kind) !== parent) throw null;
|
|
645
|
-
}
|
|
646
|
-
if ([...removed[0].keys()].some(name => removed[1].has(name)) &&
|
|
647
|
-
(added[0].size !== added[1].size || [...added[0].keys()].some(name => !added[1].has(name)))) throw null;
|
|
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};
|
|
666
|
-
}
|
|
667
|
-
} catch (error) { if (error !== null) throw error; return complete(); }
|
|
668
|
-
}
|
|
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);
|
|
671
|
-
if (trees.some(Boolean) && budget._budget.left < 0) return complete();
|
|
672
|
-
const edits = [mine, theirs].flatMap((text, side) => changes[side].map(change =>
|
|
673
|
-
[...change.before, text.slice(...change.after), side]));
|
|
674
|
-
edits.sort((a, b) => a[0] - b[0] || a[3] - b[3] || a[1] - b[1]);
|
|
675
|
-
const conflicts = [];
|
|
676
|
-
let at = 0, text = '';
|
|
677
|
-
for (let index = 0; index < edits.length;) {
|
|
678
|
-
const start = edits[index][0], sides = [['', start], ['', start]];
|
|
679
|
-
let end = start, changed = 0;
|
|
680
|
-
for (;;) {
|
|
681
|
-
const edit = edits[index++], side = edit[3], row = sides[side];
|
|
682
|
-
if (edit[1] >= end) {
|
|
683
|
-
if (edit[1] > end) sides[1 - side][2] = null;
|
|
684
|
-
end = edit[1]; row[2] = edit;
|
|
685
|
-
}
|
|
686
|
-
row[0] += base.slice(row[1], edit[0]) + edit[2]; changed |= 1 << side;
|
|
687
|
-
row[1] = edit[1];
|
|
688
|
-
if (index === edits.length) break;
|
|
689
|
-
const [a, b, t, side2] = edits[index], peer = sides[1 - side2][2];
|
|
690
|
-
if (!(a < end || a === start || peer &&
|
|
691
|
-
(a === end && peer[0] === end || a === b && peer[0] < end &&
|
|
692
|
-
a - end < t.length && (peer[2] + base.slice(end, a)).endsWith(t)))) break;
|
|
693
|
-
}
|
|
694
|
-
for (const row of sides) row[0] += base.slice(row[1], end);
|
|
695
|
-
text += base.slice(at, start);
|
|
696
|
-
const from = text.length;
|
|
697
|
-
text += sides[changed & 1 ? 0 : 1][0];
|
|
698
|
-
if (changed === 3 && sides[0][0] !== sides[1][0]) conflicts.push({at: [from, text.length], theirs: sides[1][0]});
|
|
699
|
-
at = end;
|
|
700
|
-
}
|
|
701
|
-
return {text: text + base.slice(at), conflicts};
|
|
702
|
-
}
|
|
2
|
+
// The package entry: compare two texts, or merge two edits of one base.
|
|
3
|
+
import {diff} from './diff.mjs';
|
|
4
|
+
import {merge} from './merge.mjs';
|
|
5
|
+
export {diff, merge};
|