rapier-markdown-kit 1.1.101 → 1.1.104

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 (60) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +213 -53
  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/kit/assets.mjs +5 -1
  27. package/dist/kit/conformance/run.mjs +4 -4
  28. package/dist/kit/export-fonts.mjs +26 -11
  29. package/dist/kit/index.mjs +1 -0
  30. package/dist/kit/ledger/carried.mjs +9 -7
  31. package/dist/kit/ledger/data.mjs +73 -13
  32. package/dist/kit/ledger/format.mjs +166 -53
  33. package/dist/kit/ledger/hash.mjs +75 -18
  34. package/dist/kit/ledger/history.mjs +143 -115
  35. package/dist/kit/ledger/inverse-source.mjs +158 -19
  36. package/dist/kit/ledger/journal-records.mjs +292 -67
  37. package/dist/kit/model.mjs +1 -1
  38. package/dist/kit/pdf-glyphs.mjs +4 -4
  39. package/dist/kit/render-markdown.mjs +259 -122
  40. package/dist/kit/render-sanitize.mjs +24 -4
  41. package/dist/kit/render-work.mjs +99 -20
  42. package/dist/kit/render.mjs +101 -65
  43. package/dist/kit/typography.mjs +2 -0
  44. package/dist/layout/hyphenation.mjs +127 -0
  45. package/dist/layout/line-plan.mjs +88 -96
  46. package/dist/layout/model.mjs +61 -80
  47. package/dist/layout/paragraph-fit.mjs +191 -0
  48. package/dist/layout/preparation.mjs +373 -0
  49. package/dist/layout/prepared-paragraph.mjs +223 -0
  50. package/dist/layout/typography.mjs +345 -111
  51. package/dist/packages/rapier-diff/index.mjs +288 -196
  52. package/dist/spec/document-fonts.mjs +13 -4
  53. package/dist/spec/md-assets.mjs +149 -30
  54. package/dist/spec/md-layout.mjs +5 -2
  55. package/dist/spec/md-marks.mjs +218 -3
  56. package/dist/spec/md-source.mjs +122 -0
  57. package/package.json +2 -1
  58. package/style.css +9 -0
  59. package/dist/agent/vendor/pretext/bidi.js +0 -151
  60. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,4 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ import {typography} from '../spec/md-marks.mjs';
2
3
  const RAPIER_SANITIZE_FORBID_TAGS = Object.freeze(['form', 'style', 'dialog', 'template', 'iframe']);
3
4
  const RAPIER_SANITIZE_FORBID_ATTR = Object.freeze(['action', 'formaction', 'form', 'for']);
4
5
  // The document profiles use one allowlist in a browser and in an inert server parser.
@@ -166,7 +167,7 @@ function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeR
166
167
  if (!_rapierImageDataAttributeAllowed(tag, key, compact, safeRasterDataUrl)) continue;
167
168
  } else if (!uri.test(compact)) continue;
168
169
  }
169
- attribs[name] = value;
170
+ attribs[name] = selected === 'raw' && tag === 'span' && key === 'style' ? typography.style(typography.parseStyle(value)) : value;
170
171
  }
171
172
  if ((tag === 'a' || tag === 'area') && attribs.target?.trim().toLowerCase() === '_blank') attribs.rel = 'noopener noreferrer';
172
173
  return {tagName, attribs};
@@ -323,6 +324,11 @@ function _rapierInstallSanitizeHooks() {
323
324
  });
324
325
  DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
325
326
  const value = String(data.attrValue == null ? '' : data.attrValue);
327
+ if (_rapierSanitizeRuntime.context === 'raw' && node?.nodeName === 'SPAN' && data.attrName?.toLowerCase() === 'style') {
328
+ data.attrValue = typography.style(typography.parseStyle(value));
329
+ data.keepAttr = !!data.attrValue;
330
+ return;
331
+ }
326
332
  // A document's own HTML never carries an id the chrome owns, remote content allowed or not: the
327
333
  // allow is about what the page fetches, and this rule is about what the page's chrome finds.
328
334
  if (String(data.attrName || '').toLowerCase() === 'id' && _rapierSanitizeRuntime.context === 'raw' && _rapierChromeOwnsId(value)) { data.keepAttr = false; return; }
