rapier-markdown-kit 1.1.104 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +8 -4
  2. package/README.md +50 -7
  3. package/dist/images/gif.mjs +49 -0
  4. package/dist/images/playback.mjs +103 -0
  5. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  6. package/dist/kit/conformance/04-wrap-box.md +1 -1
  7. package/dist/kit/conformance/05-behind.md +1 -1
  8. package/dist/kit/conformance/06-front.md +1 -1
  9. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  10. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  11. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  12. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  13. package/dist/kit/conformance/10-both-sides.md +1 -1
  14. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  15. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  16. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  17. package/dist/kit/conformance/README.md +19 -28
  18. package/dist/kit/conformance/run.mjs +10 -8
  19. package/dist/kit/ledger/authored-origin.mjs +49 -0
  20. package/dist/kit/ledger/authorship.mjs +18 -9
  21. package/dist/kit/ledger/carried.mjs +10 -5
  22. package/dist/kit/ledger/data.mjs +7 -4
  23. package/dist/kit/ledger/format.mjs +37 -11
  24. package/dist/kit/ledger/hash.mjs +31 -15
  25. package/dist/kit/ledger/history.mjs +48 -10
  26. package/dist/kit/ledger/inverse-source.mjs +111 -49
  27. package/dist/kit/ledger/journal-records.mjs +24 -29
  28. package/dist/kit/ledger/merge.mjs +51 -15
  29. package/dist/kit/ledger/text-merge.mjs +75 -29
  30. package/dist/kit/ledger/transport.mjs +30 -45
  31. package/dist/kit/pdf-vectors.mjs +6 -2
  32. package/dist/kit/print-capitals.mjs +0 -1
  33. package/dist/kit/rapier-ledger.mjs +4 -2
  34. package/dist/kit/render-ink.mjs +1 -1
  35. package/dist/kit/render-markdown.mjs +155 -31
  36. package/dist/kit/render-print.mjs +173 -61
  37. package/dist/kit/render-sanitize.mjs +25 -15
  38. package/dist/kit/render-styles.mjs +3 -3
  39. package/dist/kit/render.mjs +37 -115
  40. package/dist/layout/model.mjs +10 -8
  41. package/dist/layout/paragraph-fit.mjs +1 -15
  42. package/dist/layout/typography.mjs +2 -1
  43. package/dist/packages/rapier-diff/concerns.mjs +163 -0
  44. package/dist/packages/rapier-diff/diff.mjs +68 -0
  45. package/dist/packages/rapier-diff/index.mjs +4 -701
  46. package/dist/packages/rapier-diff/matching.mjs +157 -0
  47. package/dist/packages/rapier-diff/merge.mjs +226 -0
  48. package/dist/packages/rapier-diff/moves.mjs +130 -0
  49. package/dist/packages/rapier-diff/structure.mjs +50 -0
  50. package/dist/packages/rapier-diff/tokens.mjs +72 -0
  51. package/dist/packages/rapier-diff/walk.mjs +219 -0
  52. package/dist/spec/document-fonts.mjs +1 -1
  53. package/dist/spec/document-settings.mjs +11 -164
  54. package/dist/spec/html-blocks.mjs +618 -79
  55. package/dist/spec/md-assets.mjs +29 -6
  56. package/dist/spec/md-layout.mjs +9 -1
  57. package/dist/spec/md-marks.mjs +25 -147
  58. package/dist/spec/md-source.mjs +69 -26
  59. package/package.json +1 -1
  60. package/style.css +7 -51
@@ -2,6 +2,7 @@
2
2
  // Paired source placement, shared by live editing and canonical-history proof.
3
3
  import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
4
4
  import {projectInverseSource} from './inverse-source.mjs';
5
+ import {authoredBasis, authoredPlacement, authoredPlacements} from './authored-origin.mjs';
5
6
  import {sha256} from './hash.mjs';
6
7
  import {canonicalJSON} from './data.mjs';
7
8
 
