rapier-markdown-kit 1.2.8 → 1.2.10
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/dist/agent/will.mjs +0 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +0 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.mjs +0 -4
- package/dist/kit/markdown-it.mjs +0 -3
- package/dist/kit/pdf-glyphs.mjs +1 -10
- package/dist/kit/pdf-source.mjs +5 -5
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +12 -117
- package/dist/kit/render-print.mjs +30 -61
- package/dist/kit/render-sanitize.mjs +7 -59
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +46 -176
- package/dist/layout/hyphenation.mjs +0 -23
- package/dist/layout/line-plan.mjs +0 -20
- package/dist/layout/model.mjs +0 -55
- package/dist/layout/paragraph-fit.mjs +50 -60
- package/dist/layout/preparation.mjs +23 -56
- package/dist/layout/prepared-paragraph.mjs +0 -25
- package/dist/layout/typography.mjs +103 -41
- package/dist/packages/rapier-diff/concerns.mjs +0 -16
- package/dist/packages/rapier-diff/diff.mjs +0 -7
- package/dist/packages/rapier-diff/index.mjs +0 -1
- package/dist/packages/rapier-diff/matching.mjs +0 -19
- package/dist/packages/rapier-diff/merge.mjs +0 -9
- package/dist/packages/rapier-diff/moves.mjs +0 -7
- package/dist/packages/rapier-diff/structure.mjs +0 -2
- package/dist/packages/rapier-diff/tokens.mjs +0 -6
- package/dist/packages/rapier-diff/walk.mjs +0 -12
- package/dist/spec/document-fonts.mjs +0 -9
- package/dist/spec/document-settings.mjs +5 -24
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +0 -36
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/template-page.mjs +6 -0
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +1 -1
- package/style.css +32 -16
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {letter, within, prev, next} from './tokens.mjs';
|
|
3
3
|
|
|
4
|
-
// Separator-only pieces join the edit beside them; adjacent blocks each carry their own line break.
|
|
5
4
|
export function separate(c, endA = c.before.length, endB = c.after.length) {
|
|
6
5
|
const {before, after, safe, raw, spend} = c;
|
|
7
|
-
// A separator-only piece beside another edit joins it; equal separators at its ends stay unchanged.
|
|
8
6
|
if (!raw) for (let i = c.result.length - 1; i > 0; i--) {
|
|
9
7
|
const p = c.result[i - 1], q = c.result[i], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1, text = side ? after : before;
|
|
10
8
|
if (side >= 0 && !/\S/.test(text.slice(...p[side]))) {
|
|
@@ -16,15 +14,12 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
|
|
|
16
14
|
if (p[2] || q[2] || p[0][1] !== q[0][0] || p[1][1] !== q[1][0] ||
|
|
17
15
|
![p, q].some(r => !/\S/.test(before.slice(...r[0]) + after.slice(...r[1])))) continue;
|
|
18
16
|
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;
|
|
19
|
-
// CRLF steps as one separator.
|
|
20
17
|
for (let k; x[0] < x[1] && y[0] < y[1] && /\s/.test(before[x[1] - 1]) && before[x[1] - 1] === after[y[1] - 1] &&
|
|
21
18
|
(k = before.endsWith('\r\n', x[1]) && after.endsWith('\r\n', y[1]) ? 2 : 1) <= Math.min(x[1] - x[0], y[1] - y[0]) &&
|
|
22
19
|
safe(before, x[1] - k) && safe(after, y[1] - k);) { x[1] -= k; y[1] -= k; }
|
|
23
20
|
keep[3][0] = x[0] === x[1] ? 2 : y[0] === y[1] ? 0 : 1;
|
|
24
21
|
c.result.splice(i - 1, 2, [x, y, false, keep[3]]);
|
|
25
22
|
}
|
|
26
|
-
// Repeated closing lines can attach to the wrong block. Rotate an exact insertion/deletion to the
|
|
27
|
-
// next blank-line boundary, without crossing another edit or splitting a character.
|
|
28
23
|
if (!raw) for (let i = 0; i < c.result.length; i++) {
|
|
29
24
|
const p = c.result[i], q = c.result[i + 1], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1;
|
|
30
25
|
if (side < 0 || p[2]) continue;
|
|
@@ -38,8 +33,6 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
|
|
|
38
33
|
if (shift && p.slice(0, 2).every((span, k) => safe(k ? after : before, span[0] + shift) && safe(k ? after : before, span[1] + shift)))
|
|
39
34
|
for (const span of p.slice(0, 2)) { span[0] += shift; span[1] += shift; }
|
|
40
35
|
}
|
|
41
|
-
// Adjacent blocks deleted or inserted together: when the first carries the line break before it, each carries
|
|
42
|
-
// its own, so any one restores apart from its neighbors.
|
|
43
36
|
if (!raw) for (let i = 1; i < c.result.length; i++) {
|
|
44
37
|
const p = c.result[i - 1], q = c.result[i], kind = p[3][0], side = kind ? 1 : 0, text = side ? after : before;
|
|
45
38
|
if (kind % 2 || q[3][0] !== kind || p[2] || q[2] || p[side][1] !== q[side][0] || p[1 - side][0] !== q[1 - side][0]) continue;
|
|
@@ -48,20 +41,16 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
|
|
|
48
41
|
}
|
|
49
42
|
}
|
|
50
43
|
|
|
51
|
-
// Refine the outer walk and each move's inner walk before assigning transport ownership.
|
|
52
44
|
export function phrases(c) {
|
|
53
45
|
const {before, after, raw, code} = c;
|
|
54
|
-
// Whole-word replacements separated only by spaces read and restore as one phrase, and one number stays one number.
|
|
55
46
|
if (!raw) for (let i = c.result.length - 1; i > 0; i--) {
|
|
56
47
|
const p = c.result[i - 1], q = c.result[i], gap = before.slice(p[0][1], q[0][0]);
|
|
57
48
|
if (p[3][0] !== 1 || q[3][0] !== 1 || p[3][3] || q[3][3] || p[2] || q[2] || gap !== after.slice(p[1][1], q[1][0])) continue;
|
|
58
|
-
// A time, a date, a version or a decimal is one number.
|
|
59
49
|
const number = /^(?:[.,:/-]\d*)+$/.test(gap) && /\d/.test(before[p[0][1] - 1] + after[p[1][1] - 1]) && /\d/.test(before[q[0][0]] + after[q[1][0]]);
|
|
60
50
|
if (!number && (code || !/^[^\S\r\n]+$/.test(gap) || within(before, p[0][0]) || within(after, p[1][0]) ||
|
|
61
51
|
within(before, q[0][1]) || within(after, q[1][1]))) continue;
|
|
62
52
|
p[0][1] = q[0][1]; p[1][1] = q[1][1]; c.result.splice(i, 1);
|
|
63
53
|
}
|
|
64
|
-
// Paired inline markers added or removed around unchanged text on one line are one concern.
|
|
65
54
|
if (!raw && !code) for (let i = 1; i < c.result.length; i++) {
|
|
66
55
|
const p = c.result[i - 1], q = c.result[i], kind = p[3][0], side = kind ? 1 : 0, text = side ? after : before;
|
|
67
56
|
if (kind % 2 || kind !== q[3][0] || p[2] || q[2] || p[3][3] || q[3][3]) continue;
|
|
@@ -72,12 +61,10 @@ export function phrases(c) {
|
|
|
72
61
|
}
|
|
73
62
|
}
|
|
74
63
|
|
|
75
|
-
// Readable concerns: phrases, numbers and paired markers, then the public edits and changes with context.
|
|
76
64
|
export function concerns(c) {
|
|
77
65
|
phrases(c);
|
|
78
66
|
const {before, after, spend, safe, budget, raw, code, evidence, structures, initial, options, pa, pb} = c, lb = c.lb;
|
|
79
67
|
const overlaps = (a, b) => a[0] < b[1] && a[1] > b[0];
|
|
80
|
-
// Siblings are ordered and disjoint: the parts holding a range are found by descent, outermost first.
|
|
81
68
|
const containing = (parts, from, to, out = []) => {
|
|
82
69
|
let lo = 0, hi = parts.length;
|
|
83
70
|
while (lo < hi) { const mid = lo + hi >>> 1; if (parts[mid].end < to) lo = mid + 1; else hi = mid; }
|
|
@@ -85,7 +72,6 @@ export function concerns(c) {
|
|
|
85
72
|
if (parts[k].end >= to) { out.push(parts[k]); containing(parts[k].children, from, to, out); }
|
|
86
73
|
return out;
|
|
87
74
|
};
|
|
88
|
-
// Edits are ordered and disjoint on the after side.
|
|
89
75
|
const touched = (edits, range) => {
|
|
90
76
|
let lo = 0, hi = edits.length;
|
|
91
77
|
while (lo < hi) { const mid = lo + hi >>> 1; if (edits[mid].after[1] <= range[0]) lo = mid + 1; else hi = mid; }
|
|
@@ -116,7 +102,6 @@ export function concerns(c) {
|
|
|
116
102
|
if (!raw && !group[3]) group[3] = [[...item[0]], [...item[1]]];
|
|
117
103
|
if (!group[4]) {
|
|
118
104
|
const context = group[3];
|
|
119
|
-
// Context completes a word only where an edge falls inside it.
|
|
120
105
|
if (!raw) for (const [text, span] of [[before, context[0]], [after, context[1]]]) {
|
|
121
106
|
const start = within(text, span[0]), end = within(text, span[1]);
|
|
122
107
|
for (let c; start && letter(c = prev(text, span[0])) && spend() && safe(text, span[0] - c.length);) span[0] -= c.length;
|
|
@@ -152,7 +137,6 @@ export function concerns(c) {
|
|
|
152
137
|
}
|
|
153
138
|
}
|
|
154
139
|
if (options._stats) Object.assign(options._stats, {work: Math.max(0, initial - Math.max(0, budget.left)), coarse: edits.some(c => c.coarse)});
|
|
155
|
-
// Concern tuples: kind, unit, name, context, edits, affects, breaks. Wording inside a move shares its context.
|
|
156
140
|
const moves = new Map(changes.map((change, i) => change[0] === 3 && [change[3], i + 1]).filter(Boolean));
|
|
157
141
|
return {edits, changes: changes.map(([kind, unit, name, context, owned, affects, breaks], i) => ({
|
|
158
142
|
id: i + 1, kind: ['delete', 'replace', 'insert', 'move', 'rename', 'format'][kind], unit, edits: owned,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// One comparison: observe structure, walk the source into exact records, find moves and renames, then read them as
|
|
3
|
-
// concerns. Each stage lives in its own module and shares one comparison context.
|
|
4
2
|
import {observe} from './structure.mjs';
|
|
5
3
|
import {boundaries, tokenizer, wordSegmenter} from './tokens.mjs';
|
|
6
4
|
import {matcher} from './matching.mjs';
|
|
@@ -12,10 +10,8 @@ export function diff(before, after, options = {}) {
|
|
|
12
10
|
if (before === after) return {edits: [], changes: []};
|
|
13
11
|
const budget = options._budget || {left: Number.isFinite(options.limit) ? Math.max(0, options.limit) : 1e7};
|
|
14
12
|
const spend = (n = 1) => (budget.left -= n) >= 0, safe = boundaries(), raw = options._unit;
|
|
15
|
-
// Internal records: [before range, after range, coarse, concern, transported fragments].
|
|
16
13
|
const c = {before, after, options, budget, spend, safe, raw, chars: options.unit === 'char', initial: budget.left,
|
|
17
14
|
segmenter: wordSegmenter(), result: [], code: options.as === 'code'};
|
|
18
|
-
// A Markdown fence holds code inside prose; only code outside fences makes the text code.
|
|
19
15
|
if (options.as !== 'prose' && options.as !== 'code') c.code = /(?:^|\n)\s*(?:[{}]|(?:function|class|const|let|var|import|export|def|fn|func|package)\s+\w[^]{0,80}[({=:;])/
|
|
20
16
|
.test((before.slice(0, 4096) + '\n' + after.slice(0, 4096)).replace(/^ {0,3}(`{3,}|~{3,})[^]*?(?:^ {0,3}\1[`~]*[ \t]*$|(?![^]))/gm, ''));
|
|
21
17
|
let fallback;
|
|
@@ -33,7 +29,6 @@ export function diff(before, after, options = {}) {
|
|
|
33
29
|
}
|
|
34
30
|
Object.assign(c, {structures, pa, pb, la: structures[0].links, lb: structures[1].links, aliases});
|
|
35
31
|
} catch {
|
|
36
|
-
// A failed reader preserves an explicit prose choice and reports why structure was not used.
|
|
37
32
|
const failed = structures?.find(value => !value || value.invalid);
|
|
38
33
|
fallback = {structure: failed?.threw ? 'threw' : budget.left < 0 ? 'limit' : 'invalid', ...(failed?.threw ? {error: failed.error} : {})};
|
|
39
34
|
c.code = options.as !== 'prose';
|
|
@@ -48,8 +43,6 @@ export function diff(before, after, options = {}) {
|
|
|
48
43
|
structures?.[0]?.parts, structures?.[1]?.parts);
|
|
49
44
|
separate(c);
|
|
50
45
|
moves(c);
|
|
51
|
-
// A heading edit can re-record its children. Compare both walks after transport is known: unchanged moved
|
|
52
|
-
// fragments cost nothing, so a smaller plain walk cannot discard a move just by aligning some of its lines.
|
|
53
46
|
if (structures && !evidence && !raw && c.result.some(r => r[0][0] === r[0][1]) && c.result.some(r => r[1][0] === r[1][1]) &&
|
|
54
47
|
budget.left >= c.initial - budget.left) {
|
|
55
48
|
const size = (rows, transport = false) => rows.reduce((n, r) => n + (!transport && r[3][0] === 3 ? 0 : r[0][1] - r[0][0] + r[1][1] - r[1][0]), 0);
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Subsequence search over token keys, bounded by a shared work budget.
|
|
3
2
|
export function matcher(spend, budget) {
|
|
4
|
-
// Unique anchors and fallback equal pairs share one bounded subsequence search. Repetitive text, where equal pairs
|
|
5
|
-
// are too many to visit, anchors on runs of tokens found once on each side, then on positions, then on a shortest
|
|
6
|
-
// script; only a range none of them can afford within the work limit stays coarse.
|
|
7
3
|
function matches(a, b) {
|
|
8
4
|
const found = new Array(a.length), pending = [[0, a.length, 0, b.length]];
|
|
9
|
-
// Patience chain over candidate pairs in a order; unique anchors split the range into searched gaps.
|
|
10
5
|
const chain = (pairs, a0, a1, b0, b1, split, near) => {
|
|
11
6
|
const tails = [];
|
|
12
7
|
for (const [i, j] of pairs) {
|
|
@@ -29,7 +24,6 @@ export function matcher(spend, budget) {
|
|
|
29
24
|
while (a0 < a1 && b0 < b1 && a[a0] === b[b0]) found[a0++] = b0++;
|
|
30
25
|
while (a0 < a1 && b0 < b1 && a[a1 - 1] === b[b1 - 1]) found[--a1] = --b1;
|
|
31
26
|
if (a0 === a1 || b0 === b1 || !spend(a1 - a0 + b1 - b0)) continue;
|
|
32
|
-
// Between anchored runs the change is usually small; a token found once by chance must not misalign it.
|
|
33
27
|
if (near && shortest(a, b, a0, a1, b0, b1, found, 4 * (a1 - a0 + b1 - b0))) continue;
|
|
34
28
|
const positions = new Map();
|
|
35
29
|
for (let j = b0; j < b1; j++) {
|
|
@@ -45,12 +39,10 @@ export function matcher(spend, budget) {
|
|
|
45
39
|
if (list[0] === 1 && list.length === 2) unique++;
|
|
46
40
|
}
|
|
47
41
|
if (!count) continue;
|
|
48
|
-
// With no unique anchors, equal-length repeated runs prefer their witnessed positions to an arbitrary LIS.
|
|
49
42
|
if (!unique && a1 - a0 === b1 - b0) {
|
|
50
43
|
const anchors = diagonal(a, b, a0, a1, b0, b1);
|
|
51
44
|
if (anchors) { chain(anchors, a0, a1, b0, b1, true, true); continue; }
|
|
52
45
|
}
|
|
53
|
-
// Charge the chain before building it: one that cannot be afforded falls through to cheaper anchors.
|
|
54
46
|
const chained = unique || count, cost = chained * Math.ceil(Math.log2(chained + 2));
|
|
55
47
|
if (cost <= budget.left) {
|
|
56
48
|
const pairs = [];
|
|
@@ -63,7 +55,6 @@ export function matcher(spend, budget) {
|
|
|
63
55
|
chain(pairs, a0, a1, b0, b1, unique > 0);
|
|
64
56
|
continue;
|
|
65
57
|
}
|
|
66
|
-
// Few changes make a shortest script cheap (about n + d²); try that first at a bounded cost.
|
|
67
58
|
if (shortest(a, b, a0, a1, b0, b1, found, a1 - a0 + b1 - b0)) continue;
|
|
68
59
|
const anchors = runs(a, b, a0, a1, b0, b1) || diagonal(a, b, a0, a1, b0, b1);
|
|
69
60
|
if (anchors) chain(anchors, a0, a1, b0, b1, true, true);
|
|
@@ -72,14 +63,11 @@ export function matcher(spend, budget) {
|
|
|
72
63
|
return found;
|
|
73
64
|
}
|
|
74
65
|
|
|
75
|
-
// Runs of k tokens found once on each side anchor text whose single tokens all repeat. Window hashes are checked
|
|
76
|
-
// token by token, so a collision costs work, never correctness.
|
|
77
66
|
function runs(a, b, a0, a1, b0, b1) {
|
|
78
67
|
const ids = new Map(), id = key => ids.get(key) ?? (ids.set(key, ids.size), ids.size - 1);
|
|
79
68
|
const left = Int32Array.from({length: a1 - a0}, (_, k) => id(a[a0 + k])), right = Int32Array.from({length: b1 - b0}, (_, k) => id(b[b0 + k]));
|
|
80
69
|
for (const k of [16, 4, 64]) {
|
|
81
70
|
if (k > Math.min(left.length, right.length) || !spend(2 * (left.length + right.length))) return;
|
|
82
|
-
// A prime modulus below 2^31 and a base below 2^21 keep every product exact in a double.
|
|
83
71
|
const P = 2147483647, B = 1000003;
|
|
84
72
|
let power = 1;
|
|
85
73
|
for (let n = 1; n < k; n++) power = power * B % P;
|
|
@@ -101,7 +89,6 @@ export function matcher(spend, budget) {
|
|
|
101
89
|
while (n < k && left[i + n] === right[j + n]) n++;
|
|
102
90
|
if (n === k) pairs.push([a0 + i, b0 + j]);
|
|
103
91
|
}
|
|
104
|
-
// Overlapping windows add nothing a gap's equal-run trim does not find; keep one per k tokens.
|
|
105
92
|
pairs.sort((p, q) => p[0] - q[0]);
|
|
106
93
|
const spaced = [];
|
|
107
94
|
for (const pair of pairs) if (!spaced.length || pair[0] >= spaced.at(-1)[0] + k) spaced.push(pair);
|
|
@@ -109,16 +96,12 @@ export function matcher(spend, budget) {
|
|
|
109
96
|
}
|
|
110
97
|
}
|
|
111
98
|
|
|
112
|
-
// Repeated lines align by position: tokens equal on the start diagonal before a split and on the end diagonal after
|
|
113
|
-
// it, at the split that keeps the most. Used only when they cover at least half of the shorter side.
|
|
114
99
|
function diagonal(a, b, a0, a1, b0, b1) {
|
|
115
100
|
const n = a1 - a0, m = b1 - b0, shift = m - n, size = Math.min(n, m);
|
|
116
101
|
if (!spend(2 * (n + m))) return;
|
|
117
|
-
// head[s]: equal pairs on the start diagonal among the first s tokens; tail[s]: on the end diagonal from s on.
|
|
118
102
|
const head = new Int32Array(n + 1), tail = new Int32Array(n + 2);
|
|
119
103
|
for (let i = 0; i < n; i++) head[i + 1] = head[i] + (i < m && a[a0 + i] === b[b0 + i]);
|
|
120
104
|
for (let i = n - 1; i >= 0; i--) tail[i] = tail[i + 1] + (i + shift >= 0 && a[a0 + i] === b[b0 + i + shift]);
|
|
121
|
-
// A deletion (shift < 0) skips -shift tokens of a between the diagonals.
|
|
122
105
|
let best = -1, split = 0;
|
|
123
106
|
for (let s = 0; s <= n; s++) {
|
|
124
107
|
const rest = Math.min(n, s + Math.max(0, -shift)), kept = head[s] + tail[rest];
|
|
@@ -131,8 +114,6 @@ export function matcher(spend, budget) {
|
|
|
131
114
|
return pairs;
|
|
132
115
|
}
|
|
133
116
|
|
|
134
|
-
// Myers' greedy search: O((N + M) D) steps; each frontier is kept for the backward pass. An attempt can stop at
|
|
135
|
-
// its own share of the work and report no script.
|
|
136
117
|
function shortest(a, b, a0, a1, b0, b1, found, share = Infinity) {
|
|
137
118
|
const n = a1 - a0, m = b1 - b0, v = new Int32Array(2 * (n + m) + 3), o = n + m + 1, trace = [];
|
|
138
119
|
const step = (k = 1) => (share -= k) >= 0 && spend(k);
|
|
@@ -13,7 +13,6 @@ export function merge(base, mine, theirs, options = {}) {
|
|
|
13
13
|
const check = n => { if (!spend(n)) throw null; };
|
|
14
14
|
const safe = options.structure && boundaries();
|
|
15
15
|
const trees = options.structure && !options._edits
|
|
16
|
-
// A reader that throws gives no structure: the texts merge by source coordinates.
|
|
17
16
|
? [base, mine, base, theirs].map(text => observe(text, options.structure, spend, safe, false, true)).map(tree => tree?.threw ? null : tree) : [];
|
|
18
17
|
const repeatedBase = trees.splice(2, 1)[0];
|
|
19
18
|
const equal = (a, b) => a.length === b.length && a.every((item, i) => item === b[i]);
|
|
@@ -71,13 +70,11 @@ export function merge(base, mine, theirs, options = {}) {
|
|
|
71
70
|
check(list.length); map.set(name, null);
|
|
72
71
|
return list.map(part => content(part, side));
|
|
73
72
|
});
|
|
74
|
-
// Each occurrence keeps its base text on one side, or the whole list does.
|
|
75
73
|
if (!equal(values[0], values[1]) && !equal(values[0], values[2]) && !(values.every(list => list.length === values[0].length) &&
|
|
76
74
|
values[0].every((text, k) => text === values[1][k] || text === values[2][k]))) throw null;
|
|
77
75
|
}
|
|
78
76
|
return maps;
|
|
79
77
|
};
|
|
80
|
-
// A part renamed on one side keeps its reader key and aligns with its base part.
|
|
81
78
|
const keyed = new Map(rows[0].filter(part => part.nameAt && part.key != null).map(part => [part.key, part]));
|
|
82
79
|
const own = new Set(rows[0].map(part => part.id)), renamed = new Map();
|
|
83
80
|
for (const side of [1, 2]) {
|
|
@@ -89,7 +86,6 @@ export function merge(base, mine, theirs, options = {}) {
|
|
|
89
86
|
}
|
|
90
87
|
const id = part => renamed.get(part) ?? part.id;
|
|
91
88
|
const maps = index(id), orders = rows.map(list => list.map(id));
|
|
92
|
-
// A vanished or added copy of a repeated name keeps the order; only a different order of shared names is a reorder.
|
|
93
89
|
const within = (a, b) => { let k = 0; for (const name of b) if (name === a[k]) k++; return k === a.length; };
|
|
94
90
|
const reordered = [1, 2].map(side => {
|
|
95
91
|
const a = orders[0].filter(name => maps[side].has(name)), b = orders[side].filter(name => maps[0].has(name));
|
|
@@ -128,7 +124,6 @@ export function merge(base, mine, theirs, options = {}) {
|
|
|
128
124
|
if (child) children.set(name, child);
|
|
129
125
|
}
|
|
130
126
|
if (frame < 0 && !children.size) return;
|
|
131
|
-
// A plan is a tree of range triples; a string is an already-admitted gap.
|
|
132
127
|
const plan = [];
|
|
133
128
|
if (frame >= 0) {
|
|
134
129
|
let at = nodes[frame].start;
|
|
@@ -160,7 +155,6 @@ export function merge(base, mine, theirs, options = {}) {
|
|
|
160
155
|
if ([...removed[0].keys()].some(name => removed[1].has(name)) &&
|
|
161
156
|
(added[0].size !== added[1].size || [...added[0].keys()].some(name => !added[1].has(name)))) throw null;
|
|
162
157
|
if (plan) {
|
|
163
|
-
// Project original ranges once. Nested moves never rebuild aligned documents.
|
|
164
158
|
const chunks = [], conflicts = [];
|
|
165
159
|
let length = 0;
|
|
166
160
|
function project(item) {
|
|
@@ -183,8 +177,6 @@ export function merge(base, mine, theirs, options = {}) {
|
|
|
183
177
|
const changes = options._edits || [mine, theirs].map((text, side) => diff(base, text,
|
|
184
178
|
{...budget, structure: trees[0] && options.structure, _observed: trees[0] ? [trees[0], trees[side + 1]] : undefined}).edits);
|
|
185
179
|
if (trees.some(Boolean) && budget._budget.left < 0) return complete();
|
|
186
|
-
// An edit reaches over the words it touches; edits whose reaches overlap form one group, so letters
|
|
187
|
-
// joined from both branches never form a word neither wrote.
|
|
188
180
|
const spans = words(wordSegmenter(), base);
|
|
189
181
|
const word = at => {
|
|
190
182
|
let low = 0, high = spans.length;
|
|
@@ -210,7 +202,6 @@ export function merge(base, mine, theirs, options = {}) {
|
|
|
210
202
|
row[1] = edit[1];
|
|
211
203
|
if (index === edits.length) break;
|
|
212
204
|
const [a, b, t, side2, from] = edits[index], peer = sides[1 - side2][2];
|
|
213
|
-
// One branch's insertion and replacement at the same base position are one contiguous edit.
|
|
214
205
|
if (!(a < end || a === start || from < reach || edits[index - 1][0] === a && edits[index - 1][3] === side2 || peer &&
|
|
215
206
|
(a === end && peer[0] === end || a === b && peer[0] < end &&
|
|
216
207
|
a - end < t.length && (peer[2] + base.slice(end, a)).endsWith(t)))) break;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {separate, phrases} from './concerns.mjs';
|
|
3
3
|
|
|
4
|
-
// Moves bind equal transported fragments at both ends; renames group a binding's sites.
|
|
5
4
|
export function moves(c) {
|
|
6
5
|
const {before, after, spend, safe, budget, raw, code, tokens, matches, walk, structures, pa, pb, la, aliases} = c;
|
|
7
6
|
const overlaps = (a, b) => a[0] < b[1] && a[1] > b[0];
|
|
@@ -9,7 +8,6 @@ export function moves(c) {
|
|
|
9
8
|
c[side][0] >= range[0] && c[side][1] <= range[1]);
|
|
10
9
|
try { if (!raw && budget.left >= 0) {
|
|
11
10
|
const check = n => { if (!spend(n)) throw null; };
|
|
12
|
-
// Split transported text at wording boundaries; positions and words own distinct edits.
|
|
13
11
|
const deleted = new Map();
|
|
14
12
|
const moveKey = (item, side, text, parts = []) => {
|
|
15
13
|
const [from, to] = item[side];
|
|
@@ -28,7 +26,6 @@ export function moves(c) {
|
|
|
28
26
|
for (const item of c.result) if (item[3][0] === 2 && !item[2]) {
|
|
29
27
|
const value = after.slice(...item[1]), key = moveKey(item, 1, after, pb);
|
|
30
28
|
let old = deleted.get(key), score = 0;
|
|
31
|
-
// Edited wording still travels: plain text, or a prose part of the same kind.
|
|
32
29
|
const text = value.trim(), prose = !structures || /^(paragraph|item|task|row|quote|section)$/.test(item[3][1]);
|
|
33
30
|
if (!old && prose && text.length >= 24) for (const candidate of deleted.values()) {
|
|
34
31
|
check(1);
|
|
@@ -41,7 +38,6 @@ export function moves(c) {
|
|
|
41
38
|
if (n * 5 > Math.max(prior.length, text.length) * 3 && n >= score) { old = n === score ? null : candidate; score = n; }
|
|
42
39
|
}
|
|
43
40
|
if (!old || old[4]) continue;
|
|
44
|
-
// Separators that only one end carries stay outside the transported text.
|
|
45
41
|
const x = [...old[0]], y = [...item[1]];
|
|
46
42
|
for (const end of [0, 1]) {
|
|
47
43
|
const pattern = end ? /\s*$/ : /^\s*/, left = before.slice(...x).match(pattern)[0], right = after.slice(...y).match(pattern)[0];
|
|
@@ -52,7 +48,6 @@ export function moves(c) {
|
|
|
52
48
|
}
|
|
53
49
|
const outer = c.result; c.result = [];
|
|
54
50
|
walk(...x, ...y, 1);
|
|
55
|
-
// A changed heading can occupy a whole line while its marker still anchors the move.
|
|
56
51
|
if (c.result.length && (c.result[0][0][0] === x[0] || c.result[0][1][0] === y[0] ||
|
|
57
52
|
c.result.at(-1)[0][1] === x[1] || c.result.at(-1)[1][1] === y[1])) {
|
|
58
53
|
c.result = []; walk(...x, ...y, 2);
|
|
@@ -60,7 +55,6 @@ export function moves(c) {
|
|
|
60
55
|
separate(c, x[1], y[1]);
|
|
61
56
|
phrases(c);
|
|
62
57
|
const inner = c.result; c.result = outer;
|
|
63
|
-
// Equal boundary fragments make the exact extent recoverable without display context.
|
|
64
58
|
if (inner.length && (inner[0][0][0] === x[0] || inner[0][1][0] === y[0] ||
|
|
65
59
|
inner.at(-1)[0][1] === x[1] || inner.at(-1)[1][1] === y[1] || inner.some(c => c[2]))) continue;
|
|
66
60
|
const move = [3, item[3][2] ? item[3][1] : code ? 'line' : 'block', item[3][2], [x, y]];
|
|
@@ -69,7 +63,6 @@ export function moves(c) {
|
|
|
69
63
|
if (a0 < a1) old[4].push([[a0, a1], [old[1][0], old[1][0]], false, group]);
|
|
70
64
|
if (b0 < b1) item[4].push([[item[0][0], item[0][0]], [b0, b1], false, group]);
|
|
71
65
|
};
|
|
72
|
-
// Unmatched outer separators travel with the move, so restoring it alone keeps blocks apart.
|
|
73
66
|
fragment(old[0][0], x[0], 0, 0, move); fragment(0, 0, item[1][0], y[0], move);
|
|
74
67
|
let a = x[0], b = y[0], wording;
|
|
75
68
|
for (const edit of inner) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Capture only the reader contract before another observation can reuse its objects.
|
|
3
2
|
export function observe(text, reader, spend, safe, references = true, lenient = false) {
|
|
4
3
|
let value;
|
|
5
4
|
try { value = reader(text); } catch (error) { return {invalid: true, threw: true, error}; }
|
|
@@ -30,7 +29,6 @@ export function observe(text, reader, spend, safe, references = true, lenient =
|
|
|
30
29
|
if (!usable && !lenient) throw null;
|
|
31
30
|
const part = {start, end: stop, kind: scalar(kind), name: scalar(name), key: scalar(key, true)};
|
|
32
31
|
if (nameAt != null) part.nameAt = span(nameAt, start, stop);
|
|
33
|
-
// Discard unsafe evidence only after validating its complete subtree.
|
|
34
32
|
if (retained && usable) { parts.push(part); flat.push(part); }
|
|
35
33
|
part.children = children == null ? [] : copy(children, start, stop, depth + 1, retained && usable);
|
|
36
34
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Space-free scripts (Thai, Lao, Myanmar, Khmer, kana, Han) have no word separator; the platform dictionary finds words.
|
|
3
2
|
export const SPACELESS = /[\u0e00-\u0eff\u1000-\u109f\u1780-\u17ff\u3040-\u30ff\u3400-\u9fff\uf900-\ufaff]/;
|
|
4
3
|
export const words = (segmenter, text) => [...text.matchAll(/[\p{L}\p{N}\p{M}_]+/gu)].flatMap(word => SPACELESS.test(word[0])
|
|
5
4
|
? [...segmenter().segment(word[0])].map(s => [word.index + s.index, word.index + s.index + s.segment.length])
|
|
6
5
|
: [[word.index, word.index + word[0].length]]);
|
|
7
6
|
export const wordSegmenter = () => { let segmenter; return () => segmenter ||= new Intl.Segmenter('en', {granularity: 'word'}); };
|
|
8
7
|
|
|
9
|
-
// Segment only requested non-ASCII boundaries; observations never share a global document cache.
|
|
10
8
|
export function boundaries() {
|
|
11
9
|
const segments = Object.create(null);
|
|
12
10
|
let segmenter, last = [];
|
|
@@ -15,7 +13,6 @@ export function boundaries() {
|
|
|
15
13
|
const a = text.charCodeAt(at - 1), b = text.charCodeAt(at);
|
|
16
14
|
if ((a & 0xfc00) === 0xd800 && (b & 0xfc00) === 0xdc00 || a === 13 && b === 10) return false;
|
|
17
15
|
if (a < 0x300 && b < 0x300) return true;
|
|
18
|
-
// Retreats probe one long cluster many times; answer inside the last cluster without the segmenter.
|
|
19
16
|
if (last[0] === text && at > last[1] && at < last[2]) return false;
|
|
20
17
|
const found = (segments[text] ||= (segmenter ||= new Intl.Segmenter('en', {granularity: 'grapheme'})).segment(text)).containing(at);
|
|
21
18
|
last = [text, found.index, found.index + found.segment.length];
|
|
@@ -23,16 +20,13 @@ export function boundaries() {
|
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
// Letters are code points: an astral letter is one letter, never two lone surrogates.
|
|
27
23
|
const pair = (text, at) => (text.charCodeAt(at - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(at) & 0xfc00) === 0xdc00;
|
|
28
24
|
export const prev = (text, at) => text.slice(Math.max(0, at - 1 - pair(text, at - 1)), at), next = (text, at) => text.slice(at, at + 1 + pair(text, at + 1));
|
|
29
25
|
export const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c), within = (text, at) => letter(prev(text, at)) && letter(next(text, at));
|
|
30
26
|
|
|
31
|
-
// Tokens at a level: 0 parts, 1 lines, 2 words, spaces and marks, 3 characters.
|
|
32
27
|
export function tokenizer(c) {
|
|
33
28
|
const {spend, safe, segmenter, raw, aliases} = c;
|
|
34
29
|
return function tokens(text, start, end, level, parts) {
|
|
35
|
-
// Each source boundary is stored once, alongside comparison keys and optional owners.
|
|
36
30
|
const keys = [], at = [start], owners = [];
|
|
37
31
|
const out = [keys, at, owners];
|
|
38
32
|
const add = (a, b, part, value) => {
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {words, letter, within, prev, next} from './tokens.mjs';
|
|
3
3
|
|
|
4
|
-
// Exact source records for a range: lines, then words, then characters, or structural parts when supplied.
|
|
5
4
|
export function walker(c) {
|
|
6
5
|
const {before, after, spend, safe, budget, raw, chars, code, segmenter, tokens, matches} = c;
|
|
7
6
|
function record(a0, a1, b0, b1, unit, part, coarse = false) {
|
|
8
7
|
if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
|
|
9
|
-
// Internal records: before, after, coarse, concern, transported fragments.
|
|
10
8
|
c.result.push([[a0, a1], [b0, b1], coarse, [a0 === a1 ? 2 : b0 === b1 ? 0 : 1, part?.kind || unit, part?.name]]);
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
// A line deletion or insertion holding whole named parts and only blank text besides is one record per part,
|
|
14
|
-
// cut at line starts, so each keeps its own name (and its callers' breaks).
|
|
15
11
|
function name(from, ownersA, ownersB) {
|
|
16
12
|
for (let r = from; r < c.result.length; r++) {
|
|
17
13
|
const [[a0, a1], [b0, b1]] = c.result[r], side = a0 === a1 ? 1 : b0 === b1 ? 0 : -1;
|
|
@@ -34,7 +30,6 @@ export function walker(c) {
|
|
|
34
30
|
}
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
// A search step scans at most one fixed-size source chunk.
|
|
38
33
|
function common(a, b, end, step) {
|
|
39
34
|
let n = 0;
|
|
40
35
|
while (n < end && spend()) {
|
|
@@ -95,7 +90,6 @@ export function walker(c) {
|
|
|
95
90
|
}
|
|
96
91
|
} else if (level < 2 && raw !== 'line') lines(x.slice(ai, i + 1), y.slice(bi, j + 1), context);
|
|
97
92
|
else if (level === 2 && chars && !raw && i - ai <= 1 && j - bi <= 1 && x1 - x0 + y1 - y0 < 100) {
|
|
98
|
-
// A changed token is replaced whole unless the caller asks for the characters inside it.
|
|
99
93
|
walk(x0, x1, y0, y1, 3, null, null, context);
|
|
100
94
|
} else record(x0, x1, y0, y1, unit, context);
|
|
101
95
|
}
|
|
@@ -108,12 +102,9 @@ export function walker(c) {
|
|
|
108
102
|
}
|
|
109
103
|
}
|
|
110
104
|
|
|
111
|
-
// Unequal lines pair in order by shared words. A line sharing under a quarter of its words with every candidate is
|
|
112
|
-
// replaced whole: an inserted line never borrows a neighbor's words, and a rewrite never reads as stray fragments.
|
|
113
105
|
function lines(x, y, context) {
|
|
114
106
|
const n = x.length - 1, m = y.length - 1;
|
|
115
107
|
if (n * m > 4096 || !spend(n * m + x[n] - x[0] + y[m] - y[0])) return row(x[0], x[n], y[0], y[m], context);
|
|
116
|
-
// A renamed name's sites count as shared words.
|
|
117
108
|
if (!c.renames) {
|
|
118
109
|
c.renames = [[], []];
|
|
119
110
|
for (const [p, old] of c.aliases || []) if (before.slice(...old.nameAt) !== after.slice(...p.nameAt)) {
|
|
@@ -141,7 +132,6 @@ export function walker(c) {
|
|
|
141
132
|
}
|
|
142
133
|
let k = 0, l = 0;
|
|
143
134
|
for (const [p, q] of [...pairs, [n, m]]) {
|
|
144
|
-
// A merge compares one line against one line by character, whatever they share.
|
|
145
135
|
if (raw === 'char' && p - k === 1 && q - l === 1) row(x[k], x[p], y[l], y[q], context);
|
|
146
136
|
else if (k < p || l < q) record(x[k], x[p], y[l], y[q], 'line', context);
|
|
147
137
|
if (p < n) row(x[p], x[p + 1], y[q], y[q + 1], context);
|
|
@@ -149,7 +139,6 @@ export function walker(c) {
|
|
|
149
139
|
}
|
|
150
140
|
}
|
|
151
141
|
|
|
152
|
-
// A table row with the same number of cells on both sides changes cell by cell; a character diff stays in its line.
|
|
153
142
|
function row(a0, a1, b0, b1, context) {
|
|
154
143
|
if (raw === 'char') return walk(a0, a1, b0, b1, 3, null, null, context);
|
|
155
144
|
const cells = (text, a, b) => /^[ \t]*\|/.test(text.slice(a, b)) && [...text.slice(a, b).matchAll(/(?<!\\)\|/g)].map(m => a + m.index);
|
|
@@ -158,7 +147,6 @@ export function walker(c) {
|
|
|
158
147
|
for (let k = 0; k <= p.length; k++) walk(k ? p[k - 1] + 1 : a0, p[k] ?? a1, k ? q[k - 1] + 1 : b0, q[k] ?? b1, 2, null, null, context);
|
|
159
148
|
}
|
|
160
149
|
|
|
161
|
-
// Surviving source pieces keep the occurrence witnessed by the host.
|
|
162
150
|
function operations(rows) {
|
|
163
151
|
if (!Array.isArray(rows)) return null;
|
|
164
152
|
const pieces = [[0, before.length]];
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Embedded fonts are ordinary CommonMark reference definitions. Parsing owns spans, never bytes.
|
|
3
2
|
import {markdownParser, installMarkdownImages, normalizeLabel} from './md-assets.mjs';
|
|
4
3
|
import {splitOpeningFrontmatter} from './frontmatter.mjs';
|
|
5
4
|
import {readDocumentSettings, documentFont} from './document-settings.mjs';
|
|
6
5
|
import {normalizeFontFamily, fontFamilyCss, FONT_STACKS, typography} from './md-marks.mjs';
|
|
7
6
|
|
|
8
|
-
// Unquoted CSS generic families require no portable asset. Quoted names remain custom.
|
|
9
7
|
export const CSS_GENERIC_FONT_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
|
|
10
8
|
'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong', 'initial', 'revert', 'revert-layer']);
|
|
11
9
|
|
|
12
|
-
// No decoding or external resource loading happens here.
|
|
13
10
|
export function fontDataUrl(url) {
|
|
14
11
|
if (typeof url !== 'string') return null;
|
|
15
12
|
const match = /^data:font\/(woff2|woff|ttf|otf);base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
|
|
@@ -123,8 +120,6 @@ function escapeFontLabel(label) {
|
|
|
123
120
|
return String(label).replace(/[\\[\]]/g, '\\$&').replace(/&/g, '&');
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
// Unchanged faces keep their original definition, whitespace and line ending. Only supplied font
|
|
127
|
-
// assets are replaced; pictures, other references and malformed authored source stay untouched.
|
|
128
123
|
export function writeFontAssets(markdown, assets, parser) {
|
|
129
124
|
const source = String(markdown ?? ''), md = parserFor(parser), index = readFontAssets(source, md), wanted = new Map();
|
|
130
125
|
for (const asset of assets || []) {
|
|
@@ -189,8 +184,6 @@ function htmlFontFamilies(html, md) {
|
|
|
189
184
|
for (const attr of tag[0].matchAll(/\s(data-md-font|style)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) {
|
|
190
185
|
const value = decode(attr[2] ?? attr[3] ?? attr[4]), names = [];
|
|
191
186
|
if (/^<span(?=[\s>])/i.test(tag[0])) {
|
|
192
|
-
// Raw or unpaired spans use the same admission as paired typography. A CSS
|
|
193
|
-
// resource expression or authored data-* hook cannot request a font asset.
|
|
194
187
|
const family = attr[1].toLowerCase() === 'style' ? typography.parseStyle(value).family : null;
|
|
195
188
|
if (family?.custom) families.add(family.name);
|
|
196
189
|
continue;
|
|
@@ -212,8 +205,6 @@ function htmlFontFamilies(html, md) {
|
|
|
212
205
|
return families;
|
|
213
206
|
}
|
|
214
207
|
|
|
215
|
-
// Tokens supply decoded characters: reference payloads, link destinations and Markdown punctuation
|
|
216
|
-
// never inflate a subset. Each face in a family receives the family's complete text for shaping.
|
|
217
208
|
export function documentFontUsage(markdown, parser, defaultFont) {
|
|
218
209
|
const source = String(markdown ?? ''), md = parserFor(parser), settings = readDocumentSettings(source), use = new Map();
|
|
219
210
|
const family = setting => setting?.stack === 'named' ? setting.family : null;
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// One reader of document-wide settings and the inherited font snapshot.
|
|
3
|
-
// Framing stays in spec/frontmatter.mjs. Reading never rewrites source.
|
|
4
|
-
// The live view reads fontsize, the three font scopes and linestretch. It ignores papersize,
|
|
5
|
-
// geometry and pagestyle: a scroll has no page. Word and the PDF read all of them.
|
|
6
2
|
import {frontMatterFrame, frontMatterLine} from './frontmatter.mjs';
|
|
7
3
|
import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
|
|
8
4
|
|
|
9
|
-
// Twelve points are one reference unit (16 px at the default reading scale).
|
|
10
5
|
const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
11
6
|
export const DOCUMENT_LEADING = 1.75 / 1.1;
|
|
12
7
|
|
|
13
|
-
// Before/after are points; line is a multiple of the element's own type size.
|
|
14
|
-
// Presets and native exports read this same table. Explicit measures do not grow with font size.
|
|
15
8
|
export const SPACING_TYPES = Object.freeze(['paragraph', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
16
9
|
'list', 'item', 'table', 'picture', 'drawing', 'quote', 'code', 'rule', 'footnote', 'toc']);
|
|
17
10
|
export const SPACING_PRESETS = Object.freeze({compact: .7, standard: 1, roomy: 1.5});
|
|
@@ -60,7 +53,6 @@ export function documentSpacing(settings, kind = 'paragraph') {
|
|
|
60
53
|
return {before: before * scale * size, after: after * scale * size, line, ...spacing?.[type]};
|
|
61
54
|
}
|
|
62
55
|
|
|
63
|
-
// Word's widely supported Calibri/11 pt Normal template. Explicit document settings always win.
|
|
64
56
|
export function wordDocumentSettings(settings) {
|
|
65
57
|
return {...settings, word: true, fontsize: settings?.fontsize || fontSize('11pt'),
|
|
66
58
|
mainfont: settings?.mainfont || documentFont('Calibri'), sansfont: settings?.sansfont || documentFont('Calibri'),
|
|
@@ -80,15 +72,17 @@ export function documentSpacingCss(settings, scope = '.md-render', defaults = fa
|
|
|
80
72
|
css += selector + '{margin-block:' + length(row.before) + ' ' + length(row.after) + ';line-height:' + row.line + '}';
|
|
81
73
|
if (settings?.word && /^h[1-6]$/.test(type)) css += selector + '{font-size:' + length(documentHeadingSize(settings, Number(type[1]))) + '}';
|
|
82
74
|
}
|
|
83
|
-
|
|
84
|
-
css += scope + ' :is(li,td,th,blockquote)>:last-child{margin-bottom:0}' +
|
|
75
|
+
css += scope + ' :is(li,td,th,blockquote)>:last-child:not(input){margin-bottom:0}' +
|
|
85
76
|
scope + ' li:last-child{margin-bottom:0}' + scope + ' li>:is(ul,ol){margin-bottom:0}' +
|
|
86
77
|
scope + ' .table-scroll-wrap>table{margin-block:0}' +
|
|
87
78
|
scope + ' pre{font-size:calc(var(--md-text-body) * .7386)}' +
|
|
88
79
|
scope + ' pre code{line-height:inherit}' + scope + ' .footnotes{padding-top:0}' +
|
|
89
80
|
scope + ' .footnotes-list{margin-block:0}' + scope + ' blockquote:not(.callout){padding-block:0}';
|
|
90
81
|
const item = documentSpacing(settings, 'item');
|
|
91
|
-
css += scope + '
|
|
82
|
+
css += scope + ' li{--md-item-gap:' + length(Math.max(item.before, item.after)) + '}';
|
|
83
|
+
const list = documentSpacing(settings, 'list'), heading = documentSpacing(settings, 'h2');
|
|
84
|
+
css += scope + '{--md-list-heading-before:' + length(Math.max(list.after, heading.before)) + ';--md-list-heading-after:' + length(Math.max(heading.after, list.before)) + '}';
|
|
85
|
+
css += scope + ' ul.rapier-jump-list>li{--md-jump-pad:0px;margin-block:' + length(item.before) + ' ' + length(item.after) + '}' +
|
|
92
86
|
scope + ' ul.rapier-jump-list>li:last-child{margin-bottom:0}';
|
|
93
87
|
for (const [type, selector] of [['item','li>p'], ['table',':is(td,th)>p'], ['quote','blockquote>p'], ['footnote','.footnotes p'], ['toc','.rapier-jump-list li']])
|
|
94
88
|
css += scope + ' ' + selector + '{line-height:' + documentSpacing(settings, type).line + '}';
|
|
@@ -144,7 +138,6 @@ function marginOf(value) {
|
|
|
144
138
|
return Object.freeze({margin: match[1] + unit, twips});
|
|
145
139
|
}
|
|
146
140
|
|
|
147
|
-
// Token spans retain the source's quotes, comments and collection punctuation.
|
|
148
141
|
function quotedEnd(text, start = 0, limit = text.length) {
|
|
149
142
|
const quote = text[start];
|
|
150
143
|
for (let at = start + 1; at < limit; at++) {
|
|
@@ -223,7 +216,6 @@ function fieldLine(line, indented = false) {
|
|
|
223
216
|
function valueSpan(source, field, limit) {
|
|
224
217
|
let at = field.start;
|
|
225
218
|
const first = source[at];
|
|
226
|
-
// Tags and anchors may name other YAML values. They are retained, never rewritten by a control.
|
|
227
219
|
if (first === '&' || first === '*' || first === '!') {
|
|
228
220
|
field.opaque = true;
|
|
229
221
|
while (at < field.line.end && /[&*!]/.test(source[at])) {
|
|
@@ -268,7 +260,6 @@ function mapFields(source, lines, indent) {
|
|
|
268
260
|
return {fields, invalid};
|
|
269
261
|
}
|
|
270
262
|
|
|
271
|
-
// An append must never enter an unclosed collection or quoted value, including one under an unrelated key.
|
|
272
263
|
function structureIsClosed(source, lines) {
|
|
273
264
|
let blockIndent = null, through = 0;
|
|
274
265
|
const limit = lines.at(-1)?.end ?? 0;
|
|
@@ -305,7 +296,6 @@ export function documentFont(value) {
|
|
|
305
296
|
return Object.freeze(named ? {...named, family} : {family, stack: 'named', css: fontFamilyCss(family), word: family});
|
|
306
297
|
}
|
|
307
298
|
|
|
308
|
-
// Font controls use the same source spans as the other document settings.
|
|
309
299
|
export function writeDocumentFontSetting(markdown, key, value) {
|
|
310
300
|
if (!['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key)) throw new TypeError('font: unknown document key');
|
|
311
301
|
const family = value == null ? null : normalizeFontFamily(value);
|
|
@@ -313,7 +303,6 @@ export function writeDocumentFontSetting(markdown, key, value) {
|
|
|
313
303
|
return updateDocumentSettings(markdown, {[key]: family});
|
|
314
304
|
}
|
|
315
305
|
|
|
316
|
-
// A language tag is metadata, never CSS or markup. Its source spelling stays untouched.
|
|
317
306
|
export function languageTag(value) {
|
|
318
307
|
if (typeof value !== 'string' || value.length > 128 || !/^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/i.test(value)) return null;
|
|
319
308
|
try { return Intl.getCanonicalLocales(value)[0] || null; }
|
|
@@ -324,7 +313,6 @@ export function documentLanguage(settings, device = globalThis.navigator?.langua
|
|
|
324
313
|
return languageTag(settings?.lang) || languageTag(device) || 'en';
|
|
325
314
|
}
|
|
326
315
|
|
|
327
|
-
// Unrecognized fields remain authored bytes. A duplicate key has no single value.
|
|
328
316
|
export function readDocumentSettings(markdown) {
|
|
329
317
|
const source = String(markdown ?? ''), {frame, fields} = fieldsOf(source);
|
|
330
318
|
if (!frame.closing) return EMPTY;
|
|
@@ -347,7 +335,6 @@ export function readDocumentSettings(markdown) {
|
|
|
347
335
|
});
|
|
348
336
|
}
|
|
349
337
|
|
|
350
|
-
// Change only supplied settings. Unrelated YAML, comments, line endings, BOM and body are never reprinted.
|
|
351
338
|
export function updateDocumentSettings(markdown, patch) {
|
|
352
339
|
const source = String(markdown ?? ''), parsed = fieldsOf(source), {frame, fields} = parsed, before = readDocumentSettings(source);
|
|
353
340
|
const changes = [], append = [];
|
|
@@ -389,15 +376,11 @@ export function updateDocumentSettings(markdown, patch) {
|
|
|
389
376
|
return next;
|
|
390
377
|
}
|
|
391
378
|
|
|
392
|
-
// The page's <title>, the PDF's document title. Subtitle follows the title.
|
|
393
|
-
// An absent title returns '' so the caller keeps the filename.
|
|
394
379
|
export function documentTitle(settings) {
|
|
395
380
|
if (!settings || !settings.title) return '';
|
|
396
381
|
return settings.subtitle ? settings.title + ' \u2014 ' + settings.subtitle : settings.title;
|
|
397
382
|
}
|
|
398
383
|
|
|
399
|
-
// A supplied personal default replaces only the inherited snapshot. Explicit scopes win.
|
|
400
|
-
// Omitted means use the portable source; null means the reader's factory default.
|
|
401
384
|
export function documentSettingsFonts(settings, defaultFont) {
|
|
402
385
|
const inherited = defaultFont === undefined ? settings?.defaultfont : documentFont(defaultFont);
|
|
403
386
|
const main = settings?.mainfont || inherited;
|
|
@@ -416,8 +399,6 @@ export function documentSettingsStyle(settings, defaultFont) {
|
|
|
416
399
|
return (decls.length ? '.md-render{' + decls.join(';') + '}' : '') + documentSpacingCss(settings);
|
|
417
400
|
}
|
|
418
401
|
|
|
419
|
-
// Null when the page keys do not change the print sheet. The view never asks.
|
|
420
|
-
// The default rule stays the writer's own: size:auto; margin:12mm 14mm 14mm.
|
|
421
402
|
export function documentSettingsPageRule(settings) {
|
|
422
403
|
if (!settings || (!settings.papersize && !settings.geometry && settings.pagestyle !== 'plain')) return null;
|
|
423
404
|
const size = settings.papersize ? settings.papersize.css : 'auto';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Framing owns only offsets. No field interpretation or source normalization belongs here.
|
|
3
2
|
export function frontMatterLine(text, start) {
|
|
4
3
|
let end = start;
|
|
5
4
|
while (end < text.length && text[end] !== '\r' && text[end] !== '\n') end++;
|