@@ -371,6 +377,16 @@ function _rapierInstallSanitizeHooks() {
371
377
  }
372
378
 
373
379
  function sanitizeRapierHtml(value, context = 'render') {
380
+ return sanitize(value, context, false);
381
+ }
382
+
383
+ // The same parser, profile and hooks admit both results. A private renderer can
384
+ // adopt the sanitized nodes without serializing and parsing them again.
385
+ function sanitizeRapierFragment(value, context = 'render') {
386
+ return sanitize(value, context, true);
387
+ }
388
+
389
+ function sanitize(value, context, fragment) {
374
390
  // 'export' is the render profile under the export hooks; 'raw' is a document's own HTML (initMarkdownIt, html_block):
375
391
  // the render profile without data-* attributes, which Rapier's delegated handlers read as their own controls, and
376
392
  // (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
@@ -389,6 +405,8 @@ function sanitizeRapierHtml(value, context = 'render') {
389
405
  DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
390
406
  DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
391
407
  }
408
+ if (fragment) options = {...options, RETURN_DOM_FRAGMENT: true};
409
+ const previous = _rapierSanitizeRuntime.context;
392
410
  _rapierSanitizeRuntime.context = selected;
393
411
  try {
394
412
  return DOMPurify.sanitize(value == null ? '' : String(value), options);
@@ -398,7 +416,7 @@ function sanitizeRapierHtml(value, context = 'render') {
398
416
  DOMPurify.removeHook('uponSanitizeAttribute', diagramLabelAttribute);
399
417
  DOMPurify.removeHook('afterSanitizeAttributes', diagramLabelContained);
400
418
  }
401
- _rapierSanitizeRuntime.context = 'render';
419
+ _rapierSanitizeRuntime.context = previous;
402
420
  }
403
421
  }
404
422
 
@@ -472,7 +490,7 @@ function _rapierStyleWithoutRemoteUrls(value) {
472
490
  if (!raw) return raw;
473
491
  const declaration = _rapierCssDeclaration();
474
492
  declaration.cssText = raw;
475
- if (!declaration.length) return '';
493
+ if (!declaration.length) return typography.style(typography.parseStyle(raw));
476
494
  return _rapierDropRemoteDeclarations(declaration) ? declaration.cssText : raw;
477
495
  }
478
496
 
@@ -510,6 +528,8 @@ function _rapierVerifyRasterBytes(bytes, mime) {
510
528
  return false;
511
529
  }
512
530
 
513
- return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml, diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
531
+ return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml,
532
+ sanitizeRapierFragment: typeof DOMPurify?.sanitize === 'function' ? sanitizeRapierFragment : undefined,
533
+ diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
514
534
  }
515
535
  export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, createInertHtmlSanitizer, RAPIER_SANITIZE_OPTIONS as options, prepareMathSource};
@@ -7,6 +7,7 @@ export function finish(iterator) {
7
7
  }
8
8
 
9
9
  export function check(work) {
10
+ work?.check?.();
10
11
  if (work?.signal?.aborted) throw Object.assign(new Error('The operation was cancelled.'), {name: 'AbortError', code: 'cancelled'});
11
12
  }
12
13
 
