rapier-markdown-kit 1.1.99 → 1.1.101

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.
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import {parseLayoutAttribute} from '../spec/md-layout.mjs';
3
+ import {imageOnly, lineMetrics, linesHeight} from '../layout/model.mjs';
4
+
5
+ export function followingPrintProse(paragraph) {
6
+ let next = paragraph?.nextElementSibling;
7
+ while (next && (next.classList?.contains('rapier-will-carrier') || next.classList?.contains('rapier-image-asset-record')))
8
+ next = next.nextElementSibling;
9
+ return next;
10
+ }
11
+
12
+ // The print compositor owns pagination. Only a normal-flow picture initial moves into
13
+ // its adjacent prose, so a native float can remain with the lines it introduces.
14
+ export function markPrintCapitals(root) {
15
+ for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
16
+ const layout = parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
17
+ if (!layout?.lines || layout.wrap !== 'around' || layout.x != null || layout.y != null || layout.rotate ||
18
+ layout.align && layout.align !== 'left') continue;
19
+ const paragraph = image.closest('p'), owner = followingPrintProse(paragraph);
20
+ if (!paragraph || !imageOnly(paragraph, image) || paragraph.closest('table,pre,figure') ||
21
+ owner?.tagName !== 'P' || !owner.textContent.trim() || owner.hasAttribute('data-md-lines')) continue;
22
+ image.setAttribute('data-rapier-print-lines', String(layout.lines));
23
+ }
24
+ }
25
+
26
+ // This factory is also embedded in the offline page, with the same line metrics owner.
27
+ export function createPrintCapitals(lineMetrics, linesHeight, followingProse) {
28
+ const selector = 'img[data-rapier-print-lines]', states = new WeakMap();
29
+ const restoreStyle = (node, value) => value == null ? node.removeAttribute('style') : node.setAttribute('style', value);
30
+ function clear(root) {
31
+ for (const image of root.querySelectorAll(selector)) {
32
+ const state = states.get(image);
33
+ if (!state) continue;
34
+ const {paragraph, parent, next, occurrence, imageStyle, occurrenceStyle, paragraphStyle} = state;
35
+ parent.insertBefore(occurrence, next?.parentNode === parent ? next : null);
36
+ restoreStyle(image, imageStyle);
37
+ if (occurrence !== image) restoreStyle(occurrence, occurrenceStyle);
38
+ restoreStyle(paragraph, paragraphStyle);
39
+ states.delete(image);
40
+ }
41
+ }
42
+ function apply(root) {
43
+ for (const image of root.querySelectorAll(selector)) {
44
+ const lines = Number(image.getAttribute('data-rapier-print-lines'));
45
+ let state = states.get(image);
46
+ const paragraph = state?.paragraph || image.closest('p');
47
+ const owner = state?.owner || followingProse(paragraph);
48
+ if (!Number.isInteger(lines) || lines < 1 || lines > 12 || !image.isConnected ||
49
+ !owner || owner.tagName !== 'P' || !owner.textContent.trim()) continue;
50
+ const metrics = lineMetrics(owner), height = linesHeight(lines, metrics);
51
+ const naturalWidth = image.naturalWidth || Number(image.getAttribute('width'));
52
+ const naturalHeight = image.naturalHeight || Number(image.getAttribute('height'));
53
+ if (!(height > 0 && naturalWidth > 0 && naturalHeight > 0)) continue;
54
+ const style = owner.ownerDocument.defaultView.getComputedStyle(owner);
55
+ const availableWidth = owner.getBoundingClientRect().width -
56
+ ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'].reduce((sum, name) => sum + (parseFloat(style[name]) || 0), 0);
57
+ if (!(availableWidth > 0)) continue;
58
+ // As imageBox does, cap the width first and retain the picture's natural ratio.
59
+ const width = Math.min(availableWidth, height * naturalWidth / naturalHeight);
60
+ const occurrence = state?.occurrence || (image.parentElement.tagName === 'A' ? image.parentElement : image);
61
+ if (!state) {
62
+ state = {paragraph, owner, occurrence, parent: occurrence.parentNode, next: occurrence.nextSibling,
63
+ imageStyle: image.getAttribute('style'), occurrenceStyle: occurrence.getAttribute('style'),
64
+ paragraphStyle: paragraph.getAttribute('style')};
65
+ states.set(image, state);
66
+ owner.prepend(occurrence);
67
+ paragraph.style.setProperty('display', 'none', 'important');
68
+ }
69
+ const gap = (parseFloat(style.fontSize) || 16) * .4;
70
+ for (const [name, value] of Object.entries({display: 'block', position: 'static', float: 'left',
71
+ width: width + 'px', height: width * naturalHeight / naturalWidth + 'px', 'max-width': '100%',
72
+ 'margin-top': metrics.capTop + 'px', 'margin-right': gap + 'px', 'margin-bottom': '0', 'margin-left': '0',
73
+ transform: 'none', 'object-fit': 'contain', 'break-inside': 'avoid'})) image.style.setProperty(name, value, 'important');
74
+ if (occurrence !== image) {
75
+ occurrence.style.setProperty('display', 'contents', 'important');
76
+ occurrence.style.setProperty('float', 'none', 'important');
77
+ }
78
+ }
79
+ }
80
+ return {apply, clear};
81
+ }
82
+
83
+ const capitals = createPrintCapitals(lineMetrics, linesHeight, followingPrintProse);
84
+ export const applyPrintCapitals = capitals.apply;
85
+ export const clearPrintCapitals = capitals.clear;
@@ -7,8 +7,8 @@ import {merge, transportInterval, transportTouchedInterval} from './ledger/merge
7
7
  export {merge, transportInterval, transportTouchedInterval};
