rapier-markdown-kit 1.2.7 → 1.2.9

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 (67) hide show
  1. package/README.md +32 -30
  2. package/dist/agent/will.mjs +1 -6
  3. package/dist/images/gif.mjs +0 -3
  4. package/dist/images/playback.mjs +1 -10
  5. package/dist/kit/assets.mjs +0 -1
  6. package/dist/kit/conformance/measurer.mjs +0 -0
  7. package/dist/kit/conformance/run.mjs +1 -8
  8. package/dist/kit/export-fonts.mjs +0 -14
  9. package/dist/kit/index.mjs +1 -1
  10. package/dist/kit/ledger/authored-origin.mjs +0 -5
  11. package/dist/kit/ledger/authorship.mjs +0 -2
  12. package/dist/kit/ledger/carried.d.mts +20 -0
  13. package/dist/kit/ledger/carried.mjs +4 -7
  14. package/dist/kit/ledger/data.mjs +0 -8
  15. package/dist/kit/ledger/format.mjs +0 -25
  16. package/dist/kit/ledger/hash.mjs +11 -7
  17. package/dist/kit/ledger/history.mjs +10 -29
  18. package/dist/kit/ledger/inverse-source.mjs +0 -46
  19. package/dist/kit/ledger/journal-records.mjs +24 -56
  20. package/dist/kit/ledger/merge.mjs +0 -27
  21. package/dist/kit/ledger/text-merge.mjs +0 -20
  22. package/dist/kit/ledger/transport.mjs +0 -47
  23. package/dist/kit/ledger.d.mts +88 -0
  24. package/dist/kit/ledger.mjs +14 -0
  25. package/dist/kit/markdown-it.d.mts +4 -0
  26. package/dist/kit/markdown-it.mjs +36 -0
  27. package/dist/kit/pdf-glyphs.mjs +39 -45
  28. package/dist/kit/pdf-source.d.mts +5 -0
  29. package/dist/kit/pdf-source.mjs +125 -0
  30. package/dist/kit/pdf-vectors.mjs +0 -3
  31. package/dist/kit/print-capitals.mjs +0 -4
  32. package/dist/kit/render-entry.mjs +0 -1
  33. package/dist/kit/render-ink.mjs +0 -5
  34. package/dist/kit/render-markdown.mjs +15 -119
  35. package/dist/kit/render-print.mjs +4 -49
  36. package/dist/kit/render-sanitize.mjs +22 -58
  37. package/dist/kit/render-styles.mjs +0 -7
  38. package/dist/kit/render-work.mjs +0 -12
  39. package/dist/kit/render.mjs +18 -173
  40. package/dist/kit/will.d.mts +42 -0
  41. package/dist/layout/hyphenation.mjs +0 -23
  42. package/dist/layout/line-plan.mjs +0 -20
  43. package/dist/layout/model.mjs +0 -55
  44. package/dist/layout/paragraph-fit.mjs +50 -60
  45. package/dist/layout/preparation.mjs +23 -56
  46. package/dist/layout/prepared-paragraph.mjs +0 -25
  47. package/dist/layout/typography.mjs +103 -41
  48. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  49. package/dist/packages/rapier-diff/diff.mjs +0 -7
  50. package/dist/packages/rapier-diff/index.mjs +0 -1
  51. package/dist/packages/rapier-diff/matching.mjs +0 -19
  52. package/dist/packages/rapier-diff/merge.mjs +0 -9
  53. package/dist/packages/rapier-diff/moves.mjs +0 -7
  54. package/dist/packages/rapier-diff/structure.mjs +0 -2
  55. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  56. package/dist/packages/rapier-diff/walk.mjs +0 -12
  57. package/dist/spec/document-fonts.mjs +0 -9
  58. package/dist/spec/document-settings.mjs +6 -25
  59. package/dist/spec/frontmatter.mjs +0 -1
  60. package/dist/spec/html-blocks.mjs +3 -37
  61. package/dist/spec/md-assets.mjs +2 -61
  62. package/dist/spec/md-layout.mjs +0 -34
  63. package/dist/spec/md-marks.mjs +0 -51
  64. package/dist/spec/md-source.mjs +0 -16
  65. package/dist/spec/word-sidecar.mjs +0 -2
  66. package/package.json +21 -4
  67. package/style.css +38 -21