@@ -74,6 +75,18 @@ export function transposeRecord(base, splices, id, records) {
74
75
  if (next === null) throw new RangeError('canonical_transport_invalid');
75
76
  current = next;
76
77
  }
78
+ // A concurrent act that already removed these same authored units and wrote the
79
+ // same replacement is one physical edit. This act stays in history with no second
80
+ // deletion. Pure insertions are never collapsed: each one is its own unit. An
81
+ // inverse of that act means the replacement is not still in force.
82
+ const sameRows = (left, right) => left.length === right.length && left.every((row, index) =>
83
+ row.pos === right[index].pos && row.removed === right[index].removed && row.inserted === right[index].inserted);
84
+ const matched = crossed.filter(record => sameRows(record.splices, splices));
85
+ const targetsOf = record => record.transaction.sourceTransactionIds || (record.transaction.sourceTransactionId ? [record.transaction.sourceTransactionId] : []);
86
+ const withdrawn = matched.some(record => crossed.some(other => targetsOf(other).includes(record.transaction.id) ||
87
+ other.transaction.reverts === record.transaction.id || other.transaction.reapplies === record.transaction.id));
88
+ if (splices.some(row => row.removed.length) && matched.length && !withdrawn)
89
+ return {splices: [], sourceIndices: [], against: crossed, text: current};
77
90
  const moved = [], sourceIndices = [];
78
91
  for (const [sourceIndex, row] of splices.entries()) {
79
92
  const submitted = _rapierTransformSplices(source, [row]);
@@ -120,49 +133,7 @@ export function transposeSource(base, splices, client, log) {
120
133
  return transposeRecord(base, splices, client, records);
121
134
  }
122
135
 
123
- // The frontier names only causal maxima. Linear history needs one predecessor,
124
- // and concurrent predecessors remain separate until an act observes both.
125
- function authoredFrontier(records, visit = null) {
126
- const dependencies = new Map(), frontier = new Set();
127
- for (const record of records) {
128
- const id = record.transaction.id, basis = record.authored?.basis;
129
- if (visit) visit(record, basis ?? [...frontier].sort());
130
- if (!basis) { dependencies.set(id, [...frontier]); frontier.clear(); }
131
- else {
132
- const pending = [...basis], seen = new Set();
133
- while (pending.length) {
134
- const prior = pending.pop();
135
- if (seen.has(prior)) continue;
136
- if (!dependencies.has(prior)) throw new RangeError('canonical_origin_invalid');
137
- seen.add(prior); frontier.delete(prior); pending.push(...dependencies.get(prior));
138
- }
139
- dependencies.set(id, basis);
140
- }
141
- frontier.add(id);
142
- }
143
- return [...frontier].sort();
144
- }
145
-
146
- export function authoredBasis(records) { return authoredFrontier(records); }
147
-
148
- export function authoredPlacements(base, records) {
149
- const origins = new Map(); let source = base;
150
- authoredFrontier(records, (record, basis) => {
151
- origins.set(record.transaction.id, record.authored ?? authoredPlacement(source, record.splices, basis));
152
- source = _rapierTransformSplices(source, record.splices);
153
- if (source === null) throw new RangeError('canonical_origin_invalid');
154
- });
155
- return origins;
156
- }
157
-
158
- // Immutable authored placement stays on its own act. Basis IDs refer only to this
159
- // canonical history; no source snapshot or duplicate history is stored.
160
- export function authoredPlacement(source, splices, basis) {
161
- const origin = {basis: basis.slice().sort(), source: sha256(source), splices: splices.map(row => ({...row}))};
162
- if (!_rapierValidAuthored(origin) || _rapierTransformSplices(source, splices) === null)
163
- throw new RangeError('canonical_origin_invalid');
164
- return origin;
165
- }
136
+ export {authoredBasis, authoredPlacement, authoredPlacements};
166
137
 
167
138
  // Reconstruct an authored view from the same records, then prove its exact physical
168
139
  // placement. Equality of resulting text alone cannot prove a repeated-text anchor.
@@ -233,6 +204,19 @@ function authoredReplay(base, records, append = null) {
233
204
  if (source === null) throw new RangeError('canonical_origin_invalid');
234
205
  }
235
206
  }
