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.
Files changed (60) hide show
  1. package/LICENSE +8 -4
  2. package/README.md +50 -7
  3. package/dist/images/gif.mjs +49 -0
  4. package/dist/images/playback.mjs +103 -0
  5. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  6. package/dist/kit/conformance/04-wrap-box.md +1 -1
  7. package/dist/kit/conformance/05-behind.md +1 -1
  8. package/dist/kit/conformance/06-front.md +1 -1
  9. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  10. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  11. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  12. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  13. package/dist/kit/conformance/10-both-sides.md +1 -1
  14. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  15. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  16. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  17. package/dist/kit/conformance/README.md +19 -28
  18. package/dist/kit/conformance/run.mjs +10 -8
  19. package/dist/kit/ledger/authored-origin.mjs +49 -0
  20. package/dist/kit/ledger/authorship.mjs +18 -9
  21. package/dist/kit/ledger/carried.mjs +10 -5
  22. package/dist/kit/ledger/data.mjs +7 -4
  23. package/dist/kit/ledger/format.mjs +37 -11
  24. package/dist/kit/ledger/hash.mjs +31 -15
  25. package/dist/kit/ledger/history.mjs +48 -10
  26. package/dist/kit/ledger/inverse-source.mjs +111 -49
  27. package/dist/kit/ledger/journal-records.mjs +24 -29
  28. package/dist/kit/ledger/merge.mjs +51 -15
  29. package/dist/kit/ledger/text-merge.mjs +75 -29
  30. package/dist/kit/ledger/transport.mjs +30 -45
  31. package/dist/kit/pdf-vectors.mjs +6 -2
  32. package/dist/kit/print-capitals.mjs +0 -1
  33. package/dist/kit/rapier-ledger.mjs +4 -2
  34. package/dist/kit/render-ink.mjs +1 -1
  35. package/dist/kit/render-markdown.mjs +155 -31
  36. package/dist/kit/render-print.mjs +173 -61
  37. package/dist/kit/render-sanitize.mjs +25 -15
  38. package/dist/kit/render-styles.mjs +3 -3
  39. package/dist/kit/render.mjs +37 -115
  40. package/dist/layout/model.mjs +10 -8
  41. package/dist/layout/paragraph-fit.mjs +1 -15
  42. package/dist/layout/typography.mjs +2 -1
  43. package/dist/packages/rapier-diff/concerns.mjs +163 -0
  44. package/dist/packages/rapier-diff/diff.mjs +68 -0
  45. package/dist/packages/rapier-diff/index.mjs +4 -701
  46. package/dist/packages/rapier-diff/matching.mjs +157 -0
  47. package/dist/packages/rapier-diff/merge.mjs +226 -0
  48. package/dist/packages/rapier-diff/moves.mjs +130 -0
  49. package/dist/packages/rapier-diff/structure.mjs +50 -0
  50. package/dist/packages/rapier-diff/tokens.mjs +72 -0
  51. package/dist/packages/rapier-diff/walk.mjs +219 -0
  52. package/dist/spec/document-fonts.mjs +1 -1
  53. package/dist/spec/document-settings.mjs +11 -164
  54. package/dist/spec/html-blocks.mjs +618 -79
  55. package/dist/spec/md-assets.mjs +29 -6
  56. package/dist/spec/md-layout.mjs +9 -1
  57. package/dist/spec/md-marks.mjs +25 -147
  58. package/dist/spec/md-source.mjs +69 -26
  59. package/package.json +1 -1
  60. 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
- // Also relocated into the parse worker. No decoding or external resource loading happens here.
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, linestretch and space. It ignores papersize,
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, space: 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 || number > 6) return null;
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, space: spaceOf(source, field('space')), papersize, geometry,
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, indent = 0) => {
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('#') ? ' '.repeat(indent) + suffix + line.eol : ''});
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', 'space'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
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) : key === 'linestretch' ? leading(supplied) : spaceValue(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 : key === 'linestretch' ? String(value.factor) : encodeSpace(value);
386
- if (key !== 'space' && value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : before.linestretch.factor === value.factor)) continue;
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)));