rapier-markdown-kit 1.1.104 → 1.2.3

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 (41) hide show
  1. package/LICENSE +5 -2
  2. package/README.md +34 -3
  3. package/dist/images/gif.mjs +47 -0
  4. package/dist/images/playback.mjs +101 -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 +9 -7
  19. package/dist/kit/ledger/carried.mjs +10 -5
  20. package/dist/kit/ledger/format.mjs +12 -3
  21. package/dist/kit/ledger/hash.mjs +31 -15
  22. package/dist/kit/ledger/history.mjs +17 -5
  23. package/dist/kit/ledger/merge.mjs +4 -3
  24. package/dist/kit/pdf-vectors.mjs +4 -1
  25. package/dist/kit/rapier-ledger.mjs +2 -2
  26. package/dist/kit/render-ink.mjs +1 -1
  27. package/dist/kit/render-markdown.mjs +146 -31
  28. package/dist/kit/render-print.mjs +131 -55
  29. package/dist/kit/render-sanitize.mjs +6 -3
  30. package/dist/kit/render.mjs +34 -77
  31. package/dist/layout/model.mjs +10 -8
  32. package/dist/packages/rapier-diff/index.mjs +75 -32
  33. package/dist/spec/document-fonts.mjs +1 -1
  34. package/dist/spec/document-settings.mjs +11 -164
  35. package/dist/spec/html-blocks.mjs +618 -79
  36. package/dist/spec/md-assets.mjs +29 -6
  37. package/dist/spec/md-layout.mjs +8 -0
  38. package/dist/spec/md-marks.mjs +25 -147
  39. package/dist/spec/md-source.mjs +54 -19
  40. package/package.json +1 -1
  41. package/style.css +7 -51
