rapier-markdown-kit 1.1.102 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +5 -2
  2. package/README.md +245 -54
  3. package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
  4. package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
  5. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
  6. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
  7. package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
  8. package/dist/agent/vendor/pretext/LICENSE +208 -0
  9. package/dist/agent/vendor/pretext/NOTICE +26 -13
  10. package/dist/agent/vendor/pretext/SOURCE.json +213 -45
  11. package/dist/agent/vendor/pretext/analysis.js +675 -1224
  12. package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
  13. package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
  14. package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
  15. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
  16. package/dist/agent/vendor/pretext/graphemes.js +98 -0
  17. package/dist/agent/vendor/pretext/han-kerning.js +253 -0
  18. package/dist/agent/vendor/pretext/layout.js +352 -290
  19. package/dist/agent/vendor/pretext/line-break.js +1154 -588
  20. package/dist/agent/vendor/pretext/line-breaks.js +892 -0
  21. package/dist/agent/vendor/pretext/line-text.js +81 -39
  22. package/dist/agent/vendor/pretext/measurement.js +1105 -176
  23. package/dist/agent/vendor/pretext/prepare.js +807 -0
  24. package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
  25. package/dist/agent/will.mjs +2 -0
  26. package/dist/images/gif.mjs +47 -0
  27. package/dist/images/playback.mjs +101 -0
  28. package/dist/kit/assets.mjs +5 -1
  29. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  30. package/dist/kit/conformance/04-wrap-box.md +1 -1
  31. package/dist/kit/conformance/05-behind.md +1 -1
  32. package/dist/kit/conformance/06-front.md +1 -1
  33. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  34. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  35. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  36. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  37. package/dist/kit/conformance/10-both-sides.md +1 -1
  38. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  39. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  40. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  41. package/dist/kit/conformance/README.md +19 -28
  42. package/dist/kit/conformance/run.mjs +13 -11
  43. package/dist/kit/export-fonts.mjs +26 -11
  44. package/dist/kit/index.mjs +1 -0
  45. package/dist/kit/ledger/carried.mjs +17 -10
  46. package/dist/kit/ledger/data.mjs +73 -13
  47. package/dist/kit/ledger/format.mjs +176 -54
  48. package/dist/kit/ledger/hash.mjs +91 -18
  49. package/dist/kit/ledger/history.mjs +156 -116
  50. package/dist/kit/ledger/inverse-source.mjs +158 -19
  51. package/dist/kit/ledger/journal-records.mjs +292 -67
  52. package/dist/kit/ledger/merge.mjs +4 -3
  53. package/dist/kit/model.mjs +1 -1
  54. package/dist/kit/pdf-glyphs.mjs +4 -4
  55. package/dist/kit/pdf-vectors.mjs +4 -1
  56. package/dist/kit/rapier-ledger.mjs +2 -2
  57. package/dist/kit/render-ink.mjs +1 -1
  58. package/dist/kit/render-markdown.mjs +400 -148
  59. package/dist/kit/render-print.mjs +131 -55
  60. package/dist/kit/render-sanitize.mjs +30 -7
  61. package/dist/kit/render-work.mjs +99 -20
  62. package/dist/kit/render.mjs +133 -140
  63. package/dist/kit/typography.mjs +2 -0
  64. package/dist/layout/hyphenation.mjs +127 -0
  65. package/dist/layout/line-plan.mjs +88 -96
  66. package/dist/layout/model.mjs +71 -88
  67. package/dist/layout/paragraph-fit.mjs +191 -0
  68. package/dist/layout/preparation.mjs +373 -0
  69. package/dist/layout/prepared-paragraph.mjs +223 -0
  70. package/dist/layout/typography.mjs +345 -111
  71. package/dist/packages/rapier-diff/index.mjs +359 -224
  72. package/dist/spec/document-fonts.mjs +14 -5
  73. package/dist/spec/document-settings.mjs +11 -164
  74. package/dist/spec/html-blocks.mjs +618 -79
  75. package/dist/spec/md-assets.mjs +178 -36
  76. package/dist/spec/md-layout.mjs +13 -2
  77. package/dist/spec/md-marks.mjs +220 -127
  78. package/dist/spec/md-source.mjs +157 -0
  79. package/package.json +2 -1
  80. package/style.css +15 -50
  81. package/dist/agent/vendor/pretext/bidi.js +0 -151
  82. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,11 +1,10 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {htmlImageRecord, mountHtmlBlocks} from '../spec/html-blocks.mjs';
2
+ import {htmlImageRecord, htmlRuntimeSource} from '../spec/html-blocks.mjs';
3
3
  import {finish, finishAsync, cloneTree, cleanTree, serializeTree, encodeUtf8Steps, pause} from './render-work.mjs';
4
- import {formatFontOpen, fontFamilyCss, FONT_CLOSE} from '../spec/md-marks.mjs';
4
+ import {fontFamilyCss} from '../spec/md-marks.mjs';
5
5
  import {readFontAssets, documentFontsCss} from '../spec/document-fonts.mjs';
6
6
  import {fontRequests, requireDocumentFonts} from './export-fonts.mjs';
7
7
  import {documentLanguage, documentSettingsFonts} from '../spec/document-settings.mjs';
8
- import {createTypography} from '../layout/typography.mjs';
9
8
  import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
10
9
  import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
11
10
  import {freezePublishedInk} from './render-ink.mjs';
@@ -69,7 +68,7 @@ export function positionNativeDiagram(drawing) {
69
68
  }
70
69
 
