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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {words, letter, within, prev, next} from './tokens.mjs';
|
|
3
|
+
|
|
4
|
+
// Exact source records for a range: lines, then words, then characters, or structural parts when supplied.
|
|
5
|
+
export function walker(c) {
|
|
6
|
+
const {before, after, spend, safe, budget, raw, chars, code, segmenter, tokens, matches} = c;
|
|
7
|
+
function record(a0, a1, b0, b1, unit, part, coarse = false) {
|
|
8
|
+
if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
|
|
9
|
+
// Internal records: before, after, coarse, concern, transported fragments.
|
|
10
|
+
c.result.push([[a0, a1], [b0, b1], coarse, [a0 === a1 ? 2 : b0 === b1 ? 0 : 1, part?.kind || unit, part?.name]]);
|
|
11
|
+
}
|
|
12
|
+
|
|
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
|
+
function name(from, ownersA, ownersB) {
|
|
16
|
+
for (let r = from; r < c.result.length; r++) {
|
|
17
|
+
const [[a0, a1], [b0, b1]] = c.result[r], side = a0 === a1 ? 1 : b0 === b1 ? 0 : -1;
|
|
18
|
+
if (side < 0) continue;
|
|
19
|
+
const [s0, s1] = c.result[r][side], text = side ? after : before;
|
|
20
|
+
const parts = (side ? ownersB : ownersA).filter(part => part?.name && part.start >= s0 && part.end <= s1);
|
|
21
|
+
if (!parts.length || !spend(s1 - s0)) continue;
|
|
22
|
+
let blank = text.slice(s0, parts[0].start);
|
|
23
|
+
for (let k = 1; k <= parts.length; k++) blank += text.slice(parts[k - 1].end, parts[k]?.start ?? s1);
|
|
24
|
+
if (/\S/.test(blank)) continue;
|
|
25
|
+
const rows = parts.map((part, k) => {
|
|
26
|
+
const start = k ? Math.max(parts[k - 1].end, text.lastIndexOf('\n', part.start - 1) + 1) : s0;
|
|
27
|
+
return [start, part];
|
|
28
|
+
});
|
|
29
|
+
c.result.splice(r, 1, ...rows.map(([start, part], k) => {
|
|
30
|
+
const end = rows[k + 1]?.[0] ?? s1, here = [side ? [a0, a0] : [start, end], side ? [start, end] : [b0, b0]];
|
|
31
|
+
return [here[0], here[1], false, [side ? 2 : 0, part.kind, part.name]];
|
|
32
|
+
}));
|
|
33
|
+
r += rows.length - 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A search step scans at most one fixed-size source chunk.
|
|
38
|
+
function common(a, b, end, step) {
|
|
39
|
+
let n = 0;
|
|
40
|
+
while (n < end && spend()) {
|
|
41
|
+
const stop = Math.min(end, n + 256);
|
|
42
|
+
while (n < stop && before.charCodeAt(a) === after.charCodeAt(b)) { n++; a += step; b += step; }
|
|
43
|
+
if (n < stop) break;
|
|
44
|
+
}
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function walk(a0, a1, b0, b1, level, ap, bp, context) {
|
|
49
|
+
if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
|
|
50
|
+
if (!spend()) { record(a0, a1, b0, b1, 'block', context, true); return; }
|
|
51
|
+
const structural = level <= 0 && level > -128 && (ap?.length || bp?.length);
|
|
52
|
+
if (!structural && raw !== 'word') {
|
|
53
|
+
let n = common(a0, b0, Math.min(a1 - a0, b1 - b0), 1);
|
|
54
|
+
while (n && (!safe(before, a0 + n) || !safe(after, b0 + n) || !raw && level < 3 && (within(before, a0 + n) || within(after, b0 + n)))) {
|
|
55
|
+
if (!spend()) { n = 0; break; } n--;
|
|
56
|
+
}
|
|
57
|
+
if (level === 1 && n) {
|
|
58
|
+
const next = Math.max(0, before.lastIndexOf('\n', a0 + n - 1) + 1 - a0);
|
|
59
|
+
spend(Math.ceil((n - next) / 256)); n = next;
|
|
60
|
+
}
|
|
61
|
+
a0 += n; b0 += n;
|
|
62
|
+
n = common(a1 - 1, b1 - 1, Math.min(a1 - a0, b1 - b0), -1);
|
|
63
|
+
while (n && (!safe(before, a1 - n) || !safe(after, b1 - n) || !raw && level < 3 && (within(before, a1 - n) || within(after, b1 - n)))) {
|
|
64
|
+
if (!spend()) { n = 0; break; } n--;
|
|
65
|
+
}
|
|
66
|
+
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--; }
|
|
67
|
+
a1 -= n; b1 -= n;
|
|
68
|
+
if (budget.left < 0) { record(a0, a1, b0, b1, 'block', context, true); return; }
|
|
69
|
+
}
|
|
70
|
+
let unit = level >= 3 ? 'char' : level === 2 ? 'word' : 'line';
|
|
71
|
+
if (!raw && level === 2 && a1 - a0 + b1 - b0 <= 4 && [...before.slice(a0, a1), ...after.slice(b0, b1)].length <= 2 &&
|
|
72
|
+
letter(prev(before, a0) || prev(after, b0)) && letter(next(before, a1) || next(after, b1))) unit = 'char';
|
|
73
|
+
if (!structural && (a0 === a1 || b0 === b1)) { record(a0, a1, b0, b1, unit, context); return; }
|
|
74
|
+
const left = tokens(before, a0, a1, structural ? 0 : Math.max(1, level), ap);
|
|
75
|
+
const right = tokens(after, b0, b1, structural ? 0 : Math.max(1, level), bp);
|
|
76
|
+
if (!left || !right) { record(a0, a1, b0, b1, unit, context, true); return; }
|
|
77
|
+
const [a, x, ownersA] = left, [b, y, ownersB] = right, pairs = matches(a, b);
|
|
78
|
+
let ai = 0, bi = 0;
|
|
79
|
+
for (let i = 0; i <= a.length; i++) {
|
|
80
|
+
const j = i === a.length ? b.length : pairs[i];
|
|
81
|
+
if (j === undefined) continue;
|
|
82
|
+
const x0 = x[ai], x1 = x[i], y0 = y[bi], y1 = y[j];
|
|
83
|
+
if (ai < i || bi < j) {
|
|
84
|
+
if (budget.left < 0 || pairs.coarse?.has(ai)) record(x0, x1, y0, y1, unit, context, true);
|
|
85
|
+
else if (structural && (ai === i || bi === j)) {
|
|
86
|
+
for (let k = ai; k < i; k++) record(x[k], x[k + 1], y0, y0, unit, ownersA[k] || context);
|
|
87
|
+
for (let k = bi; k < j; k++) record(x1, x1, y[k], y[k + 1], unit, ownersB[k] || context);
|
|
88
|
+
} else if (structural) {
|
|
89
|
+
const p = ownersA[ai], q = ownersB[bi];
|
|
90
|
+
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);
|
|
91
|
+
else {
|
|
92
|
+
const from = c.result.length;
|
|
93
|
+
walk(x0, x1, y0, y1, 1, null, null, context);
|
|
94
|
+
name(from, ownersA.slice(ai, i), ownersB.slice(bi, j));
|
|
95
|
+
}
|
|
96
|
+
} else if (level < 2 && raw !== 'line') lines(x.slice(ai, i + 1), y.slice(bi, j + 1), context);
|
|
97
|
+
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
|
+
walk(x0, x1, y0, y1, 3, null, null, context);
|
|
100
|
+
} else record(x0, x1, y0, y1, unit, context);
|
|
101
|
+
}
|
|
102
|
+
if (i < a.length) {
|
|
103
|
+
const p = ownersA[i], q = ownersB[j];
|
|
104
|
+
if ((p || q || level <= 1 && !raw) && before.slice(x[i], x[i + 1]) !== after.slice(y[j], y[j + 1]))
|
|
105
|
+
walk(x[i], x[i + 1], y[j], y[j + 1], p || q ? level - 1 : 3, p?.children, q?.children, q || p || context);
|
|
106
|
+
}
|
|
107
|
+
ai = i + 1; bi = j + 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
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 neighbour's words, and a rewrite never reads as stray fragments.
|
|
113
|
+
function lines(x, y, context) {
|
|
114
|
+
const n = x.length - 1, m = y.length - 1;
|
|
115
|
+
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
|
+
if (!c.renames) {
|
|
118
|
+
c.renames = [[], []];
|
|
119
|
+
for (const [p, old] of c.aliases || []) if (before.slice(...old.nameAt) !== after.slice(...p.nameAt)) {
|
|
120
|
+
c.renames[0].push(old.nameAt, ...c.la.filter(l => l.key === old.key).map(l => l.at));
|
|
121
|
+
c.renames[1].push(p.nameAt, ...c.lb.filter(l => l.key === p.key).map(l => l.at));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const weight = (side, a, b) => {
|
|
125
|
+
const counts = new Map(), text = side ? after : before;
|
|
126
|
+
for (const [c, d] of words(segmenter, text.slice(a, b))) counts.set(text.slice(a + c, a + d), (counts.get(text.slice(a + c, a + d)) || 0) + d - c);
|
|
127
|
+
return [counts, [...counts.values()].reduce((n, v) => n + v, 0), c.renames[side].reduce((n, at) => n + Math.max(0, Math.min(b, at[1]) - Math.max(a, at[0])), 0)];
|
|
128
|
+
};
|
|
129
|
+
const left = x.slice(0, -1).map((at, k) => weight(0, at, x[k + 1])), right = y.slice(0, -1).map((at, k) => weight(1, at, y[k + 1]));
|
|
130
|
+
const best = Array.from({length: n + 1}, () => new Float64Array(m + 1)), pairs = [];
|
|
131
|
+
for (let k = 1; k <= n; k++) for (let l = 1; l <= m; l++) {
|
|
132
|
+
const [a, sa, ra] = left[k - 1], [b, sb, rb] = right[l - 1];
|
|
133
|
+
let shared = sa || sb ? Math.max(ra, rb) : 0.5;
|
|
134
|
+
for (const [key, v] of a) shared += Math.min(v, b.get(key) || 0);
|
|
135
|
+
best[k][l] = Math.max(best[k - 1][l], best[k][l - 1], shared * 4 > Math.max(sa, sb) && best[k - 1][l - 1] + shared);
|
|
136
|
+
}
|
|
137
|
+
for (let k = n, l = m; k && l;) {
|
|
138
|
+
if (best[k][l] === best[k - 1][l]) k--;
|
|
139
|
+
else if (best[k][l] === best[k][l - 1]) l--;
|
|
140
|
+
else pairs.unshift([--k, --l]);
|
|
141
|
+
}
|
|
142
|
+
let k = 0, l = 0;
|
|
143
|
+
for (const [p, q] of [...pairs, [n, m]]) {
|
|
144
|
+
// A merge compares one line against one line by character, whatever they share.
|
|
145
|
+
if (raw === 'char' && p - k === 1 && q - l === 1) row(x[k], x[p], y[l], y[q], context);
|
|
146
|
+
else if (k < p || l < q) record(x[k], x[p], y[l], y[q], 'line', context);
|
|
147
|
+
if (p < n) row(x[p], x[p + 1], y[q], y[q + 1], context);
|
|
148
|
+
k = p + 1; l = q + 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
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
|
+
function row(a0, a1, b0, b1, context) {
|
|
154
|
+
if (raw === 'char') return walk(a0, a1, b0, b1, 3, null, null, context);
|
|
155
|
+
const cells = (text, a, b) => /^[ \t]*\|/.test(text.slice(a, b)) && [...text.slice(a, b).matchAll(/(?<!\\)\|/g)].map(m => a + m.index);
|
|
156
|
+
const p = !code && cells(before, a0, a1), q = p && cells(after, b0, b1);
|
|
157
|
+
if (!q || p.length !== q.length || !spend(p.length)) return walk(a0, a1, b0, b1, 2, null, null, context);
|
|
158
|
+
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
|
+
}
|
|
160
|
+
|
|
161
|
+
// Surviving source pieces keep the occurrence witnessed by the host.
|
|
162
|
+
function operations(rows) {
|
|
163
|
+
if (!Array.isArray(rows)) return null;
|
|
164
|
+
const pieces = [[0, before.length]];
|
|
165
|
+
let length = before.length, steps = 1e7;
|
|
166
|
+
const verify = n => {
|
|
167
|
+
if ((steps -= n) >= 0) return true;
|
|
168
|
+
budget.left = -1; return false;
|
|
169
|
+
};
|
|
170
|
+
const split = at => {
|
|
171
|
+
let position = 0;
|
|
172
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
173
|
+
const [start, end, text] = pieces[i], size = end - start;
|
|
174
|
+
if (position === at) return i;
|
|
175
|
+
if (position + size > at) {
|
|
176
|
+
const n = at - position;
|
|
177
|
+
pieces.splice(i, 1, [start, start + n, text], [start + n, end, text]);
|
|
178
|
+
return i + 1;
|
|
179
|
+
}
|
|
180
|
+
position += size;
|
|
181
|
+
}
|
|
182
|
+
return pieces.length;
|
|
183
|
+
};
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
if (!row || !Number.isSafeInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
|
|
186
|
+
row.pos < 0 || row.pos + row.removed.length > length || !verify(pieces.length * 2 + 1)) return null;
|
|
187
|
+
const left = split(row.pos), right = split(row.pos + row.removed.length);
|
|
188
|
+
let at = 0;
|
|
189
|
+
for (let i = left; i < right; i++) {
|
|
190
|
+
const [start, end, text = before] = pieces[i];
|
|
191
|
+
if (!row.removed.startsWith(text.slice(start, end), at)) return null;
|
|
192
|
+
at += end - start;
|
|
193
|
+
}
|
|
194
|
+
if (row.removed === row.inserted) continue;
|
|
195
|
+
pieces.splice(left, right - left, [0, row.inserted.length, row.inserted]);
|
|
196
|
+
length += row.inserted.length - row.removed.length;
|
|
197
|
+
}
|
|
198
|
+
if (length !== after.length) return null;
|
|
199
|
+
const spans = [];
|
|
200
|
+
let a = 0, b = 0, at = 0;
|
|
201
|
+
for (const [start, end, text] of pieces) {
|
|
202
|
+
const size = end - start;
|
|
203
|
+
if (!verify(1) || !after.startsWith((text ?? before).slice(start, end), at)) return null;
|
|
204
|
+
if (text === undefined) {
|
|
205
|
+
let x = start, y = at, tail = end;
|
|
206
|
+
while (x < tail && (!safe(before, x) || !safe(after, y))) { x++; y++; if (!verify(1)) return null; }
|
|
207
|
+
while (tail > x && (!safe(before, tail) || !safe(after, at + tail - start))) { tail--; if (!verify(1)) return null; }
|
|
208
|
+
if (tail > x) {
|
|
209
|
+
if (a !== x || b !== y) spans.push([a, x, b, y]);
|
|
210
|
+
a = tail; b = at + tail - start;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
at += size;
|
|
214
|
+
}
|
|
215
|
+
if (a !== before.length || b !== after.length) spans.push([a, before.length, b, after.length]);
|
|
216
|
+
return spans;
|
|
217
|
+
}
|
|
218
|
+
return {record, name, walk, operations};
|
|
219
|
+
}
|
|
@@ -9,7 +9,7 @@ import {normalizeFontFamily, fontFamilyCss, FONT_STACKS, typography} from './md-
|
|
|
9
9
|
export const CSS_GENERIC_FONT_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
|
|
10
10
|
'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong', 'initial', 'revert', 'revert-layer']);
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// No decoding or external resource loading happens here.
|
|
13
13
|
export function fontDataUrl(url) {
|
|
14
14
|
if (typeof url !== 'string') return null;
|
|
15
15
|
const match = /^data:font\/(woff2|woff|ttf|otf);base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// One reader of document-wide settings and the inherited font snapshot.
|
|
3
3
|
// Framing stays in spec/frontmatter.mjs. Reading never rewrites source.
|
|
4
|
-
// The live view reads fontsize, the three font scopes
|
|
4
|
+
// The live view reads fontsize, the three font scopes and linestretch. It ignores papersize,
|
|
5
5
|
// geometry and pagestyle: a scroll has no page. Word and the PDF read all of them.
|
|
6
6
|
import {frontMatterFrame, frontMatterLine} from './frontmatter.mjs';
|
|
7
7
|
import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
|
|
8
8
|
|
|
9
9
|
// Twelve points are one reference unit (16 px at the default reading scale).
|
|
10
10
|
const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
11
|
-
const SPACE = Object.freeze({paragraph: .5, heading: Object.freeze([1.5, .5]), block: 1});
|
|
12
11
|
export const DOCUMENT_LEADING = 1.75 / 1.1;
|
|
13
12
|
|
|
14
13
|
function measure(value, positive = false) {
|
|
@@ -41,7 +40,7 @@ const PAPERS = Object.freeze({
|
|
|
41
40
|
});
|
|
42
41
|
const EMPTY = Object.freeze({
|
|
43
42
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
44
|
-
papersize: null, geometry: null, pagestyle: null,
|
|
43
|
+
papersize: null, geometry: null, pagestyle: null,
|
|
45
44
|
});
|
|
46
45
|
|
|
47
46
|
function unquote(value) {
|
|
@@ -53,7 +52,7 @@ function marginOf(value) {
|
|
|
53
52
|
const match = /^margin\s*=\s*(\d+(?:\.\d+)?)(cm|mm|in|pt)$/i.exec(unquote(value));
|
|
54
53
|
if (!match) return null;
|
|
55
54
|
const number = Number(match[1]);
|
|
56
|
-
if (!Number.isFinite(number) || number < 0
|
|
55
|
+
if (!Number.isFinite(number) || number < 0) return null;
|
|
57
56
|
const unit = match[2].toLowerCase();
|
|
58
57
|
const twips = Math.round(unit === 'in' ? number * 1440 : unit === 'pt' ? number * 20 : unit === 'cm' ? number * 1440 / 2.54 : number * 1440 / 25.4);
|
|
59
58
|
if (!Number.isSafeInteger(twips) || twips < 0 || twips > 1440 * 6) return null;
|
|
@@ -214,97 +213,6 @@ function fieldsOf(source) {
|
|
|
214
213
|
return {frame, fields: parsed.fields, invalid: parsed.invalid || !structureIsClosed(source, lines)};
|
|
215
214
|
}
|
|
216
215
|
|
|
217
|
-
function flowItems(source, start, stop) {
|
|
218
|
-
const items = [];
|
|
219
|
-
for (let at = start; at < stop;) {
|
|
220
|
-
if (!source.slice(at, stop).trim()) break;
|
|
221
|
-
let end = at;
|
|
222
|
-
for (; end < stop; end++) {
|
|
223
|
-
const char = source[end];
|
|
224
|
-
if ((char === '"' || char === "'") && (end === at || /[\s:[{,]/.test(source[end - 1]))) {
|
|
225
|
-
const next = quotedEnd(source, end, stop);
|
|
226
|
-
if (next === null) return null;
|
|
227
|
-
end = next - 1;
|
|
228
|
-
} else if (char === '[' || char === '{') {
|
|
229
|
-
const next = flowEnd(source, end, stop);
|
|
230
|
-
if (next === null) return null;
|
|
231
|
-
end = next - 1;
|
|
232
|
-
} else if (char === ',') break;
|
|
233
|
-
}
|
|
234
|
-
if (!source.slice(at, end).trim()) return null;
|
|
235
|
-
items.push({start: at, end, comma: end < stop ? end : null});
|
|
236
|
-
at = end + 1;
|
|
237
|
-
}
|
|
238
|
-
return items;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function spaceFields(source, field) {
|
|
242
|
-
if (!field || field.opaque || field.invalid || field.multiline) return null;
|
|
243
|
-
const value = source.slice(field.start, field.end);
|
|
244
|
-
if (value.startsWith('{') && value.endsWith('}') && !field.children.length) {
|
|
245
|
-
const chunks = flowItems(source, field.start + 1, field.end - 1), fields = [];
|
|
246
|
-
if (!chunks) return null;
|
|
247
|
-
for (const chunk of chunks) {
|
|
248
|
-
const start = chunk.start + source.slice(chunk.start, chunk.end).match(/^\s*/)[0].length;
|
|
249
|
-
const line = {start, end: chunk.end, next: chunk.end, text: source.slice(start, chunk.end), eol: ''};
|
|
250
|
-
const item = fieldLine(line);
|
|
251
|
-
if (!item) return null;
|
|
252
|
-
valueSpan(source, item, chunk.end);
|
|
253
|
-
if (item.invalid || item.key === null) return null;
|
|
254
|
-
fields.push({...item, entryStart: chunk.start, entryEnd: chunk.end, comma: chunk.comma});
|
|
255
|
-
}
|
|
256
|
-
return {fields, flow: true};
|
|
257
|
-
}
|
|
258
|
-
if (value) return null;
|
|
259
|
-
if (!field.children.length) return {fields: [], flow: false};
|
|
260
|
-
const indent = /^ */.exec(field.children[0].text)[0].length;
|
|
261
|
-
const parsed = mapFields(source, field.children, indent);
|
|
262
|
-
return parsed.invalid ? null : {...parsed, flow: false, indent};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function headingParts(source, field) {
|
|
266
|
-
if (field.opaque || field.invalid || field.multiline) return null;
|
|
267
|
-
let spans;
|
|
268
|
-
if (field.children.length) {
|
|
269
|
-
if (field.start !== field.end) return null;
|
|
270
|
-
let indent = null;
|
|
271
|
-
spans = field.children.map(line => {
|
|
272
|
-
const match = /^( +)-[ \t]+/.exec(line.text);
|
|
273
|
-
if (!match || indent !== null && match[1].length !== indent) return null;
|
|
274
|
-
indent = match[1].length;
|
|
275
|
-
const span = valueSpan(source, {start: line.start + match[0].length, line, children: []}, line.end);
|
|
276
|
-
return span.invalid || span.opaque ? null : span;
|
|
277
|
-
});
|
|
278
|
-
} else {
|
|
279
|
-
if (source[field.start] !== '[' || source[field.end - 1] !== ']') return null;
|
|
280
|
-
spans = flowItems(source, field.start + 1, field.end - 1);
|
|
281
|
-
}
|
|
282
|
-
if (!spans || spans.length !== 2 || spans.some(span => !span)) return null;
|
|
283
|
-
const values = spans.map(span => measure(source.slice(span.start, span.end)));
|
|
284
|
-
return values.includes(null) ? null : {values, spans};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function spaceOf(source, field) {
|
|
288
|
-
const parsed = spaceFields(source, field);
|
|
289
|
-
if (!parsed) return null;
|
|
290
|
-
const value = {...SPACE}, seen = new Set();
|
|
291
|
-
for (const item of parsed.fields) {
|
|
292
|
-
if (seen.has(item.key)) return null;
|
|
293
|
-
seen.add(item.key);
|
|
294
|
-
if (!Object.hasOwn(SPACE, item.key)) continue;
|
|
295
|
-
if (item.key === 'heading') {
|
|
296
|
-
const pair = headingParts(source, item);
|
|
297
|
-
if (!pair) return null;
|
|
298
|
-
value.heading = Object.freeze(pair.values);
|
|
299
|
-
} else {
|
|
300
|
-
if (item.children.length || item.opaque || item.invalid || item.multiline) return null;
|
|
301
|
-
value[item.key] = measure(source.slice(item.start, item.end));
|
|
302
|
-
if (value[item.key] === null) return null;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return Object.keys(SPACE).some(key => seen.has(key)) ? Object.freeze(value) : null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
216
|
export function documentFont(value) {
|
|
309
217
|
const family = normalizeFontFamily(value);
|
|
310
218
|
if (!family) return null;
|
|
@@ -348,94 +256,37 @@ export function readDocumentSettings(markdown) {
|
|
|
348
256
|
const page = String(take('pagestyle') || '').toLowerCase();
|
|
349
257
|
return Object.freeze({
|
|
350
258
|
title: take('title') || null, subtitle: take('subtitle') || null, lang: languageTag(take('lang')),
|
|
351
|
-
fontsize, mainfont, sansfont, monofont, defaultfont, linestretch,
|
|
259
|
+
fontsize, mainfont, sansfont, monofont, defaultfont, linestretch, papersize, geometry,
|
|
352
260
|
pagestyle: page === 'plain' || page === 'empty' ? page : null,
|
|
353
261
|
});
|
|
354
262
|
}
|
|
355
263
|
|
|
356
|
-
function spaceValue(value) {
|
|
357
|
-
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !Object.hasOwn(SPACE, key))) return null;
|
|
358
|
-
const result = {...SPACE, ...value};
|
|
359
|
-
if (!Array.isArray(result.heading) || result.heading.length !== 2) return null;
|
|
360
|
-
return [result.paragraph, ...result.heading, result.block].every(number => typeof number === 'number' && Number.isFinite(number) && number >= 0) ? result : null;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
264
|
// Change only supplied settings. Unrelated YAML, comments, line endings, BOM and body are never reprinted.
|
|
364
265
|
export function updateDocumentSettings(markdown, patch) {
|
|
365
266
|
const source = String(markdown ?? ''), parsed = fieldsOf(source), {frame, fields} = parsed, before = readDocumentSettings(source);
|
|
366
267
|
const changes = [], append = [];
|
|
367
268
|
const eol = frame.closing ? frame.opening.eol : (source.match(/\r\n|\r|\n/) || ['\n'])[0];
|
|
368
|
-
const encodeSpace = value => '{ paragraph: ' + value.paragraph + ', heading: [' + value.heading.join(', ') + '], block: ' + value.block + ' }';
|
|
369
269
|
const same = (left, right) => Math.abs(left - right) <= Number.EPSILON * Math.max(Math.abs(left), Math.abs(right)) * 4;
|
|
370
270
|
const refuse = key => { throw new RangeError('Edit this setting in source: ' + key); };
|
|
371
|
-
const removeLine = (line, end
|
|
271
|
+
const removeLine = (line, end) => {
|
|
372
272
|
const suffix = source.slice(end, line.end);
|
|
373
|
-
changes.push({start: line.start, end: line.next, text: suffix.trimStart().startsWith('#') ?
|
|
273
|
+
changes.push({start: line.start, end: line.next, text: suffix.trimStart().startsWith('#') ? suffix + line.eol : ''});
|
|
374
274
|
};
|
|
375
275
|
const replaceValue = (field, text) => changes.push({start: field.start, end: field.end,
|
|
376
276
|
text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
|
|
377
277
|
for (const [key, supplied] of Object.entries(patch || {})) {
|
|
378
278
|
const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
|
|
379
|
-
if (!font && !['fontsize', 'linestretch'
|
|
279
|
+
if (!font && !['fontsize', 'linestretch'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
380
280
|
const matches = fields.filter(item => item.key === key);
|
|
381
281
|
if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
|
|
382
282
|
const field = matches[0];
|
|
383
|
-
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) :
|
|
283
|
+
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : leading(supplied);
|
|
384
284
|
if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
|
|
385
|
-
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step :
|
|
386
|
-
if (
|
|
285
|
+
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : String(value.factor);
|
|
286
|
+
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : before.linestretch.factor === value.factor)) continue;
|
|
287
|
+
if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
|
|
387
288
|
if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
|
|
388
289
|
if (field.opaque || field.invalid || field.multiline || field.block) refuse(key);
|
|
389
|
-
if (key === 'space') {
|
|
390
|
-
const map = spaceFields(source, field);
|
|
391
|
-
if (!map) refuse(key);
|
|
392
|
-
const known = map.fields.filter(item => Object.hasOwn(SPACE, item.key));
|
|
393
|
-
if (new Set(map.fields.map(item => item.key)).size !== map.fields.length) throw new RangeError('Duplicate document setting: space');
|
|
394
|
-
if (known.length && !before.space) refuse(key);
|
|
395
|
-
if (value === null) {
|
|
396
|
-
const unknown = map.fields.length !== known.length;
|
|
397
|
-
for (const item of known) {
|
|
398
|
-
if (map.flow) {
|
|
399
|
-
if (unknown) changes.push({start: item.entryStart, end: item.comma === null ? item.entryEnd : item.comma + 1, text: ''});
|
|
400
|
-
} else {
|
|
401
|
-
for (const child of item.children) {
|
|
402
|
-
const own = headingParts(source, item)?.spans.find(span => span.line === child);
|
|
403
|
-
if (!own) refuse(key);
|
|
404
|
-
removeLine(child, own.end, /^ */.exec(child.text)[0].length);
|
|
405
|
-
}
|
|
406
|
-
removeLine(item.line, item.end, item.indent);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (!unknown) removeLine(field.line, field.end);
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
if (before.space && encodeSpace(before.space) === encoded) continue;
|
|
413
|
-
if (!map.fields.length) { replaceValue(field, encoded); continue; }
|
|
414
|
-
const missing = [];
|
|
415
|
-
for (const name of Object.keys(SPACE)) {
|
|
416
|
-
if (before.space && JSON.stringify(before.space[name]) === JSON.stringify(value[name])) continue;
|
|
417
|
-
const item = map.fields.find(item => item.key === name);
|
|
418
|
-
const text = name === 'heading' ? '[' + value.heading.join(', ') + ']' : String(value[name]);
|
|
419
|
-
if (!item) { missing.push(name + ': ' + text); continue; }
|
|
420
|
-
if (name === 'heading') {
|
|
421
|
-
const pair = headingParts(source, item);
|
|
422
|
-
for (let i = 0; i < 2; i++) if (pair.values[i] !== value.heading[i]) {
|
|
423
|
-
const span = pair.spans[i], raw = source.slice(span.start, span.end);
|
|
424
|
-
changes.push({start: span.start + raw.length - raw.trimStart().length, end: span.end - raw.length + raw.trimEnd().length, text: String(value.heading[i])});
|
|
425
|
-
}
|
|
426
|
-
} else replaceValue(item, text);
|
|
427
|
-
}
|
|
428
|
-
if (missing.length) {
|
|
429
|
-
if (map.flow) {
|
|
430
|
-
const last = map.fields.at(-1);
|
|
431
|
-
changes.push({start: field.end - 1, end: field.end - 1, text: (last.comma === null ? ', ' : ' ') + missing.join(', ')});
|
|
432
|
-
} else {
|
|
433
|
-
const last = field.children.at(-1), indent = ' '.repeat(map.indent);
|
|
434
|
-
changes.push({start: last.next, end: last.next, text: missing.map(item => indent + item + eol).join('')});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
290
|
if (field.children.length) refuse(key);
|
|
440
291
|
if (value === null) removeLine(field.line, field.end);
|
|
441
292
|
else replaceValue(field, encoded);
|
|
@@ -472,10 +323,6 @@ export function documentSettingsStyle(settings, defaultFont) {
|
|
|
472
323
|
const decls = [];
|
|
473
324
|
if (settings.fontsize) decls.push('--md-text-body:' + settings.fontsize.css);
|
|
474
325
|
if (settings.linestretch || settings.fontsize) decls.push('--md-line:calc(var(--md-text-body) * ' + (settings.linestretch?.factor ?? DOCUMENT_LEADING) + ')');
|
|
475
|
-
if (settings.space) {
|
|
476
|
-
decls.push('--md-space-paragraph:' + settings.space.paragraph, '--md-space-heading-before:' + settings.space.heading[0],
|
|
477
|
-
'--md-space-heading-after:' + settings.space.heading[1], '--md-space-block:' + settings.space.block);
|
|
478
|
-
}
|
|
479
326
|
const {main, sans, mono} = documentSettingsFonts(settings, defaultFont);
|
|
480
327
|
if (sans) decls.push('--md-font-sans:' + (sans.css || fontFamilyCss(sans.family)));
|
|
481
328
|
if (main) decls.push('font-family:' + (main.css || fontFamilyCss(main.family)));
|