@@ -5,9 +5,10 @@ import {fontRequests, waitForFonts} from './export-fonts.mjs';
5
5
  import {apply as applyTypography} from '../layout/typography.mjs';
6
6
  import {documentLanguage} from '../spec/document-settings.mjs';
7
7
  import {applyPrintCapitals} from './print-capitals.mjs';
8
+ import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will.mjs';
9
+ import {quotedWillSpan} from '../spec/md-marks.mjs';
8
10
  const RAPIER_WILL_SENTINEL_MARK = '⁣';
9
11
  // SPDX-License-Identifier: MIT
10
- // The document's renderer. DOM, codecs and host state are explicit inputs.
11
12
  function createPrintRenderer(runtime) {
12
13
  const {Node, _rapierWillParse, crypto, document, globalThis} = runtime;
13
14
  function _rapierWillSentinelPlan(canonical, kind) {
@@ -27,12 +28,11 @@ function _rapierWillSentinelPlan(canonical, kind) {
27
28
  return { canonical: out + source.slice(cursor), markers, sentinel };
28
29
  }
29
30
 
30
- const _rapierWillStripOneTerminator = globalThis.RapierAgentWill.stripOneTerminator;
31
31
 
32
32
 
33
33
 
34
34
  function _rapierPrintWillAdmit(canonical, plan) {
35
- const quoted = globalThis.RapierMarkdownSpec.quotedWillSpan(canonical, _rapierWillParse(canonical).blocks.map(block => [block.start, block.end]));
35
+ const quoted = quotedWillSpan(canonical, _rapierWillParse(canonical).blocks.map(block => [block.start, block.end]));
36
36
  if (quoted && quoted.where === 'frontmatter') throw _rapierPrintWillLost('a marker stands in the front matter, which is not on the printed page', 'Take it out of the front matter');
37
37
  if (quoted) 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');
38
38
  for (const marker of plan.markers) {
@@ -45,8 +45,6 @@ function _rapierPrintWillAdmit(canonical, plan) {
45
45
  function _rapierPrintWillFont(markers, advanceOverride) {
46
46
  const metrics = _rapierPrintWillMetrics(markers);
47
47
  const size = metrics.size;
48
- // The page's carrier face keeps the short marker advance. Searchable text is
49
- // positioned at 500 units, which is also /DW, so this copy can be asked to match it.
50
48
  const advance = advanceOverride ?? metrics.advance;
51
49
  const scalars = Array.from(new Set(markers.flatMap(marker => Array.from(marker, ch => ch.codePointAt(0)))));
52
50
  const order = scalars.filter(code => code < 0x10000).sort((a, b) => a - b)
@@ -74,8 +72,6 @@ function _rapierPrintWillFont(markers, advanceOverride) {
74
72
  const cmap = narrow.length <= 0xffff
75
73
  ? cat(u16(0), u16(2), u16(3), u16(1), u32(20), u16(3), u16(10), u32(20 + narrow.length), narrow, full)
76
74
  : cat(u16(0), u16(1), u16(3), u16(10), u32(12), full);
77
- // The collinear contour has zero area and enough bounds for native print at 2px.
78
- // Keep it below the baseline so a carrier at a page break does not straddle both page clips.
79
75
  const bounds = [0, -400, 10, 0];
80
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]);
81
77
  const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
@@ -119,7 +115,6 @@ function _rapierPrintWillMetrics(markers) {
119
115
  }
120
116
 
121
117
  function _rapierPrintWillPlace(root, plan) {
122
- // 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.
123
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'); };
124
119
  const measure = _rapierPrintWillMetrics(plan.markers);
125
120
  const tokens = new Map(plan.markers.map((marker, index) => [plan.sentinel + index + RAPIER_WILL_SENTINEL_MARK, index]));
@@ -152,8 +147,6 @@ function _rapierPrintWillPlace(root, plan) {
152
147
  carrier.textContent = plan.markers[row.index];
153
148
  return carrier;
154
149
  });
155
- // 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
156
- // moves to the run, so the gap the rule makes with the block above is the one it made.
157
150
  if (next && next.tagName === 'HR') {
158
151
  if (first.previousElementSibling || first.parentNode !== root) carriers[0].style.marginTop = 'var(--md-line)';
159
152
  next.style.marginTop = '0';
@@ -176,7 +169,6 @@ function _rapierPrintWillPageCss() {
176
169
  + '}';
177
170
  }
178
171
 
179
- // The capture's own limits (agent/visual.mjs VISUAL_LIMITS): the longest edge and the most pixels one capture returns.
180
172
  const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
181
173
 
182
174
  async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
@@ -186,8 +178,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
186
178
  if (current?.() === false) refuse('document_changed');
187
179
  };
188
180
  if (!root?.ownerDocument || typeof capture !== 'function') refuse('pdf_render_unavailable');
189
- // The native printer chooses its default paper. A file made without a printer uses A4,
190
- // with the same explicit document settings and default margins as the print artifact.
191
181
  const paper = settings?.papersize || {width: 11906, height: 16838};
192
182
  const width = paper.width / 20, height = paper.height / 20;
193
183
  const margin = settings?.geometry ? settings.geometry.twips / 20 : null;
@@ -220,8 +210,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
220
210
  check();
221
211
  const into = frame.contentDocument;
222
212
  if (!into?.body) refuse('pdf_render_unavailable');
223
- // The capture and native printer use standards layout. An initial blank
224
- // iframe otherwise measures preformatted inline struts in quirks mode.
225
213
  into.open(); into.write('<!doctype html><html><head></head><body></body></html>'); into.close();
226
214
  into.documentElement.setAttribute('data-rapier-theme', 'light');
227
215
  into.documentElement.setAttribute('lang', documentLanguage(settings, document.defaultView?.navigator?.language));
@@ -245,7 +233,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
245
233
  strip.append(root); surface.append(strip); into.body.append(surface);
246
234
  const main = root.querySelector('main');
247
235
  if (!main) refuse('pdf_render_unavailable');
248
- // A file for paper is light whatever the person's own scheme: the page's text takes its colour scheme from this attribute.
249
236
  main.setAttribute('data-md-theme', 'light');
250
237
  for (const details of main.querySelectorAll('details')) details.open = true;
251
238
  main.getBoundingClientRect();
@@ -253,12 +240,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
253
240
  await step(into.fonts.ready);
254
241
  for (const image of main.querySelectorAll('img')) { image.loading = 'eager'; await step(image.decode()); }
255
242
  check();
256
- // The print sheet owns the words and styles. CSS fragmentation supplies the file's
257
- // pages without a print dialog; its break constraints become column constraints here.
258
243
  for (const node of [main, ...main.querySelectorAll('*')]) {
259
244
  const style = into.defaultView.getComputedStyle(node);
260
- // Clipped, out-of-flow source has no page ink. Leaving it in a multicolumn
261
- // container can still create blank columns and expose its text to PDF search.
262
245
  if (!node.closest('.rapier-will-carrier') && ['absolute', 'fixed'].includes(style.position) && /^rect\(/.test(style.clip)) {
263
246
  const box = style.clip.slice(5, -1).trim().split(/[,\s]+/).map(Number.parseFloat);
264
247
  if (box.length === 4 && box.every(Number.isFinite) && (box[1] <= box[3] || box[2] <= box[0])) {
@@ -277,12 +260,9 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
277
260
  globalThis.RapierMath?.fit(main);
278
261
  applyPrintCapitals(main);
279
262
  applyTypography(main, {all: true});
280
- // The document's ink marks, drawn on the words where they now stand on the page.
281
263
  ink?.(main);
282
264
  const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
283
265
  if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
284
- // Captured document font bytes own a same-name family; factory glyphs must
285
- // never replace their page paint. This uses the artifact, not a live catalog.
286
266
  const customFamilies = new Set(fontCss ? fontRequests(main, {fontCss, css, settings}).map(face => face.family.toLowerCase()) : []);
287
267
  nativeGlyphs = await collectPdfGlyphs(main, {width: bodyWidth, check,
288
268
  faces: globalThis.RapierDocxFonts?.DOCX_FONTS?.filter(face => !customFamilies.has(face.family.toLowerCase()))});
@@ -303,11 +283,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
303
283
  const font = _rapierPrintWillFont(text, 500);
304
284
  strip.style.width = count * bodyWidth + 'px';
305
285
  strip.style.transform = 'none';
306
- // One styled film is reused for every page window. Batches stay inside the capture edge
307
- // and pixel limits. A picture no longer forces its own capture. Pixels are deflated once.
308
286
  if (typeof film !== 'function') refuse('pdf_render_unavailable');
309
- // step() can reject after the film resolves (the document changed, or the attempt was
310
- // cancelled). The session would otherwise keep its abort listener with no owner.
311
287
  let pendingFilm = null, abandonFilm = false;
312
288
  const trackedFilm = Promise.resolve(film({root: surface, signal, current, frame: {width: count * bodyWidth, height: bodyHeight}})).then(session => {
313
289
  if (abandonFilm) session?.close?.();
@@ -365,8 +341,6 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
365
341
  } finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
366
342
  }
367
343
 
368
- // Ranges locate the actual text fragments after column layout. Splitting a text node at
369
- // its line and page boundaries keeps a PDF search on the page that carries those words.
370
344
  async function _rapierPrintTextPages(main, pageWidth, count, check, origin = main.getBoundingClientRect(), nativeNodes = null) {
371
345
  const into = main.ownerDocument;
372
346
  const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 5);
@@ -381,8 +355,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
381
355
  if (node.nodeType === 1 && !formula || node.nodeType === 3 && element.closest('.math-rendered[data-math-src]')) continue;
382
356
  let value;
383
357
  if (formula) {
384
- // The math owner paints SVG paths and retains the equation's exact TeX alternative.
385
- // Carry that semantic text at the SVG's measured position for search and copy.
386
358
  value = formula.getAttribute('aria-label') || '';
387
359
  } else value = node.data;
388
360
  const style = into.defaultView.getComputedStyle(element);
@@ -396,14 +368,11 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
396
368
  const visit = (start, end) => {
397
369
  if (++operations % 128 === 0) check();
398
370
  if (!formula) { range.setStart(node, start); range.setEnd(node, end); }
399
- // A tiny carrier font can have its advance rounded to zero by a browser. Its
400
- // measured baseline still locates the marker; the font's authored metrics keep its text width.
401
371
  const rects = Array.from(formula ? [formula.getBoundingClientRect()] : range.getClientRects()).filter(rect => rect.height > 0 && (rect.width > 0 || carrier));
402
372
  if (!rects.length) { if (carrier) markerLost(); return; }
403
373
  const first = rects[0], page = pageOf(first.left);
404
374
  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);
405
375
  let middle = formula ? start : Math.floor((start + end) / 2);
406
- // A line never breaks inside a character: halve between whole ones (a letter and its marks, a conjunct, an emoji).
407
376
  if (middle > start) middle = (characters ||= segmenter.segment(value)).containing(middle).index;
408
377
  const cluster = native?.find(item => item.start < middle && item.end > middle);
409
378
  if (cluster) middle = cluster.start > start ? cluster.start : cluster.end;
@@ -435,9 +404,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
435
404
  visit(0, value.length);
436
405
  }
437
406
  check();
438
- // Trailing-carrier print CSS pulls a last marker up so it stays on the sheet.
439
- // Keep that paint; put the searchable object after the preceding words so
440
- // reading order matches the source (the marker itself has no visible ink).
441
407
  const pageHeight = origin.height;
442
408
  for (const page of pages) {
443
409
  let prev = null;
@@ -453,8 +419,6 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
453
419
  prev = run;
454
420
  }
455
421
  }
456
- // Each run carries the tags of its block's structure (outermost first): a PDF reader finds the document's headings, lists and
457
- // tables from them rather than from positions.
458
422
  const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
459
423
  H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
460
424
  const tags = block => {
@@ -497,8 +461,6 @@ function _rapierPrintLinkPages(main, width, height, count) {
497
461
  return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
498
462
  }
499
463
 
500
- // Where each scalar of an invisible text run stands, in character slots from the run's left edge: a mark shares its letter's
501
- // 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).
502
464
  function _rapierPrintSlots(text, rtl) {
503
465
  const pieces = [];
504
466
  let scalars = 0, count = 0;
@@ -519,8 +481,6 @@ function _rapierPrintSlots(text, rtl) {
519
481
  return {slots, count};
520
482
  }
521
483
 
522
- // A standard PDF page tree, image streams, Unicode text and an associated source file.
523
- // It accepts already-rendered pages, so there is no second document grammar or layout engine.
524
484
  async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
525
485
  const encoder = new TextEncoder(), encode = value => encoder.encode(value);
526
486
  const fail = code => { throw Object.assign(new Error(code), {code}); };
@@ -588,7 +548,6 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
588
548
  nativeResources.push(entry.name + ' ' + type + ' 0 R');
589
549
  }
590
550
  const sharedImages = new Map();
591
- // The structure tree (PDF 1.7, 14.7): one element per tagged DOM element, each run marked with its page's next MCID.
592
551
  const structs = new Map(), documentElement = {role: 'Document', kids: []}, parentTree = [];
593
552
  const structOf = tags => {
594
553
  let parent = documentElement;
@@ -606,7 +565,6 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
606
565
  const held = pages[index], flate = !!held?.rgb;
607
566
  const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
608
567
  pages[index] = null;
609
- // The page's picture is how its tagged text looks, not a figure of its own: an artifact.
610
568
  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';
611
569
  const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
612
570
  for (const drawing of vectorPages[index] || []) {
@@ -637,9 +595,6 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
637
595
  const textAlphas = new Map();
638
596
  for (const run of textPages[index]) {
639
597
  if (!run.text) continue;
640
- // Keep one logical Unicode text object, resetting its font before each scalar's direction
641
- // can change. Readers that ignore ActualText otherwise discard earlier words in mixed RTL/LTR.
642
- // Express width in the font size: some readers ignore horizontal text scaling when joining.
643
598
  const element = structOf(run.tags || []);
644
599
  if (element) {element.kids.push({page: index, mcid: marked.length}); commands += `/${element.role} << /MCID ${marked.length} >> BDC\n`; marked.push(element);}
645
600
  commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
@@ -682,7 +637,7 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
682
637
  const parents = add('<< /Nums [' + parentTree.map((marked, index) => index + ' [' + marked.map(element => ids.get(element) + ' 0 R').join(' ') + ']').join(' ') + '] >>');
683
638
  put(tree, '<< /Type /StructTreeRoot /K [' + ids.get(documentElement) + ' 0 R] /ParentTree ' + parents + ' 0 R /ParentTreeNextKey ' + parentTree.length + ' >>');
684
639
  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] >>');
685
- const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
640
+ const info = add('<< /Producer (Rapier) /Creator (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
686
641
  const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
687
642
  let cursor = header.length;
688
643
  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,17 +75,18 @@ 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
- function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl}) {
95
- if (![parseFragment, serialize, sanitizeHtml, safeRasterDataUrl].every(port => typeof port === 'function'))
88
+ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId = id => /^rapier-(?!anchor-)/.test(id)}) {
89
+ if (![parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId].every(port => typeof port === 'function'))
96
90
  throw new TypeError('The inert HTML sanitizer requires every parser and filtering port');
97
91
  const uri = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
98
92
  const uriSafe = new Set(['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);
@@ -101,8 +95,6 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
101
95
  const svgTags = new Set(RAPIER_SANITIZE_PROFILES.svgTags), mathTags = new Set(RAPIER_SANITIZE_PROFILES.mathTags);
102
96
  const mathIntegration = new Set(['mi', 'mo', 'mn', 'ms', 'mtext']), htmlIntegration = new Set(['annotation-xml']);
103
97
  const sharedHtmlSvg = new Set(['title', 'style', 'font', 'a', 'script']);
104
- // DOMPurify's default namespace admission applies in addition to its tag profile. In
105
- // particular a MathML name parsed as HTML, or HTML inside SVG desc, is not admitted.
106
98
  const validNamespace = (node, parent) => {
107
99
  const tag = node.tagName.toLowerCase(), parentTag = (parent.tagName || 'template').toLowerCase();
108
100
  const namespace = node.namespaceURI, parentNamespace = parent.namespaceURI || htmlNamespace;
@@ -156,6 +148,7 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
156
148
  const key = name.toLowerCase();
157
149
  const value = name === 'value' ? inputValue : inputValue.trim();
158
150
  if (forbiddenAttributes.has(key) || key.startsWith('on')) continue;
151
+ if (key === 'id' && chromeOwnsId(value)) continue;
159
152
  if (/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i.test(value)
160
153
  || key === 'attributename' && value.includes('href')) continue;
161
154
  const data = /^data-[\-\w.\u00b7-\uffff]+$/.test(key), aria = /^aria-[\-\w]+$/.test(key);
@@ -177,13 +170,9 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
177
170
  };
178
171
  }
179
172
 
180
- // The document's renderer. DOM, codecs and host state are explicit inputs.
181
173
  function createRenderSanitizer(runtime) {
182
174
  const {CSSStyleSheet, DOMPurify, RAPIER_RASTER_DATA_URL_RE, URL, _rapierCssDeclaration, _rapierDropRemoteDeclarations, _rapierRemoteContent, _rapierRuleDescriptorIsRemote, _rapierSanitizeRuntime, document, globalThis, location} = runtime;
183
175
 
184
- // Only the diagram reader and its styled export admit static foreignObject labels. The
185
- // ordinary SVG reader still rejects HTML subdocuments. The export class selects scope;
186
- // every descendant is checked again, so a copied class never confers authority.
187
176
  function diagramLabelRoot(node) {
188
177
  for (let current = node; current?.nodeType === 1; current = current.parentElement) {
189
178
  if (current.localName?.toLowerCase() === 'foreignobject') return current;
@@ -203,7 +192,6 @@ function diagramLabelStyle(property, value) {
203
192
  declaration.setProperty(property, value);
204
193
  const parsed = declaration.getPropertyValue(property);
205
194
  if (!parsed || /(?:url|image-set|element|paint|attr)\s*\(/i.test(parsed)) return false;
206
- // SVG paint accepts resource references; a custom property must not smuggle one in.
207
195
  return !/^(?:fill|stroke)$/.test(property) || !/var\s*\(/i.test(parsed);
208
196
  }
209
197
 
@@ -277,14 +265,6 @@ function _rapierInstallSanitizeHooks() {
277
265
  && _rapierImageDataAttributeAllowed(tag, data.attrName.toLowerCase(), compact, _rapierSafeRasterDataUrl);
278
266
  if (!rasterAllowed) data.keepAttr = false;
279
267
  });
280
- // Paste, render and export sanitise: a remote <img src> would fetch the instant its markup lands in the page,
281
- // so the src the hook below is about to strip never survives. Move it to a data-* attribute first
282
- // (DOMPurify already leaves data-* alone, and the sanitizer's own inert document never fetches it
283
- // either), so the picture and its alt text still reach Turndown's rapierImage rule as a live
284
- // pasted-page picture and write out `![alt](url)` -- not vanish -- and an <img> in a document's own
285
- // HTML (a table kept as HTML) is written again with its URL: the raw profile admits no data-*, so
286
- // only the three this hook sets on the node it holds back stay. A picture inside a link is the same
287
- // node with the same fix: the outer <a> rule reads this img's own converted markdown as its content.
288
268
  DOMPurify.addHook('uponSanitizeElement', node => {
289
269
  if (_rapierRemoteContent.allowed || !/^(?:paste|render|raw|export)$/.test(_rapierSanitizeRuntime.context)) return;
290
270
  if (!node || node.nodeName !== 'IMG' || node.hasAttribute('data-rapier-remote-src')) return;
@@ -306,18 +286,15 @@ function _rapierInstallSanitizeHooks() {
306
286
  node.setAttribute('data-rapier-remote-src', href);
307
287
  }
308
288
  });
309
- // A list from Word is named by the style of its paragraph and of the span that holds its marker (mso-list,
310
- // spec/html-reading.mjs), and the CSS pass below empties a style of every property the browser does not
311
- // know: a marker's style is only `mso-list:Ignore`. The paste carries both as data-* first, where they stay,
312
- // for the conversion that reads them (_rapierPasteWordLists).
313
289
  DOMPurify.addHook('uponSanitizeElement', node => {
314
290
  if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|LI|H[1-6]|SPAN)$/.test(node.nodeName)) return;
315
- // Only the style names them: a page cannot bring these attributes of its own.
316
291
  node.removeAttribute('data-rapier-word-level');
317
292
  node.removeAttribute('data-rapier-word-marker');
318
293
  const style = node.getAttribute('style');
319
294
  if (!style || !/mso-list/i.test(style)) return;
320
- const {wordListLevel, wordListIsMarker} = globalThis.RapierMarkdownSpec;
295
+ const spec = globalThis.RapierMarkdownSpec;
296
+ if (!spec) return;
297
+ const {wordListLevel, wordListIsMarker} = spec;
321
298
  const level = /^(?:P|LI|H[1-6])$/.test(node.nodeName) ? wordListLevel(style) : 0;
322
299
  if (level) node.setAttribute('data-rapier-word-level', String(level));
323
300
  else if (node.nodeName === 'SPAN' && wordListIsMarker(style)) node.setAttribute('data-rapier-word-marker', '1');
@@ -329,9 +306,7 @@ function _rapierInstallSanitizeHooks() {
329
306
  data.keepAttr = !!data.attrValue;
330
307
  return;
331
308
  }
332
- // A document's own HTML never carries an id the chrome owns, remote content allowed or not: the
333
- // allow is about what the page fetches, and this rule is about what the page's chrome finds.
334
- 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; }
335
310
  if (_rapierRemoteContent.allowed || _rapierSanitizeRuntime.context === 'source') return;
336
311
  if (String(data.attrName || '').toLowerCase() === 'style') {
337
312
  const cleaned = _rapierStyleWithoutRemoteUrls(value);
@@ -339,7 +314,6 @@ function _rapierInstallSanitizeHooks() {
339
314
  return;
340
315
  }
341
316
  const attribute = String(data.attrName || '').toLowerCase();
342
- // These attributes carry authored text, never resource requests.
343
317
  if (attribute === 'alt' || attribute === 'title') return;
344
318
  if (/^data-rapier-remote-(?:src|alt|title)$/.test(attribute) && node && node._rapierHeldBack === true) { data.forceKeepAttr = true; return; }
345
319
  if (attribute.startsWith('data-')) return;
@@ -358,9 +332,6 @@ function _rapierInstallSanitizeHooks() {
358
332
  data.forceKeepAttr = true;
359
333
  }
360
334
  });
361
- // A link that opens a new tab never hands that tab a window.opener or a Referer: the editor's own
362
- // click router already opens with noopener,noreferrer; the sanitized markup that Share and the
363
- // standalone export carry gets the same rule in its bytes.
364
335
  DOMPurify.addHook('afterSanitizeAttributes', node => {
365
336
  if (!node || !/^(?:A|AREA)$/i.test(node.nodeName) || !node.hasAttribute('target')) return;
366
337
  if (String(node.getAttribute('target') || '').trim().toLowerCase() !== '_blank') return;
@@ -382,25 +353,17 @@ function sanitizeRapierHtml(value, context = 'render') {
382
353
  return sanitize(value, context, false);
383
354
  }
384
355
 
385
- // The same parser, profile and hooks admit both results. A private renderer can
386
- // adopt the sanitized nodes without serializing and parsing them again.
387
356
  function sanitizeRapierFragment(value, context = 'render') {
388
357
  return sanitize(value, context, true);
389
358
  }
390
359
 
391
360
  function sanitize(value, context, fragment) {
392
- // 'export' is the render profile under the export hooks; 'raw' is a document's own HTML (initMarkdownIt, html_block):
393
- // the render profile without data-* attributes, which Rapier's delegated handlers read as their own controls, and
394
- // (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
395
- // profile's admission, and the block on remote content stays the reader's (a remote picture's URL is the person's words).
396
361
  const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
397
362
  const labels = selected === 'diagram' || selected === 'export', key = selected + (fragment ? ' fragment' : '');
398
363
  let options = purifyOptions.get(key);
399
364
  if (!options) {
400
365
  options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
401
366
  if (selected === 'source') options = {...options, ADD_DATA_URI_TAGS: ['image']};
402
- // DOMPurify's USE_PROFILES replaces explicit allowlists. Add MathML to the
403
- // selected profile without discarding its admitted HTML and SVG vocabulary.
404
367
  if (labels) options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
405
368
  ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'], ADD_DATA_URI_TAGS: ['image'],
406
369
  HTML_INTEGRATION_POINTS: {foreignobject: true}, FORBID_TAGS: options.FORBID_TAGS.filter(tag => tag !== 'foreignobject')};
@@ -412,8 +375,6 @@ function sanitize(value, context, fragment) {
412
375
  DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
413
376
  DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
414
377
  }
415
- // DOMPurify reparses a per-call configuration on every call (its cache compares its own copy), about 0.45 ms
416
- // each in Chrome 155. A profile is set once and kept until another profile is asked for; every caller comes here.
417
378
  if (purifyConfigured.get(DOMPurify) !== options) { DOMPurify.setConfig(options); purifyConfigured.set(DOMPurify, options); }
418
379
  const previous = _rapierSanitizeRuntime.context;
419
380
  _rapierSanitizeRuntime.context = selected;
@@ -439,13 +400,19 @@ function escapeRapierHtmlText(value) {
439
400
  }
440
401
 
441
402
  function _rapierChromeOwnsId(id) {
442
- // Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it),
443
- // and every rapier- id: the chrome makes its dialogs, overlays, parts and plug-in list later, under that prefix
444
- // (rapier-anchor- is the renderer's own name for a repeated document anchor, and stays the document's).
445
403
  if (!_rapierSanitizeRuntime.chromeIds) _rapierSanitizeRuntime.chromeIds = new Set(Array.from(typeof document?.querySelectorAll === 'function' ? document.querySelectorAll('[id]') : [], element => element.id));
446
404
  return _rapierSanitizeRuntime.chromeIds.has(String(id)) || /^rapier-(?!anchor-)/.test(id);
447
405
  }
448
406
 
407
+ const RAPIER_BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
408
+ function _rapierLinkDestinationToConfirm(href, text = '') {
409
+ let url, address = String(href || '');
410
+ try { url = new URL(address); } catch (_) { return ''; }
411
+ try { address = decodeURIComponent(address); } catch (_) {}
412
+ return RAPIER_BIDI_CONTROL.test(String(text)) || RAPIER_BIDI_CONTROL.test(address) ||
413
+ url.hostname.split('.').some(label => /^xn--/i.test(label)) ? url.href : '';
414
+ }
415
+
449
416
  function _rapierSafeRasterDataUrl(value) {
450
417
  const match = RAPIER_RASTER_DATA_URL_RE.exec(value);
451
418
  if (!match) return false;
@@ -468,9 +435,6 @@ function _rapierCssPresentationIsRemote(property, value) {
468
435
  return declaration.length ? _rapierDropRemoteDeclarations(declaration) : false;
469
436
  }
470
437
 
471
- // 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
472
- // host whatever its path, so its answer is the base's, found once for each base and page: a long document's render asked
473
- // this of every token of every attribute, a URL built each time.
474
438
  const relativeReference = {base: null, here: null, origin: ''};
475
439
  function _rapierRemoteSubresourceOrigin(value) {
476
440
  const raw = String(value == null ? '' : value)
@@ -528,7 +492,7 @@ function _rapierDropRemoteRules(rules, owner) {
528
492
  }
529
493
 
530
494
  function _rapierVerifyRasterBytes(bytes, mime) {
531
- if (mime === 'image/jxl') return globalThis.RapierImageAssets.isJxl(bytes);
495
+ if (mime === 'image/jxl') return globalThis.RapierImageAssets?.isJxl(bytes) === true;
532
496
 
533
497
  // declaration, doctype or generator/license comment before <svg itself, of whatever length its
534
498
 
@@ -542,6 +506,6 @@ function _rapierVerifyRasterBytes(bytes, mime) {
542
506
 
543
507
  return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml,
544
508
  sanitizeRapierFragment: typeof DOMPurify?.sanitize === 'function' ? sanitizeRapierFragment : undefined,
545
- diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
509
+ diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierLinkDestinationToConfirm, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
546
510
  }
547
511
  export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, createInertHtmlSanitizer, RAPIER_SANITIZE_OPTIONS as options, prepareMathSource};
@@ -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;