207
+ const coverKey = origin => origin?.splices.some(row => row.removed.length)
208
+ ? canonicalJSON({basis: origin.basis, splices: origin.splices}) : null;
209
+ const coverCounts = new Map();
210
+ for (const [id, origin] of origins) {
211
+ if (effects.references.get(id)?.length) continue;
212
+ const key = coverKey(origin);
213
+ if (key) coverCounts.set(key, (coverCounts.get(key) || 0) + 1);
214
+ }
215
+ // Two inverses can name different acts and still withdraw one physical replacement.
216
+ const sharesCover = id => (effects.references.get(id) || []).some(ref => {
217
+ const key = coverKey(origins.get(ref));
218
+ return !!key && coverCounts.get(key) > 1;
219
+ });
236
220
  const ordered = (ids, selected = null) => {
237
221
  const order = [], known = new Set(ids), waiting = new Map(), dependents = new Map(), ready = [];
238
222
  const rank = id => selected ? Number(selected.has(id)) : Number(origins.get(id).splices.some(row => row.removed.length));
@@ -351,10 +335,11 @@ function authoredReplay(base, records, append = null) {
351
335
  if (aligned.text !== text) throw new RangeError('canonical_origin_invalid');
352
336
  const crossed = aligned.records.slice(basisOrder.length);
353
337
  let moved;
354
- if (sharedInverses && effects.references.get(id)?.length) {
338
+ if (effects.references.get(id)?.length && (sharedInverses || sharesCover(id))) {
355
339
  // Each inverse retains its own targets. First prove the exact intention
356
340
  // in its causal source, then place that choice in the accepted effect
357
- // graph. Another inverse may already suppress the same original bytes.
341
+ // graph. Another inverse may already suppress the same original bytes,
342
+ // including when the two inverses name different authors of one replacement.
358
343
  const authored = projectInverse(basis.text, basis.records, id);
359
344
  if (!same(origin.splices, authored.splices) ||
360
345
  _rapierTransformSplices(basis.text, origin.splices) !== authored.after)
@@ -132,7 +132,8 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
132
132
  root = document.importNode(parsed.documentElement, true);
133
133
  root.style.setProperty('width', originalBox.width + 'px', 'important'); root.style.setProperty('height', originalBox.height + 'px', 'important');
134
134
  mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${originalBox.left}px;top:${originalBox.top}px;width:${originalBox.width}px;height:${originalBox.height}px;pointer-events:none`;
135
- mount.append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
135
+ // A shadow root keeps the picture's own <style> on the picture, as an <img> does.
136
+ mount.attachShadow({mode: 'open'}).append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
136
137
  textRoots.push(root);
137
138
  }
138
139
  const box = root.getBoundingClientRect(), nodes = [root, ...root.querySelectorAll('*')], sequence = [];
@@ -151,7 +152,10 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
151
152
  const left = Math.max(0, bounds.left - box.left - 2), top = Math.max(0, bounds.top - box.top - 2);
152
153
  const right = Math.min(box.width, bounds.right - box.left + 2), bottom = Math.min(box.height, bounds.bottom - box.top + 2);
153
154
  if (!(right > left && bottom > top)) return;
154
- const result = await capture({root: wrap, clip: {x: left, y: top, width: right - left, height: bottom - top}, transparent: true, scale: 2, signal, current}); check();
155
+ const result = await capture({root: wrap, clip: {x: left, y: top, width: right - left, height: bottom - top}, transparent: true, scale: 2, signal, current, pixels: true}); check();
156
+ if (result?.rgb instanceof Uint8Array && result?.alpha instanceof Uint8Array) {
157
+ return {image: {rgb: await compress(result.rgb), alpha: await compress(result.alpha), width: result.width, height: result.height}, x: box.left - origin.left + left, y: box.top - origin.top + top, width: right - left, height: bottom - top};
158
+ }
155
159
  const image = new window.Image(); image.src = 'data:' + result.mimeType + ';base64,' + result.data; await image.decode(); check();
156
160
  const canvas = document.createElement('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
157
161
  try {
@@ -82,4 +82,3 @@ export function createPrintCapitals(lineMetrics, linesHeight, followingProse) {
82
82
 
83
83
  const capitals = createPrintCapitals(lineMetrics, linesHeight, followingPrintProse);
84
84
  export const applyPrintCapitals = capitals.apply;
85
- export const clearPrintCapitals = capitals.clear;
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {exportLedger, replaceLedgerText, readLedger, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
3
- export {exportLedger, replaceLedgerText, readLedger, textRoot, rootAfter, historyEnvelope, agentActorId};
2
+ import {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
3
+ export {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId};
4
4
  import {authorship, readAuthorship, ledgerFromAuthorship} from './ledger/authorship.mjs';
5
5
  export {authorship, readAuthorship, ledgerFromAuthorship};
6
6
  import {merge, transportInterval, transportTouchedInterval} from './ledger/merge.mjs';
@@ -10,6 +10,8 @@ export {_rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord,
10
10
  import {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices} from './ledger/history.mjs';
11
11
  export {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices};
12
12
 
13
+ import {sha256State} from './ledger/hash.mjs';
14
+ export {sha256State};
13
15
  import {transposeSource} from './ledger/transport.mjs';
14
16
  export {transposeSource};
15
17
 
@@ -63,7 +63,7 @@ export async function freezePublishedInk(root, {document: doc, css, fontCss = ''
63
63
  frame.srcdoc = '<!doctype html><html lang="' + documentLanguage(settings, language) + '" data-rapier-theme="' + theme + '"><head><meta charset="utf-8">'
64
64
  + '<meta http-equiv="Content-Security-Policy" content="' + policy + '"><meta name="referrer" content="no-referrer">'
65
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>';
66
+ + '"><main class="rapier-page md-render" data-md-theme="' + theme + '">' + root.innerHTML + '</main></body></html>';
67
67
  doc.body.appendChild(frame);
68
68
  await wait(loaded);
69
69
  const paper = frame.contentDocument, main = paper?.querySelector('main.rapier-page');
@@ -1,5 +1,5 @@
1
- import {htmlPlaceholder, mountHtmlBlocks, htmlImageRecord, htmlLayoutEdit} from '../spec/html-blocks.mjs';
2
1
  // SPDX-License-Identifier: MIT
2
+ import {htmlPlaceholder, mountHtmlBlocks, materializeObjects, objectPosterLabel, htmlImageRecord, htmlLayoutEdit, htmlModeEdit, mergeHtmlObjectBlocks} from '../spec/html-blocks.mjs';
3
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({
@@ -72,10 +72,8 @@ function initMarkdownIt() {
72
72
  'data-md-typography-close="' + md.utils.escapeHtml(encodeURIComponent(held.close)) + '"');
73
73
  return '<span ' + attrs.join(' ') + ' style="' + md.utils.escapeHtml(css.join(';')) + '">';
74
74
  };
75
- for (const kind of ['typography', 'color', 'font']) {
76
- md.renderer.rules['rapier_' + kind + '_open'] = renderTypography;
77
- md.renderer.rules['rapier_' + kind + '_close'] = () => '</span>';
78
- }
75
+ md.renderer.rules.rapier_typography_open = renderTypography;
76
+ md.renderer.rules.rapier_typography_close = () => '</span>';
79
77
  // Ink: the span carries its opener's body (the grammar's inkOpenBody: a sanitizer drops an attribute holding the
80
78
  // comment's close); the ink layer reads it back through the one grammar and draws the stroke over the words' own boxes.
81
79
  // The span itself shows nothing.
@@ -138,8 +136,8 @@ function initMarkdownIt() {
138
136
  const asset = globalThis.RapierImageAssets.dataImage(src), reference = token.meta?.mdImage?.reference;
139
137
  // A connected host's own asset; a host without one (the server) supplies no port.
140
138
  if (!env.rapierPage && typeof globalThis.RapierEmbeddedImages?.imageHtml === 'function' &&
141
- (_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
142
- 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);
143
141
  const drawingAlt = typeof rawAlt === 'string' && alt === '' && asset?.codec === 'image/svg+xml' ? globalThis.RapierImageAssets.drawingImageAlt(src) : null;
144
142
  const shownAlt = drawingAlt ?? alt;
145
143
  let loadable = false;
@@ -158,19 +156,28 @@ function initMarkdownIt() {
158
156
  return html.replace('<span ', '<span data-rapier-markdown-image="" ' +
159
157
  (token.meta?.mdImage?.source ? 'data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '" ' : ''));
160
158
  }
159
+ let animation = null;
160
+ try {animation = globalThis.RapierImageAssets.imageAnimation?.(src);} catch (_) {}
161
161
  const attrs = [
162
162
  'data-rapier-markdown-image=""',
163
- 'src="' + md.utils.escapeHtml(src) + '"',
163
+ 'src="' + md.utils.escapeHtml(animation?.first || src) + '"',
164
164
  'alt="' + md.utils.escapeHtml(shownAlt) + '"',
165
165
  'loading="lazy"',
166
166
  'decoding="async"',
167
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 + '"');
168
170
  if (token.meta?.mdImage?.source && token.meta.mdImage.source.length <= 4096)
169
171
  attrs.push('data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '"');
170
172
  if (title) attrs.push('title="' + md.utils.escapeHtml(title) + '"');
171
173
  if (shownAlt !== alt || size && !token.meta?.mdLayout?.layout?.width) attrs.push('data-rapier-image-alt-source="' + md.utils.escapeHtml(rawAlt) + '"');
172
174
  if (layout) attrs.push('data-rapier-image-layout="' + md.utils.escapeHtml(encodeURIComponent(layout)) + '"');
173
- 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 || {}) + '>';
174
181
  };
175
182
 
176
183
  md.renderer.rules.hr = function (tokens, idx) {
@@ -186,7 +193,9 @@ function initMarkdownIt() {
186
193
  // markup reaches DOMPurify: sanitizing each HTML token closes containers before their body.
187
194
  // The harness runs this installer alone for the parser's acceptance; without the page's sanitizer the tokens pass.
188
195
  const inPage = typeof sanitizeRapierHtml === 'function' && typeof _rapierChromeOwnsId === 'function';
189
- 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;
190
199
  // The id is read as HTML reads it: a numeric character reference (`&#115;ource-textarea` is `source-textarea`,
191
200
  // `&#x73;` the same) and the named references that spell a character an id can hold are decoded before the
192
201
  // chrome's ids are asked; every other name stays literal (no named reference spells a letter, a digit or a
@@ -196,7 +205,7 @@ function initMarkdownIt() {
196
205
  equals: '=', comma: ',', lpar: '(', rpar: ')', lsqb: '[', lbrack: '[', rsqb: ']', rbrack: ']', lcub: '{', lbrace: '{',
197
206
  rcub: '}', rbrace: '}', verbar: '|', vert: '|', VerticalLine: '|', bsol: '\\', Hat: '^', grave: '`', DiacriticalGrave: '`', Tab: '\t', NewLine: '\n' };
198
207
  const attributeText = value => String(value || '').replace(/^["']|["']$/g, '')
199
- .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) => {
200
209
  const code = dec ? Number(dec) : hex ? parseInt(hex, 16) : -1;
201
210
  if (code >= 0) return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '\ufffd';
202
211
  return Object.hasOwn(RAW_ID_NAMED, name) ? RAW_ID_NAMED[name] : whole;
@@ -213,9 +222,15 @@ function initMarkdownIt() {
213
222
  return key.startsWith('data-') || key === 'for' || key === 'id' && _rapierChromeOwnsId(attributeText(value)) ? '' : whole;
214
223
  }) : tag;
215
224
  md.renderer.rules.html_inline = (tokens, idx) => cleanTag(tokens[idx].content);
216
- md.renderer.rules.html_block = (tokens, idx, _options, env) => _rapierRawHtmlFragment(tokens[idx].content,
217
- 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);
218
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
+ };
219
234
 
220
235
  const _ulOpenDefault = md.renderer.rules.bullet_list_open
221
236
  || function (tokens, idx, opts, _env, self) { return self.renderToken(tokens, idx, opts); };
@@ -308,8 +323,7 @@ function initMarkdownIt() {
308
323
  '</code> fence. Text below is still inside the block.</p>' + sourceHtml + '</figure>\n';
309
324
  }
310
325
  const provider = env.rapierProviders === false ? null : _rapierProviders.mermaid;
311
- const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true ||
312
- provider?.status === 'ready' && globalThis.RapierDiagrams?.parseDiagram(src).ok === true;
326
+ const native = globalThis.RapierFlowchart?.parseFlowchart(src).ok === true;
313
327
  const ready = native || !!(provider && provider.status === 'ready' && typeof provider.renderToString === 'function');
314
328
  if (!ready && env.rapierProviders !== false && !_diagramPromptedOnce) {
315
329
  _diagramPromptedOnce = true;
@@ -360,11 +374,33 @@ function initMarkdownIt() {
360
374
  + literal + '</span>';
361
375
  }
362
376
  try {
363
- 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
+ }
364
397
  return '<span class="math-rendered" data-math-src="'
365
- + 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>';
366
403
  }
367
- catch (_) { return '<span data-rapier-math-source="">' + literal + '</span>'; }
368
404
  };
369
405
  md.renderer.rules.math_inline = (tokens, idx, _opts, env) => _renderMath(tokens[idx].content, false, env, tokens[idx].meta?.mathSource);
370
406
  md.renderer.rules.math_block = (tokens, idx, _opts, env) => wrapDisplayMath(_renderMath(tokens[idx].content, true, env, tokens[idx].meta?.mathSource)) + '\n';
@@ -393,9 +429,10 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
393
429
  const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
394
430
  // Source-semantic output is independent of installed rendering providers. Complete
395
431
  // artifacts keep the default provider path and materialize their visual content.
396
- const env = {...(options.page ? {rapierPage: true} : {}), ...(options.providers === false ? {rapierProviders: false} : {})};
397
- env.rapierDocumentSpace = !!globalThis.RapierMarkdownSpec?.readDocumentSettings?.(canonical)?.space;
398
- 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;
399
436
  let rendered = md
400
437
  ? md.render(markdown, env)
401
438
  : '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
@@ -430,11 +467,10 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
430
467
  const fallbackEnv = options.page ? {rapierPage: true} : {};
431
468
  let parsed = options.parsed || (runtime.parseAsync ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv});
432
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};
433
471
  await pause(work, .3);
434
- env.rapierDocumentSpace = !!globalThis.RapierMarkdownSpec?.readDocumentSettings?.(canonical)?.space;
435
472
  const root = document.createElement('div'), part = document.createElement('div'), boundary = htmlBoundary();
436
473
  root.setAttribute('data-rapier-semantic-root', 'true');
437
- if (env.rapierDocumentSpace) root.setAttribute('data-md-space', '');
438
474
  let html = '', began = performance.now(), complete = false;
439
475
  const append = () => {
440
476
  if (md.renderer.rapierTransformHtml) html = md.renderer.rapierTransformHtml(html);
@@ -653,16 +689,81 @@ function _rapierImageAltText(value, env = {}) {
653
689
  return md.renderer.renderInlineAsText(tokens[0]?.children || [], md.options, env);
654
690
  }
655
691
 
656
- 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) {
657
743
  const layout = globalThis.RapierMarkdownLayout.parseLayout(marker || '');
658
744
  // Plain (rung 0, docs/standard-adoption.md): every picture renders at its natural width, the
659
745
  // same as a reader with no layout facts at all -- Obsidian's `![alt|N]` alt-text width (the
660
746
  // `size` fallback below) is a different, established convention and is unaffected.
661
- const style = layout && !_rapierPlainLayout() ? globalThis.RapierMarkdownLayout.imageStyle(layout) : '';
662
- 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 + '"';
663
750
  // A fade rides beside any width, the alt-text's own included.
664
- return size ? ' width="' + size.width + '" data-rapier-image-size="' + size.width +
665
- '" 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 + '"' : '');
666
767
  }
667
768
 
668
769
  function _rapierRawHtmlFragment(source, rewrite, parser) {
@@ -1216,7 +1317,8 @@ function* _rapierProjectPortableNodesSteps(semanticRoot, options) {
1216
1317
  (name === 'data-rapier-remote-src' || name === 'data-rapier-remote-alt')) continue;
1217
1318
  if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-layout' || name === 'data-md-align' || name === 'data-rapier-ink'
1218
1319
  || name === 'data-md-first-line-end' || name === 'data-md-initial-font' || name === 'data-md-initial-size'
1219
- || 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;
1220
1322
  if (name === 'class' || name === 'style' || name.startsWith('data-')
1221
1323
  || name === 'contenteditable' || name === 'spellcheck'
1222
1324
  || name === 'tabindex' || name === 'disabled' || name === 'aria-disabled') {
@@ -1614,6 +1716,15 @@ function* _rapierResolveSoftBreakTokensSteps(root) {
1614
1716
  return root;
1615
1717
  }
1616
1718
 
1719
+ // Markup parsed in a document of its own, which runs no script and loads nothing (Copy's clipboard tree): one element per
1720
+ // markup, or no boxes where the joined parse did not keep them apart.
1721
+ function _rapierInertParse(markups) {
1722
+ const doc = new DOMParser().parseFromString(markups.map((html, at) => '<div data-rapier-batch="' + at + '">' + html + '</div>').join(''), 'text/html');
1723
+ const boxes = [...doc.body.childNodes];
1724
+ const intact = boxes.length === markups.length && boxes.every((box, at) => box.getAttribute?.('data-rapier-batch') === String(at));
1725
+ return {doc, boxes: intact ? boxes : null};
1726
+ }
1727
+
1617
1728
  function _rapierUnwrapElement(element) {
1618
1729
  while (element.firstChild) element.parentNode.insertBefore(element.firstChild, element);
1619
1730
  element.remove();
@@ -1627,8 +1738,21 @@ function _rapierBidiStrong(cp) {
1627
1738
  return null;
1628
1739
  }
1629
1740
 
1630
- return {_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, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
1741
+ // A consuming operation, not an editable DOM mode. Both outputs share one admitted
1742
+ // parse per equation; callers decide whether and how to expose the MathML to people.
1743
+ function renderWithMathML(source, metadata = {}) {
1744
+ const provider = _rapierProviders.math;
1745
+ if (!provider || provider.status !== 'ready' || typeof provider.renderToString !== 'function' ||
1746
+ typeof provider.renderWithMathML !== 'function')
1747
+ throw new Error('The Rapier Math shared-result renderer is unavailable.');
1748
+ if (!md) initMarkdownIt();
1749
+ const equations = [];
1750
+ const html = _rapierRenderSemanticRoot(source, metadata, {mathEquations: equations}).innerHTML;
1751
+ return {html, equations};
1752
+ }
1753
+
1754
+ 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, _rapierInertParse, _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};
1631
1755
  }
1632
1756
  export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
1633
1757
 
1634
- export {mountHtmlBlocks, htmlImageRecord, htmlLayoutEdit};
1758
+ export {mountHtmlBlocks, materializeObjects, objectPosterLabel, htmlImageRecord, htmlLayoutEdit, htmlModeEdit, mergeHtmlObjectBlocks};