@@ -251,13 +251,11 @@ export function bandsProfile(bands) {
251
251
  export function serializeProfile(profile, count = shapeGrid) {
252
252
  if (!profile) return '';
253
253
  const bands = [];
254
- let any = false;
255
254
  for (let index = 0; index < count; index++) {
256
255
  const t = (index + .5) / count;
257
256
  let runs = profile.runsAt ? profile.runsAt(t) : null;
258
257
  if (!runs) { const span = profile.spanAt ? profile.spanAt(t) : null; runs = span ? [span] : []; }
259
258
  const clean = (runs || []).map(([a, b]) => [Math.max(0, Math.min(1, +a)), Math.max(0, Math.min(1, +b))]).filter(([a, b]) => Number.isFinite(a) && Number.isFinite(b) && b > a);
260
- if (clean.length) any = true;
261
259
  bands.push(clean.map(([a, b]) => {
262
260
  let left = Math.round(a * 1e4), right = Math.round(b * 1e4);
263
261
  // A narrow occupied run must not round to an invalid zero-width descriptor.
@@ -265,13 +263,13 @@ export function serializeProfile(profile, count = shapeGrid) {
265
263
  return (left / 1e4) + '-' + (right / 1e4);
266
264
  }).join(','));
267
265
  }
268
- return any ? bands.join('|') : '';
266
+ return bands.join('|');
269
267
  }
270
268
 
271
269
  export function parseProfile(text) {
272
- if (typeof text !== 'string' || !text) return null;
270
+ if (typeof text !== 'string' || !text || text.length > 262144) return null;
273
271
  const bands = text.split('|').map(band => band ? band.split(',').map(run => run.split('-').map(Number)) : []);
274
- if (bands.length < 2 || bands.some(band => band.some(run => run.length !== 2 || !run.every(Number.isFinite) || run[0] < 0 || run[1] > 1 || run[1] <= run[0]))) return null;
272
+ if (bands.length < 2 || bands.length > 192 || bands.some(band => band.length > 192 || band.some(run => run.length !== 2 || !run.every(Number.isFinite) || run[0] < 0 || run[1] > 1 || run[1] <= run[0]))) return null;
275
273
  return bandsProfile(bands);
276
274
  }
277
275
 
@@ -309,9 +307,13 @@ export function pictureSlices(profile, x, y, width, height, scale = 1) {
309
307
  return span ? [span] : [];
310
308
  }).filter(run => Number.isFinite(run[0]) && Number.isFinite(run[1]) && run[1] > run[0])
311
309
  .sort((a, b) => a[0] - b[0]);
312
- // Words wrap the outer contour: a band reserves from its leftmost ink to its rightmost, so no word lands
313
- // inside a hollow outline or between two strokes of one picture.
314
- const merged = runs.length ? [[runs[0][0], Math.max(...runs.map(run => run[1]))]] : [];
310
+ // Preserve transparent holes and gaps; only overlapping runs share an obstacle.
311
+ const merged = [];
312
+ for (const run of runs) {
313
+ const tail = merged.at(-1);
314
+ if (tail && run[0] <= tail[1]) tail[1] = Math.max(tail[1], run[1]);
315
+ else merged.push([...run]);
316
+ }
315
317
  const current = new Map();
316
318
  for (const [left, right] of merged) {
317
319
  const key = left + ':' + right;
@@ -68,7 +68,7 @@ export function diff(before, after, options = {}) {
68
68
  const initial = budget.left, raw = options._unit;
69
69
  let result = [], code = options.as === 'code', structures, pa, pb, la, lb, aliases;
70
70
  const spend = (n = 1) => (budget.left -= n) >= 0;
71
- const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c);
71
+ const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c), within = (text, at) => letter(text[at - 1]) && letter(text[at]);
72
72
  const overlaps = (a, b) => a[0] < b[1] && a[1] > b[0];
73
73
  const inside = (c, a, b) => [a, b].every((range, side) => c[side][0] === c[side][1] ||
74
74
  c[side][0] >= range[0] && c[side][1] <= range[1]);
@@ -146,7 +146,11 @@ export function diff(before, after, options = {}) {
146
146
  if (!count) continue;
147
147
  // A chain is admitted only after visiting every candidate. Reject a
148
148
  // search whose mandatory visits cannot fit before allocating its nodes.
149
- if ((unique || count) > Math.min(131072 - nodes, budget.left)) { budget.left = -1; continue; }
149
+ if ((unique || count) > Math.min(131072 - nodes, budget.left)) {
150
+ // Repetitive text has too many equal pairs; a shortest script needs no anchors.
151
+ if (!shortest(a, b, a0, a1, b0, b1, found) && budget.left >= 0) (found.coarse ||= new Set()).add(a0);
152
+ continue;
153
+ }
150
154
  }
151
155
  search: for (let i = a0; i < a1; i++) {
152
156
  const list = positions[a[i]];
@@ -178,6 +182,27 @@ export function diff(before, after, options = {}) {
178
182
  return found;
179
183
  }
180
184
 
185
+ // Myers' greedy search: O((N + M) D) steps; each frontier is kept for the backward pass.
186
+ function shortest(a, b, a0, a1, b0, b1, found) {
187
+ const n = a1 - a0, m = b1 - b0, v = new Int32Array(2 * (n + m) + 3), o = n + m + 1, trace = [];
188
+ for (let d = 0; d <= 2048 && spend(2 * d + 1); d++) {
189
+ trace.push(v.slice(o - d, o + d + 1));
190
+ for (let k = -d; k <= d; k += 2) {
191
+ let x = k === -d || k !== d && v[o + k - 1] < v[o + k + 1] ? v[o + k + 1] : v[o + k - 1] + 1, y = x - k;
192
+ while (x < n && y < m && a[a0 + x] === b[b0 + y] && spend()) { x++; y++; }
193
+ v[o + k] = x;
194
+ if (x < n || y < m) continue;
195
+ for (x = n, y = m; d >= 0; d--) {
196
+ const w = trace[d], k = x - y, prev = k === -d || k !== d && w[d + k - 1] < w[d + k + 1] ? k + 1 : k - 1;
197
+ const px = d ? w[d + prev] : 0, py = px - prev;
198
+ while (x > px && y > py) if (a[a0 + --x] === b[b0 + --y]) found[a0 + x] = b0 + y;
199
+ x = px; y = py;
200
+ }
201
+ return true;
202
+ }
203
+ }
204
+ }
205
+
181
206
  // A search step scans at most one fixed-size source chunk.
182
207
  function common(a, b, end, step) {
183
208
  let n = 0;
@@ -195,14 +220,18 @@ export function diff(before, after, options = {}) {
195
220
  const structural = level <= 0 && level > -128 && (ap?.length || bp?.length);
196
221
  if (!structural && raw !== 'word') {
197
222
  let n = common(a0, b0, Math.min(a1 - a0, b1 - b0), 1);
198
- while (n && (!safe(before, a0 + n) || !safe(after, b0 + n))) { if (!spend()) { n = 0; break; } n--; }
223
+ while (n && (!safe(before, a0 + n) || !safe(after, b0 + n) || !raw && level < 3 && (within(before, a0 + n) || within(after, b0 + n)))) {
224
+ if (!spend()) { n = 0; break; } n--;
225
+ }
199
226
  if (level === 1 && n) {
200
227
  const next = Math.max(0, before.lastIndexOf('\n', a0 + n - 1) + 1 - a0);
201
228
  spend(Math.ceil((n - next) / 256)); n = next;
202
229
  }
203
230
  a0 += n; b0 += n;
204
231
  n = common(a1 - 1, b1 - 1, Math.min(a1 - a0, b1 - b0), -1);
205
- while (n && (!safe(before, a1 - n) || !safe(after, b1 - n))) { if (!spend()) { n = 0; break; } n--; }
232
+ while (n && (!safe(before, a1 - n) || !safe(after, b1 - n) || !raw && level < 3 && (within(before, a1 - n) || within(after, b1 - n)))) {
233
+ if (!spend()) { n = 0; break; } n--;
234
+ }
206
235
  if (level === 1) while (n && (before[a1 - n - 1] !== '\n' || after[b1 - n - 1] !== '\n' || before[a1 - n] === '\n' || after[b1 - n] === '\n')) { if (!spend()) { n = 0; break; } n--; }
207
236
  a1 -= n; b1 -= n;
208
237
  if (budget.left < 0) { record(a0, a1, b0, b1, 'block', context, true); return; }
@@ -220,7 +249,7 @@ export function diff(before, after, options = {}) {
220
249
  if (j === undefined) continue;
221
250
  const x0 = x[ai], x1 = x[i], y0 = y[bi], y1 = y[j];
222
251
  if (ai < i || bi < j) {
223
- if (budget.left < 0) record(x0, x1, y0, y1, unit, context, true);
252
+ if (budget.left < 0 || pairs.coarse?.has(ai)) record(x0, x1, y0, y1, unit, context, true);
224
253
  else if (structural && (ai === i || bi === j)) {
225
254
  for (let k = ai; k < i; k++) record(x[k], x[k + 1], y0, y0, unit, ownersA[k] || context);
226
255
  for (let k = bi; k < j; k++) record(x1, x1, y[k], y[k + 1], unit, ownersB[k] || context);
@@ -233,12 +262,9 @@ export function diff(before, after, options = {}) {
233
262
  const start = result.length;
234
263
  walk(x0, x1, y0, y1, 3, null, null, context);
235
264
  const edits = result.slice(start);
265
+ // A letter-sized correction stays a letter; any other word change marks the whole word.
236
266
  if (edits.length !== 1 || edits[0][0][1] - edits[0][0][0] + edits[0][1][1] - edits[0][1][0] > Math.max(2, (x1 - x0 + y1 - y0) / 3)) {
237
- const reading = [[x0, x1], [y0, y1]], group = [edits[0][3][0], context?.kind || 'word', edits[0][3][2], reading];
238
- for (const edit of edits) {
239
- if (group[0] !== edit[3][0]) group[0] = 1;
240
- edit[3] = group;
241
- }
267
+ result.length = start; record(x0, x1, y0, y1, unit, context);
242
268
  }
243
269
  } else record(x0, x1, y0, y1, unit, context);
244
270
  }
@@ -328,6 +354,23 @@ export function diff(before, after, options = {}) {
328
354
  else walk(0, before.length, 0, after.length, structures ? 0 : raw === 'char' ? 3 : raw === 'word' ? 2 : 1,
329
355
  structures?.[0]?.parts, structures?.[1]?.parts);
330
356
 
357
+ // A separator-only piece beside another edit joins it; equal separators at its ends stay unchanged.
358
+ if (!raw) for (let i = result.length - 1; i > 0; i--) {
359
+ const p = result[i - 1], q = result[i], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1, text = side ? after : before;
360
+ if (side >= 0 && !/\S/.test(text.slice(...p[side]))) {
361
+ let n = 0;
362
+ while (p[0][1] + n < q[0][0] && text[p[side][0] + n] === text[p[side][1] + n]) n++;
363
+ if (p[0][1] + n === q[0][0] && p.slice(0, 2).every((span, k) => safe(k ? after : before, span[0] + n) && safe(k ? after : before, span[1] + n)))
364
+ for (const span of p.slice(0, 2)) { span[0] += n; span[1] += n; }
365
+ }
366
+ if (p[2] || q[2] || p[0][1] !== q[0][0] || p[1][1] !== q[1][0] ||
367
+ ![p, q].some(r => !/\S/.test(before.slice(...r[0]) + after.slice(...r[1])))) continue;
368
+ 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;
369
+ while (x[0] < x[1] && y[0] < y[1] && /\s/.test(before[x[1] - 1]) && before[x[1] - 1] === after[y[1] - 1] &&
370
+ safe(before, x[1] - 1) && safe(after, y[1] - 1)) { x[1]--; y[1]--; }
371
+ keep[3][0] = x[0] === x[1] ? 2 : y[0] === y[1] ? 0 : 1;
372
+ result.splice(i - 1, 2, [x, y, false, keep[3]]);
373
+ }
331
374
  try { if (!raw && budget.left >= 0) {
332
375
  const check = n => { if (!spend(n)) throw null; };
333
376
  // Split transported text at wording boundaries; positions and words own distinct edits.
@@ -353,11 +396,14 @@ export function diff(before, after, options = {}) {
353
396
  if (n * 5 > Math.max(prior.length, key.length) * 3 && n >= score) { old = n === score ? null : candidate; score = n; }
354
397
  }
355
398
  if (!old || old[4]) continue;
356
- const x = [...old[0]], y = [...item[1]], left = before.slice(...x).match(/\s*$/)[0], right = after.slice(...y).match(/\s*$/)[0];
357
- let common = 0;
358
- while (common < Math.min(left.length, right.length) && left[common] === right[common]) common++;
359
- if (common && safe(before, x[1] - left.length + common) && safe(after, y[1] - right.length + common)) {
360
- x[1] -= left.length - common; y[1] -= right.length - common;
399
+ // Separators that only one end carries stay outside the transported text.
400
+ const x = [...old[0]], y = [...item[1]];
401
+ for (const end of [0, 1]) {
402
+ const pattern = end ? /\s*$/ : /^\s*/, left = before.slice(...x).match(pattern)[0], right = after.slice(...y).match(pattern)[0];
403
+ let common = 0;
404
+ while (common < Math.min(left.length, right.length) && left.at(end ? common : -1 - common) === right.at(end ? common : -1 - common)) common++;
405
+ const dx = (left.length - common) * (end ? -1 : 1), dy = (right.length - common) * (end ? -1 : 1);
406
+ if (safe(before, x[end] + dx) && safe(after, y[end] + dy)) { x[end] += dx; y[end] += dy; }
361
407
  }
362
408
  const outer = result; result = [];
363
409
  walk(...x, ...y, 1);
@@ -371,7 +417,8 @@ export function diff(before, after, options = {}) {
371
417
  if (a0 < a1) old[4].push([[a0, a1], [old[1][0], old[1][0]], false, group]);
372
418
  if (b0 < b1) item[4].push([[item[0][0], item[0][0]], [b0, b1], false, group]);
373
419
  };
374
- let a = old[0][0], b = item[1][0], wording;
420
+ fragment(old[0][0], x[0], 0, 0, [0, 'char']); fragment(0, 0, item[1][0], y[0], [2, 'char']);
421
+ let a = x[0], b = y[0], wording;
375
422
  for (const edit of inner) {
376
423
  if (edit[3][3]) wording = edit[3];
377
424
  else if (a !== edit[0][0] || b !== edit[1][0]) wording = [edit[3][0], edit[3][1]];
@@ -428,6 +475,14 @@ export function diff(before, after, options = {}) {
428
475
  result = result.flatMap(item => item[4] || [item]);
429
476
  }
430
477
 
478
+ // Whole-word replacements separated only by spaces read and restore as one phrase.
479
+ if (!raw && !code) for (let i = result.length - 1; i > 0; i--) {
480
+ const p = result[i - 1], q = result[i], gap = before.slice(p[0][1], q[0][0]);
481
+ if (p[3][0] !== 1 || q[3][0] !== 1 || p[3][3] || q[3][3] || p[2] || q[2] || !/^[^\S\r\n]+$/.test(gap) ||
482
+ gap !== after.slice(p[1][1], q[1][0]) || within(before, p[0][0]) || within(after, p[1][0]) ||
483
+ within(before, q[0][1]) || within(after, q[1][1])) continue;
484
+ p[0][1] = q[0][1]; p[1][1] = q[1][1]; result.splice(i, 1);
485
+ }
431
486
  const changes = [], edits = [];
432
487
  let normalized;
433
488
  const meaning = structures?.[0]?.meaning;
@@ -453,9 +508,11 @@ export function diff(before, after, options = {}) {
453
508
  if (!raw && !group[3]) group[3] = [[...item[0]], [...item[1]]];
454
509
  if (!group[4]) {
455
510
  const context = group[3];
511
+ // Context completes a word only where an edge falls inside it.
456
512
  if (!raw) for (const [text, span] of [[before, context[0]], [after, context[1]]]) {
457
- while (span[0] && spend() && letter(text[span[0] - 1]) && safe(text, span[0] - 1)) span[0]--;
458
- while (span[1] < text.length && spend() && letter(text[span[1]]) && safe(text, span[1] + 1)) span[1]++;
513
+ const start = within(text, span[0]), end = within(text, span[1]);
514
+ while (start && span[0] && spend() && letter(text[span[0] - 1]) && safe(text, span[0] - 1)) span[0]--;
515
+ while (end && span[1] < text.length && spend() && letter(text[span[1]]) && safe(text, span[1] + 1)) span[1]++;
459
516
  }
460
517
  group[4] = []; changes.push(group);
461
518
  if (format) group[0] = 5;
@@ -463,20 +520,6 @@ export function diff(before, after, options = {}) {
463
520
  group[4].push(edits.length);
464
521
  edits.push({before: item[0], after: item[1], ...(item[2] ? {coarse: true} : {})});
465
522
  }
466
- if (!raw && !structures && !code && !evidence) for (let i = 0; i < changes.length; i++) {
467
- let end = i + 1;
468
- for (; end < changes.length; end++) {
469
- const a = changes[end - 1], b = changes[end];
470
- const gap = before.slice(a[3][0][1], b[3][0][0]);
471
- if (a[0] !== 1 || b[0] !== 1 || a[1] !== 'word' || b[1] !== 'word' ||
472
- /\S|\r|\n/.test(gap) || gap !== after.slice(a[3][1][1], b[3][1][0])) break;
473
- }
474
- if (end - i < 4) continue;
475
- const group = changes[i]; group[1] = 'sentence';
476
- for (const next of changes.splice(i + 1, end - i - 1)) {
477
- group[4].push(...next[4]); group[3][0][1] = next[3][0][1]; group[3][1][1] = next[3][1][1];
478
- }
479
- }
480
523
  if (structures && !raw && budget.left >= 0) {
481
524
  const oldNames = new Set(pa.filter(p => p.nameAt).map(p => p.name)), newNames = new Set(pb.filter(p => p.nameAt).map(p => p.name));
482
525
  for (const change of changes) {
@@ -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)));