rapier-markdown-kit 1.2.7 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +32 -30
  2. package/dist/agent/will.mjs +1 -6
  3. package/dist/images/gif.mjs +0 -3
  4. package/dist/images/playback.mjs +1 -10
  5. package/dist/kit/assets.mjs +0 -1
  6. package/dist/kit/conformance/measurer.mjs +0 -0
  7. package/dist/kit/conformance/run.mjs +1 -8
  8. package/dist/kit/export-fonts.mjs +0 -14
  9. package/dist/kit/index.mjs +1 -1
  10. package/dist/kit/ledger/authored-origin.mjs +0 -5
  11. package/dist/kit/ledger/authorship.mjs +0 -2
  12. package/dist/kit/ledger/carried.d.mts +20 -0
  13. package/dist/kit/ledger/carried.mjs +4 -7
  14. package/dist/kit/ledger/data.mjs +0 -8
  15. package/dist/kit/ledger/format.mjs +0 -25
  16. package/dist/kit/ledger/hash.mjs +11 -7
  17. package/dist/kit/ledger/history.mjs +10 -29
  18. package/dist/kit/ledger/inverse-source.mjs +0 -46
  19. package/dist/kit/ledger/journal-records.mjs +24 -56
  20. package/dist/kit/ledger/merge.mjs +0 -27
  21. package/dist/kit/ledger/text-merge.mjs +0 -20
  22. package/dist/kit/ledger/transport.mjs +0 -47
  23. package/dist/kit/ledger.d.mts +88 -0
  24. package/dist/kit/ledger.mjs +14 -0
  25. package/dist/kit/markdown-it.d.mts +4 -0
  26. package/dist/kit/markdown-it.mjs +36 -0
  27. package/dist/kit/pdf-glyphs.mjs +39 -45
  28. package/dist/kit/pdf-source.d.mts +5 -0
  29. package/dist/kit/pdf-source.mjs +125 -0
  30. package/dist/kit/pdf-vectors.mjs +0 -3
  31. package/dist/kit/print-capitals.mjs +0 -4
  32. package/dist/kit/render-entry.mjs +0 -1
  33. package/dist/kit/render-ink.mjs +0 -5
  34. package/dist/kit/render-markdown.mjs +15 -119
  35. package/dist/kit/render-print.mjs +4 -49
  36. package/dist/kit/render-sanitize.mjs +22 -58
  37. package/dist/kit/render-styles.mjs +0 -7
  38. package/dist/kit/render-work.mjs +0 -12
  39. package/dist/kit/render.mjs +18 -173
  40. package/dist/kit/will.d.mts +42 -0
  41. package/dist/layout/hyphenation.mjs +0 -23
  42. package/dist/layout/line-plan.mjs +0 -20
  43. package/dist/layout/model.mjs +0 -55
  44. package/dist/layout/paragraph-fit.mjs +50 -60
  45. package/dist/layout/preparation.mjs +23 -56
  46. package/dist/layout/prepared-paragraph.mjs +0 -25
  47. package/dist/layout/typography.mjs +103 -41
  48. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  49. package/dist/packages/rapier-diff/diff.mjs +0 -7
  50. package/dist/packages/rapier-diff/index.mjs +0 -1
  51. package/dist/packages/rapier-diff/matching.mjs +0 -19
  52. package/dist/packages/rapier-diff/merge.mjs +0 -9
  53. package/dist/packages/rapier-diff/moves.mjs +0 -7
  54. package/dist/packages/rapier-diff/structure.mjs +0 -2
  55. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  56. package/dist/packages/rapier-diff/walk.mjs +0 -12
  57. package/dist/spec/document-fonts.mjs +0 -9
  58. package/dist/spec/document-settings.mjs +6 -25
  59. package/dist/spec/frontmatter.mjs +0 -1
  60. package/dist/spec/html-blocks.mjs +3 -37
  61. package/dist/spec/md-assets.mjs +2 -61
  62. package/dist/spec/md-layout.mjs +0 -34
  63. package/dist/spec/md-marks.mjs +0 -51
  64. package/dist/spec/md-source.mjs +0 -16
  65. package/dist/spec/word-sidecar.mjs +0 -2
  66. package/package.json +21 -4
  67. package/style.css +38 -21
@@ -8,7 +8,6 @@ import {documentLanguage, documentSettingsFonts} from '../spec/document-settings
8
8
  import {createLineMeasurer, linesHeight} from '../layout/model.mjs';
9
9
  import {markPrintCapitals, createPrintCapitals, followingPrintProse} from './print-capitals.mjs';
10
10
  import {freezePublishedInk} from './render-ink.mjs';
