rapier-markdown-kit 1.2.8 → 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 (60) hide show
  1. package/dist/agent/will.mjs +0 -6
  2. package/dist/images/gif.mjs +0 -3
  3. package/dist/images/playback.mjs +0 -10
  4. package/dist/kit/assets.mjs +0 -1
  5. package/dist/kit/conformance/measurer.mjs +0 -0
  6. package/dist/kit/conformance/run.mjs +1 -8
  7. package/dist/kit/export-fonts.mjs +0 -14
  8. package/dist/kit/ledger/authored-origin.mjs +0 -5
  9. package/dist/kit/ledger/authorship.mjs +0 -2
  10. package/dist/kit/ledger/carried.mjs +4 -7
  11. package/dist/kit/ledger/data.mjs +0 -8
  12. package/dist/kit/ledger/format.mjs +0 -25
  13. package/dist/kit/ledger/hash.mjs +11 -7
  14. package/dist/kit/ledger/history.mjs +10 -29
  15. package/dist/kit/ledger/inverse-source.mjs +0 -46
  16. package/dist/kit/ledger/journal-records.mjs +24 -56
  17. package/dist/kit/ledger/merge.mjs +0 -27
  18. package/dist/kit/ledger/text-merge.mjs +0 -20
  19. package/dist/kit/ledger/transport.mjs +0 -47
  20. package/dist/kit/ledger.mjs +0 -4
  21. package/dist/kit/markdown-it.mjs +0 -3
  22. package/dist/kit/pdf-glyphs.mjs +1 -10
  23. package/dist/kit/pdf-source.mjs +5 -5
  24. package/dist/kit/pdf-vectors.mjs +0 -3
  25. package/dist/kit/print-capitals.mjs +0 -4
  26. package/dist/kit/render-entry.mjs +0 -1
  27. package/dist/kit/render-ink.mjs +0 -5
  28. package/dist/kit/render-markdown.mjs +12 -117
  29. package/dist/kit/render-print.mjs +1 -47
  30. package/dist/kit/render-sanitize.mjs +7 -59
  31. package/dist/kit/render-styles.mjs +0 -7
  32. package/dist/kit/render-work.mjs +0 -12
  33. package/dist/kit/render.mjs +18 -173
  34. package/dist/layout/hyphenation.mjs +0 -23
  35. package/dist/layout/line-plan.mjs +0 -20
  36. package/dist/layout/model.mjs +0 -55
  37. package/dist/layout/paragraph-fit.mjs +50 -60
  38. package/dist/layout/preparation.mjs +23 -56
  39. package/dist/layout/prepared-paragraph.mjs +0 -25
  40. package/dist/layout/typography.mjs +103 -41
  41. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  42. package/dist/packages/rapier-diff/diff.mjs +0 -7
  43. package/dist/packages/rapier-diff/index.mjs +0 -1
  44. package/dist/packages/rapier-diff/matching.mjs +0 -19
  45. package/dist/packages/rapier-diff/merge.mjs +0 -9
  46. package/dist/packages/rapier-diff/moves.mjs +0 -7
  47. package/dist/packages/rapier-diff/structure.mjs +0 -2
  48. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  49. package/dist/packages/rapier-diff/walk.mjs +0 -12
  50. package/dist/spec/document-fonts.mjs +0 -9
  51. package/dist/spec/document-settings.mjs +4 -23
  52. package/dist/spec/frontmatter.mjs +0 -1
  53. package/dist/spec/html-blocks.mjs +0 -36
  54. package/dist/spec/md-assets.mjs +2 -61
  55. package/dist/spec/md-layout.mjs +0 -34
  56. package/dist/spec/md-marks.mjs +0 -51
  57. package/dist/spec/md-source.mjs +0 -16
  58. package/dist/spec/word-sidecar.mjs +0 -2
  59. package/package.json +1 -1
  60. package/style.css +32 -16
