rapier-markdown-kit 1.1.102 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +5 -2
  2. package/README.md +245 -54
  3. package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
  4. package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
  5. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
  6. package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
  7. package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
  8. package/dist/agent/vendor/pretext/LICENSE +208 -0
  9. package/dist/agent/vendor/pretext/NOTICE +26 -13
  10. package/dist/agent/vendor/pretext/SOURCE.json +213 -45
  11. package/dist/agent/vendor/pretext/analysis.js +675 -1224
  12. package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
  13. package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
  14. package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
  15. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
  16. package/dist/agent/vendor/pretext/graphemes.js +98 -0
  17. package/dist/agent/vendor/pretext/han-kerning.js +253 -0
  18. package/dist/agent/vendor/pretext/layout.js +352 -290
  19. package/dist/agent/vendor/pretext/line-break.js +1154 -588
  20. package/dist/agent/vendor/pretext/line-breaks.js +892 -0
  21. package/dist/agent/vendor/pretext/line-text.js +81 -39
  22. package/dist/agent/vendor/pretext/measurement.js +1105 -176
  23. package/dist/agent/vendor/pretext/prepare.js +807 -0
  24. package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
  25. package/dist/agent/will.mjs +2 -0
  26. package/dist/images/gif.mjs +47 -0
  27. package/dist/images/playback.mjs +101 -0
  28. package/dist/kit/assets.mjs +5 -1
  29. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  30. package/dist/kit/conformance/04-wrap-box.md +1 -1
  31. package/dist/kit/conformance/05-behind.md +1 -1
  32. package/dist/kit/conformance/06-front.md +1 -1
  33. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  34. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  35. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  36. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  37. package/dist/kit/conformance/10-both-sides.md +1 -1
  38. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  39. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  40. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  41. package/dist/kit/conformance/README.md +19 -28
  42. package/dist/kit/conformance/run.mjs +13 -11
  43. package/dist/kit/export-fonts.mjs +26 -11
  44. package/dist/kit/index.mjs +1 -0
  45. package/dist/kit/ledger/carried.mjs +17 -10
  46. package/dist/kit/ledger/data.mjs +73 -13
  47. package/dist/kit/ledger/format.mjs +176 -54
  48. package/dist/kit/ledger/hash.mjs +91 -18
  49. package/dist/kit/ledger/history.mjs +156 -116
  50. package/dist/kit/ledger/inverse-source.mjs +158 -19
  51. package/dist/kit/ledger/journal-records.mjs +292 -67
  52. package/dist/kit/ledger/merge.mjs +4 -3
  53. package/dist/kit/model.mjs +1 -1
  54. package/dist/kit/pdf-glyphs.mjs +4 -4
  55. package/dist/kit/pdf-vectors.mjs +4 -1
  56. package/dist/kit/rapier-ledger.mjs +2 -2
  57. package/dist/kit/render-ink.mjs +1 -1
  58. package/dist/kit/render-markdown.mjs +400 -148
  59. package/dist/kit/render-print.mjs +131 -55
  60. package/dist/kit/render-sanitize.mjs +30 -7
  61. package/dist/kit/render-work.mjs +99 -20
  62. package/dist/kit/render.mjs +133 -140
  63. package/dist/kit/typography.mjs +2 -0
  64. package/dist/layout/hyphenation.mjs +127 -0
  65. package/dist/layout/line-plan.mjs +88 -96
  66. package/dist/layout/model.mjs +71 -88
  67. package/dist/layout/paragraph-fit.mjs +191 -0
  68. package/dist/layout/preparation.mjs +373 -0
  69. package/dist/layout/prepared-paragraph.mjs +223 -0
  70. package/dist/layout/typography.mjs +345 -111
  71. package/dist/packages/rapier-diff/index.mjs +359 -224
  72. package/dist/spec/document-fonts.mjs +14 -5
  73. package/dist/spec/document-settings.mjs +11 -164
  74. package/dist/spec/html-blocks.mjs +618 -79
  75. package/dist/spec/md-assets.mjs +178 -36
  76. package/dist/spec/md-layout.mjs +13 -2
  77. package/dist/spec/md-marks.mjs +220 -127
  78. package/dist/spec/md-source.mjs +157 -0
  79. package/package.json +2 -1
  80. package/style.css +15 -50
  81. package/dist/agent/vendor/pretext/bidi.js +0 -151
  82. package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
@@ -1,6 +1,6 @@
1
- import {htmlPlaceholder, mountHtmlBlocks, htmlImageRecord, htmlLayoutEdit} from '../spec/html-blocks.mjs';
2
1
  // SPDX-License-Identifier: MIT
3
- import {finish, finishAsync, cloneTree, cleanTree, serializeTree, htmlBoundary, pause, check} from './render-work.mjs';
2
+ import {htmlPlaceholder, mountHtmlBlocks, materializeObjects, objectPosterLabel, htmlImageRecord, htmlLayoutEdit, htmlModeEdit, mergeHtmlObjectBlocks} from '../spec/html-blocks.mjs';
3
+ import {finish, finishAsync, cloneTree, cleanTree, serializeTree, htmlParts, admitHtml, htmlBoundary, pause, check} from './render-work.mjs';
4
4
  const RAPIER_TABLE_CAPTION_PREFIX_RE = /^(?:Table:|:) /;
