rapier-markdown-kit 1.2.7 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +32 -30
  2. package/dist/agent/will.mjs +1 -6
  3. package/dist/images/gif.mjs +0 -3
  4. package/dist/images/playback.mjs +1 -10
  5. package/dist/kit/assets.mjs +0 -1
  6. package/dist/kit/conformance/measurer.mjs +0 -0
  7. package/dist/kit/conformance/run.mjs +1 -8
  8. package/dist/kit/export-fonts.mjs +0 -14
  9. package/dist/kit/index.mjs +1 -1
  10. package/dist/kit/ledger/authored-origin.mjs +0 -5
  11. package/dist/kit/ledger/authorship.mjs +0 -2
  12. package/dist/kit/ledger/carried.d.mts +20 -0
  13. package/dist/kit/ledger/carried.mjs +4 -7
  14. package/dist/kit/ledger/data.mjs +0 -8
  15. package/dist/kit/ledger/format.mjs +0 -25
  16. package/dist/kit/ledger/hash.mjs +11 -7
  17. package/dist/kit/ledger/history.mjs +10 -29
  18. package/dist/kit/ledger/inverse-source.mjs +0 -46
  19. package/dist/kit/ledger/journal-records.mjs +24 -56
  20. package/dist/kit/ledger/merge.mjs +0 -27
  21. package/dist/kit/ledger/text-merge.mjs +0 -20
  22. package/dist/kit/ledger/transport.mjs +0 -47
  23. package/dist/kit/ledger.d.mts +88 -0
  24. package/dist/kit/ledger.mjs +14 -0
  25. package/dist/kit/markdown-it.d.mts +4 -0
  26. package/dist/kit/markdown-it.mjs +36 -0
  27. package/dist/kit/pdf-glyphs.mjs +39 -45
  28. package/dist/kit/pdf-source.d.mts +5 -0
  29. package/dist/kit/pdf-source.mjs +125 -0
  30. package/dist/kit/pdf-vectors.mjs +0 -3
  31. package/dist/kit/print-capitals.mjs +0 -4
  32. package/dist/kit/render-entry.mjs +0 -1
  33. package/dist/kit/render-ink.mjs +0 -5
  34. package/dist/kit/render-markdown.mjs +15 -119
  35. package/dist/kit/render-print.mjs +4 -49
  36. package/dist/kit/render-sanitize.mjs +22 -58
  37. package/dist/kit/render-styles.mjs +0 -7
  38. package/dist/kit/render-work.mjs +0 -12
  39. package/dist/kit/render.mjs +18 -173
  40. package/dist/kit/will.d.mts +42 -0
  41. package/dist/layout/hyphenation.mjs +0 -23
  42. package/dist/layout/line-plan.mjs +0 -20
  43. package/dist/layout/model.mjs +0 -55
  44. package/dist/layout/paragraph-fit.mjs +50 -60
  45. package/dist/layout/preparation.mjs +23 -56
  46. package/dist/layout/prepared-paragraph.mjs +0 -25
  47. package/dist/layout/typography.mjs +103 -41
  48. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  49. package/dist/packages/rapier-diff/diff.mjs +0 -7
  50. package/dist/packages/rapier-diff/index.mjs +0 -1
  51. package/dist/packages/rapier-diff/matching.mjs +0 -19
  52. package/dist/packages/rapier-diff/merge.mjs +0 -9
  53. package/dist/packages/rapier-diff/moves.mjs +0 -7
  54. package/dist/packages/rapier-diff/structure.mjs +0 -2
  55. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  56. package/dist/packages/rapier-diff/walk.mjs +0 -12
  57. package/dist/spec/document-fonts.mjs +0 -9
  58. package/dist/spec/document-settings.mjs +6 -25
  59. package/dist/spec/frontmatter.mjs +0 -1
  60. package/dist/spec/html-blocks.mjs +3 -37
  61. package/dist/spec/md-assets.mjs +2 -61
  62. package/dist/spec/md-layout.mjs +0 -34
  63. package/dist/spec/md-marks.mjs +0 -51
  64. package/dist/spec/md-source.mjs +0 -16
  65. package/dist/spec/word-sidecar.mjs +0 -2
  66. package/package.json +21 -4
  67. package/style.css +38 -21
@@ -1,17 +1,10 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // One reader of document-wide settings and the inherited font snapshot.
3
- // Framing stays in spec/frontmatter.mjs. Reading never rewrites source.
4
- // The live view reads fontsize, the three font scopes and linestretch. It ignores papersize,
5
- // geometry and pagestyle: a scroll has no page. Word and the PDF read all of them.
6
2
  import {frontMatterFrame, frontMatterLine} from './frontmatter.mjs';
7
3
  import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
8
4
 
9
- // Twelve points are one reference unit (16 px at the default reading scale).
10
5
  const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
11
6
  export const DOCUMENT_LEADING = 1.75 / 1.1;
12
7
 
13
- // Before/after are points; line is a multiple of the element's own type size.
14
- // Presets and native exports read this same table. Explicit measures do not grow with font size.
15
8
  export const SPACING_TYPES = Object.freeze(['paragraph', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
16
9
  'list', 'item', 'table', 'picture', 'drawing', 'quote', 'code', 'rule', 'footnote', 'toc']);
17
10
  export const SPACING_PRESETS = Object.freeze({compact: .7, standard: 1, roomy: 1.5});
@@ -19,7 +12,7 @@ const SPACING_SELECTORS = Object.freeze({paragraph: 'p', h1: 'h1', h2: 'h2', h3:
19
12
  list: ':is(ul,ol)', item: 'li', table: ':is(table,.table-scroll-wrap)', picture: 'p[data-md-picture]',
20
13
  drawing: ':is(.diagram-block,p[data-md-picture]:has(img[data-md-svg],img[src^="data:image/svg+xml"]))', quote: 'blockquote',
21
14
  code: 'pre', rule: 'hr:not(.footnotes-sep):not([data-md-break])', footnote: '.footnotes', toc: '.rapier-jump-list'});
22
- const HEADING_LINE = [2 / 2.6, 1.5 / 1.9, 1.2 / 1.5, 1 / 1.25, .9 / 1.1, .82];
15
+ const HEADING_LINE = [1.75 / 2.2, 1.5 / 1.75, 1.2 / 1.5, 1 / 1.25, .9 / 1.1, .82];
23
16
 
