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
@@ -9,7 +9,6 @@ import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will
9
9
  import {quotedWillSpan} from '../spec/md-marks.mjs';
10
10
  const RAPIER_WILL_SENTINEL_MARK = '⁣';
11
11
  // SPDX-License-Identifier: MIT
12
- // The document's renderer. DOM, codecs and host state are explicit inputs.
13
12
  function createPrintRenderer(runtime) {
14
13
  const {Node, _rapierWillParse, crypto, document, globalThis} = runtime;
15
14
  function _rapierWillSentinelPlan(canonical, kind) {
@@ -46,8 +45,6 @@ function _rapierPrintWillAdmit(canonical, plan) {
46
45
  function _rapierPrintWillFont(markers, advanceOverride) {
47
46
  const metrics = _rapierPrintWillMetrics(markers);
48
47
  const size = metrics.size;
49
- // The page's carrier face keeps the short marker advance. Searchable text is
50
- // positioned at 500 units, which is also /DW, so this copy can be asked to match it.
51
48
  const advance = advanceOverride ?? metrics.advance;
52
49
  const scalars = Array.from(new Set(markers.flatMap(marker => Array.from(marker, ch => ch.codePointAt(0)))));
53
50
  const order = scalars.filter(code => code < 0x10000).sort((a, b) => a - b)
@@ -75,8 +72,6 @@ function _rapierPrintWillFont(markers, advanceOverride) {
75
72
  const cmap = narrow.length <= 0xffff
76
73
  ? cat(u16(0), u16(2), u16(3), u16(1), u32(20), u16(3), u16(10), u32(20 + narrow.length), narrow, full)
77
74
  : cat(u16(0), u16(1), u16(3), u16(10), u32(12), full);
78
- // The collinear contour has zero area and enough bounds for native print at 2px.
79
- // Keep it below the baseline so a carrier at a page break does not straddle both page clips.
80
75
  const bounds = [0, -400, 10, 0];
81
76
  const outline = cat(u16(1), bounds.map(u16), u16(2), u16(0), [1, 1, 1], u16(0), u16(10), u16(-5), u16(bounds[1]), u16(bounds[3] - bounds[1]), u16((bounds[1] - bounds[3]) / 2), [0]);
82
77
  const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
@@ -120,7 +115,6 @@ function _rapierPrintWillMetrics(markers) {
120
115
  }
121
116
 
122
117
  function _rapierPrintWillPlace(root, plan) {
123
- // A marker the page holds inside other content (a code block, say) is quoted text, which a reader of the PDF's text could not tell from a carried marker.
124
118
  const lost = () => { throw _rapierPrintWillLost('a marker stands inside other content, where a reader of the PDF\'s text could not tell it from a carried marker', 'Take the quoted marker out'); };
125
119
  const measure = _rapierPrintWillMetrics(plan.markers);
126
120
  const tokens = new Map(plan.markers.map((marker, index) => [plan.sentinel + index + RAPIER_WILL_SENTINEL_MARK, index]));
@@ -153,8 +147,6 @@ function _rapierPrintWillPlace(root, plan) {
153
147
  carrier.textContent = plan.markers[row.index];
154
148
  return carrier;
155
149
  });
156
- // A rule's own top margin is a line of the sheet's (none at the head of the page, where the sheet takes it off the first child): it
157
- // moves to the run, so the gap the rule makes with the block above is the one it made.
158
150
  if (next && next.tagName === 'HR') {
159
151
  if (first.previousElementSibling || first.parentNode !== root) carriers[0].style.marginTop = 'var(--md-line)';
160
152
  next.style.marginTop = '0';
@@ -177,18 +169,15 @@ function _rapierPrintWillPageCss() {
177
169
  + '}';
178
170
  }
179
171
 
180
- // The capture's own limits (agent/visual.mjs VISUAL_LIMITS): the longest edge and the most pixels one capture returns.
181
172
  const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
182
173
 
183
- async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
174
+ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings, templatePage = null}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
184
175
  const refuse = code => { throw Object.assign(new Error(code), {code}); };
185
176
  const check = () => {
186
177
  if (signal?.aborted) refuse('cancelled');
187
178
  if (current?.() === false) refuse('document_changed');
188
179
  };
189
180
  if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
190
- // The native printer chooses its default paper. A file made without a printer uses A4,
191
- // with the same explicit document settings and default margins as the print artifact.
192
181
  const paper = settings?.papersize || {width: 11906, height: 16838};
193
182
  const width = paper.width / 20, height = paper.height / 20;
194
183
  const margin = settings?.geometry ? settings.geometry.twips / 20 : null;
@@ -198,7 +187,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
198
187
  const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
199
188
  if (bodyWidth < 1 || bodyHeight < 1 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
200
189
  const frame = document.createElement('iframe');
201
- let vectors, nativeGlyphs, filmSession;
190
+ let vectors, backdrop = null, nativeGlyphs, filmSession;
202
191
  let rejectAbort;
203
192
  const aborted = new Promise((_, reject) => { rejectAbort = reject; });
204
193
  aborted.catch(() => {});
@@ -221,8 +210,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
221
210
  check();
222
211
  const into = frame.contentDocument;
223
212
  if (!into?.body) refuse('pdf_render_unavailable');
224
- // The capture and native printer use standards layout. An initial blank
225
- // iframe otherwise measures preformatted inline struts in quirks mode.
226
213
  into.open(); into.write('<!doctype html><html><head></head><body></body></html>'); into.close();
227
214
  into.documentElement.setAttribute('data-rapier-theme', 'light');
228
215
  into.documentElement.setAttribute('lang', documentLanguage(settings, document.defaultView?.navigator?.language));
@@ -246,7 +233,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
246
233
  strip.append(root); surface.append(strip); into.body.append(surface);
247
234
  const main = root.querySelector('main');
248
235
  if (!main) refuse('pdf_render_unavailable');
249
- // A file for paper is light whatever the person's own scheme: the page's text takes its colour scheme from this attribute.
236
+ if (templatePage) for (const image of main.querySelectorAll('img')) if (image.getAttribute('src') === templatePage.url) (image.closest('p,figure') || image).remove();
250
237
  main.setAttribute('data-md-theme', 'light');
251
238
  for (const details of main.querySelectorAll('details')) details.open = true;
252
239
  main.getBoundingClientRect();
@@ -254,12 +241,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
254
241
  await step(into.fonts.ready);
255
242
  for (const image of main.querySelectorAll('img')) { image.loading = 'eager'; await step(image.decode()); }
256
243
  check();
257
- // The print sheet owns the words and styles. CSS fragmentation supplies the file's
258
- // pages without a print dialog; its break constraints become column constraints here.
259
244
  for (const node of [main, ...main.querySelectorAll('*')]) {
260
245
  const style = into.defaultView.getComputedStyle(node);
261
- // Clipped, out-of-flow source has no page ink. Leaving it in a multicolumn
262
- // container can still create blank columns and expose its text to PDF search.
263
246
  if (!node.closest('.rapier-will-carrier') && ['absolute', 'fixed'].includes(style.position) && /^rect\(/.test(style.clip)) {
264
247
  const box = style.clip.slice(5, -1).trim().split(/[,\s]+/).map(Number.parseFloat);
265
248
  if (box.length === 4 && box.every(Number.isFinite) && (box[1] <= box[3] || box[2] <= box[0])) {
@@ -273,17 +256,14 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
273
256
  }
274
257
  for (const [name, value] of Object.entries({width: bodyWidth + 'px', height: bodyHeight + 'px',
275
258
  'max-width': 'none', 'min-height': '0', padding: '0', margin: '0', overflow: 'visible',
276
- 'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto'}))
259
+ 'column-width': bodyWidth + 'px', 'column-gap': '0', 'column-count': 'auto', 'column-fill': 'auto', background: 'none'}))
277
260
  main.style.setProperty(name, value, 'important');
278
261
  globalThis.RapierMath?.fit(main);
279
262
  applyPrintCapitals(main);
280
263
  applyTypography(main, {all: true});
281
- // The document's ink marks, drawn on the words where they now stand on the page.
282
264
  ink?.(main);
283
265
  const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
284
266
  if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
285
- // Captured document font bytes own a same-name family; factory glyphs must
286
- // never replace their page paint. This uses the artifact, not a live catalog.
287
267
  const customFamilies = new Set(fontCss ? fontRequests(main, {fontCss, css, settings}).map(face => face.family.toLowerCase()) : []);
288
268
  nativeGlyphs = await collectPdfGlyphs(main, {width: bodyWidth, check,
289
269
  faces: globalThis.RapierDocxFonts?.DOCX_FONTS?.filter(face => !customFamilies.has(face.family.toLowerCase()))});
@@ -304,11 +284,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
304
284
  const font = _rapierPrintWillFont(text, 500);
305
285
  strip.style.width = count * bodyWidth + 'px';
306
286
  strip.style.transform = 'none';
307
- // One styled film is reused for every page window. Batches stay inside the capture edge
308
- // and pixel limits. A picture no longer forces its own capture. Pixels are deflated once.
309
287
  if (typeof film !== 'function') refuse('pdf_render_unavailable');
310
- // step() can reject after the film resolves (the document changed, or the attempt was
311
- // cancelled). The session would otherwise keep its abort listener with no owner.
312
288
  let pendingFilm = null, abandonFilm = false;
313
289
  const trackedFilm = Promise.resolve(film({root: surface, signal, current, frame: {width: count * bodyWidth, height: bodyHeight}})).then(session => {
314
290
  if (abandonFilm) session?.close?.();
@@ -359,15 +335,24 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
359
335
  first += span;
360
336
  }
361
337
  check();
362
- const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins,
338
+ if (templatePage) {
339
+ const sheetWidth = Math.round(width * 4 / 3), sheetHeight = Math.round(height * 4 / 3);
340
+ const holder = into.createElement('div'), picture = into.createElement('img');
341
+ holder.style.cssText = `position:fixed;left:0;top:0;width:${sheetWidth}px;height:${sheetHeight}px;pointer-events:none`;
342
+ picture.style.cssText = 'display:block;width:100%;height:100%'; picture.src = templatePage.url;
343
+ holder.append(picture); into.body.append(holder);
344
+ await step(picture.decode());
345
+ backdrop = await collectPdfVectors(holder, {width: sheetWidth, height: sheetHeight, count: 1, capture, signal, current, check, maxBytes,
346
+ sanitizeSvg: globalThis.RapierImageAssets?.sanitizeSvgText});
347
+ if (!backdrop.pages[0].length) refuse('pdf_template_border_unavailable');
348
+ }
349
+ const bytes = await writeRasterPdf({pages, textPages, linkPages, vectorPages: vectors.pages, canonical, bom, filename, title, font, width, height, margins, backdrop: backdrop?.pages[0] || null,
363
350
  pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
364
351
  check();
365
352
  return {bytes, pages: count};
366
- } finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
353
+ } finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); backdrop?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
367
354
  }
368
355
 
369
- // Ranges locate the actual text fragments after column layout. Splitting a text node at
370
- // its line and page boundaries keeps a PDF search on the page that carries those words.
371
356
  async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null) {
372
357
  const into = main.ownerDocument;
373
358
  const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 5);
@@ -382,8 +367,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
382
367
  if (node.nodeType === 1 && !formula || node.nodeType === 3 && element.closest('.math-rendered[data-math-src]')) continue;
383
368
  let value;
384
369
  if (formula) {
385
- // The math owner paints SVG paths and retains the equation's exact TeX alternative.
386
- // Carry that semantic text at the SVG's measured position for search and copy.
387
370
  value = formula.getAttribute('aria-label') || '';
388
371
  } else value = node.data;
389
372
  const style = into.defaultView.getComputedStyle(element);
@@ -397,14 +380,11 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
397
380
  const visit = (start, end) => {
398
381
  if (++operations % 128 === 0) check();
399
382
  if (!formula) { range.setStart(node, start); range.setEnd(node, end); }
400
- // A tiny carrier font can have its advance rounded to zero by a browser. Its
401
- // measured baseline still locates the marker; the font's authored metrics keep its text width.
402
383
  const rects = Array.from(formula ? [formula.getBoundingClientRect()] : range.getClientRects()).filter(rect => rect.height > 0 && (rect.width > 0 || carrier));
403
384
  if (!rects.length) { if (carrier) markerLost(); return; }
404
385
  const first = rects[0], page = pageOf(first.left);
405
386
  const oneLine = rects.every(rect => pageOf(rect.left) === page && pageOf(rect.width ? rect.right - .02 : rect.left) === page && Math.abs(rect.top - first.top) < .5);
406
387
  let middle = formula ? start : Math.floor((start + end) / 2);
407
- // A line never breaks inside a character: halve between whole ones (a letter and its marks, a conjunct, an emoji).
408
388
  if (middle > start) middle = (characters ||= segmenter.segment(value)).containing(middle).index;
409
389
  const cluster = native?.find(item => item.start < middle && item.end > middle);
410
390
  if (cluster) middle = cluster.start > start ? cluster.start : cluster.end;
@@ -436,9 +416,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
436
416
  visit(0, value.length);
437
417
  }
438
418
  check();
439
- // Trailing-carrier print CSS pulls a last marker up so it stays on the sheet.
440
- // Keep that paint; put the searchable object after the preceding words so
441
- // reading order matches the source (the marker itself has no visible ink).
442
419
  const pageHeight = origin.height;
443
420
  for (const page of pages) {
444
421
  let prev = null;
@@ -454,8 +431,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
454
431
  prev = run;
455
432
  }
456
433
  }
457
- // Each run carries the tags of its block's structure (outermost first): a PDF reader finds the document's headings, lists and
458
- // tables from them rather than from positions.
459
434
  const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
460
435
  H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
461
436
  const tags = block => {
@@ -498,8 +473,6 @@ function _rapierPrintLinkPages(main, width, height, count) {
498
473
  return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
499
474
  }
500
475
 
501
- // Where each scalar of an invisible text run stands, in character slots from the run's left edge: a mark shares its letter's
502
- // slot, a right-to-left word runs from the right, and so do the words of a right-to-left line (strong letters only; one line).
503
476
  function _rapierPrintSlots(text, rtl) {
504
477
  const pieces = [];
505
478
  let scalars = 0, count = 0;
@@ -520,9 +493,7 @@ function _rapierPrintSlots(text, rtl) {
520
493
  return {slots, count};
521
494
  }
522
495
 
523
- // A standard PDF page tree, image streams, Unicode text and an associated source file.
524
- // It accepts already-rendered pages, so there is no second document grammar or layout engine.
525
- async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
496
+ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], backdrop = null, canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
526
497
  const encoder = new TextEncoder(), encode = value => encoder.encode(value);
527
498
  const fail = code => { throw Object.assign(new Error(code), {code}); };
528
499
  const concat = parts => {
@@ -589,7 +560,6 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
589
560
  nativeResources.push(entry.name + ' ' + type + ' 0 R');
590
561
  }
591
562
  const sharedImages = new Map();
592
- // The structure tree (PDF 1.7, 14.7): one element per tagged DOM element, each run marked with its page's next MCID.
593
563
  const structs = new Map(), documentElement = {role: 'Document', kids: []}, parentTree = [];
594
564
  const structOf = tags => {
595
565
  let parent = documentElement;
@@ -607,12 +577,10 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
607
577
  const held = pages[index], flate = !!held?.rgb;
608
578
  const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
609
579
  pages[index] = null;
610
- // The page's picture is how its tagged text looks, not a figure of its own: an artifact.
611
- let commands = '/Artifact BMC\nq\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\nEMC\n';
612
580
  const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
613
- for (const drawing of vectorPages[index] || []) {
614
- commands += `q\n.75 0 0 -.75 ${decimal(margins.left)} ${decimal(height - margins.top)} cm\n0 0 ${pixelWidth} ${pixelHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
615
- commands += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
581
+ const emit = (drawing, left, top, clipWidth, clipHeight) => {
582
+ let out = `q\n.75 0 0 -.75 ${decimal(left)} ${decimal(top)} cm\n0 0 ${clipWidth} ${clipHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
583
+ out += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
616
584
  for (const piece of drawing.sequence) {
617
585
  if (piece.image) {
618
586
  let shared = sharedImages.get(piece.image);
@@ -626,21 +594,22 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
626
594
  sharedImages.set(piece.image, shared);
627
595
  }
628
596
  if (!pictures.includes(shared.resource)) pictures.push(shared.resource);
629
- commands += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${shared.name} Do\nQ\n`;
597
+ out += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${shared.name} Do\nQ\n`;
630
598
  } else {
631
599
  const state = '/Gs' + states.length;
632
600
  states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + decimal(piece.fillAlpha) + ' /CA ' + decimal(piece.strokeAlpha) + ' >>') + ' 0 R');
633
- commands += 'q\n' + state + ' gs\n' + piece.commands + 'Q\n';
601
+ out += 'q\n' + state + ' gs\n' + piece.commands + 'Q\n';
634
602
  }
635
603
  }
636
- commands += 'Q\n';
637
- }
604
+ return out + 'Q\n';
605
+ };
606
+ let commands = '';
607
+ for (const drawing of backdrop || []) commands += emit(drawing, 0, height, Math.round(width * 4 / 3), Math.round(height * 4 / 3));
608
+ commands += '/Artifact BMC\nq\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\nEMC\n';
609
+ for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
638
610
  const textAlphas = new Map();
639
611
  for (const run of textPages[index]) {
640
612
  if (!run.text) continue;
641
- // Keep one logical Unicode text object, resetting its font before each scalar's direction
642
- // can change. Readers that ignore ActualText otherwise discard earlier words in mixed RTL/LTR.
643
- // Express width in the font size: some readers ignore horizontal text scaling when joining.
644
613
  const element = structOf(run.tags || []);
645
614
  if (element) {element.kids.push({page: index, mcid: marked.length}); commands += `/${element.role} << /MCID ${marked.length} >> BDC\n`; marked.push(element);}
646
615
  commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
@@ -683,7 +652,7 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
683
652
  const parents = add('<< /Nums [' + parentTree.map((marked, index) => index + ' [' + marked.map(element => ids.get(element) + ' 0 R').join(' ') + ']').join(' ') + '] >>');
684
653
  put(tree, '<< /Type /StructTreeRoot /K [' + ids.get(documentElement) + ' 0 R] /ParentTree ' + parents + ' 0 R /ParentTreeNextKey ' + parentTree.length + ' >>');
685
654
  put(1, '<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot ' + tree + ' 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
686
- const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
655
+ const info = add('<< /Producer (Rapier) /Creator (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
687
656
  const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
688
657
  let cursor = header.length;
689
658
  for (let id = 1; id < objects.length; id++) {
@@ -1,10 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {typography} from '../spec/md-marks.mjs';
3
3
  const RAPIER_SANITIZE_FORBID_TAGS = Object.freeze(['form', 'style', 'dialog', 'template', 'iframe']);
4
- // Invokers (DOMPurify 3.4.16 allows them) act on another element by id, and a document shares the page with the editor's own
5
- // controls: a document's button could open or close them.
6
4
  const RAPIER_SANITIZE_FORBID_ATTR = Object.freeze(['action', 'formaction', 'form', 'for', 'command', 'commandfor', 'popover', 'popovertarget', 'popovertargetaction']);
7
- // The document profiles use one allowlist in a browser and in an inert server parser.
8
5
  const RAPIER_SANITIZE_PROFILES = Object.freeze({
9
6
  htmlTags: Object.freeze(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),
10
7
  svgTags: Object.freeze(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),
@@ -56,7 +53,6 @@ const DIAGRAM_LABEL_SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
56
53
  const DIAGRAM_LABEL_HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
57
54
  const DIAGRAM_LABEL_MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
58
55
 
59
- // The profile each DOMPurify instance holds (setConfig), shared by every renderer made over it.
60
56
  const purifyConfigured = new WeakMap();
61
57
 
62
58
  function _rapierImageDataAttributeAllowed(tag, attribute, value, verify) {
@@ -64,9 +60,6 @@ function _rapierImageDataAttributeAllowed(tag, attribute, value, verify) {
64
60
  && /^data:image\/(?:png|jpeg|webp|jxl);base64,/i.test(value)) && verify(value);
65
61
  }
66
62
 
67
- // Snapshot current math text before a Markdown writer collapses whitespace. Rendered SVG
68
- // glyphs have no textContent: put their retained source in this inert copy so the writer's
69
- // blank-node rule cannot discard an equation or its containing paragraph/list item.
70
63
  function prepareMathSource(input, DOMParser) {
71
64
  const html = typeof input === 'string';
72
65
  const MARK = 'span[data-rapier-math-source],span.math-rendered[data-math-src]';
@@ -82,18 +75,17 @@ function prepareMathSource(input, DOMParser) {
82
75
  try { node.textContent = decodeURIComponent(node.getAttribute('data-math-src')); } catch (_) {}
83
76
  continue;
84
77
  }
78
+ try {
79
+ const held = node.getAttribute('data-rapier-math-source'), source = decodeURIComponent(held);
80
+ if (held && (source === node.textContent || source.replace(/\r\n?/g, '\n').replace(/\0/g, '') === node.textContent)) continue;
81
+ } catch (_) {}
85
82
  try { node.setAttribute('data-rapier-math-source', encodeURIComponent(node.textContent || '')); }
86
83
  catch (_) { node.removeAttribute('data-rapier-math-source'); }
87
84
  }
88
85
  return sourceRoot ? root.outerHTML : root;
89
86
  }
90
87
 
91
- // An inert parser and a string sanitizer use the same profile as DOMPurify. Neither port may
92
- // execute document scripts or fetch resources. HTML5 parsing precedes filtering so foreign
93
- // content, entities and malformed table markup cross the same boundary as a browser's parser.
94
88
  function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId = id => /^rapier-(?!anchor-)/.test(id)}) {
95
- // chromeOwnsId defaults to _rapierChromeOwnsId's prefix rule: every page Rapier writes carries rapier- elements
96
- // (rapier-document, rapier-ledger). The factories are serialized into written pages, so neither reads a module constant.
97
89
  if (![parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId].every(port => typeof port === 'function'))
98
90
  throw new TypeError('The inert HTML sanitizer requires every parser and filtering port');
99
91
  const uri = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
@@ -103,8 +95,6 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
103
95
  const svgTags = new Set(RAPIER_SANITIZE_PROFILES.svgTags), mathTags = new Set(RAPIER_SANITIZE_PROFILES.mathTags);
104
96
  const mathIntegration = new Set(['mi', 'mo', 'mn', 'ms', 'mtext']), htmlIntegration = new Set(['annotation-xml']);
105
97
  const sharedHtmlSvg = new Set(['title', 'style', 'font', 'a', 'script']);
106
- // DOMPurify's default namespace admission applies in addition to its tag profile. In
107
- // particular a MathML name parsed as HTML, or HTML inside SVG desc, is not admitted.
108
98
  const validNamespace = (node, parent) => {
109
99
  const tag = node.tagName.toLowerCase(), parentTag = (parent.tagName || 'template').toLowerCase();
110
100
  const namespace = node.namespaceURI, parentNamespace = parent.namespaceURI || htmlNamespace;
@@ -158,6 +148,7 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
158
148
  const key = name.toLowerCase();
159
149
  const value = name === 'value' ? inputValue : inputValue.trim();
160
150
  if (forbiddenAttributes.has(key) || key.startsWith('on')) continue;
151
+ if (key === 'id' && chromeOwnsId(value)) continue;
161
152
  if (/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i.test(value)
162
153
  || key === 'attributename' && value.includes('href')) continue;
163
154
  const data = /^data-[\-\w.\u00b7-\uffff]+$/.test(key), aria = /^aria-[\-\w]+$/.test(key);
@@ -179,13 +170,9 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
179
170
  };
180
171
  }
181
172
 
182
- // The document's renderer. DOM, codecs and host state are explicit inputs.
183
173
  function createRenderSanitizer(runtime) {
184
174
  const {CSSStyleSheet, DOMPurify, RAPIER_RASTER_DATA_URL_RE, URL, _rapierCssDeclaration, _rapierDropRemoteDeclarations, _rapierRemoteContent, _rapierRuleDescriptorIsRemote, _rapierSanitizeRuntime, document, globalThis, location} = runtime;
185
175
 
186
- // Only the diagram reader and its styled export admit static foreignObject labels. The
187
- // ordinary SVG reader still rejects HTML subdocuments. The export class selects scope;
188
- // every descendant is checked again, so a copied class never confers authority.
189
176
  function diagramLabelRoot(node) {
190
177
  for (let current = node; current?.nodeType === 1; current = current.parentElement) {
191
178
  if (current.localName?.toLowerCase() === 'foreignobject') return current;
@@ -205,7 +192,6 @@ function diagramLabelStyle(property, value) {
205
192
  declaration.setProperty(property, value);
206
193
  const parsed = declaration.getPropertyValue(property);
207
194
  if (!parsed || /(?:url|image-set|element|paint|attr)\s*\(/i.test(parsed)) return false;
208
- // SVG paint accepts resource references; a custom property must not smuggle one in.
209
195
  return !/^(?:fill|stroke)$/.test(property) || !/var\s*\(/i.test(parsed);
210
196
  }
211
197
 
@@ -279,14 +265,6 @@ function _rapierInstallSanitizeHooks() {
279
265
  && _rapierImageDataAttributeAllowed(tag, data.attrName.toLowerCase(), compact, _rapierSafeRasterDataUrl);
280
266
  if (!rasterAllowed) data.keepAttr = false;
281
267
  });
282
- // Paste, render and export sanitise: a remote <img src> would fetch the instant its markup lands in the page,
283
- // so the src the hook below is about to strip never survives. Move it to a data-* attribute first
284
- // (DOMPurify already leaves data-* alone, and the sanitizer's own inert document never fetches it
285
- // either), so the picture and its alt text still reach Turndown's rapierImage rule as a live
286
- // pasted-page picture and write out `![alt](url)` -- not vanish -- and an <img> in a document's own
287
- // HTML (a table kept as HTML) is written again with its URL: the raw profile admits no data-*, so
288
- // only the three this hook sets on the node it holds back stay. A picture inside a link is the same
289
- // node with the same fix: the outer <a> rule reads this img's own converted markdown as its content.
290
268
  DOMPurify.addHook('uponSanitizeElement', node => {
291
269
  if (_rapierRemoteContent.allowed || !/^(?:paste|render|raw|export)$/.test(_rapierSanitizeRuntime.context)) return;
292
270
  if (!node || node.nodeName !== 'IMG' || node.hasAttribute('data-rapier-remote-src')) return;
@@ -308,18 +286,12 @@ function _rapierInstallSanitizeHooks() {
308
286
  node.setAttribute('data-rapier-remote-src', href);
309
287
  }
310
288
  });
311
- // A list from Word is named by the style of its paragraph and of the span that holds its marker (mso-list,
312
- // spec/html-reading.mjs), and the CSS pass below empties a style of every property the browser does not
313
- // know: a marker's style is only `mso-list:Ignore`. The paste carries both as data-* first, where they stay,
314
- // for the conversion that reads them (_rapierPasteWordLists).
315
289
  DOMPurify.addHook('uponSanitizeElement', node => {
316
290
  if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|LI|H[1-6]|SPAN)$/.test(node.nodeName)) return;
317
- // Only the style names them: a page cannot bring these attributes of its own.
318
291
  node.removeAttribute('data-rapier-word-level');
319
292
  node.removeAttribute('data-rapier-word-marker');
320
293
  const style = node.getAttribute('style');
321
294
  if (!style || !/mso-list/i.test(style)) return;
322
- // Word's list reading is the editor's (AGPL) grammar; a host without it keeps the paste's paragraphs as they are.
323
295
  const spec = globalThis.RapierMarkdownSpec;
324
296
  if (!spec) return;
325
297
  const {wordListLevel, wordListIsMarker} = spec;
@@ -334,9 +306,7 @@ function _rapierInstallSanitizeHooks() {
334
306
  data.keepAttr = !!data.attrValue;
335
307
  return;
336
308
  }
337
- // A document's own HTML never carries an id the chrome owns, remote content allowed or not: the
338
- // allow is about what the page fetches, and this rule is about what the page's chrome finds.
339
- if (String(data.attrName || '').toLowerCase() === 'id' && _rapierSanitizeRuntime.context === 'raw' && _rapierChromeOwnsId(value)) { data.keepAttr = false; return; }
309
+ if (String(data.attrName || '').toLowerCase() === 'id' && _rapierChromeOwnsId(value)) { data.keepAttr = false; return; }
340
310
  if (_rapierRemoteContent.allowed || _rapierSanitizeRuntime.context === 'source') return;
341
311
  if (String(data.attrName || '').toLowerCase() === 'style') {
342
312
  const cleaned = _rapierStyleWithoutRemoteUrls(value);
@@ -344,7 +314,6 @@ function _rapierInstallSanitizeHooks() {
344
314
  return;
345
315
  }
346
316
  const attribute = String(data.attrName || '').toLowerCase();
347
- // These attributes carry authored text, never resource requests.
348
317
  if (attribute === 'alt' || attribute === 'title') return;
349
318
  if (/^data-rapier-remote-(?:src|alt|title)$/.test(attribute) && node && node._rapierHeldBack === true) { data.forceKeepAttr = true; return; }
350
319
  if (attribute.startsWith('data-')) return;
@@ -363,9 +332,6 @@ function _rapierInstallSanitizeHooks() {
363
332
  data.forceKeepAttr = true;
364
333
  }
365
334
  });
366
- // A link that opens a new tab never hands that tab a window.opener or a Referer: the editor's own
367
- // click router already opens with noopener,noreferrer; the sanitized markup that Share and the
368
- // standalone export carry gets the same rule in its bytes.
369
335
  DOMPurify.addHook('afterSanitizeAttributes', node => {
370
336
  if (!node || !/^(?:A|AREA)$/i.test(node.nodeName) || !node.hasAttribute('target')) return;
371
337
  if (String(node.getAttribute('target') || '').trim().toLowerCase() !== '_blank') return;
@@ -387,25 +353,17 @@ function sanitizeRapierHtml(value, context = 'render') {
387
353
  return sanitize(value, context, false);
388
354
  }
389
355
 
390
- // The same parser, profile and hooks admit both results. A private renderer can
391
- // adopt the sanitized nodes without serializing and parsing them again.
392
356
  function sanitizeRapierFragment(value, context = 'render') {
393
357
  return sanitize(value, context, true);
394
358
  }
395
359
 
396
360
  function sanitize(value, context, fragment) {
397
- // 'export' is the render profile under the export hooks; 'raw' is a document's own HTML (initMarkdownIt, html_block):
398
- // the render profile without data-* attributes, which Rapier's delegated handlers read as their own controls, and
399
- // (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
400
- // profile's admission, and the block on remote content stays the reader's (a remote picture's URL is the person's words).
401
361
  const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
402
362
  const labels = selected === 'diagram' || selected === 'export', key = selected + (fragment ? ' fragment' : '');
403
363
  let options = purifyOptions.get(key);
404
364
  if (!options) {
405
365
  options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
406
366
  if (selected === 'source') options = {...options, ADD_DATA_URI_TAGS: ['image']};
407
- // DOMPurify's USE_PROFILES replaces explicit allowlists. Add MathML to the
408
- // selected profile without discarding its admitted HTML and SVG vocabulary.
409
367
  if (labels) options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
410
368
  ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'], ADD_DATA_URI_TAGS: ['image'],
411
369
  HTML_INTEGRATION_POINTS: {foreignobject: true}, FORBID_TAGS: options.FORBID_TAGS.filter(tag => tag !== 'foreignobject')};
@@ -417,8 +375,6 @@ function sanitize(value, context, fragment) {
417
375
  DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
418
376
  DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
419
377
  }
420
- // DOMPurify reparses a per-call configuration on every call (its cache compares its own copy), about 0.45 ms
421
- // each in Chrome 155. A profile is set once and kept until another profile is asked for; every caller comes here.
422
378
  if (purifyConfigured.get(DOMPurify) !== options) { DOMPurify.setConfig(options); purifyConfigured.set(DOMPurify, options); }
423
379
  const previous = _rapierSanitizeRuntime.context;
424
380
  _rapierSanitizeRuntime.context = selected;
@@ -444,15 +400,10 @@ function escapeRapierHtmlText(value) {
444
400
  }
445
401
 
446
402
  function _rapierChromeOwnsId(id) {
447
- // Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it),
448
- // and every rapier- id: the chrome makes its dialogs, overlays, parts and plug-in list later, under that prefix
449
- // (rapier-anchor- is the renderer's own name for a repeated document anchor, and stays the document's).
450
403
  if (!_rapierSanitizeRuntime.chromeIds) _rapierSanitizeRuntime.chromeIds = new Set(Array.from(typeof document?.querySelectorAll === 'function' ? document.querySelectorAll('[id]') : [], element => element.id));
451
404
  return _rapierSanitizeRuntime.chromeIds.has(String(id)) || /^rapier-(?!anchor-)/.test(id);
452
405
  }
453
406
 
454
- // A link whose words or address carry a bidi control, or whose host is spelled in punycode, can show a destination it does
455
- // not have. Opening one names its real address first; the answer is that address, or '' when the link opens at once.
456
407
  const RAPIER_BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
457
408
  function _rapierLinkDestinationToConfirm(href, text = '') {
458
409
  let url, address = String(href || '');
@@ -484,9 +435,6 @@ function _rapierCssPresentationIsRemote(property, value) {
484
435
  return declaration.length ? _rapierDropRemoteDeclarations(declaration) : false;
485
436
  }
486
437
 
487
- // A reference with no scheme and no host of its own ("photo.png", "#top", each word of a class) resolves on the base's scheme and
488
- // host whatever its path, so its answer is the base's, found once for each base and page: a long document's render asked
489
- // this of every token of every attribute, a URL built each time.
490
438
  const relativeReference = {base: null, here: null, origin: ''};
491
439
  function _rapierRemoteSubresourceOrigin(value) {
492
440
  const raw = String(value == null ? '' : value)
@@ -544,7 +492,7 @@ function _rapierDropRemoteRules(rules, owner) {
544
492
  }
545
493
 
546
494
  function _rapierVerifyRasterBytes(bytes, mime) {
547
- if (mime === 'image/jxl') return globalThis.RapierImageAssets?.isJxl(bytes) === true; // without the editor's codec a JPEG XL picture is refused
495
+ if (mime === 'image/jxl') return globalThis.RapierImageAssets?.isJxl(bytes) === true;
548
496
 
549
497
  // declaration, doctype or generator/license comment before <svg itself, of whatever length its
550
498
 
@@ -1,12 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // The document's renderer. DOM, codecs and host state are explicit inputs.
3
2
  function createRenderStyles(runtime) {
4
3
  const {_rapierArtifactAccent, _rapierStyleText} = runtime;
5
4
  function _rapierArtifactThemeCss(houseDefaults = false) {
6
- // Geometry and the selected accent belong to this page; palette and type belong to the
7
5
  // MIT reference sheet. Static code highlighting reads the app token names.
8
- // The live editor resolves System to Light before applying the reference heading tokens;
9
- // a carried System page keeps following the reader's device, including those light tokens.
10
6
  const accent = houseDefaults ? '' : ':root,.md-render{--md-color-accent:' + _rapierArtifactAccent() + '}';
11
7
  return `
12
8
  ${accent}
@@ -82,7 +78,6 @@ function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = f
82
78
  else if (includeFonts) parts.push(_rapierStyleText('rapier-font-style'));
83
79
  parts.push(_rapierStyleText('rapier-content-style'));
84
80
  parts.push(_rapierArtifactThemeCss(houseDefaults));
85
- // Local faces keep an offline fallback. The HTML writer may append a small factory Sans subset.
86
81
  if (!includeFonts && fontCss === null) parts.push(`@font-face{font-family:'Geist/Roboto';src:local('Roboto');font-weight:400 500;size-adjust:105%;ascent-override:95.7%;descent-override:28.1%;line-gap-override:0%}
87
82
  @font-face{font-family:'Geist/Roboto';src:local('Roboto Bold');font-weight:600 700;size-adjust:105%;ascent-override:95.7%;descent-override:28.1%;line-gap-override:0%}
88
83
  @font-face{font-family:'Geist/Helvetica Neue';src:local('Helvetica Neue');font-weight:400 500;size-adjust:103.8%;ascent-override:96.8%;descent-override:28.4%;line-gap-override:0%}
@@ -102,8 +97,6 @@ function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = f
102
97
  :root,.md-render{--md-font-sans:'Geist','Geist/Roboto','Geist/Helvetica Neue','Geist/Segoe UI','Geist/Arial','Geist/Noto Sans','Geist/Ubuntu','Geist/Liberation Sans','Geist/DejaVu Sans',system-ui,sans-serif}`);
103
98
  const highlightCss = _rapierStyleText('rapier-highlight-style');
104
99
  parts.push(highlightCss);
105
- // A page written for the system theme has no body.light to switch: under a light system
106
- // setting each palette takes its light values from the sheet's own body.light rule.
107
100
  if (theme === 'system') for (const [, palette, light] of highlightCss.matchAll(/html\[data-highlights="(standard|accent)"\] body\.light\s*\{([^}]*)\}/g))
108
101
  parts.push('@media (prefers-color-scheme:light){html[data-rapier-theme="system"][data-highlights="' + palette + '"] body{' + light.trim() + '}}');
109
102
  if (printMode) parts.push(_rapierArtifactPrintCss());
@@ -1,5 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // The synchronous and cooperative writers consume the same document transformations.
3
2
  export function finish(iterator) {
4
3
  let next;
5
4
  do { next = iterator.next(); } while (!next.done);
@@ -19,8 +18,6 @@ export async function pause(work, fraction, label) {
19
18
  check(work);
20
19
  }
21
20
 
22
- // This locates complete fragments; the existing HTML parser and sanitizer still admit
23
- // every byte. Uncertain nesting stays together instead of guessing a new boundary.
24
21
  export function htmlBoundary() {
25
22
  const stack = [], voids = new Set('area base br col embed hr img input link meta param source track wbr'.split(' '));
26
23
  const rawTags = new Set('script style textarea title xmp iframe noembed noframes'.split(' '));
@@ -101,9 +98,6 @@ export function* cloneTree(root) {
101
98
  return copy;
102
99
  }
103
100
 
104
- // Null entries are traversal checkpoints, never fragment boundaries. A large
105
- // nested unit is serialized cooperatively but is still admitted as one complete
106
- // HTML/namespace unit by the synchronous parser and sanitizer.
107
101
  export function* htmlParts(root, limit = 16384, consume = false) {
108
102
  limit = Math.max(2, limit);
109
103
  const text = root.ownerDocument.createElement('div');
@@ -135,11 +129,7 @@ export function* htmlParts(root, limit = 16384, consume = false) {
135
129
  if (at.nodeType === 1) {
136
130
  if (small(at)) pieces.push(at.outerHTML);
137
131
  else {
138
- // Native shallow serialization owns tag/attribute spelling and
139
- // escaping. Native text serialization below keeps its parent context.
140
132
  const empty = at.cloneNode(false);
141
- // A foreign element with children must retain its native closing tag
142
- // even in an inert DOM that self-closes an empty SVG element.
143
133
  if (at.firstChild) empty.appendChild(root.ownerDocument.createTextNode(''));
144
134
  const shell = empty.outerHTML, close = /<\/[^<>]+>$/.exec(shell)?.[0] || '';
145
135
  if (close && at.firstChild) {
@@ -182,7 +172,6 @@ export function* htmlParts(root, limit = 16384, consume = false) {
182
172
  export function admitHtml(root, html, sanitize, fragment = null, context = 'export') {
183
173
  if (fragment) root.appendChild(fragment(html, context));
184
174
  else {
185
- // Inert string-only hosts retain their explicitly supported admission path.
186
175
  const part = root.ownerDocument.createElement('div');
187
176
  part.innerHTML = sanitize(html, context);
188
177
  while (part.firstChild) root.appendChild(part.firstChild);
@@ -208,7 +197,6 @@ export function* serializeTree(root, sanitize = null, trim = false) {
208
197
  return trim ? result.trim() : result;
209
198
  }
210
199
 
211
- // Chunk boundaries never split a UTF-16 pair; TextEncoder remains the byte authority.
212
200
  export function* encodeUtf8Steps(source, size = 65536) {
213
201
  const encoder = new TextEncoder(), parts = [];
214
202
  let length = 0;