11
- // Set the first view once. Subsequent rendering keeps the person's horizontal position.
12
11
  export function positionNativeDiagram(drawing) {
13
12
  const surface=drawing?.closest?.('.diagram-block,.diagram-live-preview');
14
13
  if(!surface?.clientWidth)return;
@@ -30,7 +29,6 @@ export function positionNativeDiagram(drawing) {
30
29
  surface.scrollLeft=Math.max(0,Math.min(surface.scrollWidth-surface.clientWidth,left));
31
30
  }
32
31
 
33
- // The document's renderer. DOM, codecs and host state are explicit inputs.
34
32
  function createRenderer(runtime) {
35
33
  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;
36
34
  let md = runtime.md;
@@ -66,7 +64,6 @@ async function _rapierBuildArtifact(options, providedContext) {
66
64
  : _rapierProjectPortableRoot(context.semanticRoot, { baseName: base, keepDiagrams: true });
67
65
  for (const element of portableRoot.children) if (!element.hasAttribute('lang')) element.setAttribute('lang', language);
68
66
  if (fontCss || settings?.defaultfont) {
69
- // Keep authored font scopes inside this fragment; no page wrapper or global settings rules.
70
67
  const {main, sans: heading, mono: code} = documentSettingsFonts(settings), body = main || heading;
71
68
  if (body) for (const element of portableRoot.children)
72
69
  if (!element.matches('pre,code,kbd,samp,svg,math') && !element.style.fontFamily)
@@ -80,15 +77,12 @@ async function _rapierBuildArtifact(options, providedContext) {
80
77
  css: _rapierArtifactStyles('system', false, false, false, '') + '\n' + _rapierDocumentSettingsCss(settings)});
81
78
  const html = work ? await runtime._rapierPortableHtmlAsync(portableRoot, work) : _rapierPortableHtml(portableRoot);
82
79
  return {
83
- // Only validated data-font rules bypass the content sanitizer, which still rejects authored styles.
84
80
  html: (fontCss ? '<style>' + fontCss + '</style>\n' : '') + html,
85
81
  filename: base + '-publishing.html',
86
82
  metadata,
87
83
  };
88
84
  }
89
85
 
90
- // The semantic export root is detached and has never entered the live visibility queue.
91
- // Await the same materialization owner before any projection clones or serializes it.
92
86
  const unavailable = plugin => Object.assign(new Error('Offline web page not written; install the ' + plugin + ' plug-in before exporting this document.'),
93
87
  {code: 'offline_plugin_unavailable', plugin});
94
88
  if (completePage && context.semanticRoot.querySelector('.math-placeholder[data-rapier-math-source]')) throw unavailable('math');
@@ -97,15 +91,11 @@ async function _rapierBuildArtifact(options, providedContext) {
97
91
  if (!(completePage && retained && _rapierProviders?.mermaid?.status !== 'ready')) await _rapierFillDiagram(diagram, true);
98
92
  if (!completePage || diagram.hasAttribute('data-diagram-unclosed')) continue;
99
93
  if (diagram.getAttribute('data-diagram-state') === 'absent') throw unavailable('mermaid');
100
- // A completed diagram must have its drawing. Syntax and renderer failures both refuse
101
- // the file; an unclosed fence above is still authored source, not a completed diagram.
102
94
  if (diagram.getAttribute('data-diagram-state') !== 'ready' || !diagram.querySelector('.diagram-cache svg'))
103
95
  throw Object.assign(new Error('Web page not written; a required diagram could not be rendered'), {code: 'EXPORT_DIAGRAM_UNAVAILABLE'});
104
96
  }
105
97
 
106
98
  if (completePage) {
107
- // Invalid TeX renders as a static error SVG. A source-only span means
108
- // rendering itself failed, so publishing it would silently omit the equation.
109
99
  if (context.semanticRoot.querySelector('span[data-rapier-math-source]'))
110
100
  throw new Error('Web page not written; a required equation could not be rendered');
111
101
  await _rapierRequireOfflinePageImages(context.semanticRoot);
@@ -120,10 +110,7 @@ async function _rapierBuildArtifact(options, providedContext) {
120
110
  : _rapierProjectStyledRoot(context.semanticRoot, styledOptions);
121
111
  styledRoot.setAttribute('lang', language);
122
112
  if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
123
- // _rapierProjectStyledRoot already returns the sanitized projection.
124
- // Picture-shape annotations have no consumer in a printed artifact.
125
113
  if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
126
- // Paper keeps each picture's wrap through the same static float layout a page uses.
127
114
  if (completePage || opts.print) {
128
115
  if (work) await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
129
116
  else _rapierSharedPageLayout(styledRoot);
@@ -145,11 +132,6 @@ async function _rapierBuildArtifact(options, providedContext) {
145
132
  const docClass = metadata.docKind === 'markdown'
146
133
  ? 'rapier-page md-render'
147
134
  : 'rapier-page rapier-document--flat';
148
- // Rapier's own stylesheet, not a lookalike: the theme frame, spec/markdown-style.css's whole
149
- // .md-render set and the highlight rules, so a numbered list, a checkbox, a callout and a
150
- // table are drawn in an exported page exactly as they are drawn here. The one caller with
151
- // rules of its own -- Share, for the no-script float it alone writes -- appends them.
152
- // Print retains its existing factory resources. HTML adds only the optional small Sans subset below.
153
135
  const includeFonts = !!opts.print;
154
136
  const willFont = opts.print && opts.willFont ? opts.willFont : null;
155
137
  const settingsCss = _rapierDocumentSettingsCss(settings);
@@ -161,8 +143,6 @@ async function _rapierBuildArtifact(options, providedContext) {
161
143
  + (printWidth > 0 ? '.rapier-page.md-render{width:' + printWidth + 'px!important}' : '') + '}' : '';
162
144
  const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
163
145
  let css = _rapierArtifactStyles(theme, includeFonts, !!opts.print || completePage, false, opts.print && opts.kind === 'standalone' ? '' : null)
164
- // Print already floats pictures. Tables, code, figures and rules clear them
165
- // through the same clearing rule a complete page uses; print keeps its own page flow.
166
146
  + (completePage || opts.print ? '\n\n' + _rapierSharedPageFallbackCss(!completePage) : '')
167
147
  + (opts.extraCss ? '\n\n' + opts.extraCss : '')
168
148
  + (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
@@ -184,13 +164,7 @@ async function _rapierBuildArtifact(options, providedContext) {
184
164
  const bodyHtml = work
185
165
  ? await finishAsync(serializeTree(styledRoot), work) : styledRoot.innerHTML;
186
166
  if (work) await pause(work, .95);
187
- // Every written page is offline. Only the writer's nonce script may run; it, pictures,
188
- // styles and other page resources get no network authority. Following an ordinary link
189
- // is the reader's navigation, with no Referer, not a background resource request.
190
167
  const nonce = _rapierArtifactNonce();
191
- // 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
192
- // 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
193
- // nonce out of it and be admitted.
194
168
  const layoutScript = opts.scripts !== false ? _rapierArtifactLayoutScript(styledRoot, nonce, {pictures:!opts.print}) : '';
195
169
  const inkScript = !opts.print && opts.scripts !== false ? _rapierArtifactInkScript(styledRoot, nonce) : '';
196
170
  const htmlScript = !opts.print && opts.scripts !== false && styledRoot.querySelector('[data-rapier-html],[data-rapier-animation]') ? '<script nonce="' + nonce + '">' + htmlRuntimeSource().replace(/<\/script/gi, '<\\/script') + '</script>' : '';
@@ -198,14 +172,11 @@ async function _rapierBuildArtifact(options, providedContext) {
198
172
  + '<html lang="' + escapeRapierHtmlText(language) + '" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '" data-invert-image-alpha="' + (invertImageAlpha ? 'on' : 'off') + '">\n'
199
173
  + '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
200
174
  + '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
201
- // Chrome 154's responsively sized frames: a host frame styled `frame-sizing: content-height` sizes
202
- // itself to a page that opts in with this meta, so a document exported from Rapier and carried in
203
- // someone's page shows whole, with no inner scroll. Any origin may read the height: the page is the
204
- // document, published by the person who exported it.
205
175
  + '<meta name="responsive-embedded-sizing" content="allow-origins=*">\n'
206
176
  + '<meta http-equiv="Content-Security-Policy" content="' + _rapierExportedPageCsp(layoutScript || inkScript || htmlScript ? nonce : '', includeFonts || !!fontCss || !!factoryFontCss) + '">\n'
207
177
  + '<meta name="referrer" content="no-referrer">\n'
208
178
  + '<link rel="icon" href="data:,">\n'
179
+ + _rapierPagePreviewMeta(styledRoot, pageTitle)
209
180
  + '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
210
181
  + '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
211
182
  + '<style>' + css + '</style>\n</head>\n'
@@ -227,11 +198,19 @@ async function _rapierBuildArtifact(options, providedContext) {
227
198
  };
228
199
  }
229
200
 
201
+ function _rapierPagePreviewMeta(root, fallbackTitle) {
202
+ const words = node => String(node?.textContent || '').replace(/\s+/g, ' ').trim();
203
+ const title = words(root.querySelector('h1,h2,h3,h4,h5,h6')) || String(fallbackTitle || '');
204
+ const text = [...root.querySelectorAll('p')].map(words).find(line => line.length >= 20) || '';
205
+ const sentence = /^.{20,300}?[.!?](?=\s|$)/.exec(text)?.[0] || (text.length <= 200 ? text : text.slice(0, 200).replace(/\s+\S*$/, '') + '…');
206
+ const meta = (key, name, value) => '<meta ' + key + '="' + name + '" content="' + escapeRapierHtmlText(value) + '">\n';
207
+ return (sentence ? meta('name', 'description', sentence) : '') + (title ? meta('property', 'og:title', title) : '') +
208
+ (sentence ? meta('property', 'og:description', sentence) : '');
209
+ }
210
+
230
211
  async function _rapierBuildPublishedPage(context) {
231
212
  if (typeof context?.canonical !== 'string' || !context.semanticRoot)
232
213
  throw new TypeError('Publishing requires a captured document.');
233
- // Public readers receive only the current document. The editing session's previous
234
- // source and attribution ledger never acquire public custody through an export option.
235
214
  const current = {...context, ledger: null, carried: 'none', comparisonBase: null};
236
215
  return _rapierBuildArtifact({
237
216
  kind: 'standalone', scripts: false, extraCss: _rapierSharedPageFallbackCss(),
@@ -250,6 +229,11 @@ async function _rapierProjectStyledRootAsync(semanticRoot, options = {}) { retur
250
229
  function* _rapierProjectStyledRootSteps(semanticRoot, options) {
251
230
  const opts = options || {};
252
231
  const root = yield* cloneTree(semanticRoot);
232
+ for (const token of root.querySelectorAll('.rapier-source-token--softbreak')) { yield;
233
+ if (token.getAttribute('data-rapier-source') !== '%0A') token.replaceWith(...token.childNodes);
234
+ else if (token.previousSibling?.classList?.contains('callout__label')) token.remove();
235
+ else token.replaceWith(root.ownerDocument.createTextNode(' '));
236
+ }
253
237
  const metadata = opts.metadata || {};
254
238
 
255
239
  if (metadata.docKind !== 'markdown') {
@@ -299,7 +283,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
299
283
  }
300
284
  input.disabled = !!opts.print || !item;
301
285
  if (input.disabled) { input.setAttribute('aria-disabled', 'true'); continue; }
302
- // Native checked state changes this reader's view, never the carried source or disk.
303
286
  input.removeAttribute('aria-disabled');
304
287
  input.removeAttribute('aria-checked');
305
288
  input.setAttribute('autocomplete', 'off');
@@ -315,8 +298,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
315
298
  }
316
299
 
317
300
  function _rapierDocxRenderStyles(root) {
318
- // The semantic root is detached and its code is normalized text. Use the same
319
- // highlighter and light reference sheet that paint a page, then retain native run facts.
320
301
  const codes = Array.from(root.querySelectorAll('pre > code'));
321
302
  for (const code of codes) {
322
303
  const lang = _rapierLanguageClass(code);
@@ -345,8 +326,6 @@ function _rapierDocxRenderStyles(root) {
345
326
  if (!paint) throw new Error('Word text colors could not be read.');
346
327
  const hex = colour => {
347
328
  if (!colours.has(colour)) {
348
- // CSS may resolve an accent to oklch/color-mix. The browser supplies its
349
- // sRGB paper colour; the writer does not invent a second colour parser.
350
329
  paint.fillStyle = '#ffffff'; paint.fillRect(0, 0, 1, 1);
351
330
  paint.fillStyle = colour; paint.fillRect(0, 0, 1, 1);
352
331
  colours.set(colour, '#' + Array.from(paint.getImageData(0, 0, 1, 1).data).slice(0, 3)
@@ -361,7 +340,6 @@ function _rapierDocxRenderStyles(root) {
361
340
  Array.from(probe.querySelectorAll('span')).forEach((span, index) => {
362
341
  const style = into.defaultView.getComputedStyle(span), target = originals[index];
363
342
  target.style.color = hex(style.color);
364
- // Carry generated syntax emphasis as Word's semantic bold slot.
365
343
  target.style.fontWeight ||= style.fontWeight === style.getPropertyValue('--md-fw-semibold').trim() ? 'bold' : style.fontWeight;
366
344
  target.style.fontStyle = style.fontStyle;
367
345
  });
@@ -371,8 +349,6 @@ function _rapierDocxRenderStyles(root) {
371
349
  for (const quote of quotes) {
372
350
  const probe = into.createElement('blockquote'); probe.className = quote.className;
373
351
  probe.style.borderInlineStartColor = quote.style.borderInlineStartColor;
374
- // Nested quotes have their own reference border colour. Preserve the quote
375
- // ancestry without cloning the document's pictures or other resources.
376
352
  const parent = quoteProbes.get(quote.parentElement?.closest('blockquote')) || surface;
377
353
  parent.appendChild(probe); quoteProbes.set(quote, probe);
378
354
  }
@@ -388,22 +364,17 @@ function _rapierPaintCode(el, html) {
388
364
 
389
365
  function _rapierPrefixPortableAnchors(root, baseName) { return finish(_rapierPrefixPortableAnchorsSteps(root, baseName)); }
390
366
 
391
- // 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.
392
367
  function _rapierPortableAnchorId(baseName, id) {
393
368
  const slug = String(baseName || 'document').toLowerCase()
394
369
  .replace(/[^a-z0-9]+/g, '-')
395
370
  .replace(/^-+|-+$/g, '') || 'document';
396
- return 'rapier-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
371
+ return 'rapier-anchor-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
397
372
  }
398
373
 
399
374
  function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
400
375
  const ids = new Map(), svgIds = new Map();
401
376
  const used = new Set();
402
377
  const scopeOf = element => {
403
- // querySelectorAll('*') hands back whatever the host document holds, and not every host gives
404
- // each node Element.closest -- the export writer runs against parsed and stubbed documents as
405
- // well as a browser's. Without this guard the whole anchor pass would throw TypeError. No scope
406
- // simply means that node's references resolve document-wide, as an unscoped drawing's do.
407
378
  if (typeof element?.closest !== 'function') return null;
408
379
  let svg = element.closest('svg');
409
380
  while (typeof svg?.parentElement?.closest === 'function' && svg.parentElement.closest('svg')) svg = svg.parentElement.closest('svg');
@@ -418,8 +389,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
418
389
  let suffix = 2;
419
390
  while (used.has(nextId)) nextId = first + '-' + suffix++;
420
391
  used.add(nextId);
421
- // A document link resolves the first matching anchor, as it did before renaming.
422
- // SVG references resolve within their own drawing, never the last repeated drawing.
423
392
  if (!ids.has(oldId)) ids.set(oldId, nextId);
424
393
  const svg = scopeOf(element);
425
394
  if (svg) {
@@ -433,7 +402,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
433
402
  for (const element of root.querySelectorAll('*')) { yield;
434
403
  const svg = scopeOf(element), local = svgIds.get(svg);
435
404
  const rewriteId = value => (svg ? local?.get(value) : ids.get(value)) || '';
436
- // URL fragments percent-decode once; ARIA/for/headers are literal IDREFs.
437
405
  const decodeFragment = value => { try { return decodeURIComponent(value); } catch (_) { return value; } };
438
406
  const iriProperty = /^(?:fill|stroke|filter|clip-path|mask|marker(?:-start|-mid|-end)?|cursor)$/;
439
407
  const rewriteUrls = value => value.replace(/url\(\s*(?:"((?:\\[\s\S]|[^"\\])*)"|'((?:\\[\s\S]|[^'\\])*)'|((?:\\(?:[0-9a-f]{1,6}\s?|[^\r\n\f])|[^\s\\()])+))\s*\)/gi, (whole, double, single, bare) => {
@@ -462,8 +430,6 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
462
430
  if (next) element.setAttribute(name, next);
463
431
  else element.removeAttribute(name);
464
432
  } else if (svg && name === 'style') {
465
- // Interpret only URL-bearing presentation properties: content/metadata strings
466
- // that merely SAY "url(#id)" are not a reference and retain their own words.
467
433
  for (const property of Array.from(element.style)) {
468
434
  if (!iriProperty.test(property)) continue;
469
435
  const before = element.style.getPropertyValue(property), next = rewriteUrls(before);
@@ -518,12 +484,6 @@ function _rapierAnnotateExportBoxPolygons(root) {
518
484
  if (!recipe) continue;
519
485
  const view = recipe.view && recipe.view.w > 0 && recipe.view.h > 0 ? recipe.view : { x: 0, y: 0, w: recipe.canvas.w, h: recipe.canvas.h };
520
486
  if (layout.wrap === 'around') {
521
- // One occupancy for live view and export: the drawing's own profile -- closed faces reserved,
522
- // open strokes as ribbons, labels as boxes -- computed from the same recipe the live view
523
- // reads, sampled into bands the exported page reads back (layout/model.mjs parseProfile). Text
524
- // and paint shapes need rendered pixels for their glyph silhouettes, so the live editor's own
525
- // <img> supplies those when it can be found; a raster or a foreign SVG writes nothing and the
526
- // export measures alpha, as the live view does.
527
487
  try {
528
488
  let glyphs = null;
529
489
  if (recipe.shapes.some(shape => shape.recognized === 'text' || shape.recognized === 'paint' || shape.label)) {
@@ -572,8 +532,6 @@ function* _rapierSharedSourceFormsSteps(source, root, preparedDestinations = nul
572
532
  const used = [], definitions = [];
573
533
  for (const row of destinations) {
574
534
  yield;
575
- // Share can display a converted picture. Only identical authored bytes can be
576
- // borrowed from that picture; otherwise the source keeps its own data URL.
577
535
  const image = byUrl.get(row.destination);
578
536
  if (!image) continue;
579
537
  let id = image.id;
@@ -608,7 +566,6 @@ async function _rapierSharedSourceCarrier(context, root) {
608
566
  '"' + (forms.ids.length ? ' data-images="' + forms.ids.join(' ') + '"' : '') +
609
567
  (forms.definitions.length ? ' data-image-definitions="' + forms.definitions.map(encodeURIComponent).join(' ') + '"' : '') + '>\n' +
610
568
  forms.carried + '\n</script>\n' +
611
- // The optional edit ledger: carried only when the host binds its owner; a host without one writes none.
612
569
  (RapierLedgerCarried ? RapierLedgerCarried.writeParts(forms.resolved, _rapierLedgerParts(forms.resolved, context.ledger, context.carried)) +
613
570
  RapierLedgerCarried.writeBase(forms.resolved, context.comparisonBase) : '');
614
571
  }
@@ -622,10 +579,7 @@ function _rapierSharedResolve(text, ids, images, definitions) {
622
579
  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; }
623
580
  return found;
624
581
  };
625
- // Authored # characters were encoded. A raw id after ]( can only be a structural
626
- // substitution; matching its destination lets nested alt brackets survive unchanged.
627
582
  const inline = new RegExp('(\\]\\([ \\t\\r\\n]*(?:&lt;)?)#' + id + '(?=>?[ \\t\\r\\n]*\\)|>?[ \\t\\r\\n]+["\'(])', 'g');
628
- // A definition may sit in a quote or a list item, as the writer found it: its line starts with their markers.
629
583
  const definition = new RegExp('(^[ \\t>]*(?:(?:[-+*]|\\d{1,9}[.)])[ \\t]+[ \\t>]*)*\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t>]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
630
584
  const swapInline = (match, lead, value) => { const found = src(value); return found == null ? match : lead + found; };
631
585
  const swapDefinition = (match, lead, label, value) => {
@@ -640,7 +594,6 @@ function _rapierSharedResolve(text, ids, images, definitions) {
640
594
  async function _rapierReadSharedDocument(text, filename) {
641
595
  const html = String(text);
642
596
  if (!/\.html?$/i.test(filename || '') || !html.includes('type="' + _RAPIER_SHARED_SOURCE_TYPE + '"')) return null;
643
- // Template contents remain inert, including pictures and scripts in ordinary HTML.
644
597
  const template = document.createElement('template');
645
598
  template.innerHTML = html;
646
599
  const invalid = () => { throw new Error('The editable source in this web page is incomplete or changed'); };
@@ -652,15 +605,12 @@ async function _rapierReadSharedDocument(text, filename) {
652
605
  const definitionTokens = (carrier.getAttribute('data-image-definitions') || '').split(/\s+/).filter(Boolean);
653
606
  if (!_rapierDocumentNameIsAdmissible(name) || !['markdown', 'text', 'code'].includes(kind) || !/^[0-9a-f]{64}$/.test(sha256) ||
654
607
  ids.some(id => !_RAPIER_SHARED_ID.test(id)) || definitionTokens.some(token => !_RAPIER_SHARED_DEFINITION.test(token))) return invalid();
655
- // Only a definition the page declares is an image definition.
656
608
  let definitions;
657
609
  try { definitions = new Set(definitionTokens.map(decodeURIComponent)); } catch (_) { return invalid(); }
658
610
  let source = carrier.textContent;
659
611
  if (source.startsWith('\n')) source = source.slice(1);
660
612
  if (source.endsWith('\n')) source = source.slice(0, -1);
661
613
  source = source.replace(/&#13;/g, '\r');
662
- // A duplicate id (two <img id="…">) is ambiguous, so it is recorded as null rather than either
663
- // src -- ties into _rapierSharedResolve's `broken` check the same way a missing image does.
664
614
  const images = new Map();
665
615
  for (const element of template.content.querySelectorAll('img[id]')) images.set(element.id, images.has(element.id) ? null : element.getAttribute('src'));
666
616
  const resolved = _rapierSharedResolve(source, ids, images, definitions);
@@ -686,7 +636,6 @@ function _rapierShareWrapKind(element) {
686
636
  if (globalThis.RapierMarkdownLayout.wrapTextBlock(element)) return 'prose';
687
637
  const image = element.tagName === 'P' ? element.querySelector('img') : null;
688
638
  if (image && globalThis.RapierImageLayout.imageOnly(element, image)) return 'picture';
689
- // An empty paragraph is transparent to the owner search, as in the editor and the export.
690
639
  if (element.tagName === 'P' && !element.textContent.trim() && !image) return 'metadata';
691
640
  return null;
692
641
  }
@@ -701,14 +650,6 @@ function _rapierSharePictureParagraph(image) {
701
650
 
702
651
  function _rapierSharedPageLayout(root) { return finish(_rapierSharedPageLayoutSteps(root)); }
703
652
  function* _rapierSharedPageLayoutSteps(root) {
704
- // Owner search (wrapNeighbour) reads the sibling chain live, and applying one assignment
705
- // splices the tree -- moving the owner into a new container moves it out of the very chain
706
- // a later picture's own search would walk, and a barrier-free skip past an earlier picture
707
- // can land on that new container instead (it is not a <p>, so it reads as a barrier). So
708
- // every owner is resolved first, against the one original, unspliced tree, in a pass that
709
- // only reads; only once all of them are known does a second pass apply any of them. Same
710
- // two-phase shape as layout/interchange.js's reflow(), which resolves every picture's owner
711
- // from one upfront `[...root.children]` before it changes anything about any of them.
712
653
  const assignments = [];
713
654
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
714
655
  yield;
@@ -716,19 +657,9 @@ function* _rapierSharedPageLayoutSteps(root) {
716
657
  if (layout?.wrap !== 'around' && layout?.wrap !== 'box') continue;
717
658
  const paragraph = _rapierSharePictureParagraph(image);
718
659
  if (!paragraph) continue;
719
- // Prefer following text, then preceding text, looking past pictures and metadata.
720
660
  const owner = globalThis.RapierMarkdownLayout.wrapNeighbour(paragraph, _rapierShareWrapKind);
721
661
  if (owner) assignments.push({image, paragraph, layout, owner});
722
662
  }
723
- // A float affects every line box after it in the same flow, which is the wrap law: prose and
724
- // headings carry on beside the picture across a heading or section boundary as though the blocks
725
- // between were not there. So the picture paragraph is placed right before its owner's text (a
726
- // float only reaches what follows it) and left in the page's own flow, uncontained; the blocks
727
- // that never wrap -- tables, code, figures and rules -- clear it through the page
728
- // stylesheet instead, and a bordered h1 forms its own block so its rule does not run under the
729
- // picture. Static CSS, no script: the no-script fallback only -- a reader with scripting off
730
- // still gets a sensible nearest-side float; the inlined planner (same owner as standalone)
731
- // replaces it when scripts run.
732
663
  for (const {image, paragraph, layout, owner} of assignments) {
733
664
  yield;
734
665
  const side = (layout.x ?? 0) < 50 ? 'left' : 'right';
@@ -738,31 +669,9 @@ function* _rapierSharedPageLayoutSteps(root) {
738
669
  paragraph.style.setProperty('shape-outside', 'inset(' + layout.y + 'em 0 0)');
739
670
  }
740
671
  image.style.margin = '0';
741
- // The reserved box comes from the same rotated-bounds geometry every other consumer uses
742
- // (layout/browser.js, layout/interchange.js's `geometry.rotatedBoundsRad`, layout/model.mjs's
743
- // one owner), expressed as a percentage of the column the way the unrotated branch below already
744
- // is -- never the picture's own pixel dimensions, which have no relation to a requested
745
- // percentage width (a 4000px-wide source at width=40% would float 1788px wide in a 600px
746
- // column). Intrinsic width/height supply only the aspect ratio; a percentage padding-top/bottom
747
- // resolves against the same containing-block width CSS already resolves the float's own
748
- // percentage width against, so one basis serves both the box and its vertical reservation, and
749
- // the box stays exactly as wide as the column at every viewport width. The turned box is
750
- // centred, horizontally and vertically, in the reserved (wider, taller) box rather than
751
- // left-flush inside it, so the space this paragraph clears for it (the float's own width) is
752
- // evenly split around the picture on every side, not just above and below. A missing `width`
753
- // falls back to the picture's own natural pixel size in pixels, the same fallback the unrotated
754
- // branch below uses; without either width/height attribute the picture still turns, just without
755
- // the extra reservation (the same graceful narrowing Share already accepts elsewhere).
756
672
  const naturalWidth = Number(image.getAttribute('width')) || 0, naturalHeight = Number(image.getAttribute('height')) || 0;
757
673
  if (layout.rotate && naturalWidth > 0 && naturalHeight > 0) {
758
674
  const rad = layout.rotate * Math.PI / 180, aspect = naturalHeight / naturalWidth;
759
- // A steep turn (e.g. 90deg on a landscape source) can need the image WIDER than its own
760
- // reserved box's width -- the rotated bounding box is narrower than the picture's own
761
- // unrotated width whenever the turn swaps a wide dimension for a narrow one. Share's own
762
- // global `img{max-width:100%}` (a sensible default everywhere else) would silently clamp
763
- // exactly that case, shrinking the image and, with it, its auto height -- `max-width:none`
764
- // here overrides that single rule with a more specific one, only for a picture this pass
765
- // already sizes deliberately.
766
675
  image.style.maxWidth = 'none';
767
676
  if (layout.width != null) {
768
677
  let width = layout.width, height = width * aspect;
@@ -787,7 +696,6 @@ function* _rapierSharedPageLayoutSteps(root) {
787
696
  image.style.marginLeft = image.style.marginRight = ((bounds.width - naturalWidth) / 2) + 'px';
788
697
  }
789
698
  } else if (layout.lines != null) {
790
- // Sized in lines: the float shrinks to the picture, whose height is the lines (spec/md-layout.mjs imageStyle).
791
699
  image.style.width = 'auto';
792
700
  image.style.height = globalThis.RapierMarkdownLayout.linesHeightCss(layout.lines);
793
701
  } else {
@@ -796,32 +704,16 @@ function* _rapierSharedPageLayoutSteps(root) {
796
704
  if (layout.width != null || image.getAttribute('width')) image.style.width = '100%';
797
705
  }
798
706
  if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
799
- // `wrap=around` wraps to the picture's own alpha silhouette the way standard-adoption.md's
800
- // rung-2 paragraph promises -- `shape-outside:url(<the same picture>)`, which every engine that
801
- // reads shape-outside re-samples from the referenced image's own pixels, no script required. A
802
- // CSS transform does not turn the shape a `shape-outside` url() samples (it stays the picture's
803
- // unturned alpha), so a rotated `around` picture keeps the plain rectangular reservation above
804
- // instead -- an honest conservative box rather than a silhouette shaped for the wrong angle.
805
- // `layout.y` already owns `shape-outside` for its own inset reservation (below); the two are not
806
- // combinable (a float has exactly one `shape-outside` value), so a `y`-offset `around` picture
807
- // keeps that inset instead of silhouette wrapping -- a documented, narrow degradation.
808
707
  if (layout.wrap === 'around' && !layout.rotate && !layout.y && !image.dataset.rapierContour?.endsWith('-box')) {
809
708
  const src = image.getAttribute('src');
810
709
  if (src) {
811
710
  paragraph.style.setProperty('shape-outside', 'url("' + src + '")');
812
- // Standard 10% alpha cutoff (`layout/model.mjs` alphaThreshold = .1).
813
711
  paragraph.style.setProperty('shape-image-threshold', '0.1');
814
712
  }
815
713
  }
816
714
  paragraph.classList.add('rapier-wrap-picture');
817
715
  owner.before(paragraph);
818
716
  }
819
- // `behind`/`front` position the picture exactly where `x`/`y`/`width` say, out of the words' flow
820
- // entirely (z-index below or above them) -- the absolutely positioned picture
821
- // standard-adoption.md's rung-2 paragraph promises for these two placements, no obstacle
822
- // avoidance, no script. The picture's own paragraph collapses to zero height in place (the same
823
- // collapse-in-place anchor layout/interchange.js's own reflow uses for every positioned kind) so
824
- // removing the picture from flow costs the surrounding text nothing.
825
717
  const positionedAssignments = [];
826
718
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
827
719
  yield;
@@ -833,7 +725,6 @@ function* _rapierSharedPageLayoutSteps(root) {
833
725
  for (const {image, paragraph, layout} of positionedAssignments) {
834
726
  yield;
835
727
  paragraph.style.cssText = 'position:relative;height:0;margin:0;padding:0;line-height:0';
836
- // Without x the picture starts at the left edge, as everywhere else (layout/model.mjs imageBox).
837
728
  const x = layout.x, naturalWidth = Number(image.getAttribute('width')) || 0;
838
729
  if (layout.width != null) image.style.width = layout.width + '%';
839
730
  else if (naturalWidth > 0) image.style.width = naturalWidth + 'px';
@@ -843,14 +734,9 @@ function* _rapierSharedPageLayoutSteps(root) {
843
734
  image.style.top = (layout.y || 0) + 'em';
844
735
  image.style.margin = '0';
845
736
  if (layout.rotate) { image.style.transform = 'rotate(' + layout.rotate + 'deg)'; image.style.transformOrigin = '50% 50%'; }
846
- // Same rule as the live editor and the styled export: `behind` sinks under the owner's
847
- // own in-flow, non-positioned text; `front` keeps the default stack, already above it.
848
737
  if (layout.wrap === 'behind') image.style.zIndex = '-1';
849
738
  paragraph.classList.add('rapier-positioned-picture');
850
739
  }
851
- // A turned picture Share does not otherwise position (inline, which this static page does not
852
- // float at all) still turns -- the same picture, painted in place, never upright just because
853
- // Share's own float model has nothing else to say about it.
854
740
  const positioned = new Set([...assignments, ...positionedAssignments].map(row => row.image));
855
741
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
856
742
  yield;
@@ -903,8 +789,6 @@ async function _rapierRequireOfflinePageImages(root) {
903
789
  const picture = image.parentElement?.tagName.toLowerCase() === 'picture' && image.parentElement;
904
790
  const alternative = picture && [...picture.querySelectorAll('source[srcset]')].some(source => source.getAttribute('srcset').trim());
905
791
  if (tag !== 'source' && (src || !srcset && !alternative)) sources.push(src);
906
- // A data URL contains its own comma. Read each URL before its optional
907
- // density/width descriptor, as HTML's image candidate list does.
908
792
  for (let at = 0; at < srcset.length;) {
909
793
  while (at < srcset.length && /[\t\n\f\r ,]/.test(srcset[at])) at++;
910
794
  const start = at;
@@ -922,8 +806,6 @@ async function _rapierRequireOfflinePageImages(root) {
922
806
  missing.push('Picture ' + (index + 1) + (alt ? ' — ' + alt : '') + ': ' + destination);
923
807
  }
924
808
  }
925
- // A source candidate or an SVG/CSS picture is still an automatic resource. Inspect
926
- // the retained data; ordinary links remain navigation and local fragment references stay local.
927
809
  const sourceTags = new Set(['audio', 'video', 'source', 'track', 'input', 'iframe', 'embed', 'script']);
928
810
  const cssAttributes = new Set(['style', 'fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', 'marker-mid', 'marker-end', 'cursor', 'color-profile']);
929
811
  for (const element of root.querySelectorAll('*')) {
@@ -940,13 +822,10 @@ async function _rapierRequireOfflinePageImages(root) {
940
822
  }
941
823
  }
942
824
  if (!missing.length) return;
943
- // The writer refuses immediately; interactive callers own any explanatory sheet.
944
825
  throw Object.assign(new Error('Web page not written; embed these pictures and try again. ' + missing.join('; ')),
945
826
  {code: 'offline_image_unavailable'});
946
827
  }
947
828
 
948
- // Shared by live CSS admission and offline resource admission. Quoted strings are
949
- // resource URLs only inside url() or as an image-set option, never arbitrary CSS text.
950
829
  function _rapierCssValueUrls(value) {
951
830
  const urls = [], functions = [];
952
831
  let at = 0;
@@ -1022,10 +901,7 @@ function _rapierCssValueUrls(value) {
1022
901
 
1023
902
  async function _rapierBuildSharedPage(captured) {
1024
903
  const context = await _rapierPrepareInterchangeContext({kind: 'share'}, captured);
1025
- // Share and Export write the same complete page, including native disclosures.
1026
904
  const artifact = await _rapierBuildArtifact({kind: 'standalone'}, context);
1027
- // No cap and no warning: the page is as large as the document is. A page over the editor's own 25
1028
- // MiB open limit still opens in any browser.
1029
905
  const blob = new Blob([artifact.html], {type: 'text/html;charset=utf-8'});
1030
906
  return {blob, filename: context.baseName + '.html'};
1031
907
  }
@@ -1067,7 +943,6 @@ async function _rapierDataImageDestinationsAsync(source, work) {
1067
943
  }
1068
944
 
1069
945
  function _rapierImageDestinations(source, accepts) {
1070
- // Only a rendered Markdown image consumes a definition; unused definitions stay literal source.
1071
946
  if (!source.includes('![')) return [];
1072
947
  const env = {}, tokens = md.parse(source, env), offsets = _rapierLineStartOffsets(source);
1073
948
  const hrefs = _rapierImageSources(tokens), labels = new Set(), destinations = [], seen = new Set();
@@ -1354,7 +1229,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1354
1229
  steps.length = 0;
1355
1230
  }
1356
1231
 
1357
- // One prepared rich paragraph retains original item text. Unsupported shaping keeps native flow.
1358
1232
  function prepare(paragraph) {
1359
1233
  const refuse = () => { prepared.forget(paragraph); return null; };
1360
1234
  if (!/^(P|H[1-6])$/.test(paragraph.tagName) || !paragraph.textContent.trim() || paragraph.textContent.length > 8192 ||
@@ -1371,7 +1245,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1371
1245
  !preparation.supportsRichStyle(computed, language) || computed.whiteSpace !== 'normal' ||
1372
1246
  computed.transform !== 'none' ||
1373
1247
  ['::before', '::after'].some(pseudo => !['none', 'normal'].includes(view.getComputedStyle(paragraph, pseudo).content)) ||
1374
- // Lines start at the padding box's top-left: left, right or top inset is refused.
1375
1248
  ['paddingLeft', 'paddingRight', 'paddingTop', 'borderLeftWidth', 'borderRightWidth', 'borderTopWidth']
1376
1249
  .some(key => parseFloat(computed[key]) > 0)) return refuse();
1377
1250
  const runs = [], items = [], walker = doc.createTreeWalker(paragraph, 4);
@@ -1417,11 +1290,9 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1417
1290
  balance: 0};
1418
1291
  }
1419
1292
 
1420
- // An id survives on the first fragment only.
1421
1293
  function fragmentNode(record, fragment, idsKept) {
1422
1294
  const run = record.runs[fragment.itemIndex], mapped = geometry.mapFragment(run, fragment, {whiteSpace: record.whiteSpace});
1423
1295
  if (!mapped) return null;
1424
- // 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.
1425
1296
  const kept = run.parents.length ? mapped.text.replace(/[ \t\n\r\f]+$/, '') : mapped.text;
1426
1297
  const split = kept && kept.length < mapped.text.length;
1427
1298
  let child = doc.createTextNode(split ? kept : mapped.text);
@@ -1488,8 +1359,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1488
1359
  return found;
1489
1360
  }
1490
1361
 
1491
- // wrap=around reads data-rapier-occupancy, else alpha. Cache key: src, dimensions, layout.wrap, rotate angle.
1492
- // A turned raster's profile is recomputed here as pictureProfile does, never shipped.
1493
1362
  function profile(image, layout) {
1494
1363
  if (layout.wrap === 'around' && image.dataset?.rapierContour?.endsWith('-box')) return null;
1495
1364
  const src = image.currentSrc || image.getAttribute('src') || '', rotateRad = (layout.rotate || 0) * Math.PI / 180;
@@ -1509,7 +1378,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1509
1378
  return value;
1510
1379
  }
1511
1380
 
1512
- // wrap=box reads data-rapier-box-polygon; this page never parses a recipe or bundles Draw. No polygon: whole rectangle.
1513
1381
  function boxProfile(image) {
1514
1382
  const raw = image.getAttribute('data-rapier-box-polygon');
1515
1383
  if (!raw) return null;
@@ -1520,7 +1388,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1520
1388
  return geometry.polygonProfile(corners);
1521
1389
  }
1522
1390
 
1523
- // As the live editor (browser.js).
1524
1391
  function syncInlineRotatedPictures(exclude) {
1525
1392
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
1526
1393
  if (exclude.has(image) || !image.complete || !image.naturalWidth || !image.naturalHeight) continue;
@@ -1529,7 +1396,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1529
1396
  const width = image.offsetWidth, height = image.offsetHeight;
1530
1397
  if (!(width > 0 && height > 0)) continue;
1531
1398
  const paragraph = image.closest('p');
1532
- // geometry.imageOnly (layout/model.mjs).
1533
1399
  const alone = !!paragraph && imageOnly(paragraph, image);
1534
1400
  const baseLeft = parseFloat(css(image).marginLeft) || 0;
1535
1401
  const rotated = geometry.rotatedBoundsRad(width, height, layout.rotate * Math.PI / 180);
@@ -1541,8 +1407,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1541
1407
  }
1542
1408
  }
1543
1409
 
1544
- // A block that cannot wrap stands with its box just below the outline, as in the editor. Its own margin moves it, so
1545
- // nothing is inserted between a picture and its words (a reader and the witnesses read nextElementSibling).
1546
1410
  function clearTo(element, bottom, origin) {
1547
1411
  for (let pass = 0; pass < 3; pass++) {
1548
1412
  const distance = bottom - (box(element).top - origin);
@@ -1557,17 +1421,13 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1557
1421
  if (metadata.wrapTextBlock(element)) return 'prose';
1558
1422
  const image = element.tagName === 'P' ? element.querySelector('img') : null;
1559
1423
  if (image && imageOnly(element, image)) return 'picture';
1560
- // An empty paragraph is transparent to the owner search, as in the editor.
1561
1424
  if (element.tagName === 'P' && !element.textContent.trim() && !image) return 'metadata';
1562
1425
  return null;
1563
1426
  }
1564
1427
 
1565
- // Float fallback as the editor's floatAround.
1566
1428
  const FLOAT_BLOCK = /^(P|H[1-6]|UL|OL|BLOCKQUOTE|DETAILS|SUMMARY|DL)$/;
1567
1429
  function floatAround(element, width, height, top, obstacles) {
1568
- // As the editor: a paragraph or heading floats whatever it holds; a list, quote or disclosure holding a rigid block does not.
1569
1430
  if (!FLOAT_BLOCK.test(element.tagName) || !/^(P|H[1-6])$/.test(element.tagName) && element.querySelector('table, pre, figure, img, .math-rendered')) return false;
1570
- // As the editor: a disclosure's box ends where the outline begins (layout/model.mjs boxClearance).
1571
1431
  if (element.tagName === 'DETAILS') {
1572
1432
  const computed = css(element), outer = box(element), floor = metadata.wrapColumnFloor(parseFloat(computed.fontSize) || 16);
1573
1433
  const offset = (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0);
@@ -1603,8 +1463,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1603
1463
  const inside = obstacles.filter(obstacle => obstacle.y < bottom + 4096 && obstacle.y + obstacle.height > top &&
1604
1464
  obstacle.x < width && obstacle.x + obstacle.width > 0);
1605
1465
  if (!inside.length) return false;
1606
- // layout/model.mjs wrapShape; `geometry` is modules["layout/model.mjs"]. As the editor, the start side reaches from
1607
- // the box's own start edge.
1608
1466
  const start = computed.direction === 'rtl' ? 'right' : 'left';
1609
1467
  const startInset = start === 'left' ? (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0)
1610
1468
  : -((parseFloat(computed.paddingRight) || 0) + (parseFloat(computed.borderRightWidth) || 0));
@@ -1621,12 +1479,11 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1621
1479
  const shape = shapes[side];
1622
1480
  if (!shape) continue;
1623
1481
  const float = doc.createElement('span');
1482
+ float.className = 'rapier-flow-float';
1624
1483
  float.setAttribute('aria-hidden', 'true');
1625
1484
  float.style.cssText = `float:${side};width:${pixels(shape.boxWidth)};height:${pixels(shape.boxHeight)};margin-top:${pixels(shape.startY)};shape-outside:${shape.shape};pointer-events:none`;
1626
1485
  element.prepend(float); floats.push(float); placed.push([float, shape]);
1627
1486
  }
1628
- // As the editor: each float ends at the block's own content bottom and the block keeps its own display, so its
1629
- // margins collapse as they do with no picture. A float the content does not reach goes.
1630
1487
  const contentBottom = () => {
1631
1488
  let first = element.firstChild;
1632
1489
  while (first && placed.some(([float]) => float === first)) first = first.nextSibling;
@@ -1644,7 +1501,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1644
1501
  }
1645
1502
  if (!changed) break;
1646
1503
  }
1647
- // As the editor: a list item's marker and a quote's bar move with the words the floats moved.
1648
1504
  const {markers, bars, steps: breaks} = geometry.edgeMarks(element, before);
1649
1505
  for (const {item, shift} of markers) style(item, {'--md-marker': 'calc(' + (item.style.getPropertyValue('--md-marker') || '0px') + ' + ' + pixels(shift) + ')'});
1650
1506
  for (const item of breaks) { item.setAttribute('data-rapier-wrap-step', ''); steps.push(item); }
@@ -1670,7 +1526,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1670
1526
  if (!root.isConnected || printing || view.matchMedia?.('print').matches) return;
1671
1527
  const children = [...root.children].filter(element => !element.hasAttribute('data-rapier-html-preview'));
1672
1528
  if (children.length > 8192) return;
1673
- // behind/front collapse and place like around/box but are never obstacles.
1674
1529
  const outOfFlow = value => value?.wrap === 'behind' || value?.wrap === 'front';
1675
1530
  const origin = box(root).top, obstacles = [], anchors = new Map(), wrapped = new Set(), placed = [], positionedImages = new Set();
1676
1531
  for (const element of children) {
@@ -1688,7 +1543,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1688
1543
  style(element, {position: 'relative', height: '0', 'min-height': '0', 'margin-top': '0', 'margin-bottom': '0', 'padding-top': '0', 'padding-bottom': '0', 'line-height': '0'});
1689
1544
  style(image, {position: 'absolute', margin: '0'});
1690
1545
  }
1691
- // As the live editor.
1692
1546
  syncInlineRotatedPictures(positionedImages);
1693
1547
  for (const element of children) {
1694
1548
  if (wrapped.has(element)) continue;
@@ -1698,7 +1552,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1698
1552
  const width = bounds.width - inset - (parseFloat(computed.paddingRight) || 0) - (parseFloat(computed.borderRightWidth) || 0);
1699
1553
 
1700
1554
  for (const {element: source, image, layout, width: imageWidth} of anchors.get(element) || []) {
1701
- // A picture sized in lines (`lines`) reads its anchor's line and stands on the first line's cap height, as the live editor.
1702
1555
  const lines = layout.lines != null ? geometry.lineMetrics(element) : null;
1703
1556
  const unrotated = geometry.imageBox(width, image.naturalWidth, image.naturalHeight, layout, imageWidth, lines);
1704
1557
  if (!unrotated) { restore(); return; }
@@ -1707,7 +1560,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1707
1560
  const line = parseFloat(computed.lineHeight) || (parseFloat(computed.fontSize) || 16) * 1.6;
1708
1561
  unrotated.height = metadata.objectHeight(unrotated.width, Number(image.dataset.rapierNaturalHeight) || box(image).height, attrs, line);
1709
1562
  }
1710
- // rasterReserved (layout/model.mjs).
1711
1563
  const rasterRad = (layout.rotate || 0) * Math.PI / 180;
1712
1564
  const fit = geometry.rasterReserved(width, unrotated, rasterRad);
1713
1565
  if (!fit) { restore(); return; }
@@ -1717,10 +1569,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1717
1569
  const topInset = (parseFloat(computed.paddingTop) || 0) + (parseFloat(computed.borderTopWidth) || 0);
1718
1570
  const height = Math.max(0, bounds.height - topInset - (parseFloat(computed.paddingBottom) || 0) - (parseFloat(computed.borderBottomWidth) || 0));
1719
1571
  let y = bounds.top - origin + topInset + geometry.linesTop(layout, lines) + Math.min(layout.y || 0, height / em) * em;
1720
- // The standoff and the room between pictures, in the reference scale's pixels, as the live editor.
1721
1572
  const scale = geometry.frameScale(image), room = 10 * scale;
1722
1573
  if (!outOfFlow(layout)) {
1723
- // A picture an earlier one stands in moves down alone; its owner keeps its place, as in the editor.
1724
1574
  for (const obstacle of [...obstacles].sort((left, right) => left.y - right.y)) {
1725
1575
  if (obstacle.x < left + rectangle.width + room && obstacle.x + obstacle.width > left - room &&
1726
1576
  obstacle.y + obstacle.height > y - room && obstacle.y < y + rectangle.height + room)
@@ -1737,7 +1587,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1737
1587
  const own = active.map(obstacle => ({...obstacle, x: obstacle.x - bounds.left - inset}));
1738
1588
  const prepared = prepare(element);
1739
1589
  if (!(prepared && width > 0 && project(prepared, width, top, own)) && !(width > 0 && floatAround(element, width, bounds.height, top, own))) {
1740
- // As the editor: only an outline that meets the block's box moves it.
1741
1590
  const overlapping = active.filter(obstacle => obstacle.y < top + bounds.height && obstacle.x < bounds.right && obstacle.x + obstacle.width > bounds.left);
1742
1591
  if (overlapping.length) clearTo(element, Math.max(...overlapping.map(obstacle => obstacle.y + obstacle.height)), origin);
1743
1592
  }
@@ -1748,7 +1597,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1748
1597
  style(image, {position: 'absolute', left: pixels((visualLeft ?? left) - anchor.left), top: pixels((visualTop ?? y) - (anchor.top - origin)),
1749
1598
  width: pixels(visualWidth ?? rectangle.width), height: pixels(visualHeight ?? rectangle.height), 'max-width': 'none', margin: '0',
1750
1599
  transform: rotateDeg ? `rotate(${rotateDeg}deg)` : 'none', 'transform-origin': '50% 50%',
1751
- // As the live editor.
1752
1600
  ...(wrap === 'behind' ? {'z-index': '-1'} : {})});
1753
1601
  }
1754
1602
  if (obstacles.length) {
@@ -1807,7 +1655,6 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1807
1655
 
1808
1656
  function _rapierArtifactLayoutScript(root, nonce = '', {pictures = true} = {}) {
1809
1657
  const dependencies = globalThis.RapierArtifactLayoutDependencies;
1810
- // Every wrap value earns the reflow script, and an inline turned raster (keyed off data-rapier-image-layout).
1811
1658
  const layoutOf = element => globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'));
1812
1659
  const pictureLayoutOf = image => globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
1813
1660
  const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
@@ -1843,8 +1690,6 @@ function _rapierArtifactTypographyScript(root) {
1843
1690
  return '(() => {\nconst typography = modules["layout/typography.mjs"].createTypography();\n' +
1844
1691
  (pictures ? 'const capitals = (' + createPrintCapitals.toString() + ')((' + createLineMeasurer.toString() + ')(),(' + linesHeight.toString() + '),(' + followingPrintProse.toString() + '));\n' : '') +
1845
1692
  'const root = document.querySelector("main.rapier-page"); const printMedia = matchMedia("print"); let frame = 0, width = -1, ready = null, printing = printMedia.matches;\n' +
1846
- // beforeprint can still expose the screen viewport, not the selected paper's content width.
1847
- // Only an authored paper width supplies a fixed target; automatic paper keeps native wrapping.
1848
1693
  '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' +
1849
1694
  'const schedule = () => { if (!frame) frame = requestAnimationFrame(apply); };\n' +
1850
1695
  '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' +