24
17
  export function normalizeDocumentSpacing(value) {
25
18
  if (typeof value === 'string') {
@@ -60,7 +53,6 @@ export function documentSpacing(settings, kind = 'paragraph') {
60
53
  return {before: before * scale * size, after: after * scale * size, line, ...spacing?.[type]};
61
54
  }
62
55
 
63
- // Word's widely supported Calibri/11 pt Normal template. Explicit document settings always win.
64
56
  export function wordDocumentSettings(settings) {
65
57
  return {...settings, word: true, fontsize: settings?.fontsize || fontSize('11pt'),
66
58
  mainfont: settings?.mainfont || documentFont('Calibri'), sansfont: settings?.sansfont || documentFont('Calibri'),
@@ -68,7 +60,7 @@ export function wordDocumentSettings(settings) {
68
60
  }
69
61
  export function documentHeadingSize(settings, level) {
70
62
  const body = (settings?.fontsize?.unit || 1.1) * 12;
71
- return body * (settings?.word ? [16/11,13/11,12/11,1,1,1] : [2.6,1.9,1.5,1.25,1.1,1])[level - 1];
63
+ return body * (settings?.word ? [16/11,13/11,12/11,1,1,1] : [2.2,1.75,1.5,1.25,1.1,1])[level - 1];
72
64
  }
73
65
 
74
66
  export function documentSpacingCss(settings, scope = '.md-render', defaults = false) {
@@ -80,7 +72,6 @@ export function documentSpacingCss(settings, scope = '.md-render', defaults = fa
80
72
  css += selector + '{margin-block:' + length(row.before) + ' ' + length(row.after) + ';line-height:' + row.line + '}';
81
73
  if (settings?.word && /^h[1-6]$/.test(type)) css += selector + '{font-size:' + length(documentHeadingSize(settings, Number(type[1]))) + '}';
82
74
  }
83
- // Compound blocks own the outside gap; the last child does not add it again.
84
75
  css += scope + ' :is(li,td,th,blockquote)>:last-child{margin-bottom:0}' +
85
76
  scope + ' li:last-child{margin-bottom:0}' + scope + ' li>:is(ul,ol){margin-bottom:0}' +
86
77
  scope + ' .table-scroll-wrap>table{margin-block:0}' +
@@ -88,7 +79,10 @@ export function documentSpacingCss(settings, scope = '.md-render', defaults = fa
88
79
  scope + ' pre code{line-height:inherit}' + scope + ' .footnotes{padding-top:0}' +
89
80
  scope + ' .footnotes-list{margin-block:0}' + scope + ' blockquote:not(.callout){padding-block:0}';
90
81
  const item = documentSpacing(settings, 'item');
91
- css += scope + ' ul.rapier-jump-list>li{padding-block:0;margin-block:' + length(item.before) + ' ' + length(item.after) + '}' +
82
+ css += scope + ' li{--md-item-gap:' + length(Math.max(item.before, item.after)) + '}';
83
+ const list = documentSpacing(settings, 'list'), heading = documentSpacing(settings, 'h2');
84
+ css += scope + '{--md-list-heading-before:' + length(Math.max(list.after, heading.before)) + ';--md-list-heading-after:' + length(Math.max(heading.after, list.before)) + '}';
85
+ css += scope + ' ul.rapier-jump-list>li{--md-jump-pad:0px;margin-block:' + length(item.before) + ' ' + length(item.after) + '}' +
92
86
  scope + ' ul.rapier-jump-list>li:last-child{margin-bottom:0}';
93
87
  for (const [type, selector] of [['item','li>p'], ['table',':is(td,th)>p'], ['quote','blockquote>p'], ['footnote','.footnotes p'], ['toc','.rapier-jump-list li']])
94
88
  css += scope + ' ' + selector + '{line-height:' + documentSpacing(settings, type).line + '}';
@@ -144,7 +138,6 @@ function marginOf(value) {
144
138
  return Object.freeze({margin: match[1] + unit, twips});
145
139
  }
146
140
 
147
- // Token spans retain the source's quotes, comments and collection punctuation.
148
141
  function quotedEnd(text, start = 0, limit = text.length) {
149
142
  const quote = text[start];
150
143
  for (let at = start + 1; at < limit; at++) {
@@ -223,7 +216,6 @@ function fieldLine(line, indented = false) {
223
216
  function valueSpan(source, field, limit) {
224
217
  let at = field.start;
225
218
  const first = source[at];
226
- // Tags and anchors may name other YAML values. They are retained, never rewritten by a control.
227
219
  if (first === '&' || first === '*' || first === '!') {
228
220
  field.opaque = true;
229
221
  while (at < field.line.end && /[&*!]/.test(source[at])) {
@@ -268,7 +260,6 @@ function mapFields(source, lines, indent) {
268
260
  return {fields, invalid};
269
261
  }
270
262
 
271
- // An append must never enter an unclosed collection or quoted value, including one under an unrelated key.
272
263
  function structureIsClosed(source, lines) {
273
264
  let blockIndent = null, through = 0;
274
265
  const limit = lines.at(-1)?.end ?? 0;
@@ -305,7 +296,6 @@ export function documentFont(value) {
305
296
  return Object.freeze(named ? {...named, family} : {family, stack: 'named', css: fontFamilyCss(family), word: family});
306
297
  }
307
298
 
308
- // Font controls use the same source spans as the other document settings.
309
299
  export function writeDocumentFontSetting(markdown, key, value) {
310
300
  if (!['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key)) throw new TypeError('font: unknown document key');
311
301
  const family = value == null ? null : normalizeFontFamily(value);
@@ -313,7 +303,6 @@ export function writeDocumentFontSetting(markdown, key, value) {
313
303
  return updateDocumentSettings(markdown, {[key]: family});
314
304
  }
315
305
 
316
- // A language tag is metadata, never CSS or markup. Its source spelling stays untouched.
317
306
  export function languageTag(value) {
318
307
  if (typeof value !== 'string' || value.length > 128 || !/^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/i.test(value)) return null;
319
308
  try { return Intl.getCanonicalLocales(value)[0] || null; }
@@ -324,7 +313,6 @@ export function documentLanguage(settings, device = globalThis.navigator?.langua
324
313
  return languageTag(settings?.lang) || languageTag(device) || 'en';
325
314
  }
326
315
 
327
- // Unrecognized fields remain authored bytes. A duplicate key has no single value.
328
316
  export function readDocumentSettings(markdown) {
329
317
  const source = String(markdown ?? ''), {frame, fields} = fieldsOf(source);
330
318
  if (!frame.closing) return EMPTY;
@@ -347,7 +335,6 @@ export function readDocumentSettings(markdown) {
347
335
  });
348
336
  }
349
337
 
350
- // Change only supplied settings. Unrelated YAML, comments, line endings, BOM and body are never reprinted.
351
338
  export function updateDocumentSettings(markdown, patch) {
352
339
  const source = String(markdown ?? ''), parsed = fieldsOf(source), {frame, fields} = parsed, before = readDocumentSettings(source);
353
340
  const changes = [], append = [];
@@ -389,15 +376,11 @@ export function updateDocumentSettings(markdown, patch) {
389
376
  return next;
390
377
  }
391
378
 
392
- // The page's <title>, the PDF's document title. Subtitle follows the title.
393
- // An absent title returns '' so the caller keeps the filename.
394
379
  export function documentTitle(settings) {
395
380
  if (!settings || !settings.title) return '';
396
381
  return settings.subtitle ? settings.title + ' \u2014 ' + settings.subtitle : settings.title;
397
382
  }
398
383
 
399
- // A supplied personal default replaces only the inherited snapshot. Explicit scopes win.
400
- // Omitted means use the portable source; null means the reader's factory default.
401
384
  export function documentSettingsFonts(settings, defaultFont) {
402
385
  const inherited = defaultFont === undefined ? settings?.defaultfont : documentFont(defaultFont);
403
386
  const main = settings?.mainfont || inherited;
@@ -416,8 +399,6 @@ export function documentSettingsStyle(settings, defaultFont) {
416
399
  return (decls.length ? '.md-render{' + decls.join(';') + '}' : '') + documentSpacingCss(settings);
417
400
  }
418
401
 
419
- // Null when the page keys do not change the print sheet. The view never asks.
420
- // The default rule stays the writer's own: size:auto; margin:12mm 14mm 14mm.
421
402
  export function documentSettingsPageRule(settings) {
422
403
  if (!settings || (!settings.papersize && !settings.geometry && settings.pagestyle !== 'plain')) return null;
423
404
  const size = settings.papersize ? settings.papersize.css : 'auto';
@@ -1,5 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // Framing owns only offsets. No field interpretation or source normalization belongs here.
3
2
  export function frontMatterLine(text, start) {
4
3
  let end = start;
5
4
  while (end < text.length && text[end] !== '\r' && text[end] !== '\n') end++;
@@ -1,15 +1,12 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // Fenced HTML stays source. Its preview has no document authority.
3
2
  import {inspectGIF} from '../images/gif.mjs';
4
3
  import {createMediaPlayback} from '../images/playback.mjs';
5
4
  import {sha256} from '../kit/ledger/hash.mjs';
6
5
  import {formatLayout, parseLayout, validLayout, imageStyle, objectHeight} from './md-layout.mjs';
7
6
 
8
- // The elements a preview keeps; every other element is removed with its content.
9
7
  export const HTML_ELEMENTS = Object.freeze('a abbr address article aside b bdi bdo blockquote br button caption circle clipPath code col colgroup dd defs del details dfn div dl dt ellipse em fieldset figcaption figure filter feBlend feColorMatrix feComponentTransfer feComposite feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feMerge feMergeNode feOffset footer g h1 h2 h3 h4 h5 h6 header hr i image img input kbd label legend li line linearGradient main mark mask meter nav ol optgroup option p path pattern picture polygon polyline pre progress q radialGradient rect s samp section select small source span stop strong style sub summary sup svg table tbody td text textarea th thead time title tr tspan u ul use var animate animateTransform animateMotion set mpath'.toLowerCase().split(' '));
10
8
  const VOID = new Set('area base br col embed hr img input link meta source track wbr'.split(' '));
11
9
 
12
- // The element names a preview removes from this body, in first-seen order. Content inside a removed element goes with it.
13
10
  export function htmlRemoved(body) {
14
11
  const allowed = new Set(HTML_ELEMENTS), removed = new Set(), tag = /<(\/?)([A-Za-z][\w:-]*)[^>]*?(\/?)>/g;
15
12
  for (let found; (found = tag.exec(body));) {
@@ -17,7 +14,6 @@ export function htmlRemoved(body) {
17
14
  if (found[1] || allowed.has(name)) continue;
18
15
  removed.add(name);
19
16
  if (VOID.has(name) || found[3]) continue;
20
- // Skip to this element's own end tag; nested same-name elements stay counted.
21
17
  const inner = new RegExp('<(/?)' + name + '\\b[^>]*>', 'gi'); inner.lastIndex = tag.lastIndex;
22
18
  for (let depth = 1, next; depth && (next = inner.exec(body));) {depth += next[1] ? -1 : 1; tag.lastIndex = inner.lastIndex;}
23
19
  if (!new RegExp('</' + name + '\\b', 'i').test(body.slice(found.index))) break;
@@ -25,7 +21,6 @@ export function htmlRemoved(body) {
25
21
  return [...removed];
26
22
  }
27
23
 
28
- // Placement belongs to the trailing md-layout comment alone. Unknown keys are kept and ignored.
29
24
  export function htmlAttributes(info) {
30
25
  const match = /^html(?:\s+\{([^{}]*)\})?\s*$/i.exec(String(info));
31
26
  if (!match) return null;
@@ -64,7 +59,6 @@ export function htmlFence(raw) {
64
59
  layout: layout || HTML_FLOW, marker: attached ? {start: end + attached[1].length, end: end + attached[1].length + attached[2].length, text: attached[2]} : null};
65
60
  }
66
61
 
67
- // Without a layout comment a block is in normal flow across the column.
68
62
  const HTML_FLOW = Object.freeze({width: 100});
69
63
 
70
64
  export function htmlImageRecord(raw) {
@@ -73,8 +67,6 @@ export function htmlImageRecord(raw) {
73
67
  tokenEnd: raw.length, source: raw, placement: fence.layout, standalone: true, imageOnly: true};
74
68
  }
75
69
 
76
- // The first phase of the editor's bounded worker splits before layout annotation. Join only
77
- // the exact adjacent marker proven by this same source grammar, preserving offsets.
78
70
  export function mergeHtmlObjectBlocks(blocks, source) {
79
71
  for (let index = 0; index + 1 < blocks.length; index++) {
80
72
  const block = blocks[index], next = blocks[index + 1];
@@ -91,7 +83,6 @@ export function mergeHtmlObjectBlocks(blocks, source) {
91
83
  export function htmlLayoutEdit(record, value) {
92
84
  const fence = htmlFence(record.block.raw); if (!fence) return null;
93
85
  const layout = Object.fromEntries(Object.entries(value).filter(([,v]) => v != null));
94
- // HTML height is its clipping viewport in document lines, not drop-cap sizing.
95
86
  const height = layout.lines; delete layout.lines;
96
87
  if (!validLayout(layout)) return null;
97
88
  const resizedHeight = height ?? (fence.attrs.resize === 'scale' && fence.attrs.height && fence.attrs.height !== 'auto' && layout.width && fence.layout.width
@@ -103,7 +94,6 @@ export function htmlLayoutEdit(record, value) {
103
94
  text: info + fence.source.slice(fence.infoEnd, fence.fenceEnd) + (marker ? line + marker : '') + tail};
104
95
  }
105
96
 
106
- // Change only owned header tokens. Body, fence spelling, class/id and unrelated attributes stay exact.
107
97
  function htmlInfoEdit(info, changes) {
108
98
  let text = info;
109
99
  for (const [key, value] of Object.entries(changes)) {
@@ -125,13 +115,10 @@ export function htmlPlaceholder(token, escape) {
125
115
  const layout = token.meta?.mdLayout?.layout || HTML_FLOW, marker = encodeURIComponent(formatLayout(layout));
126
116
  const height = attrs.height && attrs.height !== 'auto' ? attrs.height : 8;
127
117
  const source = encodeURIComponent(JSON.stringify({info: token.info, body: token.content, attrs, layout}));
128
- // The image is the existing placement owner's measurable box. Preview content is mounted separately.
129
118
  const pixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=';
130
119
  return '<p class="rapier-html-block" data-md-layout="' + escape(marker) + '"><img data-rapier-markdown-image="" data-rapier-image-index="0" data-rapier-html="' + escape(source) + '" data-rapier-image-layout="' + escape(marker) + '" src="' + pixel + '" alt="' + escape(attrs.id || 'HTML block') + '" style="' + imageStyle(layout) + ';height:calc(' + height + ' * var(--md-line, 1.6em));object-fit:fill" /></p>\n';
131
120
  }
132
121
 
133
- // The frame owns CSS layout and pixels only. All commands are scoped to this
134
- // opaque window instance; no measurement or playback reply can edit source.
135
122
  function htmlFrameRuntime(config, media, win) {
136
123
  const send = value => win.parent.postMessage({type: 'rapier-object-state', id: config.id, ...value}, '*');
137
124
  let paused = true, disposed = false, generation = 0, timer = 0, motion = 0, signature = '', busy = false;
@@ -219,7 +206,6 @@ function htmlFrameRuntime(config, media, win) {
219
206
  for (const property of style) copy.style.setProperty(property, style.getPropertyValue(property), 'important');
220
207
  if (element.tagName === 'IMG' && element.dataset.rapierAnimation) copy.style.setProperty('visibility', rasters.find(row => row.image === element)?.visibility || 'visible', 'important');
221
208
  copy.style.setProperty('animation', 'none', 'important'); copy.style.setProperty('transition', 'none', 'important');
222
- // SVG animated geometry is not represented by serialized timeline state.
223
209
  for (const attr of element.attributes) {
224
210
  const value = element[attr.name]?.animVal;
225
211
  if (value && typeof value.value === 'number') copy.setAttribute(attr.name, String(value.value));
@@ -236,8 +222,6 @@ function htmlFrameRuntime(config, media, win) {
236
222
  const p = element.animatedPoints.getItem(index); return p.x + ',' + p.y;
237
223
  }).join(' '));
238
224
  if (element.tagName === 'IMG' && element.dataset.rapierFirstFrame) copy.src = element.dataset.rapierFirstFrame;
239
- // Pseudo content is a separate painted box; materialize it before removing
240
- // stylesheets so media queries cannot run again in the smaller raster SVG.
241
225
  for (const pseudo of ['::before', '::after']) {
242
226
  const computed = win.getComputedStyle(element, pseudo), content = computed.content;
243
227
  if (!content || content === 'none' || content === 'normal') continue;
@@ -266,7 +250,6 @@ function htmlFrameRuntime(config, media, win) {
266
250
  if (!disposed && (width !== Math.ceil(win.innerWidth) || height !== Math.ceil(win.innerHeight))) {signature = ''; schedule();}
267
251
  }
268
252
  }
269
- // A pending snapshot is never pushed back: a steady stream of resize notices (Firefox sends one per frame) must not starve it.
270
253
  const schedule = () => {if (!timer) timer = win.setTimeout(snapshot, 80); for (const row of rasters) rasterPosition(row);};
271
254
  const observer = new win.ResizeObserver(schedule); observer.observe(root);
272
255
  win.addEventListener('resize', schedule);
@@ -286,8 +269,6 @@ export function htmlRuntimeSource() {
286
269
  return '{const HTML_ELEMENTS=' + JSON.stringify(HTML_ELEMENTS) + ';(' + mountHtmlBlocks.toString() + ')(document,{media:(' + createMediaPlayback.toString() + ')(window,' + inspectGIF.toString() + '),frameRuntime:' + htmlFrameRuntime.toString() + ',heightOf:' + objectHeight.toString() + '});}';
287
270
  }
288
271
 
289
- // A poster is an ordinary existing Markdown image definition, bound to source
290
- // and the canonical export viewport. Placement still belongs only to md-layout.
291
272
  export function objectPosterLabel(image, width = 640) {
292
273
  if (!image.hasAttribute('data-rapier-html')) return posterLabel(image.getAttribute('data-rapier-animation'));
293
274
  const source = JSON.parse(decodeURIComponent(image.dataset.rapierHtml));
@@ -302,12 +283,8 @@ const posterLabel = (object, layout, width = 640) => {
302
283
  return 'rapier-poster-v1-' + sha256('rapier-object-poster-v1\nlight\n' + value);
303
284
  };
304
285
 
305
- // An act that removes or changes an object removes the still that belonged to it, in the same act.
306
- // Only objects the act touched are hashed; an identical untouched object keeps the shared still.
307
- // Picture definitions the same act retires (`retired`, offsets in after) take their stills with them.
308
286
  export function retiredPosterSplices(before, after, splices, retired = []) {
309
287
  if (!splices.length || !after.includes('rapier-poster-v1-')) return [];
310
- // The changed window: the one splice itself, or the common prefix and suffix of a several-splice act.
311
288
  let start = 0, endBefore = before.length, endAfter = after.length;
312
289
  if (splices.length === 1) {start = splices[0].pos; endBefore = start + splices[0].removed.length; endAfter = start + splices[0].inserted.length;}
313
290
  else {
@@ -340,7 +317,6 @@ export function retiredPosterSplices(before, after, splices, retired = []) {
340
317
  return rows.reverse();
341
318
  }
342
319
 
343
- // One host presentation adapter: existing image geometry remains the authority.
344
320
  export function mountHtmlBlocks(doc, options = {}) {
345
321
  if (!options.root && doc.rapierObjects) return doc.rapierObjects;
346
322
  const root = options.root || doc.body;
@@ -351,7 +327,6 @@ export function mountHtmlBlocks(doc, options = {}) {
351
327
  const owned = new Map(), local = new Map(), reduced = win.matchMedia('(prefers-reduced-motion: reduce)');
352
328
  let pending = 0, dead = false, sequence = 0, noticed = false, identity = options.identity?.();
353
329
  const pixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=';
354
- // Each frame gets its own nonce; the page's script nonce never enters a preview.
355
330
  const random = () => [...win.crypto.getRandomValues(new Uint8Array(16))].map(n => n.toString(16).padStart(2, '0')).join(''), prefix = random();
356
331
  const sheet = doc.createElement('style');
357
332
  sheet.textContent = 'img[data-rapier-live-surface]{filter:opacity(0)!important}@media print{[data-rapier-html-preview],[data-rapier-object-preview],[data-rapier-playback-control]{display:none!important}img[data-rapier-live-surface]{filter:none!important}}';
@@ -414,7 +389,6 @@ export function mountHtmlBlocks(doc, options = {}) {
414
389
  image.dispatchEvent(new win.Event('rapier-html-resize', {bubbles: true}));
415
390
  }
416
391
  }
417
- // The control stacks just above its own object, never over the editor's toolbars, menus or notices.
418
392
  if (row.button) {row.button.hidden = !width || !height; row.button.style.zIndex = String((parseInt(css.zIndex) || 0) + 2); row.button.style.left = box.right - origin.left + parent.scrollLeft - parent.clientLeft - 44 + 'px'; row.button.style.top = box.top - origin.top + parent.scrollTop - parent.clientTop + 'px';}
419
393
  }
420
394
  }
@@ -429,7 +403,6 @@ export function mountHtmlBlocks(doc, options = {}) {
429
403
  const decoded = value.replace(/\\([0-9a-f]{1,6})\s?|\\([^\r\n])/gi, (_, hex, char) => hex ? String.fromCodePoint(Math.min(0x10ffff, parseInt(hex,16))) : char);
430
404
  if (value.includes('\\') && /(?:url|image-set|image|src)\s*\(|@import/i.test(decoded)) throw new Error('Escaped CSS resource is unsupported');
431
405
  if (/(?:image-set|image|src)\s*\(/i.test(decoded)) throw new Error('CSS image resources require an image element');
432
- // A space keeps the token boundary: removing an import must not join image-@import x;set( into a new function.
433
406
  return value.replace(/@import(?![\w-])[^;{}]*;?/gi, ' ').replace(/url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)/gi, (_, a,b,c) => {
434
407
  const url = (a || b || c || '').trim();
435
408
  if (/^#[A-Za-z][\w.-]*$/.test(url)) return 'url("' + url + '")';
@@ -451,14 +424,12 @@ export function mountHtmlBlocks(doc, options = {}) {
451
424
  for (const attr of [...el.attributes]) {
452
425
  const name = attr.name.toLowerCase(), value = attr.value;
453
426
  if (name.startsWith('on') || name.startsWith('data-rapier-') || ['nonce','srcdoc','is','autofocus','form','formaction','action','target','download','ping','xmlns:xlink'].includes(name)) el.removeAttribute(attr.name);
454
- // The local name: an SVG parsed as XML may bind XLink to any prefix (x:href).
455
427
  if (['src','srcset','href'].includes(attr.localName.toLowerCase()) && !(name.endsWith('href') && /^#[A-Za-z][\w.-]*$/.test(value) ||
456
428
  (name === 'src' && tag === 'img' || name.endsWith('href') && tag === 'image') && /^data:image\/(?:png|jpeg|webp|gif|jxl|svg\+xml);/i.test(value))) el.removeAttribute(attr.name);
457
429
  }
458
430
  if (tag.startsWith('animate') || tag === 'set') {
459
431
  const attribute = (el.getAttribute('attributeName') || '').trim().toLowerCase().split(':').pop();
460
432
  if (/^(?:href|base|on.*|style|src)$/.test(attribute)) {el.remove(); continue;}
461
- // Event-triggered timelines have no interactive authority in this preview.
462
433
  if (/[a-z_]\w*\./i.test(el.getAttribute('begin') || '')) el.setAttribute('begin', '0s');
463
434
  }
464
435
  if (tag === 'button') el.setAttribute('type', 'button');
@@ -474,6 +445,7 @@ export function mountHtmlBlocks(doc, options = {}) {
474
445
  if (info) {pixels += info.width * info.height; if (pixels > 24000000) throw new Error('HTML image pixel budget'); admitted[url] = {width: info.width, height: info.height, animated: !!info.animated};}
475
446
  if (match[1].toLowerCase() === 'image/svg+xml') {
476
447
  const svgText = new win.TextDecoder('utf-8', {fatal: true}).decode(bytes);
448
+ if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svgText)) throw new Error('SVG declarations are unsupported');
477
449
  const parsed = new win.DOMParser().parseFromString(svgText, 'image/svg+xml');
478
450
  if (parsed.querySelector('parsererror') || parsed.documentElement.localName !== 'svg') throw new Error('Invalid SVG image');
479
451
  const svg = doc.importNode(parsed.documentElement, true);
@@ -505,7 +477,6 @@ export function mountHtmlBlocks(doc, options = {}) {
505
477
  const row = {image, key, parent, id: prefix + '-' + ++sequence, generation: 0, attrs: {}, visible: !visibility, paused: local.get(key) ?? reduced.matches};
506
478
  try {
507
479
  if (image.hasAttribute('data-rapier-html') || /^data:image\/svg\+xml;/i.test(key)) {
508
- // A body over 1 MiB is refused below; refusing its base64 first keeps every synchronous decode that small.
509
480
  if (!image.hasAttribute('data-rapier-html') && key.length > 1398200) return;
510
481
  const value = image.hasAttribute('data-rapier-html') ? JSON.parse(decodeURIComponent(key)) : {
511
482
  body: new win.TextDecoder('utf-8', {fatal: true}).decode(Uint8Array.from(win.atob(key.slice(key.indexOf(',') + 1)), c => c.charCodeAt(0))),
@@ -561,7 +532,6 @@ export function mountHtmlBlocks(doc, options = {}) {
561
532
  const row = [...owned.values()].find(item => item.id === value.id && item.frame?.contentWindow === event.source);
562
533
  if (!row || keyOf(row.image) !== row.key || !row.image.isConnected) return;
563
534
  if (Number.isFinite(value.height) && value.height > 0 && value.height <= 4096) {
564
- // A box whose own media queries answer its height turns back and forth forever: after a few turns it only grows.
565
535
  const turn = value.height > row.measured ? 1 : value.height < row.measured ? -1 : 0;
566
536
  if (turn && row.turn && turn !== row.turn) row.turns = (row.turns || 0) + 1;
567
537
  if (turn) row.turn = turn;
@@ -616,10 +586,8 @@ export function mountHtmlBlocks(doc, options = {}) {
616
586
  win.addEventListener('pagehide', hidden); win.addEventListener('pageshow', shown); if (!options.root) doc.rapierObjects = api; scan(); return api;
617
587
  }
618
588
 
619
- // Cold export uses the same opaque renderer, independently of the visible editor
620
- // and its caches. The caller owns original bytes; only detached display nodes change.
621
- export async function materializeObjects(root, {signal, width = 640, imageInfo, poster, bestEffort = false, issues = []} = {}) {
622
- const doc = root.ownerDocument, win = doc.defaultView;
589
+ export async function materializeObjects(root, {signal, width = 640, imageInfo, poster, bestEffort = false, issues = [], document: live} = {}) {
590
+ const doc = root.ownerDocument.defaultView ? root.ownerDocument : live, win = doc.defaultView;
623
591
  const images = [...root.querySelectorAll('img[data-rapier-html],img[data-rapier-animation]')];
624
592
  if (images.length > 64) throw new Error('Object export count budget');
625
593
  for (const image of images) {
@@ -646,7 +614,6 @@ export async function materializeObjects(root, {signal, width = 640, imageInfo,
646
614
  }
647
615
  await api.first(clone);
648
616
  const started = win.performance.now();
649
- // A block that takes its content's height is captured at that height: a frame taken before it settled shows only its top.
650
617
  const settles = image.hasAttribute('data-rapier-html') && (() => {const attrs = JSON.parse(decodeURIComponent(image.dataset.rapierHtml)).attrs || {};
651
618
  return (!attrs.height || attrs.height === 'auto') && attrs.resize !== 'scale';})();
652
619
  const first = new win.Image();
@@ -662,7 +629,6 @@ export async function materializeObjects(root, {signal, width = 640, imageInfo,
662
629
  const frame = clone.dataset.rapierFirstFrame; first.src = frame;
663
630
  await first.decode(); signal?.throwIfAborted();
664
631
  if (!settles || !first.naturalWidth || Math.abs(clone.offsetWidth * first.naturalHeight / first.naturalWidth - clone.offsetHeight) <= 2) break;
665
- // The next snapshot follows the settled viewport; wait for it.
666
632
  if (clone.dataset.rapierFirstFrame === frame) delete clone.dataset.rapierFirstFrame;
667
633
  }
668
634
  if (!first.naturalWidth || !first.naturalHeight || first.naturalWidth * first.naturalHeight > 24000000) throw new Error('First-frame image budget');
@@ -10,19 +10,14 @@ let configured = null, splitSource = null;
10
10
  export function configureParser(parser, split) { configured = parser; splitSource = split; cached.length = 0; preparedDefinitions = []; }
11
11
  function fail(reason) { throw new Error(reason); }
12
12
  export const normalizeLabel = label => String(label).trim().replace(/\s+/g, ' ').toLowerCase().toUpperCase();
13
- // New titles have one identity before hashing, serializing or matching a repeated append.
14
- // Existing document bytes are never rewritten; CommonMark entity decoding must not change a title.
15
13
  export const assetTitle = value => String(value || '').replace(/[\r\n]/g, ' ').replace(/\0/g, '\ufffd');
16
14
 
17
- // The same hook runs in the editor, its parse worker and the agent's parser.
18
15
  export function installMarkdownImages(md) {
19
16
  if (md.__rapierImagesInstalled) return md;
20
17
  md.__rapierImagesInstalled = true;
21
18
  const validate = md.validateLink;
22
19
  md.validateLink = url => /^data:image\/(?:jxl|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) || validate(url);
23
20
  const normalize = md.normalizeLink;
24
- // An exact base64 data URL contains no URI escapes or host name to normalize.
25
- // Preserve its spelling and avoid allocating one URI-normalizer item per raster byte.
26
21
  md.normalizeLink = url => /^data:image\/(?:jxl|png|jpeg|webp|gif|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) ? url : normalize(url);
27
22
  const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
28
23
  md.block.ruler.at('reference', (state, start, end, silent) => {
@@ -74,34 +69,21 @@ function parserFor(factory = configured || globalThis.markdownit) {
74
69
  export {parserFor as markdownParser};
75
70
  export const markdownBodyOffset = source => splitSource ? splitSource(source).bodyOffset : 0;
76
71
 
77
- // markdown-it's block state builds five tables with one entry per line of its whole input, and its rules index
78
- // them by line. A long body gets the parser's one block pass with tables that hold a run of lines instead: a read
79
- // past the lines they hold fills them further from the parser's own line scan, and they are emptied between
80
- // top-level blocks. No line is parsed twice, so every token and `env` are the ones a whole-input state yields.
81
72
  const BLOCK_WINDOW = 1024, TABLE_STEP = 128, WINDOW_END = Symbol('block window end');
82
73
  const LINE_TABLES = ['bMarks', 'eMarks', 'tShift', 'sCount', 'bsCount'];
83
74
  const lineIndex = key => typeof key === 'string' && /^(?:0|[1-9]\d*)$/.test(key);
84
- // An array reaches its prototype only for an index it does not hold. One prototype serves every pass, so the
85
- // parser's table reads see one more array shape, not one per pass.
86
75
  let unheldLine = null;
87
76
  const unheldLines = new Proxy(Array.prototype, {
88
77
  get: (target, key, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key)) : Reflect.get(target, key, table),
89
78
  set: (target, key, value, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key), true, value) : Reflect.set(target, key, value, table),
90
79
  });
91
80
 
92
- // The parser's block pass over source.slice(bodyOffset). `visit` receives top-level blocks as they finish:
93
- // `tokens` with line numbers counted from `firstLine`, and lineStart(line), the source offset where one of those
94
- // lines begins (undefined when the source ends before it). `env` is the whole pass's. The tables hold at most
95
- // the lines between two visits: BLOCK_WINDOW of them, or as many as one block's rules read.
96
81
  export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul = false) {
97
82
  for (const window of blockWindows(source, bodyOffset, parser, env, keepNul)) visit(window);
98
83
  }
99
84
 
100
- // The same tokenizer can pause between complete top-level windows; no grammar rule is suspended.
101
85
  export function* blockWindows(source, bodyOffset, parser, env, keepNul = false) {
102
86
  const lineEnds = source.slice(bodyOffset).replace(/\r\n?/g, '\n'), body = keepNul ? lineEnds : lineEnds.replace(/\0/g, '\ufffd');
103
- // The body is the source with every CRLF one unit shorter: count the pairs before a body offset, from the last
104
- // offset asked.
105
87
  let pair = source.indexOf('\r\n', bodyOffset), shift = bodyOffset;
106
88
  const sourceOffset = at => {
107
89
  while (pair >= 0 && at + shift > pair) { shift++; pair = source.indexOf('\r\n', pair + 2); }
@@ -110,7 +92,6 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
110
92
  };
111
93
  const tokens = [];
112
94
  let origin = 0, first = 0, state;
113
- // A line begins after the newline that ends the line before it. No rule writes eMarks.
114
95
  const report = () => ({tokens, firstLine: first, lineStart: line => {
115
96
  if (!line) return sourceOffset(origin);
116
97
  const end = Object.hasOwn(state.eMarks, line - 1) ? state.eMarks[line - 1] : body.length;
@@ -120,18 +101,17 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
120
101
  for (let at = 0; at < body.length; at++) if (body.charCodeAt(at) === 10) lines++;
121
102
  if (lines <= BLOCK_WINDOW) {
122
103
  state = new parser.block.State(body, parser, env, tokens);
104
+ state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
123
105
  parser.block.tokenize(state, state.line, state.lineMax);
124
106
  yield report(); return;
125
107
  }
126
- // The parser's own count of a last line that no newline ends: it does not count one of only spaces.
127
108
  state = new parser.block.State(body.slice(body.lastIndexOf('\n') + 1), parser, env, tokens);
109
+ state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
128
110
  lines += state.lineMax;
129
111
  state.src = body;
130
112
  const tables = new WeakMap();
131
113
  for (const name of LINE_TABLES) { state[name].length = 0; tables.set(Object.setPrototypeOf(state[name], unheldLines), name); }
132
114
  let frontier = 0, ended = false, betweenBlocks = false;
133
- // A rule may be reading a table when it grows, so the five arrays are replaced whole; an array a rule still
134
- // holds is left empty, and `answer` reads and writes the state's current one in its place.
135
115
  const fill = line => {
136
116
  while (!ended && line >= state.bMarks.length) {
137
117
  const held = state.bMarks.length;
@@ -142,7 +122,6 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
142
122
  }
143
123
  const scanned = new parser.block.State(body.slice(frontier, until), parser, env, []);
144
124
  ended = until === body.length;
145
- // The scan's last entry stands for the end of its input, which is the body's end only for the last scan.
146
125
  const added = ended ? scanned.lineMax + 1 : scanned.lineMax;
147
126
  for (const name of LINE_TABLES) {
148
127
  const old = state[name], next = scanned[name], offset = name === 'bMarks' || name === 'eMarks' ? frontier : 0, table = [];
@@ -167,8 +146,6 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
167
146
  else Object.defineProperty(current, line, {value, writable: true, enumerable: true, configurable: true});
168
147
  return true;
169
148
  };
170
- // The top-level loop calls this between two blocks with the line it stands on. There every token is a finished
171
- // block and no rule is running, so the pass can leave the tokenizer and enter it again at that line.
172
149
  const skipEmptyLines = state.skipEmptyLines;
173
150
  state.skipEmptyLines = function (line) {
174
151
  if (this.level || line !== this.line || !line) return skipEmptyLines.call(this, line);
@@ -179,7 +156,6 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
179
156
  return line;
180
157
  };
181
158
  for (;;) {
182
- // Between top-level blocks the parser's loop only steps over empty lines, so a run of them is never tabled.
183
159
  for (let at = origin; at < body.length; at++) {
184
160
  const code = body.charCodeAt(at);
185
161
  if (code === 10) { origin = at + 1; first++; }
@@ -213,7 +189,6 @@ export function dataImage(url) {
213
189
  return {codec: match[1].toLowerCase(), byteLength, payloadStart: match[0].length - match[2].length};
214
190
  }
215
191
 
216
- // Markdown-it owns recognition, nesting, duplicate precedence and escapes; block parse only.
217
192
  export function parseAssets(source, factory = configured || globalThis.markdownit, bodyOffset = 0) {
218
193
  const steps = assetSteps(source, factory, bodyOffset);
219
194
  let next = steps.next();
@@ -249,7 +224,6 @@ function* assetSteps(source, factory = configured || globalThis.markdownit, body
249
224
  let end = stop;
250
225
  if (source[end - 1] === '\n') end--;
251
226
  if (source[end - 1] === '\r') end--;
252
- // Repeated definitions are source, not additional image owners.
253
227
  const raw = source.slice(start, end), found = raw.indexOf(definition.href);
254
228
  const repeated = found >= 0 ? raw.indexOf(definition.href, found + 1) : -1;
255
229
  const labelContainsUrl = definition.href.length <= id.length && id.includes(parser.utils.normalizeReference(definition.href));
@@ -277,8 +251,6 @@ function* assetSteps(source, factory = configured || globalThis.markdownit, body
277
251
  }
278
252
  export function documentAssets(source, factory) { return parseAssets(source, factory, markdownBodyOffset(source)); }
279
253
 
280
- // The source worker admits these rows with the same Markdown grammar. Send positions and
281
- // small facts back, then take the large picture strings from the exact request's source.
282
254
  export function packAssetIndex(index, offset = 0) {
283
255
  const positions = ['start', 'end', 'urlStart', 'urlEnd', 'payloadStart', 'payloadEnd'];
284
256
  const blocks = index.blocks.map(row => {
@@ -314,8 +286,6 @@ export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, f
314
286
  const index = {assets, blocks, duplicateLabels: new Set(packet.duplicateLabels), appendixStart: packet.appendixStart + offset, references};
315
287
  cached.unshift({source, factory, bodyOffset, index});
316
288
  if (cached.length > 3) cached.pop();
317
- // A top-level reference token is exactly the block the editor keeps. Its own parse has
318
- // one first definition even when it followed a duplicate in the whole document.
319
289
  preparedDefinitions = blocks.filter(row => row.topLevel).map(row => {
320
290
  const local = {...row, start: 0, end: row.source.length, active: true, status: 'unverified'};
321
291
  for (const key of positions.slice(2)) if (Number.isSafeInteger(local[key])) local[key] -= row.start;
@@ -327,8 +297,6 @@ export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, f
327
297
  return index;
328
298
  }
329
299
 
330
- // Project only parser-recognized payload spans. Their ordinary data URL remains a data URL,
331
- // with every source newline intact; the parser never needs megabytes of picture bytes.
332
300
  export function projectImageDefinitions(source, index) {
333
301
  const records = [], parts = [];
334
302
  let at = 0, length = 0;
@@ -350,14 +318,6 @@ export function projectImageDefinitions(source, index) {
350
318
  return {source: records.length ? parts.join('') : source, records, references};
351
319
  }
352
320
 
353
- // The same index folded from the document's top-level blocks: `rows` are the blocks as markdown-it's block parse
354
- // of the body cuts them, each {start, raw} with `start` absolute in `source`, and `parse(raw, row)` is parseAssets
355
- // over one block (the caller may cache it by the row). A definition the whole parse finds is one a block's own parse
356
- // finds at the block's place, because a top-level block's lines parse the same alone as in the document; labels,
357
- // duplicates, the first definition's precedence, the count limit and the appendix fold as parseAssets folds them.
358
- // Null when the rows are not this source: a block's bytes elsewhere than its start, rows out of order or overlapping,
359
- // or text that could hold a definition (`]:`) between the body's start, the rows and the end, where no row parses
360
- // it. Positions are the source's own, never normalized.
361
321
  export function blockwiseAssets(source, rows, parse = raw => parseAssets(raw), bodyOffset = markdownBodyOffset(source)) {
362
322
  if (typeof source !== 'string' || !Array.isArray(rows)) return null;
363
323
  const blocks = [], references = Object.create(null), duplicateLabels = new Set(), seen = new Set(), assets = new Map();
@@ -368,8 +328,6 @@ export function blockwiseAssets(source, rows, parse = raw => parseAssets(raw), b
368
328
  cursor = start + raw.length;
369
329
  if (!raw.includes(']:')) continue;
370
330
  const index = parse(raw, row);
371
- // Every label the block defines, picture or not: an earlier definition of a label, of any kind, is the one
372
- // the document resolves, so a later picture under it is inactive and the label a duplicate.
373
331
  const labels = Object.keys(index.references);
374
332
  for (const label of index.duplicateLabels) duplicateLabels.add(label);
375
333
  for (const label of labels) {
@@ -439,7 +397,6 @@ function imageUses(source, factory, env = {}) {
439
397
  for (const [raw, rows] of groups) {
440
398
  const found = [];
441
399
  for (let at = body.indexOf(raw, start); at >= 0 && at + raw.length <= end; at = body.indexOf(raw, at + raw.length)) found.push(at);
442
- // Repeated text in code or an unmapped container is not deletion proof.
443
400
  if (found.length !== rows.length) continue;
444
401
  found.forEach((at, index) => images.push({...rows[index], start: originalOffset(at), end: originalOffset(at + raw.length)}));
445
402
  }
@@ -449,7 +406,6 @@ function imageUses(source, factory, env = {}) {
449
406
  return {urls, images, simple, html: html.join('\n').toUpperCase()};
450
407
  }
451
408
 
452
- // A parse failure answers true: uncertainty keeps the definition.
453
409
  export function referenceOccurs(source, id, factory) {
454
410
  try { return imageUses(source, factory).images.some(image => image.id === id); }
455
411
  catch (_) { return true; }
@@ -469,11 +425,8 @@ function occurrenceChange(image, splices) {
469
425
  return changed ? {start, end} : null;
470
426
  }
471
427
 
472
- // A permissive gate keeps ordinary typing out of full-document image parsing.
473
- // Recognition and retirement proof still belong to Markdown-it below.
474
428
  export function mayRetireImageDefinitions(source, splices, factory, hint) {
475
429
  const store = typeof source === 'string' ? null : source;
476
- // A definition's closing bracket is immediately followed by its colon (parseAssets uses the same necessary condition).
477
430
  if (store ? !store.contains(']:') || !store.contains('data:image/', true)
478
431
  : !source.includes(']:') || !/data:image\//i.test(source)) return false;
479
432
  const original = (at, right, count) => {
@@ -488,8 +441,6 @@ export function mayRetireImageDefinitions(source, splices, factory, hint) {
488
441
  if (!row.removed && !row.inserted) return false;
489
442
  const start = original(row.pos, false, index), end = original(row.pos + row.removed.length, true, index);
490
443
  if (store) {
491
- // No image use exists without this marker. A definition appendix alone must
492
- // not make a prose keystroke join the document to prove that.
493
444
  if (!store.contains('![')) return false;
494
445
  const from = Math.max(0, start - 8192);
495
446
  const local = store.readSlice(from, Math.min(store.length, end + 4));
@@ -502,9 +453,6 @@ export function mayRetireImageDefinitions(source, splices, factory, hint) {
502
453
  return false;
503
454
  };
504
455
  if (separator >= 0 || from === 0) return exclude(separator >= 0 ? from + separator + 4 : 0);
505
- // Still inside a longer paragraph. Walk back in slices to the blank line.
506
- // Finding an image there is the same "possible" answer as the 8KiB window;
507
- // finding the break, or none, does not read the joined document.
508
456
  let cursor = from;
509
457
  while (cursor > 0) {
510
458
  const sliceFrom = Math.max(0, cursor - 8192);
@@ -540,8 +488,6 @@ export function mayRetireImageDefinitions(source, splices, factory, hint) {
540
488
  } catch (_) { return true; }
541
489
  }
542
490
 
543
- // Whole deletion or a valid replacement at the rebased occurrence may retire
544
- // bytes. Unfinished source edits and pre-existing unused definitions do not.
545
491
  export function retireDeletedImageDefinitions(before, after, splices, factory) {
546
492
  if (before === after || !mayRetireImageDefinitions(before, splices, factory)) return [];
547
493
  try {
@@ -567,8 +513,6 @@ export function retireDeletedImageDefinitions(before, after, splices, factory) {
567
513
  } catch (_) { return []; }
568
514
  }
569
515
 
570
- // Disclosure stays conservative even for code examples and malformed definitions.
571
- // Keep labels and readable prose; omit only the binary destination.
572
516
  export function assetOmissions(source) {
573
517
  if (typeof source !== 'string') return [];
574
518
  const pattern = /data:(image\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\\;&%#._~-]*/ig, spans = [];
@@ -594,7 +538,6 @@ export function isAssetBlock(raw) {
594
538
  return /^[ \t\r\n]*$/.test(raw.slice(at));
595
539
  }
596
540
 
597
- // Mirrors engine.js _rapierEscapeImageAlt without importing the editor bundle.
598
541
  export function escapeImageAlt(value) {
599
542
  return String(value ?? '').replace(/[\r\n]+/g, ' ')
600
543
  .replace(/[\\[\]*_`~^+$=<&]/g, '\\$&').replace(/\|(?=[1-9]\d{1,3}$)/, '\\|');
@@ -604,8 +547,6 @@ export function serializeAsset(asset) {
604
547
  const value = assetTitle(asset.title), title = value ? ' "' + value.replace(/&/g, '&amp;').replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"' : '';
605
548
  return '[' + asset.label + ']: ' + asset.url + title;
606
549
  }
607
- // Pure: re-derived at commit against the moved document; appends only at the text's end.
608
- // A replay supplies the retained definition's line ending instead of choosing one from later prose.
609
550
  export function appendAssetText(source, asset, lineEnding, {preserveLabel = false} = {}) {
610
551
  if (typeof source !== 'string' || !asset || normalizeLabel(asset.label) !== asset.id) return fail('image_source_invalid');
611
552
  const parsed = documentAssets(source), title = assetTitle(asset.title);