8
8
  import {_rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord, _rapierRecordMetadata, _rapierValidMetadata, _rapierValidMetadataEffect, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata, _rapierHasHistoryEffect} from './ledger/journal-records.mjs';
9
9
  export {_rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord, _rapierRecordMetadata, _rapierValidMetadata, _rapierValidMetadataEffect, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata, _rapierHasHistoryEffect};
10
- import {replayHistory, sourceBefore, historyProjection, historyPlaces, selectiveUndo, groupHistoryActs} from './ledger/history.mjs';
11
- export {replayHistory, sourceBefore, historyProjection, historyPlaces, selectiveUndo, groupHistoryActs};
10
+ import {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices} from './ledger/history.mjs';
11
+ export {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices};
12
12
 
13
13
  import {transposeSource} from './ledger/transport.mjs';
14
14
  export {transposeSource};
@@ -5,3 +5,4 @@ export {createMarkdownRenderer} from './render-markdown.mjs';
5
5
  export {createRenderStyles} from './render-styles.mjs';
6
6
  export {createRenderSanitizer} from './render-sanitize.mjs';
7
7
  export {createPrintRenderer} from './render-print.mjs';
8
+ export {exportFontCss, requireDocumentFonts, fontRequests, waitForFonts, waitForFontRequests} from './export-fonts.mjs';
@@ -0,0 +1,132 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Published typography is measured once; an annotation keeps its words and paths in one inert, scalable group.
3
+ import {check, pause} from './render-work.mjs';
4
+ import {apply as applyTypography} from '../layout/typography.mjs';
5
+ import {documentLanguage} from '../spec/document-settings.mjs';
6
+ import {waitForFonts} from './export-fonts.mjs';
7
+
8
+ const MARK = 'span.rapier-ink-mark[data-rapier-ink]';
9
+ const TYPOGRAPHY = 'p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-hanging="on"]';
10
+ const SVG = 'http://www.w3.org/2000/svg';
11
+ const XHTML = 'http://www.w3.org/1999/xhtml';
12
+ const failure = message => Object.assign(new Error('The published page could not be prepared: ' + message), {code: 'EXPORT_INK_UNAVAILABLE'});
13
+
14
+ function groupsOf(root, spec) {
15
+ const nodes = [...root.childNodes], ranges = [], arrows = new Map();
16
+ const topIndex = element => {
17
+ while (element.parentElement && element.parentElement !== root) element = element.parentElement;
18
+ return nodes.indexOf(element);
19
+ };
20
+ for (const span of root.querySelectorAll(MARK)) {
21
+ const mark = spec.parseInkBody(span.getAttribute('data-rapier-ink'));
22
+ if (!mark) continue;
23
+ const index = topIndex(span);
24
+ if (index < 0) throw failure('an annotation has no containing block.');
25
+ ranges.push([index, index]);
26
+ if (mark.kind === 'arrow' || mark.kind === 'end') {
27
+ const pair = arrows.get(mark.id) || [];
28
+ pair.push(index); arrows.set(mark.id, pair);
29
+ }
30
+ }
31
+ for (const pair of arrows.values()) ranges.push([Math.min(...pair), Math.max(...pair)]);
32
+ ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
33
+ const groups = [];
34
+ for (const range of ranges) {
35
+ const prior = groups.at(-1);
36
+ if (prior && range[0] <= prior[1]) prior[1] = Math.max(prior[1], range[1]);
37
+ else groups.push(range);
38
+ }
39
+ return {nodes, groups};
40
+ }
41
+
42
+ export async function freezePublishedInk(root, {document: doc, css, fontCss = '', settings, language, theme, work, spec, ink, draw}) {
43
+ const annotated = !!root.querySelector(MARK), refined = !!root.querySelector(TYPOGRAPHY);
44
+ if (!annotated && !refined) return;
45
+ if (!doc?.body || !doc.defaultView || annotated && (!spec?.parseInkBody || !ink?.deriveMark || !draw?.drawInk))
46
+ throw failure('the browser publication renderer is unavailable.');
47
+ check(work);
48
+ const frame = doc.createElement('iframe'), width = Math.max(1, doc.documentElement.clientWidth || doc.defaultView.innerWidth);
49
+ frame.setAttribute('sandbox', 'allow-same-origin');
50
+ frame.setAttribute('aria-hidden', 'true'); frame.setAttribute('tabindex', '-1');
51
+ frame.setAttribute('referrerpolicy', 'no-referrer');
52
+ frame.style.cssText = 'position:fixed;left:-100000px;top:0;width:' + width + 'px;height:1px;border:0;visibility:hidden;pointer-events:none';
53
+ const policy = "default-src 'none'; script-src 'none'; connect-src 'none'; img-src data:; font-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'";
54
+ let timer, abort;
55
+ const stopped = new Promise((_, reject) => {
56
+ timer = setTimeout(() => reject(failure('its embedded resources did not finish loading.')), 20000);
57
+ abort = () => { try { check(work); } catch (error) { reject(error); } };
58
+ work?.signal?.addEventListener('abort', abort, {once: true});
59
+ });
60
+ const wait = promise => Promise.race([promise, stopped]);
61
+ try {
62
+ const loaded = new Promise(resolve => frame.addEventListener('load', resolve, {once: true}));
63
+ frame.srcdoc = '<!doctype html><html lang="' + documentLanguage(settings, language) + '" data-rapier-theme="' + theme + '"><head><meta charset="utf-8">'
64
+ + '<meta http-equiv="Content-Security-Policy" content="' + policy + '"><meta name="referrer" content="no-referrer">'
65
+ + '<style>' + css + '</style></head><body class="' + (theme === 'light' ? 'light' : '')
66
+ + '"><main class="rapier-page md-render" data-md-theme="' + theme + '"' + (settings?.space ? ' data-md-space' : '') + '>' + root.innerHTML + '</main></body></html>';
67
+ doc.body.appendChild(frame);
68
+ await wait(loaded);
69
+ const paper = frame.contentDocument, main = paper?.querySelector('main.rapier-page');
70
+ if (!main) throw failure('the browser could not open its isolated page.');
71
+ const captured = annotated ? groupsOf(main, spec) : {nodes: [], groups: []}, prepared = [];
72
+ for (const [first, last] of captured.groups) {
73
+ const group = paper.createElement('div');
74
+ group.setAttribute('data-rapier-published-ink', '');
75
+ group.style.cssText = 'display:flow-root;position:relative;width:100%;margin:0;padding:0;border:0;overflow:visible';
76
+ main.insertBefore(group, captured.nodes[first]);
77
+ for (let index = first; index <= last; index++) group.appendChild(captured.nodes[index]);
78
+ // A fixed group cannot grow after disclosure. Show each marked passage in full.
79
+ for (const span of group.querySelectorAll(MARK)) {
80
+ for (let parent = span.parentElement; parent && parent !== group; parent = parent.parentElement)
81
+ if (parent.localName === 'details') parent.open = true;
82
+ }
83
+ for (const summary of group.querySelectorAll('summary')) summary.setAttribute('inert', '');
84
+ prepared.push(group);
85
+ }
86
+ await wait(Promise.all([...main.querySelectorAll('img')].map(async image => {
87
+ image.loading = 'eager';
88
+ try { await image.decode(); }
89
+ catch (_) { throw failure('an embedded picture could not be decoded.'); }
90
+ })));
91
+ // The captured CSS supplies the faces; validate them before measuring typography or ink.
92
+ main.getBoundingClientRect();
93
+ await wait(waitForFonts(main, {settings, css, fontCss, install: false}));
94
+ if (paper.fonts?.ready) await wait(paper.fonts.ready);
95
+ await pause(work);
96
+ if (refined) applyTypography(main, {all: true});
97
+ const results = [];
98
+ for (const group of prepared) {
99
+ check(work);
100
+ draw.drawInk(group, {spec, ink, place: 'start'});
101
+ const box = group.getBoundingClientRect();
102
+ if (!(box.width > 0 && box.height > 0)) throw failure('an annotation has no measurable words.');
103
+ const svg = paper.createElementNS(SVG, 'svg'), content = paper.createElementNS(SVG, 'foreignObject');
104
+ svg.setAttribute('viewBox', '0 0 ' + box.width + ' ' + box.height);
105
+ svg.setAttribute('width', String(box.width)); svg.setAttribute('height', String(box.height));
106
+ svg.setAttribute('role', 'group'); svg.setAttribute('data-rapier-published-ink', '');
107
+ svg.style.cssText = 'display:block;width:100%;max-width:' + box.width + 'px;height:auto;margin:0;padding:0;overflow:visible';
108
+ content.setAttribute('width', String(box.width)); content.setAttribute('height', String(box.height));
109
+ content.style.overflow = 'visible';
110
+ group.setAttribute('xmlns', XHTML);
111
+ group.style.width = box.width + 'px';
112
+ group.style.maxWidth = 'none';
113
+ content.appendChild(group.cloneNode(true)); svg.appendChild(content);
114
+ results.push(svg);
115
+ await pause(work);
116
+ }
117
+ // Keep the measured typography outside annotated groups too. The caller's detached
118
+ // projection changes only after every resource and group has succeeded.
119
+ for (const [index, group] of prepared.entries()) group.replaceWith(results[index]);
120
+ const projected = doc.createDocumentFragment();
121
+ for (const node of main.childNodes) {
122
+ projected.appendChild(doc.importNode(node, true));
123
+ await pause(work);
124
+ }
125
+ check(work);
126
+ root.replaceChildren(projected);
127
+ } finally {
128
+ clearTimeout(timer);
129
+ work?.signal?.removeEventListener('abort', abort);
130
+ frame.remove();
131
+ }
132
+ }
@@ -1,3 +1,4 @@
1
+ import {htmlPlaceholder, mountHtmlBlocks, htmlImageRecord, htmlLayoutEdit} from '../spec/html-blocks.mjs';
1
2
  // SPDX-License-Identifier: MIT