71
70
  function createRenderer(runtime) {
72
- const {RAPIER_COLOR_CLOSE, RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierFormatColorOpen, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, rapierConfirm, sanitizeRapierHtml} = runtime;
71
+ const {RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, sanitizeRapierHtml} = runtime;
73
72
  let md = runtime.md;
74
73
  async function render(source, options = {}) {
75
74
  if (typeof source !== 'string') throw new TypeError('Document source must be a string');
@@ -158,14 +157,16 @@ async function _rapierBuildArtifact(options, providedContext) {
158
157
  styledRoot.setAttribute('lang', language);
159
158
  if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
160
159
  // _rapierProjectStyledRoot already returns the sanitized projection.
161
- // Print never emits the layout reflow script (see below), so a print artifact has no use for
162
- // this annotation -- skip it there rather than pay the cost for numbers nothing will read.
160
+ // Picture-shape annotations have no consumer in a printed artifact.
163
161
  if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
162
+ // Paper keeps each picture's wrap through the same static float layout a page uses.
163
+ if (completePage || opts.print) {
164
+ if (work) await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
165
+ else _rapierSharedPageLayout(styledRoot);
166
+ }
164
167
  if (completePage && !opts.print) {
165
- if (work) {
166
- await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
167
- await finishAsync(_rapierShareCapLongCodeBlocksSteps(styledRoot), work);
168
- } else { _rapierSharedPageLayout(styledRoot); _rapierShareCapLongCodeBlocks(styledRoot); }
168
+ if (work) await finishAsync(_rapierShareCapLongCodeBlocksSteps(styledRoot), work);
169
+ else _rapierShareCapLongCodeBlocks(styledRoot);
169
170
  }
170
171
  if (typeof opts.afterRoot === 'function') await opts.afterRoot(styledRoot);
171
172
  markPrintCapitals(styledRoot);
@@ -173,6 +174,7 @@ async function _rapierBuildArtifact(options, providedContext) {
173
174
  ? 'light'
174
175
  : _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
175
176
  const highlights = _rapierArtifactPreference('highlights', ['accent', 'standard']);
177
+ const invertImageAlpha = _rapierArtifactPreference('invertImageAlpha', ['true', 'false']) !== 'false';
176
178
  const versionMeta = document.querySelector('meta[name="rapier-version"]');
177
179
  const version = versionMeta && versionMeta.content ? versionMeta.content.trim() : '0.0.0';
178
180
  const bodyClass = theme === 'light' ? 'light' : '';
@@ -185,8 +187,6 @@ async function _rapierBuildArtifact(options, providedContext) {
185
187
  // rules of its own -- Share, for the no-script float it alone writes -- appends them.
186
188
  // Print retains its existing factory resources. HTML adds only the optional small Sans subset below.
187
189
  const includeFonts = !!opts.print;
188
- const zenFontCss = opts.print && styledRoot.querySelector('svg[aria-roledescription="zenuml"]')
189
- ? (_rapierProviders?.mermaid?.exportFontCss?.() || '') : '';
190
190
  const willFont = opts.print && opts.willFont ? opts.willFont : null;
191
191
  const settingsCss = _rapierDocumentSettingsCss(settings);
192
192
  const refined = styledRoot.querySelector('p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-caps="small"],p[data-md-hanging="on"],img[data-rapier-print-lines]');
@@ -225,12 +225,11 @@ async function _rapierBuildArtifact(options, providedContext) {
225
225
  // The reflow script is written first, because whether it exists decides the policy. A page that carries no script says so: script-src 'none' refuses every
226
226
  // script outright. Declaring a nonce a page has no use for is a widening, small but real -- anything that could inject markup into the file could read the
227
227
  // nonce out of it and be admitted.
228
- const layoutScript = !opts.print && opts.scripts !== false ? _rapierArtifactLayoutScript(styledRoot, nonce) : '';
228
+ const layoutScript = opts.scripts !== false ? _rapierArtifactLayoutScript(styledRoot, nonce, {pictures:!opts.print}) : '';
229
229
  const inkScript = !opts.print && opts.scripts !== false ? _rapierArtifactInkScript(styledRoot, nonce) : '';
230
- const typographyScript = opts.scripts !== false ? _rapierArtifactTypographyScript(styledRoot, nonce) : '';
231
- const htmlScript = opts.scripts !== false && styledRoot.querySelector('[data-rapier-html]') ? '<script nonce="' + nonce + '">(' + mountHtmlBlocks.toString().replace(/<\/script/gi, '<\\/script') + ')(document);</script>' : '';
230
+ const htmlScript = !opts.print && opts.scripts !== false && styledRoot.querySelector('[data-rapier-html],[data-rapier-animation]') ? '<script nonce="' + nonce + '">' + htmlRuntimeSource().replace(/<\/script/gi, '<\\/script') + '</script>' : '';
232
231
  const page = '<!DOCTYPE html>\n'
233
- + '<html lang="' + escapeRapierHtmlText(language) + '" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '">\n'
232
+ + '<html lang="' + escapeRapierHtmlText(language) + '" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '" data-invert-image-alpha="' + (invertImageAlpha ? 'on' : 'off') + '">\n'
234
233
  + '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
235
234
  + '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
236
235
  // Chrome 154's responsively sized frames: a host frame styled `frame-sizing: content-height` sizes
@@ -238,14 +237,14 @@ async function _rapierBuildArtifact(options, providedContext) {
238
237
  // someone's page shows whole, with no inner scroll. Any origin may read the height: the page is the
239
238
  // document, published by the person who exported it.
240
239
  + '<meta name="responsive-embedded-sizing" content="allow-origins=*">\n'
241
- + '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || inkScript || typographyScript || htmlScript ? nonce : '', includeFonts || !!fontCss || !!factoryFontCss) + '">\n'
240
+ + '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || inkScript || htmlScript ? nonce : '', includeFonts || !!fontCss || !!factoryFontCss) + '">\n'
242
241
  + '<meta name="referrer" content="no-referrer">\n'
243
242
  + '<link rel="icon" href="data:,">\n'
244
243
  + '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
245
244
  + '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
246
245
  + '<style>' + css + '</style>\n</head>\n'
247
- + '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '"' + (settings?.space ? ' data-md-space' : '') + '>'
248
- + bodyHtml + '</main>' + carrier + layoutScript + inkScript + typographyScript + htmlScript + '</body>\n</html>';
246
+ + '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '">'
247
+ + bodyHtml + '</main>' + carrier + layoutScript + inkScript + htmlScript + '</body>\n</html>';
249
248
 
250
249
  return {
251
250
  html: page,
@@ -302,49 +301,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
302
301
  if (lang) code.classList.add('language-' + String(lang).replace(/[^a-z0-9_-]/gi, ''));
303
302
  }
304
303
  } else {
305
- if (root.hasAttribute('data-md-space')) {
306
- // One raw HTML token may have several siblings. Its authored heights stay inside one line-sized box.
307
- for (let element = root.firstElementChild; element;) {
308
- const group = element.getAttribute('data-md-html-block');
309
- if (group === null) { element = element.nextElementSibling; continue; }
310
- let first = element;
311
- // Sanitizing a container or repairing a table can leave its text before the first surviving tag.
312
- while (first.previousSibling && first.previousSibling.nodeType !== 1) { first = first.previousSibling; yield; }
313
- const wrap = document.createElement('div');
314
- wrap.className = 'md-space-block';
315
- root.insertBefore(wrap, first);
316
- let node = first;
317
- while (node && (node.nodeType !== 1 || node.getAttribute('data-md-html-block') === group)) {
318
- const next = node.nextSibling;
319
- wrap.appendChild(node);
320
- node = next;
321
- yield;
322
- }
323
- element = wrap.nextElementSibling;
324
- }
325
- // A removed raw container can leave only text. Reserve that surviving content in this projection.
326
- for (let first = root.firstChild; first;) {
327
- if (first.nodeType === 1) { first = first.nextSibling; continue; }
328
- let end = first, hasText = false;
329
- while (end && end.nodeType !== 1) {
330
- if (end.nodeType === 3 && end.nodeValue.trim()) hasText = true;
331
- end = end.nextSibling;
332
- yield;
333
- }
334
- if (hasText) {
335
- const wrap = document.createElement('div');
336
- wrap.className = 'md-space-block';
337
- root.insertBefore(wrap, first);
338
- for (let node = first; node !== end;) {
339
- const next = node.nextSibling;
340
- wrap.appendChild(node);
341
- node = next;
342
- yield;
343
- }
344
- }
345
- first = end;
346
- }
347
- }
348
304
  for (const code of root.querySelectorAll('pre > code')) { yield;
349
305
  if (code.parentElement?.classList.contains('diagram-source') &&
350
306
  code.closest('.diagram-block')?.getAttribute('data-diagram-state') === 'ready') continue;
@@ -466,11 +422,15 @@ function _rapierPaintCode(el, html) {
466
422
 
467
423
  function _rapierPrefixPortableAnchors(root, baseName) { return finish(_rapierPrefixPortableAnchorsSteps(root, baseName)); }
468
424
 
469
- function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
425
+ // The id an anchor carries on a portable page: the document's name, then the anchor with what an id cannot hold made a hyphen.
426
+ function _rapierPortableAnchorId(baseName, id) {
470
427
  const slug = String(baseName || 'document').toLowerCase()
471
428
  .replace(/[^a-z0-9]+/g, '-')
472
429
  .replace(/^-+|-+$/g, '') || 'document';
473
- const prefix = 'rapier-' + slug + '-';
430
+ return 'rapier-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
431
+ }
432
+
433
+ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
474
434
  const ids = new Map(), svgIds = new Map();
475
435
  const used = new Set();
476
436
  const scopeOf = element => {
@@ -487,11 +447,10 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
487
447
  for (const element of root.querySelectorAll('[id]')) { yield;
488
448
  const oldId = element.id;
489
449
  if (!oldId) continue;
490
- const base = oldId.replace(/[^a-z0-9_-]+/gi, '-')
491
- .replace(/^-+|-+$/g, '') || 'anchor';
492
- let nextId = prefix + base;
450
+ const first = _rapierPortableAnchorId(baseName, oldId);
451
+ let nextId = first;
493
452
  let suffix = 2;
494
- while (used.has(nextId)) nextId = prefix + base + '-' + suffix++;
453
+ while (used.has(nextId)) nextId = first + '-' + suffix++;
495
454
  used.add(nextId);
496
455
  // A document link resolves the first matching anchor, as it did before renaming.
497
456
  // SVG references resolve within their own drawing, never the last repeated drawing.
@@ -694,13 +653,14 @@ function _rapierSharedResolve(text, ids, images, definitions) {
694
653
  let broken = false;
695
654
  const src = value => {
696
655
  const found = images.get(value);
697
- if (typeof found !== 'string' || !/^data:image\/(?:png|jpeg|webp|svg\+xml|jxl);base64,[A-Za-z0-9+/]+={0,2}$/i.test(found)) { broken = true; return null; }
656
+ if (typeof found !== 'string' || !/^data:image\/(?:png|jpeg|webp|gif|svg\+xml|jxl);base64,[A-Za-z0-9+/]+={0,2}$/i.test(found)) { broken = true; return null; }
698
657
  return found;
699
658
  };
700
659
  // Authored # characters were encoded. A raw id after ]( can only be a structural
701
660
  // substitution; matching its destination lets nested alt brackets survive unchanged.
702
661
  const inline = new RegExp('(\\]\\([ \\t\\r\\n]*(?:&lt;)?)#' + id + '(?=>?[ \\t\\r\\n]*\\)|>?[ \\t\\r\\n]+["\'(])', 'g');
703
- const definition = new RegExp('(^ {0,3}\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
662
+ // A definition may sit in a quote or a list item: its line starts with that container's markers.
663
+ const definition = new RegExp('(^[ \\t>]*\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t>]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
704
664
  const swapInline = (match, lead, value) => { const found = src(value); return found == null ? match : lead + found; };
705
665
  const swapDefinition = (match, lead, label, value) => {
706
666
  if (!definitions.has(md.utils.normalizeReference(_rapierSharedDecode(label)))) return match;
@@ -878,7 +838,7 @@ function* _rapierSharedPageLayoutSteps(root) {
878
838
  // `layout.y` already owns `shape-outside` for its own inset reservation (below); the two are not
879
839
  // combinable (a float has exactly one `shape-outside` value), so a `y`-offset `around` picture
880
840
  // keeps that inset instead of silhouette wrapping -- a documented, narrow degradation.
881
- if (layout.wrap === 'around' && !layout.rotate && !layout.y) {
841
+ if (layout.wrap === 'around' && !layout.rotate && !layout.y && !image.dataset.rapierContour?.endsWith('-box')) {
882
842
  const src = image.getAttribute('src');
883
843
  if (src) {
884
844
  paragraph.style.setProperty('shape-outside', 'url("' + src + '")');
@@ -906,16 +866,12 @@ function* _rapierSharedPageLayoutSteps(root) {
906
866
  for (const {image, paragraph, layout} of positionedAssignments) {
907
867
  yield;
908
868
  paragraph.style.cssText = 'position:relative;height:0;margin:0;padding:0;line-height:0';
909
- const x = layout.x ?? 50, naturalWidth = Number(image.getAttribute('width')) || 0;
910
- if (layout.width != null) {
911
- image.style.width = layout.width + '%';
912
- image.style.left = (x - layout.width / 2) + '%';
913
- } else if (naturalWidth > 0) {
914
- image.style.width = naturalWidth + 'px';
915
- image.style.left = 'calc(' + x + '% - ' + (naturalWidth / 2) + 'px)';
916
- } else {
917
- image.style.left = x + '%';
918
- }
869
+ // Without x the picture starts at the left edge, as everywhere else (layout/model.mjs imageBox).
870
+ const x = layout.x, naturalWidth = Number(image.getAttribute('width')) || 0;
871
+ if (layout.width != null) image.style.width = layout.width + '%';
872
+ else if (naturalWidth > 0) image.style.width = naturalWidth + 'px';
873
+ image.style.left = x == null ? '0' : layout.width != null ? (x - layout.width / 2) + '%'
874
+ : naturalWidth > 0 ? 'calc(' + x + '% - ' + (naturalWidth / 2) + 'px)' : x + '%';
919
875
  image.style.position = 'absolute';
920
876
  image.style.top = (layout.y || 0) + 'em';
921
877
  image.style.margin = '0';
@@ -1381,17 +1337,10 @@ function _rapierImageOpenRunsList(tokens) {
1381
1337
  const stack = [];
1382
1338
  for (const token of block.children) {
1383
1339
  if (token.type === 'image') { list.push(stack.slice()); continue; }
1384
- if (token.type === 'rapier_color_open') {
1385
- const hex = token.attrGet('data-md-color');
1386
- stack.push({open: _rapierFormatColorOpen(hex), close: RAPIER_COLOR_CLOSE});
1387
- continue;
1340
+ if (token.meta?.typography && token.nesting === 1) {
1341
+ stack.push({open: token.meta.typography.open, close: token.meta.typography.close}); continue;
1388
1342
  }
1389
- if (token.type === 'rapier_color_close') { stack.pop(); continue; }
1390
- if (token.type === 'rapier_font_open') {
1391
- stack.push({open: token.meta?.mdFont?.source || formatFontOpen(token.attrGet('data-md-font')), close: FONT_CLOSE});
1392
- continue;
1393
- }
1394
- if (token.type === 'rapier_font_close') { stack.pop(); continue; }
1343
+ if (token.meta?.typography && token.nesting === -1) { stack.pop(); continue; }
1395
1344
  if (token.type === 'strong_open' || token.type === 'em_open' || token.type === 's_open') {
1396
1345
  stack.push({open: token.markup, close: token.markup});
1397
1346
  continue;
@@ -1407,10 +1356,11 @@ function _rapierArtifactFactoryModules(dependencies, group) {
1407
1356
  return paths.map(path => 'modules[' + JSON.stringify(path) + '] = (' + Function.prototype.toString.call(dependencies.factories[path]) + ')();').join('\n');
1408
1357
  }
1409
1358
 
1410
- function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1411
- if (!root || !metadata || !geometry || !pretext) return null;
1359
+ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1360
+ if (!root || !metadata || !geometry || !preparation) return null;
1412
1361
  const doc = root.ownerDocument, view = doc.defaultView;
1413
1362
  const styles = new Map(), originals = new Map(), spacers = [], floats = [], steps = [], profiles = new WeakMap();
1363
+ const prepared = preparation.createPreparationScope(doc);
1414
1364
  const css = node => view.getComputedStyle(node);
1415
1365
  const box = node => node.getBoundingClientRect();
1416
1366
  const pixels = value => (Number.isFinite(value) ? Math.round(value * 100) / 100 : 0) + 'px';
@@ -1437,48 +1387,72 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1437
1387
  steps.length = 0;
1438
1388
  }
1439
1389
 
1440
- // Plain inline text uses Pretext. Complex blocks keep their original controls through floats.
1390
+ // One prepared rich paragraph retains original item text. Unsupported shaping keeps native flow.
1441
1391
  function prepare(paragraph) {
1392
+ const refuse = () => { prepared.forget(paragraph); return null; };
1442
1393
  if (!/^(P|H[1-6])$/.test(paragraph.tagName) || !paragraph.textContent.trim() || paragraph.textContent.length > 8192 ||
1443
- paragraph.querySelector('img,svg,math,br,input,button,iframe,canvas,.math-rendered')) return null;
1394
+ [...paragraph.querySelectorAll('[id]')].some(element => /^[\s\u00ad\u200b]*$/u.test(element.textContent)) ||
1395
+ paragraph.querySelector('img,svg,math,br,input,button,iframe,canvas,.math-rendered')) return refuse();
1444
1396
  const computed = css(paragraph);
1445
- if (computed.textAlign === 'justify' || computed.writingMode !== 'horizontal-tb' ||
1446
- computed.textTransform !== 'none' || computed.whiteSpace !== 'normal' || computed.transform !== 'none' ||
1397
+ const language = paragraph.closest('[lang]')?.getAttribute('lang') || '';
1398
+ for (let parent = paragraph; parent; parent = parent.parentElement) {
1399
+ const inherited = css(parent);
1400
+ if (parent.style.textWrapStyle === 'pretty' || inherited.transform !== 'none' ||
1401
+ !['1', 'normal'].includes(inherited.zoom)) return refuse();
1402
+ }
1403
+ if (computed.textAlign === 'justify' || computed.textWrapStyle === 'balance' ||
1404
+ !preparation.supportsRichStyle(computed, language) || computed.whiteSpace !== 'normal' ||
1405
+ computed.transform !== 'none' ||
1406
+ ['::before', '::after'].some(pseudo => !['none', 'normal'].includes(view.getComputedStyle(paragraph, pseudo).content)) ||
1447
1407
  // Lines start at the padding box's top-left: left, right or top inset is refused.
1448
1408
  ['paddingLeft', 'paddingRight', 'paddingTop', 'borderLeftWidth', 'borderRightWidth', 'borderTopWidth']
1449
- .some(key => parseFloat(computed[key]) > 0)) return null;
1409
+ .some(key => parseFloat(computed[key]) > 0)) return refuse();
1450
1410
  const runs = [], items = [], walker = doc.createTreeWalker(paragraph, 4);
1451
1411
  for (let node; (node = walker.nextNode());) {
1452
- if (runs.length === 192) return null;
1412
+ if (runs.length === 192) return refuse();
1453
1413
  const parents = [];
1454
1414
  for (let parent = node.parentElement; parent !== paragraph; parent = parent.parentElement) {
1455
- if (!parent || !/^(A|SPAN|EM|I|STRONG|B|DEL|S|MARK|CODE|U|INS|ABBR|SUP|SUB)$/.test(parent.tagName)) return null;
1415
+ if (!parent || !/^(A|SPAN|EM|I|STRONG|B|DEL|S|MARK|CODE|KBD|SAMP|U|INS|ABBR|SUP|SUB)$/.test(parent.tagName) ||
1416
+ parent.hidden || parent.getAttribute('aria-hidden') === 'true' ||
1417
+ parent.style.textWrapStyle === 'pretty' ||
1418
+ parent.hasAttribute('lang') && parent.getAttribute('lang').toLowerCase() !== (language || '').toLowerCase()) return refuse();
1419
+ const parentStyle = css(parent);
1420
+ if (!preparation.supportsRichStyle(parentStyle, language) || parentStyle.display !== 'inline' ||
1421
+ parentStyle.cssFloat !== 'none' || !['static', 'relative'].includes(parentStyle.position) ||
1422
+ parentStyle.transform !== 'none' || !['1', 'normal'].includes(parentStyle.zoom) ||
1423
+ ['::before', '::after'].some(pseudo => !['none', 'normal'].includes(view.getComputedStyle(parent, pseudo).content))) return refuse();
1424
+ if (!/^(CODE|KBD|SAMP)$/.test(parent.tagName) &&
1425
+ ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth', 'marginLeft', 'marginRight']
1426
+ .some(key => Math.abs(parseFloat(parentStyle[key]) || 0) > .01)) return refuse();
1456
1427
  parents.unshift(parent);
1457
1428
  }
1458
1429
  const computedRun = css(node.parentElement);
1459
- if (computedRun.textTransform !== 'none' || computedRun.writingMode !== 'horizontal-tb') return null;
1430
+ if (!preparation.supportsRichStyle(computedRun, language) ||
1431
+ computedRun.whiteSpace !== computed.whiteSpace && computedRun.whiteSpace !== 'nowrap') return refuse();
1460
1432
  const font = `${computedRun.fontStyle} ${computedRun.fontWeight} ${computedRun.fontSize} ${computedRun.fontFamily}`;
1461
1433
  const letterSpacing = parseFloat(computedRun.letterSpacing) || 0;
1462
- const prepared = geometry.prepareRun(node.data, font, letterSpacing);
1463
- if (!prepared) return null;
1464
- // A chip's box is repeated by each piece of it, and it breaks as the browser breaks inline code, as the editor's projection does.
1465
- const boxed = parents.some(parent => parent.tagName === 'CODE');
1434
+ const boxed = parents.some(parent => /^(CODE|KBD|SAMP)$/.test(parent.tagName));
1435
+ if (boxed && (!/^(CODE|KBD|SAMP)$/.test(node.parentElement.tagName) || node.parentElement.childNodes.length !== 1)) return refuse();
1466
1436
  const extraWidth = boxed ? ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth', 'marginLeft', 'marginRight']
1467
1437
  .reduce((width, key) => width + (parseFloat(computedRun[key]) || 0), 0) : 0;
1468
- runs.push({parents, prepared, directions: parents.map(parent => parent.getAttribute('dir') === 'auto' ? css(parent).direction : null)});
1469
- items.push({text: prepared.raw, font, letterSpacing, break: 'normal', extraWidth});
1438
+ runs.push({parents, raw: node.data, font, letterSpacing,
1439
+ directions: parents.map(parent => parent.getAttribute('dir') === 'auto' ? css(parent).direction : null)});
1440
+ items.push({text: node.data, font, letterSpacing,
1441
+ break: computedRun.whiteSpace === 'nowrap' || parents.some(parent => parent.hasAttribute('data-rapier-source')) ? 'never' : 'normal', extraWidth});
1470
1442
  }
1471
- if (!runs.length) return null;
1443
+ if (!runs.length) return refuse();
1444
+ const options = {whiteSpace: computed.whiteSpace, wordBreak: computed.wordBreak, language};
1445
+ const flow = prepared.prepare(paragraph, items, options);
1446
+ if (!flow) return refuse();
1472
1447
  const fontSize = parseFloat(computed.fontSize) || 16;
1473
- return {paragraph, runs, flow: pretext.prepareRichInline(items), direction: computed.direction,
1448
+ return {paragraph, runs, flow, whiteSpace: computed.whiteSpace, direction: computed.direction,
1474
1449
  align: computed.textAlign, fontSize, lineHeight: parseFloat(computed.lineHeight) || fontSize * 1.65,
1475
- balance: computed.textWrapStyle === 'balance' ? fontSize : 0};
1450
+ balance: 0};
1476
1451
  }
1477
1452
 
1478
1453
  // An id survives on the first fragment only.
1479
- const idsKept = new WeakSet();
1480
- function fragmentNode(record, fragment) {
1481
- const run = record.runs[fragment.itemIndex], mapped = geometry.mapFragment(run?.prepared, fragment);
1454
+ function fragmentNode(record, fragment, idsKept) {
1455
+ const run = record.runs[fragment.itemIndex], mapped = geometry.mapFragment(run, fragment, {whiteSpace: record.whiteSpace});
1482
1456
  if (!mapped) return null;
1483
1457
  // A line ends on the white space it broke at. A chip, a link or a highlight must not paint that space, so it stands after the shell.
1484
1458
  const kept = run.parents.length ? mapped.text.replace(/[ \t\n\r\f]+$/, '') : mapped.text;
@@ -1497,10 +1471,12 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1497
1471
  }
1498
1472
 
1499
1473
  function project(record, width, top, obstacles) {
1474
+ if (!prepared.current(record.flow)) return false;
1500
1475
  const plan = geometry.flowLines(record.flow, width, top, obstacles, record.lineHeight,
1501
1476
  metadata.wrapColumnFloor(record.fontSize), record.direction, record.balance, record.align);
1502
1477
  if (!plan) return false;
1503
1478
  const output = doc.createDocumentFragment();
1479
+ const idsKept = new Set();
1504
1480
  let previous = null;
1505
1481
  for (const line of plan.lines) {
1506
1482
  const element = doc.createElement('span');
@@ -1508,17 +1484,21 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1508
1484
  element.style.textAlign = ['left', 'center', 'right', 'start', 'end'].includes(record.align)
1509
1485
  ? record.align : record.direction === 'rtl' ? 'right' : 'left';
1510
1486
  for (const fragment of line.fragments) {
1511
- const child = fragmentNode(record, fragment);
1487
+ const child = fragmentNode(record, fragment, idsKept);
1512
1488
  if (!child) return false;
1513
1489
  if (hasGap(record, previous, fragment)) {
1514
1490
  const gap = doc.createElement('span');
1491
+ const gapRun = record.runs[fragment.gapItemIndex];
1492
+ if (!gapRun && fragment.gapBefore !== 0) return false;
1515
1493
  gap.style.cssText = 'display:inline-block;width:' + pixels(fragment.gapBefore);
1494
+ if (gapRun) { gap.style.font = gapRun.font; gap.style.letterSpacing = pixels(gapRun.letterSpacing); }
1516
1495
  gap.textContent = ' '; element.append(gap);
1517
1496
  }
1518
1497
  element.append(child); previous = fragment;
1519
1498
  }
1520
1499
  output.append(element);
1521
1500
  }
1501
+ if (!prepared.current(record.flow)) return false;
1522
1502
  originals.set(record.paragraph, [...record.paragraph.childNodes]);
1523
1503
  record.paragraph.replaceChildren(output);
1524
1504
  style(record.paragraph, {position: 'relative', height: pixels(plan.height), 'min-height': '0'});
@@ -1529,10 +1509,11 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1529
1509
  if (!previous) return false;
1530
1510
  let found = false;
1531
1511
  for (let index = previous.itemIndex; index <= current.itemIndex; index++) {
1532
- const run = record.runs[index].prepared;
1533
- const start = index === previous.itemIndex ? geometry.cursorOffset(run, previous.end) : 0;
1534
- const end = index === current.itemIndex ? geometry.cursorOffset(run, current.start) : run.raw.length;
1535
- if (start == null || end == null || end < start) return false;
1512
+ const run = record.runs[index];
1513
+ if (!run) return false;
1514
+ const start = index === previous.itemIndex ? previous.sourceEnd : 0;
1515
+ const end = index === current.itemIndex ? current.sourceStart : run.raw.length;
1516
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > run.raw.length) return false;
1536
1517
  const gap = run.raw.slice(start, end);
1537
1518
  if (gap && !/^[ \t\n\r\f]+$/.test(gap)) return false;
1538
1519
  found ||= !!gap;
@@ -1543,6 +1524,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1543
1524
  // wrap=around reads data-rapier-occupancy, else alpha. Cache key: src, dimensions, layout.wrap, rotate angle.
1544
1525
  // A turned raster's profile is recomputed here as pictureProfile does, never shipped.
1545
1526
  function profile(image, layout) {
1527
+ if (layout.wrap === 'around' && image.dataset?.rapierContour?.endsWith('-box')) return null;
1546
1528
  const src = image.currentSrc || image.getAttribute('src') || '', rotateRad = (layout.rotate || 0) * Math.PI / 180;
1547
1529
  const cached = profiles.get(image);
1548
1530
  if (cached?.src === src && cached.width === image.naturalWidth && cached.height === image.naturalHeight &&
@@ -1756,8 +1738,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1756
1738
  if (image.hasAttribute('data-rapier-html')) {
1757
1739
  const attrs = JSON.parse(decodeURIComponent(image.getAttribute('data-rapier-html'))).attrs;
1758
1740
  const line = parseFloat(computed.lineHeight) || (parseFloat(computed.fontSize) || 16) * 1.6;
1759
- unrotated.height = !attrs.height || attrs.height === 'auto'
1760
- ? Number(image.dataset.rapierNaturalHeight) || box(image).height : Number(attrs.height) * line;
1741
+ unrotated.height = metadata.objectHeight(unrotated.width, Number(image.dataset.rapierNaturalHeight) || box(image).height, attrs, line);
1761
1742
  }
1762
1743
  // rasterReserved (layout/model.mjs).
1763
1744
  const rasterRad = (layout.rotate || 0) * Math.PI / 180;
@@ -1823,6 +1804,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1823
1804
 
1824
1805
  function beforePrint() { printing = true; restore(); }
1825
1806
  function afterPrint() { printing = false; schedule(); }
1807
+ function fontChanged() { prepared.invalidateFonts(); schedule(); }
1826
1808
 
1827
1809
  const observer = typeof view.ResizeObserver === 'function' ? new view.ResizeObserver(() => {
1828
1810
  const width = root.clientWidth;
@@ -1837,13 +1819,13 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1837
1819
  view.addEventListener('pageshow', schedule);
1838
1820
  view.addEventListener('beforeprint', beforePrint);
1839
1821
  view.addEventListener('afterprint', afterPrint);
1840
- doc.fonts?.addEventListener?.('loadingdone', schedule);
1822
+ for (const event of ['loading', 'loadingdone', 'loadingerror']) doc.fonts?.addEventListener?.(event, fontChanged);
1841
1823
  Promise.resolve(doc.fonts?.ready).then(schedule, schedule);
1842
1824
  schedule();
1843
- return Object.freeze({reflow, destroy() {
1825
+ return Object.freeze({reflow, destroy({documentChanged = false} = {}) {
1844
1826
  stopped = true;
1845
1827
  if (scheduled) view.cancelAnimationFrame(scheduled);
1846
- observer?.disconnect(); restore();
1828
+ observer?.disconnect(); restore(); prepared.retire({documentChanged});
1847
1829
  root.removeEventListener('load', schedule, true);
1848
1830
  root.removeEventListener('error', schedule, true);
1849
1831
  root.removeEventListener('rapier-html-resize', schedule);
@@ -1852,23 +1834,28 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1852
1834
  view.removeEventListener('pageshow', schedule);
1853
1835
  view.removeEventListener('beforeprint', beforePrint);
1854
1836
  view.removeEventListener('afterprint', afterPrint);
1855
- doc.fonts?.removeEventListener?.('loadingdone', schedule);
1837
+ for (const event of ['loading', 'loadingdone', 'loadingerror']) doc.fonts?.removeEventListener?.(event, fontChanged);
1856
1838
  }});
1857
1839
  }
1858
1840
 
1859
- function _rapierArtifactLayoutScript(root, nonce = '') {
1841
+ function _rapierArtifactLayoutScript(root, nonce = '', {pictures = true} = {}) {
1860
1842
  const dependencies = globalThis.RapierArtifactLayoutDependencies;
1861
1843
  // Every wrap value earns the reflow script, and an inline turned raster (keyed off data-rapier-image-layout).
1862
1844
  const layoutOf = element => globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'));
1863
1845
  const pictureLayoutOf = image => globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
1864
- if (!dependencies?.factories ||
1865
- (![...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) &&
1866
- ![...root.querySelectorAll('img[data-rapier-image-layout]')].some(image => pictureLayoutOf(image)?.rotate))) return '';
1846
+ const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
1847
+ [...root.querySelectorAll('img[data-rapier-image-layout]')].some(image => pictureLayoutOf(image)?.rotate));
1848
+ const typography = _rapierArtifactTypographyScript(root);
1849
+ if (!wrapping && !typography) return '';
1850
+ if (!dependencies?.factories || typography && !dependencies.factories['layout/typography.mjs'])
1851
+ throw new Error('Offline typography requires the complete retained MIT layout factories');
1867
1852
  const modules = _rapierArtifactFactoryModules(dependencies, 'layout');
1868
- const script = '/* Rapier export layout: SPDX-License-Identifier: AGPL-3.0-only */\n' +
1869
- '/* Pretext 0.0.9\n' + dependencies.license + '\n*/\n' +
1870
- '(() => {\nconst modules = {};\n' + modules + '\n(' + _rapierProjectArtifactLayout.toString() +
1871
- ')(document.querySelector("main.rapier-page"), modules["spec/md-layout.mjs"], modules["layout/model.mjs"], modules["agent/vendor/pretext/rich-inline.js"]);\n})();';
1853
+ const script = '/* Rapier export layout: SPDX-License-Identifier: MIT */\n' +
1854
+ '/* Pretext bac1774c0f8b7a92e44b9da43b6d5443d55d0d9b (Unreleased)\n' + dependencies.license + '\n*/\n' +
1855
+ '(() => {\nconst modules = Object.create(null);\n' + modules +
1856
+ (wrapping ? '\n(' + _rapierProjectArtifactLayout.toString() +
1857
+ ')(document.querySelector("main.rapier-page"), modules["spec/md-layout.mjs"], modules["layout/model.mjs"], modules["layout/preparation.mjs"]);\n' : '') +
1858
+ typography + '\n})();';
1872
1859
 
1873
1860
  return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
1874
1861
  }
@@ -1883,23 +1870,29 @@ function _rapierArtifactInkScript(root, nonce = '') {
1883
1870
  return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
1884
1871
  }
1885
1872
 
1886
- function _rapierArtifactTypographyScript(root, nonce = '') {
1873
+ function _rapierArtifactTypographyScript(root) {
1887
1874
  const pictures = root.querySelector('img[data-rapier-print-lines]');
1888
1875
  if (!pictures && !root.querySelector('p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-caps="small"],p[data-md-hanging="on"]')) return '';
1889
- const script = '(() => {\nconst typography = (' + createTypography.toString() + ')();\n' +
1876
+ return '(() => {\nconst typography = modules["layout/typography.mjs"].createTypography();\n' +
1890
1877
  (pictures ? 'const capitals = (' + createPrintCapitals.toString() + ')((' + createLineMeasurer.toString() + ')(),(' + linesHeight.toString() + '),(' + followingPrintProse.toString() + '));\n' : '') +
1891
- 'const root = document.querySelector("main.rapier-page"); let frame = 0, width = -1, printing = matchMedia("print").matches;\n' +
1892
- 'const apply = () => { frame = 0; ' + (pictures ? 'if (printing) capitals.apply(root); else capitals.clear(root); ' : '') + 'typography.apply(root, {all:true}); };\n' +
1878
+ 'const root = document.querySelector("main.rapier-page"); const printMedia = matchMedia("print"); let frame = 0, width = -1, ready = null, printing = printMedia.matches;\n' +
1879
+ // beforeprint can still expose the screen viewport, not the selected paper's content width.
1880
+ // Only an authored paper width supplies a fixed target; automatic paper keeps native wrapping.
1881
+ 'const apply = () => { cancelAnimationFrame(frame); frame = 0; ' + (pictures ? 'if (printing) capitals.apply(root); else capitals.clear(root); ' : '') + 'if (printing && !(parseFloat(getComputedStyle(root).getPropertyValue("--rapier-print-width")) > 0)) { typography.clear(root); ready?.(); ready = null; return; } if (typography.apply(root, {all:printing}).pending) schedule(); else { ready?.(); ready = null; } };\n' +
1893
1882
  'const schedule = () => { if (!frame) frame = requestAnimationFrame(apply); };\n' +
1894
- 'globalThis.__RAPIER_EXPORT_READY__ = Promise.all([document.fonts?.ready, ...Array.from(root.querySelectorAll("img"), image => image.decode?.().catch(() => {}))]).then(() => new Promise(resolve => requestAnimationFrame(() => { apply(); resolve(); })));\n' +
1883
+ 'globalThis.__RAPIER_EXPORT_READY__ = Promise.all([document.fonts?.ready, ...Array.from(root.querySelectorAll("img"), image => image.decode?.().catch(() => {}))]).then(() => new Promise(resolve => { ready = resolve; schedule(); }));\n' +
1884
+ 'printMedia.addEventListener("change", () => { printing = printMedia.matches; schedule(); });\n' +
1895
1885
  'addEventListener("resize", schedule); addEventListener("beforeprint", () => { printing = true; apply(); });\n' +
1896
- 'addEventListener("afterprint", () => { printing = false; ' + (pictures ? 'capitals.clear(root); ' : '') + 'schedule(); });\n' +
1886
+ 'addEventListener("scroll", schedule, {passive:true, capture:true});\n' +
1887
+ 'for (const event of ["copy","cut"]) addEventListener(event, () => { if (typography.clearSelection(root)) schedule(); }, true);\n' +
1888
+ 'addEventListener("afterprint", () => { printing = printMedia.matches; ' + (pictures ? 'capitals.clear(root); ' : '') + 'schedule(); });\n' +
1897
1889
  'root.addEventListener("load", schedule, true);\n' +
1890
+ 'for (const event of ["loading","loadingdone","loadingerror"]) document.fonts?.addEventListener(event, () => { typography.invalidateFonts(document); schedule(); });\n' +
1891
+ 'addEventListener("pagehide", () => { cancelAnimationFrame(frame); frame = 0; typography.retire(root); }); addEventListener("pageshow", schedule);\n' +
1898
1892
  'if (typeof ResizeObserver === "function") new ResizeObserver(() => { const next = root.getBoundingClientRect().width; if (next !== width) { width = next; schedule(); } }).observe(root);\n' +
1899
1893
  '})();';
1900
- return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
1901
1894
  }
1902
1895
 
1903
- return {_rapierDataImageDestinationsAsync, _rapierSharedSourceFormsAsync, _rapierPrefixPortableAnchorsSteps, _rapierProjectStyledRootAsync, _rapierBuildArtifact, _rapierBuildPublishedPage, _rapierProjectStyledRoot, _rapierDocxRenderStyles, _rapierPaintCode, _rapierPrefixPortableAnchors, _rapierDocumentSettingsOf, _rapierDocumentSettingsCss, _rapierExportedPageCsp, _rapierArtifactNonce, _rapierAnnotateExportBoxPolygons, _rapierSharedSourceHash, _rapierSharedSourceForms, _rapierSharedSourceCarrier, _rapierSharedResolve, _rapierReadSharedDocument, _rapierSharedPageFallbackCss, _rapierShareWrapKind, _rapierSharePictureParagraph, _rapierSharedPageLayout, _rapierShareCapLongCodeBlocks, _rapierRequireOfflinePageImages, _rapierCssValueUrls, _rapierBuildSharedPage, _rapierImageSources, _rapierImageDestinations, _rapierScanMarkdownImages, _rapierParseMarkdownImageDestination, _rapierImageOpenRunsList, _rapierArtifactFactoryModules, _rapierProjectArtifactLayout, _rapierArtifactLayoutScript, _rapierArtifactInkScript , render};
1896
+ return {_rapierDataImageDestinationsAsync, _rapierSharedSourceFormsAsync, _rapierPrefixPortableAnchorsSteps, _rapierPortableAnchorId, _rapierProjectStyledRootAsync, _rapierBuildArtifact, _rapierBuildPublishedPage, _rapierProjectStyledRoot, _rapierDocxRenderStyles, _rapierPaintCode, _rapierPrefixPortableAnchors, _rapierDocumentSettingsOf, _rapierDocumentSettingsCss, _rapierExportedPageCsp, _rapierArtifactNonce, _rapierAnnotateExportBoxPolygons, _rapierSharedSourceHash, _rapierSharedSourceForms, _rapierSharedSourceCarrier, _rapierSharedResolve, _rapierReadSharedDocument, _rapierSharedPageFallbackCss, _rapierShareWrapKind, _rapierSharePictureParagraph, _rapierSharedPageLayout, _rapierShareCapLongCodeBlocks, _rapierRequireOfflinePageImages, _rapierCssValueUrls, _rapierBuildSharedPage, _rapierImageSources, _rapierImageDestinations, _rapierScanMarkdownImages, _rapierParseMarkdownImageDestination, _rapierImageOpenRunsList, _rapierArtifactFactoryModules, _rapierProjectArtifactLayout, _rapierArtifactLayoutScript, _rapierArtifactInkScript , render};
1904
1897
  }
1905
1898
  export {createRenderer};
@@ -0,0 +1,2 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export {createTypography} from '../layout/typography.mjs';