@@ -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');
@@ -506,7 +477,6 @@ export function mountHtmlBlocks(doc, options = {}) {
506
477
  const row = {image, key, parent, id: prefix + '-' + ++sequence, generation: 0, attrs: {}, visible: !visibility, paused: local.get(key) ?? reduced.matches};
507
478
  try {
508
479
  if (image.hasAttribute('data-rapier-html') || /^data:image\/svg\+xml;/i.test(key)) {
509
- // A body over 1 MiB is refused below; refusing its base64 first keeps every synchronous decode that small.
510
480
  if (!image.hasAttribute('data-rapier-html') && key.length > 1398200) return;
511
481
  const value = image.hasAttribute('data-rapier-html') ? JSON.parse(decodeURIComponent(key)) : {
512
482
  body: new win.TextDecoder('utf-8', {fatal: true}).decode(Uint8Array.from(win.atob(key.slice(key.indexOf(',') + 1)), c => c.charCodeAt(0))),
@@ -562,7 +532,6 @@ export function mountHtmlBlocks(doc, options = {}) {
562
532
  const row = [...owned.values()].find(item => item.id === value.id && item.frame?.contentWindow === event.source);
563
533
  if (!row || keyOf(row.image) !== row.key || !row.image.isConnected) return;
564
534
  if (Number.isFinite(value.height) && value.height > 0 && value.height <= 4096) {
565
- // A box whose own media queries answer its height turns back and forth forever: after a few turns it only grows.
566
535
  const turn = value.height > row.measured ? 1 : value.height < row.measured ? -1 : 0;
567
536
  if (turn && row.turn && turn !== row.turn) row.turns = (row.turns || 0) + 1;
568
537
  if (turn) row.turn = turn;
@@ -617,10 +586,7 @@ export function mountHtmlBlocks(doc, options = {}) {
617
586
  win.addEventListener('pagehide', hidden); win.addEventListener('pageshow', shown); if (!options.root) doc.rapierObjects = api; scan(); return api;
618
587
  }
619
588
 
620
- // Cold export uses the same opaque renderer, independently of the visible editor
621
- // and its caches. The caller owns original bytes; only detached display nodes change.
622
589
  export async function materializeObjects(root, {signal, width = 640, imageInfo, poster, bestEffort = false, issues = [], document: live} = {}) {
623
- // An export root is parsed in an inert document (no window, no body); the caller names the page whose sandboxed frames draw its objects.
624
590
  const doc = root.ownerDocument.defaultView ? root.ownerDocument : live, win = doc.defaultView;
625
591
  const images = [...root.querySelectorAll('img[data-rapier-html],img[data-rapier-animation]')];
626
592
  if (images.length > 64) throw new Error('Object export count budget');
@@ -648,7 +614,6 @@ export async function materializeObjects(root, {signal, width = 640, imageInfo,
648
614
  }
649
615
  await api.first(clone);
650
616
  const started = win.performance.now();
651
- // A block that takes its content's height is captured at that height: a frame taken before it settled shows only its top.
652
617
  const settles = image.hasAttribute('data-rapier-html') && (() => {const attrs = JSON.parse(decodeURIComponent(image.dataset.rapierHtml)).attrs || {};
653
618
  return (!attrs.height || attrs.height === 'auto') && attrs.resize !== 'scale';})();
654
619
  const first = new win.Image();
@@ -664,7 +629,6 @@ export async function materializeObjects(root, {signal, width = 640, imageInfo,
664
629
  const frame = clone.dataset.rapierFirstFrame; first.src = frame;
665
630
  await first.decode(); signal?.throwIfAborted();
666
631
  if (!settles || !first.naturalWidth || Math.abs(clone.offsetWidth * first.naturalHeight / first.naturalWidth - clone.offsetHeight) <= 2) break;
667
- // The next snapshot follows the settled viewport; wait for it.
668
632
  if (clone.dataset.rapierFirstFrame === frame) delete clone.dataset.rapierFirstFrame;
669
633
  }
670
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);
@@ -1,16 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
 
3
- // `rotate`: degrees in (-180, 180], one decimal, omitted when 0; never written for a Rapier drawing (its turn lives in its SVG).
4
- // `opacity`: a picture's fade, a whole percent from 5 to 100, omitted at 100; the picture's bytes never change.
5
- // `lines`: a picture or paragraph's initial letter spans 1 to 12 lines, from the first line's cap height to the Nth
6
- // baseline. A picture keeps its aspect ratio; never with `width`.
7
- // `first`/`indent`: signed levels of 2em, at most six decimals, in [-64,64], omitted at zero. `marker` moves a list
8
- // item's marker independently of its words, on the same scale; only the item's leading paragraph carries it.
9
- // `lock=on`: a picture locked in place: a tap goes to the words, never the picture; a hold takes it to unlock it. Omitted when off.
10
- // `hyphens=auto`, `caps=small`, `hanging=on`: paragraph typography; absence preserves ordinary text.
11
3
  export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'marker', 'lock', 'hyphens', 'caps', 'hanging']);
12
4
  export const alignments = new Set(['left', 'center', 'right', 'justify']);
13
- // `behind`/`front`: out of flow; the paragraph lays out as though the picture were absent.
14
5
  export const wraps = new Set(['around', 'box', 'behind', 'front']);
15
6
  const has = (value, key) => Object.hasOwn(value, key);
16
7
  const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
@@ -18,7 +9,6 @@ const lineCount = value => Number.isInteger(value) && value >= 1 && value <= 12;
18
9
  export const INDENT_LIMIT = 64;
19
10
  export const indentLevel = value => typeof value === 'number' && Number.isFinite(value) && value !== 0 &&
20
11
  Math.abs(value) <= INDENT_LIMIT && Number(value.toFixed(6)) === value;
21
- // A key press advances an imported measure by one level without truncating its fraction or jumping across zero.
22
12
  export function stepIndent(value, step) {
23
13
  const next = Number(((value || 0) + step).toFixed(6));
24
14
  if (Math.abs(next) > INDENT_LIMIT) return value || 0;
@@ -27,7 +17,6 @@ export function stepIndent(value, step) {
27
17
  const oneDecimalDegrees = value => typeof value === 'number' && Number.isFinite(value) &&
28
18
  value > -180 && value <= 180 && Math.abs(value * 10 - Math.round(value * 10)) < 1e-9;
29
19
 
30
- /** Validate values independently of their Markdown attachment context. */
31
20
  export function validLayout(value) {
32
21
  if (!value || typeof value !== 'object' || Array.isArray(value) ||
33
22
  Object.keys(value).some(key => !fields.has(key))) return false;
@@ -48,7 +37,6 @@ export function validLayout(value) {
48
37
  !(has(value, 'align') && (has(value, 'wrap') || has(value, 'x')));
49
38
  }
50
39
 
51
- /** Parse the closed v1 grammar. Geometry values are returned as numbers. */
52
40
  export function parseLayout(comment) {
53
41
  if (typeof comment !== 'string' || /[^\t\x20-\x7e]/.test(comment)) return null;
54
42
  const match = /^<!--md-layout:v1[ \t]+([^\r\n]*?)[ \t]*-->$/.exec(comment);
@@ -65,12 +53,10 @@ export function parseLayout(comment) {
65
53
  number[1] === '100' && /[1-9]/.test(number[2] || '')) return null;
66
54
  value[key] = Number(raw.slice(0, -1));
67
55
  } else if (key === 'y') {
68
- // Negative allowed; signed like rotate, never "-0em".
69
56
  const number = /^(-)?(0|[1-9]\d*)(?:\.\d+)?em$/.exec(raw);
70
57
  if (!number || (number[1] && Number(raw.slice(1, -2)) === 0)) return null;
71
58
  value.y = Number(raw.slice(0, -2));
72
59
  } else if (key === 'rotate') {
73
- // No '+', exponent, '°' or "-0deg"; 0 is unsigned.
74
60
  const number = /^(-)?(0|[1-9]\d*)(?:\.(\d))?deg$/.exec(raw);
75
61
  if (!number) return null;
76
62
  const magnitude = Number(number[2] + (number[3] ? '.' + number[3] : ''));
@@ -90,16 +76,12 @@ export function parseLayout(comment) {
90
76
  return validLayout(value) ? value : null;
91
77
  }
92
78
 
93
- // Comment delimiters are unsafe in sanitized attributes. Markdown stays literal;
94
- // rendered attributes carry encodeURIComponent(marker), including its '%' signs.
95
79
  export function decodeLayoutAttribute(value) {
96
80
  try { return decodeURIComponent(value || ''); } catch (_) { return ''; }
97
81
  }
98
82
 
99
83
  export const parseLayoutAttribute = value => parseLayout(decodeLayoutAttribute(value));
100
84
 
101
- // Indentation uses the document's body type, including on headings. Native table measurements
102
- // become ordinary CSS points; an item's margin is compensated by its paragraph and nested list.
103
85
  export function textStyle(value, {portable = false, pointSize, indentOffset = 0} = {}) {
104
86
  if (!validLayout(value)) return '';
105
87
  const property = {first: 'text-indent', indent: 'margin-inline-start'};
@@ -115,9 +97,6 @@ export function textStyle(value, {portable = false, pointSize, indentOffset = 0}
115
97
  return css.join(';');
116
98
  }
117
99
 
118
- // Raw HTML tables carry ordinary paragraph CSS. Read the browser's parsed declarations on the same
119
- // admitted scale; the native caller supplies the body's point size. A list marker belongs to its own li,
120
- // never to a paragraph or a picture.
121
100
  export function textLayout(style, {listMarker = false, pointSize, offset = 0} = {}) {
122
101
  const value = {};
123
102
  for (const [key, property] of (listMarker ? [['marker', 'margin-inline-start']] : [['first', 'text-indent'], ['indent', 'margin-inline-start']])) {
@@ -125,16 +104,12 @@ export function textLayout(style, {listMarker = false, pointSize, offset = 0} =
125
104
  const match = /^(-?(?:0|[1-9]\d*)(?:\.\d{1,6})?)(em|pt)$/.exec(raw);
126
105
  const level = Number(((match ? Number(match[1]) / (match[2] === 'pt' ? pointSize : 1) / 2 : 0) +
127
106
  (key === 'first' ? 0 : offset)).toFixed(6));
128
- // Public CSS may compensate an enclosing item's margin; the resolved native coordinate,
129
- // rather than each intermediate declaration, is the measurement the writer must keep.
130
107
  if (Number.isFinite(level) && level) value[key] = level;
131
108
  }
132
109
  if (!listMarker && style?.getPropertyValue('hyphens').trim() === 'auto') value.hyphens = 'auto';
133
110
  return value;
134
111
  }
135
112
 
136
- // The picture's own CSS: its normal-flow width and x, its height in lines, and its fade. `1cap` is the cap height of the
137
- // picture's own text; a renderer that sets the picture beside its anchor measures that paragraph's line and cap instead.
138
113
  export function imageStyle(value) {
139
114
  if (!validLayout(value)) return '';
140
115
  const parts = [];
@@ -144,7 +119,6 @@ export function imageStyle(value) {
144
119
  decimal(Number(Math.max(0, Math.min(100 - width, value.x - width / 2)).toFixed(6))) + '%;margin-right:0';
145
120
  parts.push('width:' + decimal(width) + '%;height:auto' + left);
146
121
  }
147
- // Wider than the column at its own aspect, it is contained in the N-line box, never stretched.
148
122
  if (value.lines != null) parts.push('width:auto;max-width:100%;height:' + linesHeightCss(value.lines) + ';object-fit:contain');
149
123
  if (value.opacity != null && value.opacity < 100) parts.push('opacity:' + decimal(value.opacity / 100));
150
124
  return parts.join(';');
@@ -152,23 +126,17 @@ export function imageStyle(value) {
152
126
 
153
127
  export const linesHeightCss = lines => lines === 1 ? '1cap' : 'calc(' + (lines - 1) + ' * var(--md-line, 1.6em) + 1cap)';
154
128
 
155
- // An HTML object has an outer clip box and an independent browser viewport.
156
- // SCALE keeps that authored viewport; REFLOW keeps measured content height.
157
129
  export function objectHeight(width, naturalHeight, attrs, line) {
158
130
  if (attrs.height && attrs.height !== 'auto') return Math.min(4096, Number(attrs.height) * line);
159
131
  const [w, h] = (attrs.viewport || '640x360').split('x').map(Number);
160
132
  return attrs.resize === 'scale' ? Math.min(4096, width * h / w) : naturalHeight;
161
133
  }
162
134
 
163
- // Controls remain real DOM nodes while their surrounding text flows. A checkbox, link or
164
- // disclosure is not a reason to exclude its entire block from picture placement.
165
135
  export function wrapTextBlock(element) {
166
136
  return element && /^(P|H[1-6]|UL|OL|BLOCKQUOTE|DETAILS|DL)$/.test(element.tagName) &&
167
137
  element.textContent.trim() && !element.querySelector('img,table,pre,figure,math,.math-rendered') ? element : null;
168
138
  }
169
139
 
170
- // The one wrap-owner rule (layout/browser.js, kit/render.mjs, editor/share.js): text after, else before, skipping pictures,
171
- // metadata and empty paragraphs; any other block is a barrier in that direction.
172
140
  export function wrapNeighbour(node, classify, step = (n, direction) => n && n[direction > 0 ? 'nextElementSibling' : 'previousElementSibling']) {
173
141
  for (const direction of [1, -1]) {
174
142
  for (let sibling = step(node, direction); sibling; sibling = step(sibling, direction)) {
@@ -182,7 +150,6 @@ export function wrapNeighbour(node, classify, step = (n, direction) => n && n[di
182
150
  return null;
183
151
  }
184
152
 
185
- // Narrower than this, a line breaker splits words letter by letter.
186
153
  export function wrapColumnFloor(fontSize) {
187
154
  return Math.max(40, 3.5 * (fontSize || 16));
188
155
  }
@@ -199,7 +166,6 @@ function decimal(value) {
199
166
  return digits.slice(0, point) + '.' + digits.slice(point);
200
167
  }
201
168
 
202
- /** Write only the supplied facts; no alignment is implied by absence. */
203
169
  export function formatLayout(value) {
204
170
  if (!validLayout(value)) throw new TypeError('invalid_markdown_layout');
205
171
  const pairs = [];