2
3
  import {finish, finishAsync, cloneTree, cleanTree, serializeTree, htmlBoundary, pause, check} from './render-work.mjs';
3
4
  const RAPIER_TABLE_CAPTION_PREFIX_RE = /^(?:Table:|:) /;
@@ -56,6 +57,15 @@ function initMarkdownIt() {
56
57
  return '<span data-md-color="' + hex + '" style="--md-color:' + hex + ';--md-color-dark:' + dark + '">';
57
58
  };
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) + '">';
67
+ };
68
+ md.renderer.rules.rapier_font_close = () => '</span>';
59
69
  // Ink: the span carries its opener's body (the grammar's inkOpenBody: a sanitizer drops an attribute holding the
60
70
  // comment's close); the ink layer reads it back through the one grammar and draws the stroke over the words' own boxes.
61
71
  // The span itself shows nothing.
@@ -120,6 +130,8 @@ function initMarkdownIt() {
120
130
  if (!env.rapierPage && typeof globalThis.RapierEmbeddedImages?.imageHtml === 'function' &&
121
131
  (_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
122
132
  reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src);
133
+ const drawingAlt = typeof rawAlt === 'string' && alt === '' && asset?.codec === 'image/svg+xml' ? globalThis.RapierImageAssets.drawingImageAlt(src) : null;
134
+ const shownAlt = drawingAlt ?? alt;
123
135
  let loadable = false;
124
136
  try { loadable = !!src.trim() && /^(?:https?|file|blob|data):$/.test(new URL(src, globalThis.document?.baseURI || globalThis.location?.href || 'https://rapier.invalid/').protocol); } catch (_) {}
125
137
  if (!loadable) {
@@ -139,14 +151,14 @@ function initMarkdownIt() {
139
151
  const attrs = [
140
152
  'data-rapier-markdown-image=""',
141
153
  'src="' + md.utils.escapeHtml(src) + '"',
142
- 'alt="' + md.utils.escapeHtml(alt) + '"',
154
+ 'alt="' + md.utils.escapeHtml(shownAlt) + '"',
143
155
  'loading="lazy"',
144
156
  'decoding="async"',
145
157
  ];
146
158
  if (token.meta?.mdImage?.source && token.meta.mdImage.source.length <= 4096)
147
159
  attrs.push('data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '"');
148
160
  if (title) attrs.push('title="' + md.utils.escapeHtml(title) + '"');
149
- if (size && !token.meta?.mdLayout?.layout?.width) attrs.push('data-rapier-image-alt-source="' + md.utils.escapeHtml(rawAlt) + '"');
161
+ if (shownAlt !== alt || size && !token.meta?.mdLayout?.layout?.width) attrs.push('data-rapier-image-alt-source="' + md.utils.escapeHtml(rawAlt) + '"');
150
162
  if (layout) attrs.push('data-rapier-image-layout="' + md.utils.escapeHtml(encodeURIComponent(layout)) + '"');
151
163
  return '<img ' + attrs.join(' ') + _rapierImageSizeAttributes(size, layout) + '>';
152
164
  };
@@ -186,7 +198,8 @@ function initMarkdownIt() {
186
198
  return key.startsWith('data-') || key === 'for' || key === 'id' && _rapierChromeOwnsId(attributeText(value)) ? '' : whole;
187
199
  }) : tag;
188
200
  md.renderer.rules.html_inline = (tokens, idx) => cleanTag(tokens[idx].content);
189
- md.renderer.rules.html_block = (tokens, idx) => _rapierRawHtmlFragment(tokens[idx].content, cleanTag, md);
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);
190
203
  }
191
204
 
192
205
  const _ulOpenDefault = md.renderer.rules.bullet_list_open
@@ -263,7 +276,8 @@ function initMarkdownIt() {
263
276
  token.attrSet('data-rapier-fence', token.markup || '```');
264
277
  token.attrSet('data-rapier-fence-info', token.info == null ? '' : String(token.info));
265
278
  const lang = String(token.info || '').trim().split(/\s+/)[0].toLowerCase();
266
- if (lang !== 'mermaid' || env.rapierPage) return _fenceDefault(tokens, idx, opts, env, self);
279
+ if (lang === 'html') {const block = htmlPlaceholder(token, md.utils.escapeHtml); if (block) return block;}
280
+ if (lang !== 'mermaid') return _fenceDefault(tokens, idx, opts, env, self);
267
281
  const src = String(token.content || '').replace(/\n$/, '');
268
282
  const fence = md.utils.escapeHtml(token.markup || '```');
269
283
  const info = token.info == null ? '' : String(token.info);
@@ -278,10 +292,11 @@ function initMarkdownIt() {
278
292
  '<p class="diagram-reason">Mermaid block may be missing its closing <code>' + fence +
279
293
  '</code> fence. Text below is still inside the block.</p>' + sourceHtml + '</figure>\n';
280
294
  }
281
- const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true;
282
- const provider = _rapierProviders.mermaid;
295
+ 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;
283
298
  const ready = native || !!(provider && provider.status === 'ready' && typeof provider.renderToString === 'function');
284
- if (!ready && !_diagramPromptedOnce) {
299
+ if (!ready && env.rapierProviders !== false && !_diagramPromptedOnce) {
285
300
  _diagramPromptedOnce = true;
286
301
  try { _rapierUiDiagram.request(); } catch (_) {}
287
302
  }
@@ -316,13 +331,13 @@ function initMarkdownIt() {
316
331
 
317
332
  {
318
333
  let _mathPromptedOnce = false;
319
- const _renderMath = (src, displayMode, page) => {
334
+ const _renderMath = (src, displayMode, env) => {
320
335
  const marker = displayMode ? '$$' : '$';
321
336
  const source = marker + src + marker, literal = md.utils.escapeHtml(source);
322
- if (page) return '<span data-rapier-math-source="">' + literal + '</span>';
323
- const mathProvider = _rapierProviders.math;
337
+ if (env.rapierPage) return '<span data-rapier-math-source="">' + literal + '</span>';
338
+ const mathProvider = env.rapierProviders === false ? null : _rapierProviders.math;
324
339
  if (!mathProvider || mathProvider.status !== 'ready' || typeof mathProvider.renderToString !== 'function') {
325
- if (!_mathPromptedOnce) {
340
+ if (env.rapierProviders !== false && !_mathPromptedOnce) {
326
341
  _mathPromptedOnce = true;
327
342
  try { _rapierUiMath.request(); } catch (_) {}
328
343
  }
@@ -336,8 +351,8 @@ function initMarkdownIt() {
336
351
  }
337
352
  catch (_) { return '<span data-rapier-math-source="">' + literal + '</span>'; }
338
353
  };
339
- md.renderer.rules.math_inline = (tokens, idx, _opts, env) => _renderMath(tokens[idx].content, false, env.rapierPage);
340
- md.renderer.rules.math_block = (tokens, idx, _opts, env) => wrapDisplayMath(_renderMath(tokens[idx].content, true, env.rapierPage)) + '\n';
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';
341
356
  }
342
357
  globalThis.RapierMarkdownLayout.installMarkdownLayout(md);
343
358
  globalThis.RapierImageAssets.configureParser(md, _rapierSplitOpeningFrontmatter);
@@ -361,7 +376,11 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
361
376
  }
362
377
 
363
378
  const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
364
- const env = options.page ? {rapierPage: true} : {};
379
+ // Source-semantic output is independent of installed rendering providers. Complete
380
+ // 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', '');
365
384
  let rendered = md
366
385
  ? md.render(markdown, env)
367
386
  : '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
@@ -396,8 +415,10 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
396
415
  const fallbackEnv = options.page ? {rapierPage: true} : {};
397
416
  const parsed = options.parsed || (runtime.parseAsync ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv});
398
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;
399
419
  const root = document.createElement('div'), part = document.createElement('div'), boundary = htmlBoundary();
400
420
  root.setAttribute('data-rapier-semantic-root', 'true');
421
+ if (env.rapierDocumentSpace) root.setAttribute('data-md-space', '');
401
422
  let html = '', began = performance.now();
402
423
  const append = () => {
403
424
  if (md.renderer.rapierTransformHtml) html = md.renderer.rapierTransformHtml(html);
@@ -1075,6 +1096,10 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1075
1096
  math.replaceWith(code);
1076
1097
  }
1077
1098
  for (const math of root.querySelectorAll('math')) { yield;
1099
+ if (keepLayoutData) {
1100
+ for (const element of [math, ...math.querySelectorAll('*')]) nativeElements.add(element);
1101
+ continue;
1102
+ }
1078
1103
  const code = document.createElement('code');
1079
1104
  code.textContent = String(math.textContent || '').trim() || '[equation]';
1080
1105
  math.replaceWith(code);
@@ -1093,7 +1118,7 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1093
1118
 
1094
1119
  for (const item of root.querySelectorAll('.table-scroll-wrap,.math-display-wrap')) { yield; _rapierUnwrapElement(item); }
1095
1120
 
1096
- for (const item of root.querySelectorAll('pre > code')) { yield; _rapierNormalizeCodeElement(item); }
1121
+ if (!keepLayoutData) for (const item of root.querySelectorAll('pre > code')) { yield; _rapierNormalizeCodeElement(item); }
1097
1122
 
1098
1123
  for (const svg of root.querySelectorAll('svg')) { yield;
1099
1124
  if (nativeElements.has(svg)) continue;
@@ -1106,6 +1131,7 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1106
1131
  if (runtime.prefixAnchorsSteps) yield* runtime.prefixAnchorsSteps(root, opts.baseName);
1107
1132
  else _rapierPrefixPortableAnchors(root, opts.baseName);
1108
1133
 
1134
+ const refinedProse = [];
1109
1135
  for (const element of root.querySelectorAll('*')) { yield;
1110
1136
  if (nativeElements.has(element)) continue;
1111
1137
  const lang = element.tagName === 'CODE' ? _rapierLanguageClass(element) : '';
@@ -1113,14 +1139,24 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1113
1139
  // separator and return-link spacing before it compares source-owned review notes.
1114
1140
  const footnoteRole = keepLayoutData ? ['footnote-backref', 'footnotes-sep']
1115
1141
  .filter(name => element.classList.contains(name)).join(' ') : '';
1142
+ const captionRole = keepLayoutData ? ['rapier-table-caption', 'rapier-figure-caption']
1143
+ .filter(name => element.classList.contains(name)).join(' ') : '';
1116
1144
  const columnAlignment = /^(TH|TD)$/.test(element.tagName) && /^(left|center|right)$/.test(element.style.textAlign)
1117
1145
  ? element.style.textAlign : '';
1118
- const alignment = globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'))?.align || columnAlignment;
1146
+ const paragraphLayout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'));
1147
+ const alignment = paragraphLayout?.align || columnAlignment;
1119
1148
  const imageLayout = element.tagName === 'IMG' ? globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-rapier-image-layout')) : null;
1120
1149
  const measurements = keepLayoutData && /^(?:P|H[1-6]|LI|OL|UL)$/.test(element.tagName)
1121
1150
  ? ['text-indent', 'margin-inline-start'].map(property => [property, element.style.getPropertyValue(property)]) : [];
1151
+ if (keepLayoutData && element.closest('pre')) for (const property of ['color', 'font-weight', 'font-style'])
1152
+ measurements.push([property, element.style.getPropertyValue(property)]);
1153
+ if (keepLayoutData && element.tagName === 'BLOCKQUOTE')
1154
+ measurements.push(['border-inline-start-color', element.style.borderInlineStartColor]);
1155
+ const runTypography = keepLayoutData ? ['font-family', 'font-variant-caps'].map(property => [property, element.style.getPropertyValue(property)]) : [];
1122
1156
 
1123
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) : '';
1124
1160
 
1125
1161
  const highlightHex = element.tagName === 'MARK'
1126
1162
  ? (RAPIER_PORTABLE_HIGHLIGHT_HEX[String(element.getAttribute('data-rapier-highlight') || '').toLowerCase()] || RAPIER_PORTABLE_HIGHLIGHT_HEX.yellow)
@@ -1131,7 +1167,9 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1131
1167
  const isPageBreak = element.hasAttribute('data-md-break');
1132
1168
  for (const attribute of Array.from(element.attributes)) { yield;
1133
1169
  const name = attribute.name.toLowerCase();
1134
- if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-layout' || name === 'data-md-align' || name === 'data-rapier-ink')) continue;
1170
+ if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-layout' || name === 'data-md-align' || name === 'data-rapier-ink'
1171
+ || 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;
1135
1173
  if (name === 'class' || name === 'style' || name.startsWith('data-')
1136
1174
  || name === 'contenteditable' || name === 'spellcheck'
1137
1175
  || name === 'tabindex' || name === 'disabled' || name === 'aria-disabled') {
@@ -1139,15 +1177,23 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1139
1177
  }
1140
1178
  }
1141
1179
  if (footnoteRole) element.className = footnoteRole;
1180
+ if (captionRole) element.className = captionRole;
1142
1181
  if (lang) element.className = 'language-' + lang;
1143
1182
  for (const [property, value] of measurements) if (value) element.style.setProperty(property, value);
1183
+ for (const [property, value] of runTypography) if (value) element.style.setProperty(property, value);
1144
1184
  if (alignment) element.style.textAlign = alignment;
1185
+ if (!keepLayoutData && element.tagName === 'P' && !element.closest('table,pre')) {
1186
+ if (paragraphLayout?.hyphens === 'auto') { element.style.hyphens = 'auto'; element.style.textWrap = 'pretty'; }
1187
+ if (paragraphLayout?.hanging === 'on') element.style.setProperty('hanging-punctuation', 'first allow-end');
1188
+ if (paragraphLayout?.hyphens || paragraphLayout?.hanging || paragraphLayout?.caps || paragraphLayout?.lines) refinedProse.push(element);
1189
+ }
1145
1190
  if (imageLayout && !keepLayoutData) {
1146
1191
  const pictureStyle = globalThis.RapierMarkdownLayout.imageStyle(imageLayout);
1147
1192
  if (imageLayout.width != null) element.removeAttribute('width');
1148
1193
  if (pictureStyle) element.style.cssText = pictureStyle;
1149
1194
  }
1150
1195
  if (colorHex && /^#[0-9a-f]{6}$/i.test(colorHex)) element.style.color = colorHex.toLowerCase();
1196
+ if (fontCss) element.style.fontFamily = fontCss;
1151
1197
  if (highlightHex) element.style.backgroundColor = highlightHex;
1152
1198
  if (isTableCaption) { element.style.fontStyle = 'italic'; element.style.color = '#555555'; }
1153
1199
 
@@ -1157,6 +1203,12 @@ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
1157
1203
  element.replaceWith(p);
1158
1204
  }
1159
1205
  }
1206
+ for (const paragraph of refinedProse) for (const literal of paragraph.querySelectorAll('code,kbd,samp,math')) { yield;
1207
+ literal.style.setProperty('hyphens', 'none');
1208
+ literal.style.setProperty('text-justify', 'none');
1209
+ literal.style.setProperty('font-variant-caps', 'normal');
1210
+ literal.style.setProperty('hanging-punctuation', 'none');
1211
+ }
1160
1212
 
1161
1213
  return yield* cleanTree(root, sanitizeRapierHtml);
1162
1214
  }
@@ -1441,3 +1493,5 @@ function _rapierBidiStrong(cp) {
1441
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};
1442
1494
  }
1443
1495
  export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
1496
+
1497
+ export {mountHtmlBlocks, htmlImageRecord, htmlLayoutEdit};