5
5
  const RAPIER_PORTABLE_HIGHLIGHT_HEX = Object.freeze({
6
6
  green: '#35c57a', red: '#f05a62', blue: '#4f91f7', yellow: '#e6b91e', purple: '#9b6dea',
@@ -50,22 +50,30 @@ function initMarkdownIt() {
50
50
  return markOpenDefault(tokens, idx, opts, env, self);
51
51
  };
52
52
 
53
- md.renderer.rules.rapier_color_open = (tokens, idx) => {
54
- const hex = String(tokens[idx].attrGet('data-md-color') || '').toLowerCase();
55
- if (!/^#[0-9a-f]{6}$/.test(hex)) return '<span>';
56
- const dark = _rapierDeriveDarkColor(hex);
57
- return '<span data-md-color="' + hex + '" style="--md-color:' + hex + ';--md-color-dark:' + dark + '">';
58
- };
59
- md.renderer.rules.rapier_color_close = () => '</span>';
60
- md.renderer.rules.rapier_font_open = (tokens, idx) => {
61
- const token = tokens[idx], family = token.attrGet('data-md-font'), css = globalThis.RapierMarkdownSpec.fontFamilyCss(family);
62
- if (!css) return '<span>';
63
- const original = token.attrGet('data-md-font-open');
64
- return '<span data-md-font="' + md.utils.escapeHtml(family) + '"'
65
- + (original ? ' data-md-font-open="' + md.utils.escapeHtml(original) + '"' : '')
66
- + ' style="font-family:' + md.utils.escapeHtml(css) + '">';
53
+ // Every authored spelling lowers to the same admitted style facts. Original delimiters are
54
+ // transient edit-projection data; only the source owner can write them back.
55
+ const renderTypography = (tokens, index) => {
56
+ const typed = globalThis.RapierMarkdownSpec.typography, held = tokens[index].meta?.typography;
57
+ if (!held) return '<span>';
58
+ const facts = held.style, attrs = [], css = [];
59
+ if (facts.size) {
60
+ attrs.push('data-md-size="' + facts.size + '"');
61
+ css.push('font-size:calc(' + typed.size(facts.size).points / 12 + ' * var(--md-unit))');
62
+ }
63
+ if (facts.color) {
64
+ attrs.push('data-md-color="' + facts.color + '"');
65
+ css.push('--md-color:' + facts.color, '--md-color-dark:' + _rapierDeriveDarkColor(facts.color));
66
+ }
67
+ if (facts.family) {
68
+ attrs.push('data-md-font="' + md.utils.escapeHtml(facts.family.name) + '"');
69
+ css.push('font-family:' + facts.family.css);
70
+ }
71
+ attrs.push('data-md-typography-open="' + md.utils.escapeHtml(encodeURIComponent(held.open)) + '"',
72
+ 'data-md-typography-close="' + md.utils.escapeHtml(encodeURIComponent(held.close)) + '"');
73
+ return '<span ' + attrs.join(' ') + ' style="' + md.utils.escapeHtml(css.join(';')) + '">';
67
74
  };
68
- md.renderer.rules.rapier_font_close = () => '</span>';
75
+ md.renderer.rules.rapier_typography_open = renderTypography;
76
+ md.renderer.rules.rapier_typography_close = () => '</span>';
69
77
  // Ink: the span carries its opener's body (the grammar's inkOpenBody: a sanitizer drops an attribute holding the
70
78
  // comment's close); the ink layer reads it back through the one grammar and draws the stroke over the words' own boxes.
71
79
  // The span itself shows nothing.
@@ -128,8 +136,8 @@ function initMarkdownIt() {
128
136
  const asset = globalThis.RapierImageAssets.dataImage(src), reference = token.meta?.mdImage?.reference;
129
137
  // A connected host's own asset; a host without one (the server) supplies no port.
130
138
  if (!env.rapierPage && typeof globalThis.RapierEmbeddedImages?.imageHtml === 'function' &&
131
- (_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
132
- reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src);
139
+ (_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/gif' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
140
+ reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src, env.rapierImagePresentation !== false);
133
141
  const drawingAlt = typeof rawAlt === 'string' && alt === '' && asset?.codec === 'image/svg+xml' ? globalThis.RapierImageAssets.drawingImageAlt(src) : null;
134
142
  const shownAlt = drawingAlt ?? alt;
135
143
  let loadable = false;
@@ -148,19 +156,28 @@ function initMarkdownIt() {
148
156
  return html.replace('<span ', '<span data-rapier-markdown-image="" ' +
149
157
  (token.meta?.mdImage?.source ? 'data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '" ' : ''));
150
158
  }
159
+ let animation = null;
160
+ try {animation = globalThis.RapierImageAssets.imageAnimation?.(src);} catch (_) {}
151
161
  const attrs = [
152
162
  'data-rapier-markdown-image=""',
153
- 'src="' + md.utils.escapeHtml(src) + '"',
163
+ 'src="' + md.utils.escapeHtml(animation?.first || src) + '"',
154
164
  'alt="' + md.utils.escapeHtml(shownAlt) + '"',
155
165
  'loading="lazy"',
156
166
  'decoding="async"',
157
167
  ];
168
+ if (animation) attrs.push('data-rapier-animation="' + md.utils.escapeHtml(src) + '"',
169
+ 'data-rapier-natural-width="' + animation.width + '"', 'data-rapier-natural-height="' + animation.height + '"');
158
170
  if (token.meta?.mdImage?.source && token.meta.mdImage.source.length <= 4096)
159
171
  attrs.push('data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '"');
160
172
  if (title) attrs.push('title="' + md.utils.escapeHtml(title) + '"');
161
173
  if (shownAlt !== alt || size && !token.meta?.mdLayout?.layout?.width) attrs.push('data-rapier-image-alt-source="' + md.utils.escapeHtml(rawAlt) + '"');
162
174
  if (layout) attrs.push('data-rapier-image-layout="' + md.utils.escapeHtml(encodeURIComponent(layout)) + '"');
163
- return '<img ' + attrs.join(' ') + _rapierImageSizeAttributes(size, layout) + '>';
175
+ // Portable semantic consumers keep their existing native image attributes. Only
176
+ // live editor/reader presentation needs provisional geometry and its load consumer.
177
+ if (env.rapierImagePresentation === false) return '<img ' + attrs.join(' ') + _rapierImageSizeAttributes(size, layout) + '>';
178
+ const geometry = !env.rapierPage && globalThis.RapierEmbeddedImages?.geometry
179
+ ? globalThis.RapierEmbeddedImages.geometry(src) : globalThis.RapierImageAssets.imagePlaceholderDimensions(src);
180
+ return '<img ' + attrs.join(' ') + _rapierImageSizeAttributes(size, layout, geometry || {}) + '>';
164
181
  };
165
182
 
166
183
  md.renderer.rules.hr = function (tokens, idx) {
@@ -176,7 +193,9 @@ function initMarkdownIt() {
176
193
  // markup reaches DOMPurify: sanitizing each HTML token closes containers before their body.
177
194
  // The harness runs this installer alone for the parser's acceptance; without the page's sanitizer the tokens pass.
178
195
  const inPage = typeof sanitizeRapierHtml === 'function' && typeof _rapierChromeOwnsId === 'function';
179
- const rawAttribute = /\s+([A-Za-z_:][A-Za-z0-9_.:-]*)(?:\s*=\s*("[^"]*"|'[^']*'|[^"'=<>`\s]+))?/g;
196
+ // HTML separates attributes and ends an unquoted value only at ASCII whitespace: JavaScript's \s also takes U+00A0,
197
+ // which would read a quoted value the browser never sees and keep the attributes hidden inside it.
198
+ const rawAttribute = /[\t\n\f\r ]+([A-Za-z_:][A-Za-z0-9_.:-]*)(?:[\t\n\f\r ]*=[\t\n\f\r ]*("[^"]*"|'[^']*'|[^"'=<>`\t\n\f\r ]+))?/g;
180
199
  // The id is read as HTML reads it: a numeric character reference (`&#115;ource-textarea` is `source-textarea`,
181
200
  // `&#x73;` the same) and the named references that spell a character an id can hold are decoded before the
182
201
  // chrome's ids are asked; every other name stays literal (no named reference spells a letter, a digit or a
@@ -186,7 +205,7 @@ function initMarkdownIt() {
186
205
  equals: '=', comma: ',', lpar: '(', rpar: ')', lsqb: '[', lbrack: '[', rsqb: ']', rbrack: ']', lcub: '{', lbrace: '{',
187
206
  rcub: '}', rbrace: '}', verbar: '|', vert: '|', VerticalLine: '|', bsol: '\\', Hat: '^', grave: '`', DiacriticalGrave: '`', Tab: '\t', NewLine: '\n' };
188
207
  const attributeText = value => String(value || '').replace(/^["']|["']$/g, '')
189
- .replace(/&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([A-Za-z][A-Za-z0-9]{0,31}));?/g, (whole, dec, hex, name) => {
208
+ .replace(/&(?:#(\d+)|#[xX]([0-9a-fA-F]+)|([A-Za-z][A-Za-z0-9]{0,31}));?/g, (whole, dec, hex, name) => {
190
209
  const code = dec ? Number(dec) : hex ? parseInt(hex, 16) : -1;
191
210
  if (code >= 0) return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '\ufffd';
192
211
  return Object.hasOwn(RAW_ID_NAMED, name) ? RAW_ID_NAMED[name] : whole;
@@ -195,12 +214,23 @@ function initMarkdownIt() {
195
214
  _rapierChromeOwnsId('');
196
215
  const cleanTag = tag => /^<[A-Za-z]/.test(tag) ? tag.replace(rawAttribute, (whole, name, value) => {
197
216
  const key = name.toLowerCase();
217
+ if (key === 'style' && /^<span(?=[\s>])/i.test(tag)) {
218
+ const typed = globalThis.RapierMarkdownSpec.typography;
219
+ const css = typed.style(typed.parseStyle(md.utils.unescapeAll(String(value || '').replace(/^["']|["']$/g, '').replace(/\\/g, '\\\\'))));
220
+ return css ? ' style="' + md.utils.escapeHtml(css) + '"' : '';
221
+ }
198
222
  return key.startsWith('data-') || key === 'for' || key === 'id' && _rapierChromeOwnsId(attributeText(value)) ? '' : whole;
199
223
  }) : tag;
200
224
  md.renderer.rules.html_inline = (tokens, idx) => cleanTag(tokens[idx].content);
201
- md.renderer.rules.html_block = (tokens, idx, _options, env) => _rapierRawHtmlFragment(tokens[idx].content,
202
- tag => env?.rapierDocumentSpace ? cleanTag(tag).replace(/^<([A-Za-z][A-Za-z0-9:-]*)(?=[\s/>])/, '<$1 data-md-html-block="' + idx + '"') : cleanTag(tag), md);
225
+ md.renderer.rules.html_block = (tokens, idx) => _rapierRawHtmlFragment(tokens[idx].content, cleanTag, md);
203
226
  }
227
+ // A highlight written in one of the picker's colours is that swatch, as `==🟢x==` is, and follows the theme.
228
+ const inlineHtml = md.renderer.rules.html_inline || ((tokens, idx) => tokens[idx].content);
229
+ md.renderer.rules.html_inline = (tokens, idx, ...rest) => {
230
+ const swatch = /^<mark style="background-color:(#[0-9a-f]{6});?">$/i.exec(tokens[idx].content);
231
+ const colour = swatch && Object.keys(RAPIER_PORTABLE_HIGHLIGHT_HEX).find(name => RAPIER_PORTABLE_HIGHLIGHT_HEX[name] === swatch[1].toLowerCase());
232
+ return colour ? '<mark data-rapier-highlight="' + colour + '">' : inlineHtml(tokens, idx, ...rest);
233
+ };
204
234
 
205
235
  const _ulOpenDefault = md.renderer.rules.bullet_list_open
206
236
  || function (tokens, idx, opts, _env, self) { return self.renderToken(tokens, idx, opts); };
@@ -293,8 +323,7 @@ function initMarkdownIt() {
293
323
  '</code> fence. Text below is still inside the block.</p>' + sourceHtml + '</figure>\n';
294
324
  }
295
325
  const provider = env.rapierProviders === false ? null : _rapierProviders.mermaid;
296
- const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true ||
297
- provider?.status === 'ready' && globalThis.RapierDiagrams?.parseDiagram(src).ok === true;
326
+ const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true;
298
327
  const ready = native || !!(provider && provider.status === 'ready' && typeof provider.renderToString === 'function');
299
328
  if (!ready && env.rapierProviders !== false && !_diagramPromptedOnce) {
300
329
  _diagramPromptedOnce = true;
@@ -331,9 +360,9 @@ function initMarkdownIt() {
331
360
 
332
361
  {
333
362
  let _mathPromptedOnce = false;
334
- const _renderMath = (src, displayMode, env) => {
363
+ const _renderMath = (src, displayMode, env, authored) => {
335
364
  const marker = displayMode ? '$$' : '$';
336
- const source = marker + src + marker, literal = md.utils.escapeHtml(source);
365
+ const source = authored || marker + src + marker, literal = md.utils.escapeHtml(source);
337
366
  if (env.rapierPage) return '<span data-rapier-math-source="">' + literal + '</span>';
338
367
  const mathProvider = env.rapierProviders === false ? null : _rapierProviders.math;
339
368
  if (!mathProvider || mathProvider.status !== 'ready' || typeof mathProvider.renderToString !== 'function') {
@@ -345,14 +374,36 @@ function initMarkdownIt() {
345
374
  + literal + '</span>';
346
375
  }
347
376
  try {
348
- const rendered = mathProvider.renderToString(src, { displayMode });
377
+ // The explicit output-only call keeps structured results beside the unchanged SVG
378
+ // islands. MathML never enters editable text projections or source serialization.
379
+ let rendered;
380
+ if (env.rapierMathEquations) {
381
+ const combined = mathProvider.renderWithMathML(src, {displayMode});
382
+ if (!combined || typeof combined.svg !== 'string' || !combined.semantics ||
383
+ typeof combined.semantics !== 'object' || Array.isArray(combined.semantics) || !Array.isArray(combined.errors))
384
+ throw new TypeError('The Rapier Math shared result is invalid.');
385
+ rendered = combined.svg;
386
+ env.rapierMathEquations.push({source, tex: src, displayMode,
387
+ semantics: combined.semantics, errors: combined.errors});
388
+ } else rendered = mathProvider.renderToString(src, {displayMode});
389
+ let encoded;
390
+ try { encoded = encodeURIComponent(source); }
391
+ catch (error) {
392
+ // An ill-formed UTF-16 source cannot enter the URI-encoded edit marker.
393
+ // Keep the ordinary source-only HTML and the collected refusal diagnostic.
394
+ if (!(error instanceof URIError)) throw error;
395
+ return '<span data-rapier-math-source="">' + literal + '</span>';
396
+ }
349
397
  return '<span class="math-rendered" data-math-src="'
350
- + encodeURIComponent(source) + '">' + rendered + '</span>';
398
+ + encoded + '">' + rendered + '</span>';
399
+ }
400
+ catch (error) {
401
+ if (env.rapierMathEquations) throw error;
402
+ return '<span data-rapier-math-source="">' + literal + '</span>';
351
403
  }
352
- catch (_) { return '<span data-rapier-math-source="">' + literal + '</span>'; }
353
404
  };
354
- md.renderer.rules.math_inline = (tokens, idx, _opts, env) => _renderMath(tokens[idx].content, false, env);
355
- md.renderer.rules.math_block = (tokens, idx, _opts, env) => wrapDisplayMath(_renderMath(tokens[idx].content, true, env)) + '\n';
405
+ md.renderer.rules.math_inline = (tokens, idx, _opts, env) => _renderMath(tokens[idx].content, false, env, tokens[idx].meta?.mathSource);
406
+ md.renderer.rules.math_block = (tokens, idx, _opts, env) => wrapDisplayMath(_renderMath(tokens[idx].content, true, env, tokens[idx].meta?.mathSource)) + '\n';
356
407
  }
357
408
  globalThis.RapierMarkdownLayout.installMarkdownLayout(md);
358
409
  globalThis.RapierImageAssets.configureParser(md, _rapierSplitOpeningFrontmatter);
@@ -378,9 +429,10 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
378
429
  const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
379
430
  // Source-semantic output is independent of installed rendering providers. Complete
380
431
  // artifacts keep the default provider path and materialize their visual content.
381
- const env = {...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
382
- env.rapierDocumentSpace = !!globalThis.RapierMarkdownSpec?.readDocumentSettings?.(canonical)?.space;
383
- if (env.rapierDocumentSpace) root.setAttribute('data-md-space', '');
432
+ // A portable projection keeps natural sizing. Live readers opt into the same transient
433
+ // AUTO policy as the editor; neither a saved page nor its source inherits that choice.
434
+ const env = {rapierImagePresentation: options.imagePresentation === true, ...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
435
+ if (options.mathEquations) env.rapierMathEquations = options.mathEquations;
384
436
  let rendered = md
385
437
  ? md.render(markdown, env)
386
438
  : '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
@@ -398,7 +450,7 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
398
450
  root.querySelectorAll('pre > code').forEach(_rapierNormalizeCodeElement);
399
451
  _rapierMarkTableCaptions(root);
400
452
  _rapierMarkFigureCaptions(root);
401
- _rapierMarkListContinuations(Array.from(root.children));
453
+ _rapierMarkListContinuations(_rapierChildElements(root));
402
454
  _rapierDecorateJumpLists(root);
403
455
  return root;
404
456
  }
@@ -413,20 +465,21 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
413
465
  }
414
466
  const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
415
467
  const fallbackEnv = options.page ? {rapierPage: true} : {};
416
- const parsed = options.parsed || (runtime.parseAsync ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv});
417
- const {tokens, env} = parsed, labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
418
- env.rapierDocumentSpace = !!globalThis.RapierMarkdownSpec?.readDocumentSettings?.(canonical)?.space;
468
+ let parsed = options.parsed || (runtime.parseAsync ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv});
469
+ let {tokens, env} = parsed, labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
470
+ env = {...env, rapierImagePresentation: options.imagePresentation === true};
471
+ await pause(work, .3);
419
472
  const root = document.createElement('div'), part = document.createElement('div'), boundary = htmlBoundary();
420
473
  root.setAttribute('data-rapier-semantic-root', 'true');
421
- if (env.rapierDocumentSpace) root.setAttribute('data-md-space', '');
422
- let html = '', began = performance.now();
474
+ let html = '', began = performance.now(), complete = false;
423
475
  const append = () => {
424
476
  if (md.renderer.rapierTransformHtml) html = md.renderer.rapierTransformHtml(html);
425
477
  if (labels?.length) html = _relabelFootnoteHtml(html, labels);
426
- part.innerHTML = sanitizeRapierHtml(html, options.page ? 'source' : 'export');
478
+ admitHtml(part, html, sanitizeRapierHtml, runtime.sanitizeRapierFragment, options.page ? 'source' : 'export');
427
479
  while (part.firstChild) root.appendChild(part.firstChild);
428
480
  html = '';
429
481
  };
482
+ try {
430
483
  for (let index = 0; index < tokens.length; index++) {
431
484
  const token = tokens[index], rule = md.renderer.rules[token.type];
432
485
  let rendered = token.type === 'inline' ? md.renderer.renderInline(token.children, md.options, env)
@@ -439,9 +492,15 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
439
492
  }
440
493
  }
441
494
  if (html) append();
495
+ // Token/environment objects are no longer inputs once their complete HTML
496
+ // is admitted. Drop our references before the document-wide DOM passes;
497
+ // never mutate an optional parsed result retained by another caller.
498
+ parsed = tokens = env = labels = null;
442
499
  await finishAsync(_rapierSemanticDecorations(root), work);
443
500
  await pause(work, .6);
501
+ complete = true;
444
502
  return root;
503
+ } finally { if (!complete) { root.replaceChildren(); part.replaceChildren(); } }
445
504
  }
446
505
 
447
506
  function* _rapierSemanticDecorations(root) {
@@ -451,7 +510,7 @@ function* _rapierSemanticDecorations(root) {
451
510
  for (const code of root.querySelectorAll('pre > code')) { _rapierNormalizeCodeElement(code); yield; }
452
511
  yield* _rapierMarkTableCaptionsSteps(root); yield;
453
512
  yield* _rapierMarkFigureCaptionsSteps(root); yield;
454
- yield* _rapierMarkListContinuationsSteps(Array.from(root.children)); yield;
513
+ yield* _rapierMarkListContinuationsSteps(_rapierChildElements(root)); yield;
455
514
  yield* _rapierDecorateJumpListsSteps(root); yield;
456
515
  }
457
516
 
@@ -630,16 +689,81 @@ function _rapierImageAltText(value, env = {}) {
630
689
  return md.renderer.renderInlineAsText(tokens[0]?.children || [], md.options, env);
631
690
  }
632
691
 
633
- function _rapierImageSizeAttributes(size, marker) {
692
+ // Intrinsic facts are a presentation input, never an authored width or a resource admission.
693
+ // Contain both edges within the column, sharing a 1.6-column sum-of-edges budget.
694
+ // Reciprocal portraits/landscapes have equal weight; the native-width cap never enlarges art.
695
+ function _rapierImageGeometryFacts(geometry, automatic) {
696
+ const w = Number(geometry?.width), h = Number(geometry?.height);
697
+ const known = Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 && Number.isFinite(w / h) && w / h > 0;
698
+ const width = known ? w : 0, height = known ? h : 0;
699
+ const styles = known ? [['aspect-ratio', width + '/' + height]] : automatic ? [['aspect-ratio', '1/1']] : [];
700
+ if (automatic) {
701
+ const ratio = known ? width / height : 1;
702
+ const share = Math.min(1, ratio, 1.6 / (1 + 1 / ratio));
703
+ styles.push(['--rapier-image-auto-width', Number((share * 100).toFixed(6)) + '%'],
704
+ ['--rapier-image-natural-width', known ? width + 'px' : '100%']);
705
+ }
706
+ return {width, height, styles};
707
+ }
708
+
709
+ // Native loads supply their current element's dimensions synchronously. Embedded images
710
+ // supply an admitted row only after their existing document/resource ticket checks.
711
+ function _rapierApplyImageGeometry(image, geometry) {
712
+ if (image?.tagName !== 'IMG' || image.hasAttribute('data-rapier-html')) return false;
713
+ const mode = image.getAttribute('data-rapier-image-geometry');
714
+ if (!['auto', 'sized', 'natural'].includes(mode)) return false;
715
+ const native = geometry === undefined;
716
+ if (native) {
717
+ if (!image.isConnected || !image.complete || image.hasAttribute('data-rapier-asset') || image.hasAttribute('data-rapier-image-url')) return false;
718
+ geometry = {width: image.naturalWidth, height: image.naturalHeight};
719
+ }
720
+ const facts = _rapierImageGeometryFacts(geometry, mode === 'auto');
721
+ if (!facts.width || !facts.height) return false;
722
+ const attributes = [
723
+ ['data-rapier-natural-width', String(facts.width)], ['data-rapier-natural-height', String(facts.height)],
724
+ ...(!image.hasAttribute('data-rapier-image-size') ? [['width', String(facts.width)]] : []), ['height', String(facts.height)],
725
+ ].filter(([name, value]) => image.getAttribute(name) !== value);
726
+ const styles = facts.styles.filter(([name, value]) => (name === 'aspect-ratio'
727
+ ? image.style.getPropertyValue(name).replace(/\s/g, '') : image.style.getPropertyValue(name)) !== value);
728
+ // Ordinary mutations revisit mounted images. Settled facts need no source-URL
729
+ // read or parse (which can be the entire inline image), nor any DOM writes.
730
+ if (!attributes.length && !styles.length) return false;
731
+ if (native) {
732
+ const src = image.getAttribute('src'), current = image.currentSrc;
733
+ if (!src || !current) return false;
734
+ try { if (src !== current && new URL(src, image.ownerDocument.baseURI).href !== current) return false; }
735
+ catch (_) { return false; }
736
+ }
737
+ for (const [name, value] of attributes) image.setAttribute(name, value);
738
+ for (const [name, value] of styles) image.style.setProperty(name, value);
739
+ return true;
740
+ }
741
+
742
+ function _rapierImageSizeAttributes(size, marker, geometry, automatic = true) {
634
743
  const layout = globalThis.RapierMarkdownLayout.parseLayout(marker || '');
635
744
  // Plain (rung 0, docs/standard-adoption.md): every picture renders at its natural width, the
636
745
  // same as a reader with no layout facts at all -- Obsidian's `![alt|N]` alt-text width (the
637
746
  // `size` fallback below) is a different, established convention and is unaffected.
638
- const style = layout && !_rapierPlainLayout() ? globalThis.RapierMarkdownLayout.imageStyle(layout) : '';
639
- if (layout?.width != null && style) return ' data-md-image-width="' + layout.width + '" style="' + style + '"';
747
+ let style = layout && !_rapierPlainLayout() ? globalThis.RapierMarkdownLayout.imageStyle(layout) : '';
748
+ let attributes = '';
749
+ if (layout?.width != null && style) attributes = ' data-md-image-width="' + layout.width + '"';
640
750
  // A fade rides beside any width, the alt-text's own included.
641
- return size ? ' width="' + size.width + '" data-rapier-image-size="' + size.width +
642
- '" style="--md-image-width:' + size.width + 'px' + (style ? ';' + style : '') + '"' : style ? ' style="' + style + '"' : '';
751
+ else if (size) {
752
+ attributes = ' width="' + size.width + '" data-rapier-image-size="' + size.width + '"';
753
+ style = '--md-image-width:' + size.width + 'px' + (style ? ';' + style : '');
754
+ }
755
+ if (geometry !== undefined) {
756
+ const mode = size || layout?.width != null || layout?.lines != null ? 'sized' : automatic ? 'auto' : 'natural';
757
+ const facts = _rapierImageGeometryFacts(geometry, mode === 'auto');
758
+ attributes += ' data-rapier-image-geometry="' + mode + '"';
759
+ if (facts.width) {
760
+ attributes += ' data-rapier-natural-width="' + facts.width + '" data-rapier-natural-height="' + facts.height + '"';
761
+ if (!size || !attributes.includes(' width=')) attributes += ' width="' + facts.width + '"';
762
+ attributes += ' height="' + facts.height + '"';
763
+ }
764
+ style = [...facts.styles.map(([name, value]) => name + ':' + value), ...(style ? [style] : [])].join(';');
765
+ }
766
+ return attributes + (style ? ' style="' + style + '"' : '');
643
767
  }
644
768
 
645
769
  function _rapierRawHtmlFragment(source, rewrite, parser) {
@@ -660,13 +784,20 @@ function _rapierApplyBlockDirection(root) { return finish(_rapierApplyBlockDirec
660
784
 
661
785
  function* _rapierApplyBlockDirectionSteps(root) {
662
786
  if (!root || typeof root.querySelectorAll !== 'function') return;
663
- for (const el of root.querySelectorAll(_RAPIER_DIR_BLOCKS)) {
787
+ const blocks = new Set(_RAPIER_DIR_BLOCKS.split(',')), elements = root.ownerDocument.createTreeWalker(root, 1);
788
+ for (let el = elements.nextNode(); el; el = elements.nextNode()) {
664
789
  yield;
665
- if (el.closest('pre, code')) continue;
790
+ if (!blocks.has(el.localName)) continue;
791
+ let literal = false;
792
+ for (let parent = el.parentElement; parent; parent = parent.parentElement)
793
+ if (parent.localName === 'pre' || parent.localName === 'code') { literal = true; break; }
794
+ if (literal) continue;
666
795
  let dir = 'ltr', found = false;
667
796
  const walker = el.ownerDocument.createTreeWalker(el, 4);
797
+ let scanned = 0;
668
798
  for (let text = walker.nextNode(); text && !found; text = walker.nextNode()) {
669
799
  for (const character of text.nodeValue || '') {
800
+ if (++scanned % 1024 === 0) yield;
670
801
  const strong = _rapierBidiStrong(character.codePointAt(0));
671
802
  if (strong) { dir = strong === 'R' ? 'rtl' : 'ltr'; found = true; break; }
672
803
  }
@@ -826,7 +957,9 @@ function* _rapierDecorateJumpListsSteps(root) {
826
957
  }
827
958
  }
828
959
 
829
- const only = root.children && root.children.length === 1 ? root.firstElementChild : null;
960
+ // Do not retain a live collection on the root that Copy will consume. Some
961
+ // DOM hosts eagerly refresh it on every later removal of a complete unit.
962
+ const first = root.firstElementChild, only = first && !first.nextElementSibling ? first : null;
830
963
  if (only && only.tagName === 'P' && /^jump\s+to:?$/i.test(String(only.textContent || '').trim())) {
831
964
  only.classList.add('rapier-jump-label-candidate');
832
965
  }
@@ -849,20 +982,23 @@ function* _rapierMarkFigureCaptionsSteps(root) {
849
982
 
850
983
  function _rapierMarkListContinuations(blocks) { return finish(_rapierMarkListContinuationsSteps(blocks)); }
851
984
 
985
+ function* _rapierChildElements(root) {
986
+ for (let child = root.firstElementChild; child; child = child.nextElementSibling) yield child;
987
+ }
988
+
852
989
  function* _rapierMarkListContinuationsSteps(blocks) {
853
990
  const isHeading = element => !!element && /^H[1-6]$/.test(element.tagName);
854
- for (const [index, element] of blocks.entries()) { yield;
855
- if (!element || element.tagName !== 'OL') continue;
991
+ let previous = null, headings = [], last = 0;
992
+ for (const element of blocks) { yield;
993
+ if (previous && isHeading(element)) {headings.push(element); continue;}
994
+ if (!element || element.tagName !== 'OL') {previous = null; headings = []; continue;}
856
995
  const start = parseInt(element.getAttribute('start'), 10);
857
- const last = (Number.isFinite(start) ? start : 1) + element.querySelectorAll(':scope > li').length - 1;
858
- const headings = [];
859
- let next = index + 1;
860
- while (isHeading(blocks[next])) headings.push(blocks[next++]);
861
- const following = blocks[next];
862
- if (!headings.length || !following || following.tagName !== 'OL') continue;
863
- if (parseInt(following.getAttribute('start'), 10) !== last + 1) continue;
864
- element.setAttribute('data-rapier-list-continues', '');
865
- for (const heading of headings) { yield;heading.setAttribute('data-rapier-list-through', '');}
996
+ if (previous && headings.length && start === last + 1) {
997
+ previous.setAttribute('data-rapier-list-continues', '');
998
+ for (const heading of headings) { yield;heading.setAttribute('data-rapier-list-through', '');}
999
+ }
1000
+ previous = element; headings = [];
1001
+ last = (Number.isFinite(start) ? start : 1) + element.querySelectorAll(':scope > li').length - 1;
866
1002
  }
867
1003
  }
868
1004
 
@@ -1031,6 +1167,11 @@ function _rapierProjectPortableRoot(semanticRoot, options) { return finish(_rapi
1031
1167
  async function _rapierProjectPortableRootAsync(semanticRoot, options = {}) { return finishAsync(_rapierProjectPortableRootSteps(semanticRoot, options), options.work); }
1032
1168
 
1033
1169
  function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1170
+ const root = yield* _rapierProjectPortableNodesSteps(semanticRoot, options);
1171
+ return yield* cleanTree(root, sanitizeRapierHtml, 'export', runtime.sanitizeRapierFragment);
1172
+ }
1173
+
1174
+ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
1034
1175
  const opts = options || {};
1035
1176
  // This projection is HTML-shaped (width baked into a CSS `style`, `data-*` stripped) for the
1036
1177
  // consumers that read HTML -- publishing/fragment export, TXT. DOCX is not an HTML consumer: its
@@ -1040,7 +1181,9 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1040
1181
  // the semantic root carries them. Ordinary paragraph/list CSS measurements also reach their
1041
1182
  // native reader; no derived private CSS is needed.
1042
1183
  const keepLayoutData = !!opts.keepLayoutData;
1043
- const root = yield* cloneTree(semanticRoot), nativeElements = new Set();
1184
+ // Copy owns its freshly rendered, detached root. Other callers retain their
1185
+ // semantic tree and receive the usual independent portable projection.
1186
+ const root = opts.inPlace ? semanticRoot : yield* cloneTree(semanticRoot), nativeElements = new Set();
1044
1187
  // A source soft break is a reading space, never the editor's invisible token.
1045
1188
  yield* _rapierResolveSoftBreakTokensSteps(root);
1046
1189
  // Word consumes these source-bearing islands as pictures with TeX alternatives. Keep
@@ -1148,15 +1291,16 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1148
1291
  const imageLayout = element.tagName === 'IMG' ? globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-rapier-image-layout')) : null;
1149
1292
  const measurements = keepLayoutData && /^(?:P|H[1-6]|LI|OL|UL)$/.test(element.tagName)
1150
1293
  ? ['text-indent', 'margin-inline-start'].map(property => [property, element.style.getPropertyValue(property)]) : [];
1294
+ // The native table reader admits automatic hyphenation from ordinary paragraph CSS.
1295
+ if (keepLayoutData && element.tagName === 'P' && element.closest('table') && element.style.getPropertyValue('hyphens').trim() === 'auto')
1296
+ measurements.push(['hyphens', 'auto']);
1151
1297
  if (keepLayoutData && element.closest('pre')) for (const property of ['color', 'font-weight', 'font-style'])
1152
1298
  measurements.push([property, element.style.getPropertyValue(property)]);
1153
1299
  if (keepLayoutData && element.tagName === 'BLOCKQUOTE')
1154
1300
  measurements.push(['border-inline-start-color', element.style.borderInlineStartColor]);
1155
1301
  const runTypography = keepLayoutData ? ['font-family', 'font-variant-caps'].map(property => [property, element.style.getPropertyValue(property)]) : [];
1156
1302
 
1157
- const colorHex = element.tagName === 'SPAN' ? element.getAttribute('data-md-color') : null;
1158
- const fontFamily = element.tagName === 'SPAN' ? element.getAttribute('data-md-font') : null;
1159
- const fontCss = fontFamily ? globalThis.RapierMarkdownSpec.fontFamilyCss(fontFamily) : '';
1303
+ const typed = globalThis.RapierMarkdownSpec.typography, typography = typed.nodeStyle(element);
1160
1304
 
1161
1305
  const highlightHex = element.tagName === 'MARK'
1162
1306
  ? (RAPIER_PORTABLE_HIGHLIGHT_HEX[String(element.getAttribute('data-rapier-highlight') || '').toLowerCase()] || RAPIER_PORTABLE_HIGHLIGHT_HEX.yellow)
@@ -1167,9 +1311,14 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1167
1311
  const isPageBreak = element.hasAttribute('data-md-break');
1168
1312
  for (const attribute of Array.from(element.attributes)) { yield;
1169
1313
  const name = attribute.name.toLowerCase();
1314
+ // Copy reads these inert words from the final admitted nodes, then removes
1315
+ // them before emitting HTML. They grant no rendering or resource authority.
1316
+ if (opts.heldPictures && /^(?:SPAN|IMG)$/.test(element.tagName) &&
1317
+ (name === 'data-rapier-remote-src' || name === 'data-rapier-remote-alt')) continue;
1170
1318
  if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-layout' || name === 'data-md-align' || name === 'data-rapier-ink'
1171
1319
  || name === 'data-md-first-line-end' || name === 'data-md-initial-font' || name === 'data-md-initial-size'
1172
- || name === 'data-md-picture-cap-height' || name === 'data-md-picture-cap-top')) continue;
1320
+ || name === 'data-md-picture-cap-height' || name === 'data-md-picture-cap-top' || name === 'data-rapier-highlight' || name === 'data-rapier-animation'
1321
+ || name === 'data-rapier-list-ordinal' || name === 'data-rapier-list-delimiter' || name === 'data-md-layout-tight')) continue;
1173
1322
  if (name === 'class' || name === 'style' || name.startsWith('data-')
1174
1323
  || name === 'contenteditable' || name === 'spellcheck'
1175
1324
  || name === 'tabindex' || name === 'disabled' || name === 'aria-disabled') {
@@ -1192,10 +1341,12 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1192
1341
  if (imageLayout.width != null) element.removeAttribute('width');
1193
1342
  if (pictureStyle) element.style.cssText = pictureStyle;
1194
1343
  }
1195
- if (colorHex && /^#[0-9a-f]{6}$/i.test(colorHex)) element.style.color = colorHex.toLowerCase();
1196
- if (fontCss) element.style.fontFamily = fontCss;
1197
1344
  if (highlightHex) element.style.backgroundColor = highlightHex;
1198
1345
  if (isTableCaption) { element.style.fontStyle = 'italic'; element.style.color = '#555555'; }
1346
+ // Already validated facts serialize directly. An inert host's partial CSSOM must
1347
+ // not silently discard a quoted family or round an authored decimal.
1348
+ const css = typed.style(typography);
1349
+ if (css) element.setAttribute('style', (element.getAttribute('style') ? element.getAttribute('style') + ';' : '') + css);
1199
1350
 
1200
1351
  if (isPageBreak) {
1201
1352
  const p = document.createElement('p');
@@ -1210,7 +1361,7 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1210
1361
  literal.style.setProperty('hanging-punctuation', 'none');
1211
1362
  }
1212
1363
 
1213
- return yield* cleanTree(root, sanitizeRapierHtml);
1364
+ return root;
1214
1365
  }
1215
1366
 
1216
1367
  function _rapierPortableHtml(root) {
@@ -1218,6 +1369,69 @@ function _rapierPortableHtml(root) {
1218
1369
  }
1219
1370
  async function _rapierPortableHtmlAsync(root, work) { return finishAsync(serializeTree(root, sanitizeRapierHtml, true), work); }
1220
1371
 
1372
+ // One captured document owns this pipeline and its detached nodes. Global semantic
1373
+ // facts are resolved before projection. Final admission consumes the old tree a
1374
+ // complete fragment at a time; HTML and words then share that admitted tree.
1375
+ async function _rapierClipboardPayloadAsync(canonical, metadata, options = {}) {
1376
+ const work = options.work;
1377
+ let semantic;
1378
+ const part = document.createElement('div');
1379
+ try {
1380
+ semantic = await _rapierRenderSemanticRootAsync(canonical, metadata, {work});
1381
+ if (options.formatted && options.materialize && await options.materialize(semantic) === false) return null;
1382
+ check(work);
1383
+ await finishAsync(_rapierProjectPortableNodesSteps(semantic,
1384
+ {baseName: options.baseName, inPlace: true, heldPictures: true}), work);
1385
+ const words = metadata.docKind === 'markdown' ? _rapierPortableTextWriter({heldPictures: true}) : null;
1386
+ const parts = [];
1387
+ let pictures = false;
1388
+ const plain = await finishAsync((function* () {
1389
+ for (const html of htmlParts(semantic, 16384, true)) {
1390
+ if (html === null) { yield; continue; }
1391
+ // Reparse the complete transformed unit at the existing final export
1392
+ // boundary. Both formats consume these admitted nodes before release.
1393
+ admitHtml(part, html, sanitizeRapierHtml, runtime.sanitizeRapierFragment);
1394
+ yield;
1395
+ if (words) yield* words.write(part);
1396
+ if (options.formatted) {
1397
+ for (const node of part.querySelectorAll('[data-rapier-remote-src],[data-rapier-remote-alt]')) {
1398
+ node.removeAttribute('data-rapier-remote-src'); node.removeAttribute('data-rapier-remote-alt'); yield;
1399
+ }
1400
+ pictures ||= !!part.querySelector('img[src^="data:"]');
1401
+ parts.push(yield* serializeTree(part));
1402
+ }
1403
+ part.replaceChildren(); yield;
1404
+ }
1405
+ return words ? yield* words.finish() : canonical;
1406
+ })(), work);
1407
+ check(work);
1408
+ if (!options.formatted) return {plain};
1409
+ return {plain, html: parts.join('').trim(), withoutPictures: pictures ? async () => {
1410
+ // Only this writer's finished HTML reaches this closure. On refusal,
1411
+ // reconstruct one complete fragment, change pictures to text and admit
1412
+ // that changed output. No full fallback DOM survives the operation.
1413
+ return finishAsync((function* () {
1414
+ const output = [], part = document.createElement('div');
1415
+ try {
1416
+ for (const html of parts) {
1417
+ if (!html.includes('<img')) { output.push(html); yield; continue; }
1418
+ part.innerHTML = html;
1419
+ let changed = false;
1420
+ for (const image of part.querySelectorAll('img[src^="data:"]')) {
1421
+ const replacement = document.createElement('span');
1422
+ replacement.textContent = '[image: ' + (String(image.getAttribute('alt') || 'image').trim() || 'image') + ']';
1423
+ image.replaceWith(replacement); changed = true; yield;
1424
+ }
1425
+ output.push(changed ? yield* serializeTree(part, sanitizeRapierHtml) : html);
1426
+ part.replaceChildren(); yield;
1427
+ }
1428
+ return output.join('').trim();
1429
+ } finally { part.replaceChildren(); }
1430
+ })(), work);
1431
+ } : null};
1432
+ } finally { semantic?.replaceChildren(); part.replaceChildren(); }
1433
+ }
1434
+
1221
1435
  function _rapierNormalizeInlineText(value) {
1222
1436
  return String(value || '')
1223
1437
  .replace(/\u00a0/g, ' ')
@@ -1245,79 +1459,102 @@ function* _rapierHeldPicturesAsWordsSteps(root) {
1245
1459
  return copy;
1246
1460
  }
1247
1461
 
1248
- function _rapierPortableInlineText(node, options) {
1462
+ function _rapierPortableInlineText(node, options) { return finish(_rapierPortableInlineTextSteps(node, options)); }
1463
+
1464
+ function* _rapierPortableInlineTextSteps(node, options) {
1249
1465
  const opts = options || {};
1250
1466
  if (!node) return '';
1467
+ yield;
1251
1468
  if (node.nodeType === Node.TEXT_NODE) return node.nodeValue || '';
1252
1469
  if (node.nodeType !== Node.ELEMENT_NODE) return '';
1253
1470
  const tag = node.tagName;
1471
+ if (opts.heldPictures && /^(?:SPAN|IMG)$/.test(tag) && node.hasAttribute('data-rapier-remote-src'))
1472
+ return _rapierPictureWords(String(node.getAttribute('data-rapier-remote-alt') || '').trim(),
1473
+ String(node.getAttribute('data-rapier-remote-src') || '').trim());
1254
1474
  if (tag === 'BR') return '\n';
1255
1475
  // A task's box is what the person sees of it.
1256
1476
  if (tag === 'INPUT') return /^checkbox$/i.test(node.type || '') ? (node.checked || node.hasAttribute('checked') ? '☑ ' : '☐ ') : '';
1257
1477
  if (tag === 'IMG') {
1258
1478
  if (opts.visible) return '';
1259
- const alt = String(node.getAttribute('alt') || '').trim();
1260
- const src = String(node.getAttribute('src') || '').trim();
1261
- return _rapierPictureWords(alt, src);
1479
+ return _rapierPictureWords(String(node.getAttribute('alt') || '').trim(), String(node.getAttribute('src') || '').trim());
1480
+ }
1481
+ if (tag === 'CODE' && !opts.inPre) {
1482
+ const source = String(node.textContent || '');
1483
+ return opts.visible ? source : source ? '`' + source + '`' : '';
1262
1484
  }
1485
+ const parts = [];
1486
+ for (let child = node.firstChild; child; child = child.nextSibling) parts.push(yield* _rapierPortableInlineTextSteps(child, opts));
1487
+ const text = parts.join('');
1263
1488
  if (tag === 'A') {
1264
- const label = _rapierNormalizeInlineText(Array.from(node.childNodes)
1265
- .map(child => _rapierPortableInlineText(child, opts)).join(''));
1266
- const href = String(node.getAttribute('href') || '').trim();
1489
+ const label = _rapierNormalizeInlineText(text), href = String(node.getAttribute('href') || '').trim();
1267
1490
  if (opts.visible || !href || href.charAt(0) === '#') return label;
1268
1491
  if (!label || label === href) return href;
1269
1492
  return label + ' (' + href + ')';
1270
1493
  }
1271
- if (tag === 'CODE' && !opts.inPre) {
1272
- const source = String(node.textContent || '');
1273
- return opts.visible ? source : source ? '`' + source + '`' : '';
1274
- }
1275
- return Array.from(node.childNodes)
1276
- .map(child => _rapierPortableInlineText(child, opts)).join('');
1494
+ return text;
1277
1495
  }
1278
1496
 
1279
- function _rapierPortableListText(list, depth, options) {
1280
- const opts = options || {};
1281
- const level = Math.max(0, depth | 0);
1282
- const ordered = list.tagName === 'OL';
1497
+ function _rapierPortableListText(list, depth, options) { return finish(_rapierPortableListTextSteps(list, depth, options)); }
1498
+
1499
+ function* _rapierPortableListTextSteps(list, depth, options) {
1500
+ const opts = options || {}, level = Math.max(0, depth | 0), ordered = list.tagName === 'OL';
1283
1501
  let counter = ordered ? (parseInt(list.getAttribute('start'), 10) || 1) : 0;
1284
1502
  const lines = [];
1285
- Array.from(list.children).filter(child => child.tagName === 'LI').forEach(item => {
1286
- const nested = Array.from(item.children).filter(child => child.tagName === 'UL' || child.tagName === 'OL');
1287
- const content = Array.from(item.childNodes)
1288
- .filter(child => !(child.nodeType === Node.ELEMENT_NODE && (child.tagName === 'UL' || child.tagName === 'OL')))
1289
- .map(child => _rapierPortableInlineText(child, opts)).join('');
1290
- const normalized = _rapierNormalizeInlineText(content);
1291
- const indent = ' '.repeat(level);
1503
+ for (let item = list.firstElementChild; item; item = item.nextElementSibling) {
1504
+ yield;
1505
+ if (item.tagName !== 'LI') continue;
1506
+ const nested = [], content = [];
1507
+ for (let child = item.firstChild; child; child = child.nextSibling) {
1508
+ if (child.nodeType === Node.ELEMENT_NODE && (child.tagName === 'UL' || child.tagName === 'OL')) nested.push(child);
1509
+ else content.push(yield* _rapierPortableInlineTextSteps(child, opts));
1510
+ }
1511
+ const normalized = _rapierNormalizeInlineText(content.join('')), indent = ' '.repeat(level);
1292
1512
  // A task shows its box where a bullet would be.
1293
1513
  const marker = /^[☐☑☒]/.test(normalized) ? '' : ordered ? (counter++) + '. ' : '• ';
1294
- const parts = (normalized || '').split('\n');
1514
+ const parts = normalized.split('\n');
1295
1515
  lines.push(indent + marker + (parts.shift() || ''));
1296
- parts.forEach(part => lines.push(indent + ' ' + part));
1297
- nested.forEach(child => {
1298
- const nestedText = _rapierPortableListText(child, level + 1, opts);
1299
- if (nestedText) lines.push(nestedText);
1300
- });
1301
- });
1516
+ for (const part of parts) { lines.push(indent + ' ' + part); yield; }
1517
+ for (const child of nested) {
1518
+ const text = yield* _rapierPortableListTextSteps(child, level + 1, opts);
1519
+ if (text) lines.push(text);
1520
+ }
1521
+ }
1302
1522
  return lines.join('\n');
1303
1523
  }
1304
1524
 
1305
1525
  // A table, or a part of one a selection clipped out (a head, a body, a row): its rows.
1306
- function _rapierPortableTableText(table, options) {
1307
- const opts = options || {};
1308
- const rows = table.tagName === 'TR' ? [table] : Array.from(table.querySelectorAll('tr'));
1309
- return rows.map(row => {
1310
- return Array.from(row.children)
1311
- .filter(cell => cell.tagName === 'TH' || cell.tagName === 'TD')
1312
- .map(cell => _rapierNormalizeInlineText(_rapierPortableInlineText(cell, opts)))
1313
- .join(opts.visible ? '\t' : ' | ');
1314
- }).filter(Boolean).join('\n');
1526
+ function _rapierPortableTableText(table, options) { return finish(_rapierPortableTableTextSteps(table, options)); }
1527
+
1528
+ function* _rapierPortableTableTextSteps(table, options) {
1529
+ const opts = options || {}, rows = [];
1530
+ const walker = document.createTreeWalker(table, 1 /* SHOW_ELEMENT */);
1531
+ let row = table.tagName === 'TR' ? table : walker.nextNode();
1532
+ while (row) {
1533
+ yield;
1534
+ if (row.tagName === 'TR') {
1535
+ const cells = [];
1536
+ for (let cell = row.firstElementChild; cell; cell = cell.nextElementSibling) {
1537
+ if (cell.tagName === 'TH' || cell.tagName === 'TD')
1538
+ cells.push(_rapierNormalizeInlineText(yield* _rapierPortableInlineTextSteps(cell, opts)));
1539
+ }
1540
+ const text = cells.join(opts.visible ? '\t' : ' | ');
1541
+ if (text) rows.push(text);
1542
+ }
1543
+ row = table.tagName === 'TR' ? null : walker.nextNode();
1544
+ }
1545
+ return rows.join('\n');
1315
1546
  }
1316
1547
 
1317
1548
  function _rapierPortablePlainText(root, options) { return finish(_rapierPortablePlainTextSteps(root, options)); }
1318
1549
  async function _rapierPortablePlainTextAsync(root, options = {}) { return finishAsync(_rapierPortablePlainTextSteps(root, options), options.work); }
1319
1550
 
1320
1551
  function* _rapierPortablePlainTextSteps(root, options) {
1552
+ const writer = _rapierPortableTextWriter(options);
1553
+ yield* writer.write(root);
1554
+ return yield* writer.finish();
1555
+ }
1556
+
1557
+ function _rapierPortableTextWriter(options) {
1321
1558
  const opts = options || {};
1322
1559
  const blocks = [];
1323
1560
  const blockTags = new Set([
@@ -1332,49 +1569,48 @@ function* _rapierPortablePlainTextSteps(root, options) {
1332
1569
  if (value) blocks.push({ text: value, preserve: !!preserve });
1333
1570
  };
1334
1571
 
1335
- const collectChildren = function* (parent) {
1336
- let inline = [];
1337
- const flush = () => {
1338
- if (!inline.length) return;
1339
- pushBlock(_rapierNormalizeInlineText(inline.map(node => _rapierPortableInlineText(node, opts)).join('')), false);
1340
- inline = [];
1341
- };
1572
+ const state = () => ({inline: [], table: null});
1573
+ const flush = current => {
1574
+ if (current.inline.length) {
1575
+ pushBlock(_rapierNormalizeInlineText(current.inline.join('')), false);
1576
+ current.inline = [];
1577
+ }
1578
+ if (current.table) {
1579
+ pushBlock(current.table.filter(Boolean).join('\n'), false);
1580
+ current.table = null;
1581
+ }
1582
+ };
1583
+ const collectChildren = function* (parent, current = state(), complete = true) {
1342
1584
  const tablePart = node => node.nodeType === Node.ELEMENT_NODE && /^(THEAD|TBODY|TFOOT|TR)$/.test(node.tagName);
1343
1585
  const blankText = node => node.nodeType === Node.TEXT_NODE && !String(node.nodeValue || '').trim();
1344
- const children = Array.from(parent.childNodes);
1345
- const inTableRun = index => { for (let at = index - 1; at >= 0; at--) { if (!blankText(children[at])) return tablePart(children[at]); } return false; };
1346
- for (const [index, child] of children.entries()) {
1586
+ for (let child = parent.firstChild; child; child = child.nextSibling) {
1347
1587
  yield;
1348
1588
  if (tablePart(child)) {
1349
- // The parts of a table a selection clipped out of it: one block of rows, whichever parts arrived.
1350
- if (inTableRun(index)) continue;
1351
- flush();
1352
- const parts = [];
1353
- for (let at = index; at < children.length && (tablePart(children[at]) || blankText(children[at])); at++) {
1354
- if (tablePart(children[at])) parts.push(_rapierPortableTableText(children[at], opts));
1355
- }
1356
- pushBlock(parts.filter(Boolean).join('\n'), false);
1357
- } else if (blankText(child) && inTableRun(index)) {
1589
+ // A clipped table can cross an output fragment. Retain its words,
1590
+ // not detached rows, until the next non-table node closes the block.
1591
+ if (!current.table) { flush(current); current.table = []; }
1592
+ current.table.push(yield* _rapierPortableTableTextSteps(child, opts));
1358
1593
  continue;
1359
- } else if (child.nodeType === Node.ELEMENT_NODE && blockTags.has(child.tagName)) {
1360
- flush();
1594
+ }
1595
+ if (current.table && blankText(child)) continue;
1596
+ if (current.table) flush(current);
1597
+ if (child.nodeType === Node.ELEMENT_NODE && blockTags.has(child.tagName)) {
1598
+ flush(current);
1361
1599
  yield* collectBlock(child);
1362
1600
  } else if (opts.visible && child.nodeType === Node.ELEMENT_NODE && child.tagName === 'CODE' && String(child.textContent || '').includes('\n')) {
1363
- // Lines of a code block taken without their block: the code as shown, every space kept.
1364
- flush();
1601
+ // Lines clipped from a code block retain every authored space.
1602
+ flush(current);
1365
1603
  pushBlock(String(child.textContent || ''), true);
1366
- } else {
1367
- inline.push(child);
1368
- }
1604
+ } else current.inline.push(yield* _rapierPortableInlineTextSteps(child, opts));
1369
1605
  }
1370
- flush();
1606
+ if (complete) flush(current);
1371
1607
  };
1372
1608
 
1373
1609
  const collectBlock = function* (node) {
1374
1610
  if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
1375
1611
  const tag = node.tagName;
1376
1612
  if (/^H[1-6]$/.test(tag) || tag === 'P') {
1377
- const text = _rapierNormalizeInlineText(_rapierPortableInlineText(node, opts));
1613
+ const text = _rapierNormalizeInlineText(yield* _rapierPortableInlineTextSteps(node, opts));
1378
1614
  // An empty paragraph (the line Enter makes) is a line of its own, as it is in Word's plain text: one newline more between its neighbours.
1379
1615
  if (tag === 'P' && !text && node.childNodes.length && !node.hasAttribute('data-md-break')) blocks.push({ text: '', blank: true });
1380
1616
  else pushBlock(text, false);
@@ -1391,11 +1627,11 @@ function* _rapierPortablePlainTextSteps(root, options) {
1391
1627
  return;
1392
1628
  }
1393
1629
  if (tag === 'UL' || tag === 'OL') {
1394
- pushBlock(_rapierPortableListText(node, 0, opts), false);
1630
+ pushBlock(yield* _rapierPortableListTextSteps(node, 0, opts), false);
1395
1631
  return;
1396
1632
  }
1397
1633
  if (tag === 'TABLE') {
1398
- pushBlock(_rapierPortableTableText(node, opts), false);
1634
+ pushBlock(yield* _rapierPortableTableTextSteps(node, opts), false);
1399
1635
  return;
1400
1636
  }
1401
1637
  if (tag === 'BLOCKQUOTE') {
@@ -1409,10 +1645,10 @@ function* _rapierPortablePlainTextSteps(root, options) {
1409
1645
  const children = Array.from(node.children);
1410
1646
  for (let i = 0; i < children.length; i++) {
1411
1647
  if (children[i].tagName !== 'DT') continue;
1412
- const term = _rapierNormalizeInlineText(_rapierPortableInlineText(children[i], opts));
1648
+ const term = _rapierNormalizeInlineText(yield* _rapierPortableInlineTextSteps(children[i], opts));
1413
1649
  const defs = [];
1414
1650
  while (children[i + 1] && children[i + 1].tagName === 'DD') {
1415
- defs.push(_rapierNormalizeInlineText(_rapierPortableInlineText(children[++i], opts)));
1651
+ defs.push(_rapierNormalizeInlineText(yield* _rapierPortableInlineTextSteps(children[++i], opts)));
1416
1652
  }
1417
1653
  pushBlock(term + (defs.length ? ' — ' + defs.join('\n ') : ''), false);
1418
1654
  }
@@ -1420,14 +1656,16 @@ function* _rapierPortablePlainTextSteps(root, options) {
1420
1656
  }
1421
1657
  if (tag === 'DETAILS') {
1422
1658
  const summary = node.querySelector(':scope > summary');
1423
- if (summary) pushBlock(_rapierNormalizeInlineText(_rapierPortableInlineText(summary, opts)), false);
1659
+ if (summary) pushBlock(_rapierNormalizeInlineText(yield* _rapierPortableInlineTextSteps(summary, opts)), false);
1424
1660
  for (const child of node.children) if (child !== summary) { yield; yield* collectBlock(child); }
1425
1661
  return;
1426
1662
  }
1427
1663
  yield* collectChildren(node);
1428
1664
  };
1429
1665
 
1430
- yield* collectChildren(root);
1666
+ const pending = state();
1667
+ return {write: root => collectChildren(root, pending, false), finish: function* () {
1668
+ flush(pending);
1431
1669
  // Each block stands a blank line from the next, and each empty paragraph between them adds one newline more.
1432
1670
  let joined = '', blanks = 0;
1433
1671
  for (const block of blocks) {
@@ -1438,6 +1676,7 @@ function* _rapierPortablePlainTextSteps(root, options) {
1438
1676
  }
1439
1677
  const text = joined.trim();
1440
1678
  return opts.finalNewline && text ? text + '\n' : text;
1679
+ }};
1441
1680
  }
1442
1681
 
1443
1682
  // The text file Export writes: a Markdown document's portable words and one final newline, any other document's own text.
@@ -1490,8 +1729,21 @@ function _rapierBidiStrong(cp) {
1490
1729
  return null;
1491
1730
  }
1492
1731
 
1493
- return {_rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
1732
+ // A consuming operation, not an editable DOM mode. Both outputs share one admitted
1733
+ // parse per equation; callers decide whether and how to expose the MathML to people.
1734
+ function renderWithMathML(source, metadata = {}) {
1735
+ const provider = _rapierProviders.math;
1736
+ if (!provider || provider.status !== 'ready' || typeof provider.renderToString !== 'function' ||
1737
+ typeof provider.renderWithMathML !== 'function')
1738
+ throw new Error('The Rapier Math shared-result renderer is unavailable.');
1739
+ if (!md) initMarkdownIt();
1740
+ const equations = [];
1741
+ const html = _rapierRenderSemanticRoot(source, metadata, {mathEquations: equations}).innerHTML;
1742
+ return {html, equations};
1743
+ }
1744
+
1745
+ return {renderWithMathML, _rapierClipboardPayloadAsync, _rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierApplyImageGeometry, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
1494
1746
  }
1495
1747
  export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
1496
1748
 
1497
- export {mountHtmlBlocks, htmlImageRecord, htmlLayoutEdit};
1749
+ export {mountHtmlBlocks, materializeObjects, objectPosterLabel, htmlImageRecord, htmlLayoutEdit, htmlModeEdit, mergeHtmlObjectBlocks};