@@ -72,16 +73,18 @@ export function htmlBoundary() {
72
73
 
73
74
  export async function finishAsync(iterator, work) {
74
75
  let began = performance.now(), next;
75
- check(work);
76
- do {
77
- next = iterator.next();
78
- if (!next.done && performance.now() - began >= 8) {
79
- await pause(work);
80
- began = performance.now();
81
- }
82
- } while (!next.done);
83
- check(work);
84
- return next.value;
76
+ try {
77
+ do {
78
+ check(work);
79
+ next = iterator.next();
80
+ if (!next.done && performance.now() - began >= 8) {
81
+ await pause(work);
82
+ began = performance.now();
83
+ }
84
+ } while (!next.done);
85
+ check(work);
86
+ return next.value;
87
+ } finally { if (!next?.done) iterator.return?.(); }
85
88
  }
86
89
 
87
90
  export function* cloneTree(root) {
@@ -98,25 +101,98 @@ export function* cloneTree(root) {
98
101
  return copy;
99
102
  }
100
103
 
101
- export function* htmlParts(root, limit = 16384) {
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
+ export function* htmlParts(root, limit = 16384, consume = false) {
108
+ limit = Math.max(2, limit);
102
109
  const text = root.ownerDocument.createElement('div');
110
+ const small = node => {
111
+ let count = 0, chars = 0, at = node;
112
+ while (at) {
113
+ if (++count > 128) return false;
114
+ if (at.nodeType === 1) for (const attribute of at.attributes) chars += attribute.value.length + attribute.name.length;
115
+ else chars += at.nodeValue?.length || 0;
116
+ if (chars > limit) return false;
117
+ if (at.firstChild) at = at.firstChild;
118
+ else {
119
+ while (at !== node && !at.nextSibling) at = at.parentNode;
120
+ at = at === node ? null : at.nextSibling;
121
+ }
122
+ }
123
+ return true;
124
+ };
125
+ const nativeHtml = node => {
126
+ if (node.nodeType === 1) return node.outerHTML;
127
+ text.replaceChildren(node.cloneNode(false)); return text.innerHTML;
128
+ };
129
+ const nodeHtml = function* (node) {
130
+ if (small(node)) return nativeHtml(node);
131
+ const pieces = [], stack = [];
132
+ let at = node;
133
+ while (at) {
134
+ yield null;
135
+ if (at.nodeType === 1) {
136
+ if (small(at)) pieces.push(at.outerHTML);
137
+ else {
138
+ // Native shallow serialization owns tag/attribute spelling and
139
+ // escaping. Native text serialization below keeps its parent context.
140
+ 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
+ if (at.firstChild) empty.appendChild(root.ownerDocument.createTextNode(''));
144
+ const shell = empty.outerHTML, close = /<\/[^<>]+>$/.exec(shell)?.[0] || '';
145
+ if (close && at.firstChild) {
146
+ pieces.push(shell.slice(0, -close.length));
147
+ stack.push({node: at, close}); at = at.firstChild; continue;
148
+ }
149
+ pieces.push(shell);
150
+ }
151
+ } else if (at.nodeType === 3 && at.nodeValue.length > limit) {
152
+ const parent = at !== node && at.parentNode?.nodeType === 1 ? at.parentNode.cloneNode(false) : text;
153
+ const value = at.nodeValue;
154
+ for (let start = 0; start < value.length;) {
155
+ let end = Math.min(start + limit, value.length);
156
+ if (end < value.length && value.charCodeAt(end - 1) >= 0xd800 && value.charCodeAt(end - 1) <= 0xdbff) end--;
157
+ parent.replaceChildren(root.ownerDocument.createTextNode(value.slice(start, end)));
158
+ pieces.push(parent.innerHTML); start = end; yield null;
159
+ }
160
+ parent.replaceChildren();
161
+ } else if (at.nodeType === 3 && at.parentNode?.nodeType === 1) {
162
+ const parent = at.parentNode.cloneNode(false);
163
+ parent.appendChild(at.cloneNode(false)); pieces.push(parent.innerHTML);
164
+ } else pieces.push(nativeHtml(at));
165
+ while (at !== node && !at.nextSibling) { const frame = stack.pop(); pieces.push(frame.close); at = frame.node; }
166
+ at = at === node ? null : at.nextSibling;
167
+ }
168
+ return pieces.join('');
169
+ };
103
170
  let part = '';
104
- for (let node = root.firstChild; node; node = node.nextSibling) {
105
- let html;
106
- if (node.nodeType === 1) html = node.outerHTML;
107
- else { text.replaceChildren(node.cloneNode(false)); html = text.innerHTML; }
171
+ for (let node = root.firstChild; node;) {
172
+ const next = node.nextSibling;
173
+ const html = yield* nodeHtml(node);
108
174
  if (part && part.length + html.length > limit) { yield part; part = ''; }
109
175
  part += html;
176
+ if (consume) root.removeChild(node);
177
+ node = next;
110
178
  }
111
179
  if (part) yield part;
112
180
  }
113
181
 
114
- export function* cleanTree(root, sanitize, context = 'export') {
182
+ export function admitHtml(root, html, sanitize, fragment = null, context = 'export') {
183
+ if (fragment) root.appendChild(fragment(html, context));
184
+ else {
185
+ // Inert string-only hosts retain their explicitly supported admission path.
186
+ const part = root.ownerDocument.createElement('div');
187
+ part.innerHTML = sanitize(html, context);
188
+ while (part.firstChild) root.appendChild(part.firstChild);
189
+ }
190
+ }
191
+
192
+ export function* cleanTree(root, sanitize, context = 'export', fragment = null) {
115
193
  const clean = root.ownerDocument.createElement('div');
116
- const part = root.ownerDocument.createElement('div');
117
194
  for (const html of htmlParts(root)) {
118
- part.innerHTML = sanitize(html, context);
119
- while (part.firstChild) clean.appendChild(part.firstChild);
195
+ if (html !== null) admitHtml(clean, html, sanitize, fragment, context);
120
196
  yield;
121
197
  }
122
198
  return clean;
@@ -124,7 +200,10 @@ export function* cleanTree(root, sanitize, context = 'export') {
124
200
 
125
201
  export function* serializeTree(root, sanitize = null, trim = false) {
126
202
  const parts = [];
127
- for (const html of htmlParts(root)) { parts.push(sanitize ? sanitize(html, 'export') : html); yield; }
203
+ for (const html of htmlParts(root)) {
204
+ if (html !== null) parts.push(sanitize ? sanitize(html, 'export') : html);
205
+ yield;
206
+ }
128
207
  const result = parts.join('');
129
208
  return trim ? result.trim() : result;
130
209
  }
@@ -1,11 +1,10 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {htmlImageRecord, mountHtmlBlocks} 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';
@@ -158,8 +157,8 @@ 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
+ // Print runs shared paragraph typography, but keeps its existing picture layout;
161
+ // picture-shape annotations have no consumer in that artifact.
163
162
  if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
164
163
  if (completePage && !opts.print) {
165
164
  if (work) {
@@ -173,6 +172,7 @@ async function _rapierBuildArtifact(options, providedContext) {
173
172
  ? 'light'
174
173
  : _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
175
174
  const highlights = _rapierArtifactPreference('highlights', ['accent', 'standard']);
175
+ const invertImageAlpha = _rapierArtifactPreference('invertImageAlpha', ['true', 'false']) !== 'false';
176
176
  const versionMeta = document.querySelector('meta[name="rapier-version"]');
177
177
  const version = versionMeta && versionMeta.content ? versionMeta.content.trim() : '0.0.0';
178
178
  const bodyClass = theme === 'light' ? 'light' : '';
@@ -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
230
  const htmlScript = opts.scripts !== false && styledRoot.querySelector('[data-rapier-html]') ? '<script nonce="' + nonce + '">(' + mountHtmlBlocks.toString().replace(/<\/script/gi, '<\\/script') + ')(document);</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
246
  + '<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>';
247
+ + bodyHtml + '</main>' + carrier + layoutScript + inkScript + htmlScript + '</body>\n</html>';
249
248
 
250
249
  return {
251
250
  html: page,
@@ -1381,17 +1380,10 @@ function _rapierImageOpenRunsList(tokens) {
1381
1380
  const stack = [];
1382
1381
  for (const token of block.children) {
1383
1382
  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;
1388
- }
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;
1383
+ if (token.meta?.typography && token.nesting === 1) {
1384
+ stack.push({open: token.meta.typography.open, close: token.meta.typography.close}); continue;
1393
1385
  }
1394
- if (token.type === 'rapier_font_close') { stack.pop(); continue; }
1386
+ if (token.meta?.typography && token.nesting === -1) { stack.pop(); continue; }
1395
1387
  if (token.type === 'strong_open' || token.type === 'em_open' || token.type === 's_open') {
1396
1388
  stack.push({open: token.markup, close: token.markup});
1397
1389
  continue;
@@ -1407,10 +1399,11 @@ function _rapierArtifactFactoryModules(dependencies, group) {
1407
1399
  return paths.map(path => 'modules[' + JSON.stringify(path) + '] = (' + Function.prototype.toString.call(dependencies.factories[path]) + ')();').join('\n');
1408
1400
  }
1409
1401
 
1410
- function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1411
- if (!root || !metadata || !geometry || !pretext) return null;
1402
+ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1403
+ if (!root || !metadata || !geometry || !preparation) return null;
1412
1404
  const doc = root.ownerDocument, view = doc.defaultView;
1413
1405
  const styles = new Map(), originals = new Map(), spacers = [], floats = [], steps = [], profiles = new WeakMap();
1406
+ const prepared = preparation.createPreparationScope(doc);
1414
1407
  const css = node => view.getComputedStyle(node);
1415
1408
  const box = node => node.getBoundingClientRect();
1416
1409
  const pixels = value => (Number.isFinite(value) ? Math.round(value * 100) / 100 : 0) + 'px';
@@ -1437,48 +1430,72 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1437
1430
  steps.length = 0;
1438
1431
  }
1439
1432
 
1440
- // Plain inline text uses Pretext. Complex blocks keep their original controls through floats.
1433
+ // One prepared rich paragraph retains original item text. Unsupported shaping keeps native flow.
1441
1434
  function prepare(paragraph) {
1435
+ const refuse = () => { prepared.forget(paragraph); return null; };
1442
1436
  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;
1437
+ [...paragraph.querySelectorAll('[id]')].some(element => /^[\s\u00ad\u200b]*$/u.test(element.textContent)) ||
1438
+ paragraph.querySelector('img,svg,math,br,input,button,iframe,canvas,.math-rendered')) return refuse();
1444
1439
  const computed = css(paragraph);
1445
- if (computed.textAlign === 'justify' || computed.writingMode !== 'horizontal-tb' ||
1446
- computed.textTransform !== 'none' || computed.whiteSpace !== 'normal' || computed.transform !== 'none' ||
1440
+ const language = paragraph.closest('[lang]')?.getAttribute('lang') || '';
1441
+ for (let parent = paragraph; parent; parent = parent.parentElement) {
1442
+ const inherited = css(parent);
1443
+ if (parent.style.textWrapStyle === 'pretty' || inherited.transform !== 'none' ||
1444
+ !['1', 'normal'].includes(inherited.zoom)) return refuse();
1445
+ }
1446
+ if (computed.textAlign === 'justify' || computed.textWrapStyle === 'balance' ||
1447
+ !preparation.supportsRichStyle(computed, language) || computed.whiteSpace !== 'normal' ||
1448
+ computed.transform !== 'none' ||
1449
+ ['::before', '::after'].some(pseudo => !['none', 'normal'].includes(view.getComputedStyle(paragraph, pseudo).content)) ||
1447
1450
  // Lines start at the padding box's top-left: left, right or top inset is refused.
1448
1451
  ['paddingLeft', 'paddingRight', 'paddingTop', 'borderLeftWidth', 'borderRightWidth', 'borderTopWidth']
1449
- .some(key => parseFloat(computed[key]) > 0)) return null;
1452
+ .some(key => parseFloat(computed[key]) > 0)) return refuse();
1450
1453
  const runs = [], items = [], walker = doc.createTreeWalker(paragraph, 4);
1451
1454
  for (let node; (node = walker.nextNode());) {
1452
- if (runs.length === 192) return null;
1455
+ if (runs.length === 192) return refuse();
1453
1456
  const parents = [];
1454
1457
  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;
1458
+ if (!parent || !/^(A|SPAN|EM|I|STRONG|B|DEL|S|MARK|CODE|KBD|SAMP|U|INS|ABBR|SUP|SUB)$/.test(parent.tagName) ||
1459
+ parent.hidden || parent.getAttribute('aria-hidden') === 'true' ||
1460
+ parent.style.textWrapStyle === 'pretty' ||
1461
+ parent.hasAttribute('lang') && parent.getAttribute('lang').toLowerCase() !== (language || '').toLowerCase()) return refuse();
1462
+ const parentStyle = css(parent);
1463
+ if (!preparation.supportsRichStyle(parentStyle, language) || parentStyle.display !== 'inline' ||
1464
+ parentStyle.cssFloat !== 'none' || !['static', 'relative'].includes(parentStyle.position) ||
1465
+ parentStyle.transform !== 'none' || !['1', 'normal'].includes(parentStyle.zoom) ||
1466
+ ['::before', '::after'].some(pseudo => !['none', 'normal'].includes(view.getComputedStyle(parent, pseudo).content))) return refuse();
1467
+ if (!/^(CODE|KBD|SAMP)$/.test(parent.tagName) &&
1468
+ ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth', 'marginLeft', 'marginRight']
1469
+ .some(key => Math.abs(parseFloat(parentStyle[key]) || 0) > .01)) return refuse();
1456
1470
  parents.unshift(parent);
1457
1471
  }
1458
1472
  const computedRun = css(node.parentElement);
1459
- if (computedRun.textTransform !== 'none' || computedRun.writingMode !== 'horizontal-tb') return null;
1473
+ if (!preparation.supportsRichStyle(computedRun, language) ||
1474
+ computedRun.whiteSpace !== computed.whiteSpace && computedRun.whiteSpace !== 'nowrap') return refuse();
1460
1475
  const font = `${computedRun.fontStyle} ${computedRun.fontWeight} ${computedRun.fontSize} ${computedRun.fontFamily}`;
1461
1476
  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');
1477
+ const boxed = parents.some(parent => /^(CODE|KBD|SAMP)$/.test(parent.tagName));
1478
+ if (boxed && (!/^(CODE|KBD|SAMP)$/.test(node.parentElement.tagName) || node.parentElement.childNodes.length !== 1)) return refuse();
1466
1479
  const extraWidth = boxed ? ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth', 'marginLeft', 'marginRight']
1467
1480
  .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});
1481
+ runs.push({parents, raw: node.data, font, letterSpacing,
1482
+ directions: parents.map(parent => parent.getAttribute('dir') === 'auto' ? css(parent).direction : null)});
1483
+ items.push({text: node.data, font, letterSpacing,
1484
+ break: computedRun.whiteSpace === 'nowrap' || parents.some(parent => parent.hasAttribute('data-rapier-source')) ? 'never' : 'normal', extraWidth});
1470
1485
  }
1471
- if (!runs.length) return null;
1486
+ if (!runs.length) return refuse();
1487
+ const options = {whiteSpace: computed.whiteSpace, wordBreak: computed.wordBreak, language};
1488
+ const flow = prepared.prepare(paragraph, items, options);
1489
+ if (!flow) return refuse();
1472
1490
  const fontSize = parseFloat(computed.fontSize) || 16;
1473
- return {paragraph, runs, flow: pretext.prepareRichInline(items), direction: computed.direction,
1491
+ return {paragraph, runs, flow, whiteSpace: computed.whiteSpace, direction: computed.direction,
1474
1492
  align: computed.textAlign, fontSize, lineHeight: parseFloat(computed.lineHeight) || fontSize * 1.65,
1475
- balance: computed.textWrapStyle === 'balance' ? fontSize : 0};
1493
+ balance: 0};
1476
1494
  }
1477
1495
 
1478
1496
  // 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);
1497
+ function fragmentNode(record, fragment, idsKept) {
1498
+ const run = record.runs[fragment.itemIndex], mapped = geometry.mapFragment(run, fragment, {whiteSpace: record.whiteSpace});
1482
1499
  if (!mapped) return null;
1483
1500
  // 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
1501
  const kept = run.parents.length ? mapped.text.replace(/[ \t\n\r\f]+$/, '') : mapped.text;
@@ -1497,10 +1514,12 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1497
1514
  }
1498
1515
 
1499
1516
  function project(record, width, top, obstacles) {
1517
+ if (!prepared.current(record.flow)) return false;
1500
1518
  const plan = geometry.flowLines(record.flow, width, top, obstacles, record.lineHeight,
1501
1519
  metadata.wrapColumnFloor(record.fontSize), record.direction, record.balance, record.align);
1502
1520
  if (!plan) return false;
1503
1521
  const output = doc.createDocumentFragment();
1522
+ const idsKept = new Set();
1504
1523
  let previous = null;
1505
1524
  for (const line of plan.lines) {
1506
1525
  const element = doc.createElement('span');
@@ -1508,17 +1527,21 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1508
1527
  element.style.textAlign = ['left', 'center', 'right', 'start', 'end'].includes(record.align)
1509
1528
  ? record.align : record.direction === 'rtl' ? 'right' : 'left';
1510
1529
  for (const fragment of line.fragments) {
1511
- const child = fragmentNode(record, fragment);
1530
+ const child = fragmentNode(record, fragment, idsKept);
1512
1531
  if (!child) return false;
1513
1532
  if (hasGap(record, previous, fragment)) {
1514
1533
  const gap = doc.createElement('span');
1534
+ const gapRun = record.runs[fragment.gapItemIndex];
1535
+ if (!gapRun && fragment.gapBefore !== 0) return false;
1515
1536
  gap.style.cssText = 'display:inline-block;width:' + pixels(fragment.gapBefore);
1537
+ if (gapRun) { gap.style.font = gapRun.font; gap.style.letterSpacing = pixels(gapRun.letterSpacing); }
1516
1538
  gap.textContent = ' '; element.append(gap);
1517
1539
  }
1518
1540
  element.append(child); previous = fragment;
1519
1541
  }
1520
1542
  output.append(element);
1521
1543
  }
1544
+ if (!prepared.current(record.flow)) return false;
1522
1545
  originals.set(record.paragraph, [...record.paragraph.childNodes]);
1523
1546
  record.paragraph.replaceChildren(output);
1524
1547
  style(record.paragraph, {position: 'relative', height: pixels(plan.height), 'min-height': '0'});
@@ -1529,10 +1552,11 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1529
1552
  if (!previous) return false;
1530
1553
  let found = false;
1531
1554
  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;
1555
+ const run = record.runs[index];
1556
+ if (!run) return false;
1557
+ const start = index === previous.itemIndex ? previous.sourceEnd : 0;
1558
+ const end = index === current.itemIndex ? current.sourceStart : run.raw.length;
1559
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > run.raw.length) return false;
1536
1560
  const gap = run.raw.slice(start, end);
1537
1561
  if (gap && !/^[ \t\n\r\f]+$/.test(gap)) return false;
1538
1562
  found ||= !!gap;
@@ -1823,6 +1847,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1823
1847
 
1824
1848
  function beforePrint() { printing = true; restore(); }
1825
1849
  function afterPrint() { printing = false; schedule(); }
1850
+ function fontChanged() { prepared.invalidateFonts(); schedule(); }
1826
1851
 
1827
1852
  const observer = typeof view.ResizeObserver === 'function' ? new view.ResizeObserver(() => {
1828
1853
  const width = root.clientWidth;
@@ -1837,13 +1862,13 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1837
1862
  view.addEventListener('pageshow', schedule);
1838
1863
  view.addEventListener('beforeprint', beforePrint);
1839
1864
  view.addEventListener('afterprint', afterPrint);
1840
- doc.fonts?.addEventListener?.('loadingdone', schedule);
1865
+ for (const event of ['loading', 'loadingdone', 'loadingerror']) doc.fonts?.addEventListener?.(event, fontChanged);
1841
1866
  Promise.resolve(doc.fonts?.ready).then(schedule, schedule);
1842
1867
  schedule();
1843
- return Object.freeze({reflow, destroy() {
1868
+ return Object.freeze({reflow, destroy({documentChanged = false} = {}) {
1844
1869
  stopped = true;
1845
1870
  if (scheduled) view.cancelAnimationFrame(scheduled);
1846
- observer?.disconnect(); restore();
1871
+ observer?.disconnect(); restore(); prepared.retire({documentChanged});
1847
1872
  root.removeEventListener('load', schedule, true);
1848
1873
  root.removeEventListener('error', schedule, true);
1849
1874
  root.removeEventListener('rapier-html-resize', schedule);
@@ -1852,23 +1877,28 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
1852
1877
  view.removeEventListener('pageshow', schedule);
1853
1878
  view.removeEventListener('beforeprint', beforePrint);
1854
1879
  view.removeEventListener('afterprint', afterPrint);
1855
- doc.fonts?.removeEventListener?.('loadingdone', schedule);
1880
+ for (const event of ['loading', 'loadingdone', 'loadingerror']) doc.fonts?.removeEventListener?.(event, fontChanged);
1856
1881
  }});
1857
1882
  }
1858
1883
 
1859
- function _rapierArtifactLayoutScript(root, nonce = '') {
1884
+ function _rapierArtifactLayoutScript(root, nonce = '', {pictures = true} = {}) {
1860
1885
  const dependencies = globalThis.RapierArtifactLayoutDependencies;
1861
1886
  // Every wrap value earns the reflow script, and an inline turned raster (keyed off data-rapier-image-layout).
1862
1887
  const layoutOf = element => globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'));
1863
1888
  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 '';
1889
+ const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
1890
+ [...root.querySelectorAll('img[data-rapier-image-layout]')].some(image => pictureLayoutOf(image)?.rotate));
1891
+ const typography = _rapierArtifactTypographyScript(root);
1892
+ if (!wrapping && !typography) return '';
1893
+ if (!dependencies?.factories || typography && !dependencies.factories['layout/typography.mjs'])
1894
+ throw new Error('Offline typography requires the complete retained MIT layout factories');
1867
1895
  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})();';
1896
+ const script = '/* Rapier export layout: SPDX-License-Identifier: MIT */\n' +
1897
+ '/* Pretext bac1774c0f8b7a92e44b9da43b6d5443d55d0d9b (Unreleased)\n' + dependencies.license + '\n*/\n' +
1898
+ '(() => {\nconst modules = Object.create(null);\n' + modules +
1899
+ (wrapping ? '\n(' + _rapierProjectArtifactLayout.toString() +
1900
+ ')(document.querySelector("main.rapier-page"), modules["spec/md-layout.mjs"], modules["layout/model.mjs"], modules["layout/preparation.mjs"]);\n' : '') +
1901
+ typography + '\n})();';
1872
1902
 
1873
1903
  return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
1874
1904
  }
@@ -1883,21 +1913,27 @@ function _rapierArtifactInkScript(root, nonce = '') {
1883
1913
  return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
1884
1914
  }
1885
1915
 
1886
- function _rapierArtifactTypographyScript(root, nonce = '') {
1916
+ function _rapierArtifactTypographyScript(root) {
1887
1917
  const pictures = root.querySelector('img[data-rapier-print-lines]');
1888
1918
  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' +
1919
+ return '(() => {\nconst typography = modules["layout/typography.mjs"].createTypography();\n' +
1890
1920
  (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' +
1921
+ 'const root = document.querySelector("main.rapier-page"); const printMedia = matchMedia("print"); let frame = 0, width = -1, ready = null, printing = printMedia.matches;\n' +
1922
+ // beforeprint can still expose the screen viewport, not the selected paper's content width.
1923
+ // Only an authored paper width supplies a fixed target; automatic paper keeps native wrapping.
1924
+ '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
1925
  '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' +
1926
+ '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' +
1927
+ 'printMedia.addEventListener("change", () => { printing = printMedia.matches; schedule(); });\n' +
1895
1928
  'addEventListener("resize", schedule); addEventListener("beforeprint", () => { printing = true; apply(); });\n' +
1896
- 'addEventListener("afterprint", () => { printing = false; ' + (pictures ? 'capitals.clear(root); ' : '') + 'schedule(); });\n' +
1929
+ 'addEventListener("scroll", schedule, {passive:true, capture:true});\n' +
1930
+ 'for (const event of ["copy","cut"]) addEventListener(event, () => { if (typography.clearSelection(root)) schedule(); }, true);\n' +
1931
+ 'addEventListener("afterprint", () => { printing = printMedia.matches; ' + (pictures ? 'capitals.clear(root); ' : '') + 'schedule(); });\n' +
1897
1932
  'root.addEventListener("load", schedule, true);\n' +
1933
+ 'for (const event of ["loading","loadingdone","loadingerror"]) document.fonts?.addEventListener(event, () => { typography.invalidateFonts(document); schedule(); });\n' +
1934
+ 'addEventListener("pagehide", () => { cancelAnimationFrame(frame); frame = 0; typography.retire(root); }); addEventListener("pageshow", schedule);\n' +
1898
1935
  'if (typeof ResizeObserver === "function") new ResizeObserver(() => { const next = root.getBoundingClientRect().width; if (next !== width) { width = next; schedule(); } }).observe(root);\n' +
1899
1936
  '})();';
1900
- return '<script' + (nonce ? ' nonce="' + nonce + '"' : '') + '>\n' + script.replace(/<\/script/gi, '<\\/script') + '\n</script>';
1901
1937
  }
1902
1938
 
1903
1939
  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};
@@ -0,0 +1,2 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export {createTypography} from '../layout/typography.mjs';