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.
Files changed (61) hide show
  1. package/dist/agent/will.mjs +0 -6
  2. package/dist/images/gif.mjs +0 -3
  3. package/dist/images/playback.mjs +0 -10
  4. package/dist/kit/assets.mjs +0 -1
  5. package/dist/kit/conformance/measurer.mjs +0 -0
  6. package/dist/kit/conformance/run.mjs +1 -8
  7. package/dist/kit/export-fonts.mjs +0 -14
  8. package/dist/kit/ledger/authored-origin.mjs +0 -5
  9. package/dist/kit/ledger/authorship.mjs +0 -2
  10. package/dist/kit/ledger/carried.mjs +4 -7
  11. package/dist/kit/ledger/data.mjs +0 -8
  12. package/dist/kit/ledger/format.mjs +0 -25
  13. package/dist/kit/ledger/hash.mjs +11 -7
  14. package/dist/kit/ledger/history.mjs +10 -29
  15. package/dist/kit/ledger/inverse-source.mjs +0 -46
  16. package/dist/kit/ledger/journal-records.mjs +24 -56
  17. package/dist/kit/ledger/merge.mjs +0 -27
  18. package/dist/kit/ledger/text-merge.mjs +0 -20
  19. package/dist/kit/ledger/transport.mjs +0 -47
  20. package/dist/kit/ledger.mjs +0 -4
  21. package/dist/kit/markdown-it.mjs +0 -3
  22. package/dist/kit/pdf-glyphs.mjs +1 -10
  23. package/dist/kit/pdf-source.mjs +5 -5
  24. package/dist/kit/pdf-vectors.mjs +0 -3
  25. package/dist/kit/print-capitals.mjs +0 -4
  26. package/dist/kit/render-entry.mjs +0 -1
  27. package/dist/kit/render-ink.mjs +0 -5
  28. package/dist/kit/render-markdown.mjs +12 -117
  29. package/dist/kit/render-print.mjs +30 -61
  30. package/dist/kit/render-sanitize.mjs +7 -59
  31. package/dist/kit/render-styles.mjs +0 -7
  32. package/dist/kit/render-work.mjs +0 -12
  33. package/dist/kit/render.mjs +46 -176
  34. package/dist/layout/hyphenation.mjs +0 -23
  35. package/dist/layout/line-plan.mjs +0 -20
  36. package/dist/layout/model.mjs +0 -55
  37. package/dist/layout/paragraph-fit.mjs +50 -60
  38. package/dist/layout/preparation.mjs +23 -56
  39. package/dist/layout/prepared-paragraph.mjs +0 -25
  40. package/dist/layout/typography.mjs +103 -41
  41. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  42. package/dist/packages/rapier-diff/diff.mjs +0 -7
  43. package/dist/packages/rapier-diff/index.mjs +0 -1
  44. package/dist/packages/rapier-diff/matching.mjs +0 -19
  45. package/dist/packages/rapier-diff/merge.mjs +0 -9
  46. package/dist/packages/rapier-diff/moves.mjs +0 -7
  47. package/dist/packages/rapier-diff/structure.mjs +0 -2
  48. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  49. package/dist/packages/rapier-diff/walk.mjs +0 -12
  50. package/dist/spec/document-fonts.mjs +0 -9
  51. package/dist/spec/document-settings.mjs +5 -24
  52. package/dist/spec/frontmatter.mjs +0 -1
  53. package/dist/spec/html-blocks.mjs +0 -36
  54. package/dist/spec/md-assets.mjs +2 -61
  55. package/dist/spec/md-layout.mjs +0 -34
  56. package/dist/spec/md-marks.mjs +0 -51
  57. package/dist/spec/md-source.mjs +0 -16
  58. package/dist/spec/template-page.mjs +6 -0
  59. package/dist/spec/word-sidecar.mjs +0 -2
  60. package/package.json +1 -1
  61. 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,5 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // The package entry: compare two texts, or merge two edits of one base.
3
2
  import {diff} from './diff.mjs';
4
3
  import {merge} from './merge.mjs';
5
4
  export {diff, merge};
@@ -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, '&amp;');
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
- // Compound blocks own the outside gap; the last child does not add it again.
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 + ' ul.rapier-jump-list>li{padding-block:0;margin-block:' + length(item.before) + ' ' + length(item.after) + '}' +
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++;