rapier-markdown-kit 1.1.104 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +5 -2
  2. package/README.md +34 -3
  3. package/dist/images/gif.mjs +47 -0
  4. package/dist/images/playback.mjs +101 -0
  5. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  6. package/dist/kit/conformance/04-wrap-box.md +1 -1
  7. package/dist/kit/conformance/05-behind.md +1 -1
  8. package/dist/kit/conformance/06-front.md +1 -1
  9. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  10. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  11. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  12. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  13. package/dist/kit/conformance/10-both-sides.md +1 -1
  14. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  15. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  16. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  17. package/dist/kit/conformance/README.md +19 -28
  18. package/dist/kit/conformance/run.mjs +9 -7
  19. package/dist/kit/ledger/carried.mjs +10 -5
  20. package/dist/kit/ledger/format.mjs +12 -3
  21. package/dist/kit/ledger/hash.mjs +31 -15
  22. package/dist/kit/ledger/history.mjs +17 -5
  23. package/dist/kit/ledger/merge.mjs +4 -3
  24. package/dist/kit/pdf-vectors.mjs +4 -1
  25. package/dist/kit/rapier-ledger.mjs +2 -2
  26. package/dist/kit/render-ink.mjs +1 -1
  27. package/dist/kit/render-markdown.mjs +146 -31
  28. package/dist/kit/render-print.mjs +131 -55
  29. package/dist/kit/render-sanitize.mjs +6 -3
  30. package/dist/kit/render.mjs +34 -77
  31. package/dist/layout/model.mjs +10 -8
  32. package/dist/packages/rapier-diff/index.mjs +75 -32
  33. package/dist/spec/document-fonts.mjs +1 -1
  34. package/dist/spec/document-settings.mjs +11 -164
  35. package/dist/spec/html-blocks.mjs +618 -79
  36. package/dist/spec/md-assets.mjs +29 -6
  37. package/dist/spec/md-layout.mjs +8 -0
  38. package/dist/spec/md-marks.mjs +25 -147
  39. package/dist/spec/md-source.mjs +54 -19
  40. package/package.json +1 -1
  41. package/style.css +7 -51
@@ -175,7 +175,7 @@ function _rapierPrintWillPageCss() {
175
175
  // The capture's own limits (agent/visual.mjs VISUAL_LIMITS): the longest edge and the most pixels one capture returns.
176
176
  const CAPTURE_EDGE = 4096, CAPTURE_PIXELS = 4 * 1024 * 1024;
177
177
 
178
- async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024} = {}) {
178
+ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = false, filename, title, settings}, {capture, signal, current, maxBytes = 8 * 1024 * 1024, film, ink} = {}) {
179
179
  const refuse = code => { throw Object.assign(new Error(code), {code}); };
180
180
  const check = () => {
181
181
  if (signal?.aborted) refuse('cancelled');
@@ -193,7 +193,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
193
193
  const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
194
194
  if (bodyWidth < 1 || bodyHeight < 1 || bodyWidth * bodyHeight > 4 * 1024 * 1024) refuse('pdf_page_geometry_invalid');
195
195
  const frame = document.createElement('iframe');
196
- let vectors, nativeGlyphs;
196
+ let vectors, nativeGlyphs, filmSession;
197
197
  let rejectAbort;
198
198
  const aborted = new Promise((_, reject) => { rejectAbort = reject; });
199
199
  aborted.catch(() => {});
@@ -273,6 +273,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
273
273
  globalThis.RapierMath?.fit(main);
274
274
  applyPrintCapitals(main);
275
275
  applyTypography(main, {all: true});
276
+ // The document's ink marks, drawn on the words where they now stand on the page.
277
+ ink?.(main);
276
278
  const count = Math.max(1, Math.ceil((main.scrollWidth - 1) / bodyWidth));
277
279
  if (!Number.isSafeInteger(count)) refuse('pdf_page_geometry_invalid');
278
280
  // Captured document font bytes own a same-name family; factory glyphs must
@@ -296,48 +298,59 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
296
298
  }
297
299
  const font = _rapierPrintWillFont(text);
298
300
  strip.style.width = count * bodyWidth + 'px';
299
- // A capture costs the whole document, however little of it the clip takes, so the pages that carry no picture share captures: as many to one
300
- // as the capture's own limits allow, fewer when the capture refuses its size. A page with a picture is captured alone, so the picture keeps
301
- // the capture's byte limit to itself.
302
- let share = main.querySelector('img,svg,canvas,video,picture') ? 1
303
- : Math.max(1, Math.min(Math.floor(CAPTURE_EDGE / bodyWidth), Math.floor(CAPTURE_PIXELS / (bodyWidth * bodyHeight))));
301
+ strip.style.transform = 'none';
302
+ // One styled film is reused for every page window. Batches stay inside the capture edge
303
+ // and pixel limits. A picture no longer forces its own capture. Pixels are deflated once.
304
+ if (typeof film !== 'function') refuse('pdf_render_unavailable');
305
+ // step() can reject after the film resolves (the document changed, or the attempt was
306
+ // cancelled). The session would otherwise keep its abort listener with no owner.
307
+ let pendingFilm = null, abandonFilm = false;
308
+ const trackedFilm = Promise.resolve(film({root: surface, signal, current, frame: {width: count * bodyWidth, height: bodyHeight}})).then(session => {
309
+ if (abandonFilm) session?.close?.();
310
+ else pendingFilm = session;
311
+ return session;
312
+ });
313
+ try {
314
+ filmSession = await step(trackedFilm);
315
+ pendingFilm = null;
316
+ } catch (error) {
317
+ abandonFilm = true;
318
+ pendingFilm?.close?.();
319
+ pendingFilm = null;
320
+ throw error;
321
+ }
322
+ let share = Math.max(1, Math.min(Math.floor(CAPTURE_EDGE / bodyWidth), Math.floor(CAPTURE_PIXELS / (bodyWidth * bodyHeight))));
304
323
  const pages = [];
305
324
  let imageBytes = 0;
325
+ const pack = async rgb => {
326
+ const packed = new Uint8Array(await step(new Response(new Blob([rgb]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()));
327
+ imageBytes += packed.byteLength;
328
+ if (imageBytes > maxBytes) refuse('export_too_large');
329
+ pages.push({rgb: packed});
330
+ };
306
331
  for (let first = 0; first < count;) {
307
332
  check();
308
333
  const span = Math.min(share, count - first);
309
- surface.style.width = span * bodyWidth + 'px';
310
- strip.style.transform = `translateX(${-first * bodyWidth}px)`;
311
- let captured;
312
- try { captured = await capture({root: surface, clip: {x: 0, y: 0, width: span * bodyWidth, height: bodyHeight}, signal, current}); }
313
- catch (error) {
314
- if (error?.code === 'visual_too_large' && span > 1) { share = Math.ceil(span / 2); continue; }
315
- throw error;
316
- }
317
- check();
318
- const image = new into.defaultView.Image();
319
- image.src = 'data:' + captured.mimeType + ';base64,' + captured.data;
320
334
  try {
321
- await step(image.decode());
322
- for (let offset = 0; offset < span; offset++) {
323
- const canvas = into.createElement('canvas'); canvas.width = bodyWidth; canvas.height = bodyHeight;
324
- try {
325
- const context = canvas.getContext('2d', {alpha: false});
326
- if (!context) refuse('pdf_render_unavailable');
327
- context.fillStyle = '#fff'; context.fillRect(0, 0, bodyWidth, bodyHeight);
328
- context.drawImage(image, offset * bodyWidth, 0, bodyWidth, bodyHeight, 0, 0, bodyWidth, bodyHeight);
329
- // Preserve the capture's exact pixels. A JPEG re-encode changes
330
- // glyph edges, pictures and paint even at its highest quality.
331
- const rgba = context.getImageData(0, 0, bodyWidth, bodyHeight).data;
335
+ const shot = await step(filmSession.raster({x: first * bodyWidth, y: 0, width: span * bodyWidth, height: bodyHeight}));
336
+ if (shot?.width !== span * bodyWidth || shot?.height !== bodyHeight || !(shot.rgb instanceof Uint8Array)) refuse('pdf_render_unavailable');
337
+ if (span === 1) await pack(shot.rgb);
338
+ else {
339
+ const row = bodyWidth * 3;
340
+ for (let offset = 0; offset < span; offset++) {
332
341
  const rgb = new Uint8Array(bodyWidth * bodyHeight * 3);
333
- for (let at = 0, to = 0; at < rgba.length; at += 4) {rgb[to++] = rgba[at]; rgb[to++] = rgba[at + 1]; rgb[to++] = rgba[at + 2];}
334
- const packed = new Uint8Array(await step(new Response(new Blob([rgb]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()));
335
- imageBytes += packed.byteLength;
336
- if (imageBytes > maxBytes) refuse('export_too_large');
337
- pages.push({rgb: packed});
338
- } finally { canvas.width = canvas.height = 0; }
342
+ for (let y = 0; y < bodyHeight; y++) {
343
+ const from = (y * shot.width + offset * bodyWidth) * 3;
344
+ rgb.set(shot.rgb.subarray(from, from + row), y * row);
345
+ }
346
+ await pack(rgb);
347
+ }
339
348
  }
340
- } finally { image.src = ''; }
349
+ shot.rgb = null;
350
+ } catch (error) {
351
+ if (error?.code === 'visual_too_large' && span > 1) { share = Math.ceil(span / 2); continue; }
352
+ throw error;
353
+ }
341
354
  first += span;
342
355
  }
343
356
  check();
@@ -345,7 +358,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
345
358
  pixelWidth: bodyWidth, pixelHeight: bodyHeight, pageNumbers: settings?.pagestyle === 'plain', maxBytes, check});
346
359
  check();
347
360
  return {bytes, pages: count};
348
- } finally { nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
361
+ } finally { filmSession?.close(); nativeGlyphs?.restore(); vectors?.restore(); signal?.removeEventListener('abort', abort); frame.remove(); }
349
362
  }
350
363
 
351
364
  // Ranges locate the actual text fragments after column layout. Splitting a text node at
@@ -355,7 +368,7 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
355
368
  const pages = Array.from({length: count}, () => []), walker = into.createTreeWalker(main, 5);
356
369
  const markerMetrics = _rapierPrintWillMetrics(Array.from(main.querySelectorAll('.rapier-will-carrier'), node => node.textContent));
357
370
  const markerLost = () => { throw _rapierPrintWillLost('a marker has no readable position on the page', 'Try another browser'); };
358
- const range = into.createRange();
371
+ const range = into.createRange(), segmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
359
372
  let nodes = 0, operations = 0;
360
373
  for (let node; (node = walker.nextNode());) {
361
374
  if (++nodes % 128 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); check(); }
@@ -374,6 +387,7 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
374
387
  const preserve = /pre|break-spaces/.test(style.whiteSpace);
375
388
  const block = element.closest('p,h1,h2,h3,h4,h5,h6,li,pre,td,th,figcaption,blockquote,.rapier-will-carrier') || element;
376
389
  const native = nativeNodes?.get(node);
390
+ let characters = null;
377
391
  const pageOf = x => Math.max(0, Math.min(count - 1, Math.floor((x - origin.left + .01) / pageWidth)));
378
392
  const visit = (start, end) => {
379
393
  if (++operations % 128 === 0) check();
@@ -385,7 +399,8 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
385
399
  const first = rects[0], page = pageOf(first.left);
386
400
  const oneLine = rects.every(rect => pageOf(rect.left) === page && pageOf(rect.width ? rect.right - .02 : rect.left) === page && Math.abs(rect.top - first.top) < .5);
387
401
  let middle = formula ? start : Math.floor((start + end) / 2);
388
- if (middle > start && /[\uD800-\uDBFF]/.test(value[middle - 1]) && /[\uDC00-\uDFFF]/.test(value[middle])) middle--;
402
+ // A line never breaks inside a character: halve between whole ones (a letter and its marks, a conjunct, an emoji).
403
+ if (middle > start) middle = (characters ||= segmenter.segment(value)).containing(middle).index;
389
404
  const cluster = native?.find(item => item.start < middle && item.end > middle);
390
405
  if (cluster) middle = cluster.start > start ? cluster.start : cluster.end;
391
406
  if (!oneLine && middle > start && middle < end) { visit(start, middle); visit(middle, end); return; }
@@ -398,7 +413,7 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
398
413
  if (carrier && right === x) right += Array.from(text).length * markerMetrics.advance * markerMetrics.size / 1000;
399
414
  const bottom = Math.max(...rects.map(rect => rect.bottom)) - origin.top;
400
415
  const parts = native ? native.filter(item => item.start >= start && item.end <= end && item.text).map(item => ({...item}))
401
- : [{text, x, y, width: right - x, height: bottom - y}];
416
+ : [{text, x, y, width: right - x, height: bottom - y, rtl: style.direction === 'rtl'}];
402
417
  const last = pages[page].at(-1);
403
418
  if (last && !formula && !last.math && last.block === block && last.carrier === carrier && Math.abs(last.y - y) < Math.max(2, Math.min(last.height, bottom - y) * .2)) {
404
419
  if (!preserve && last.text.endsWith(' ') && text.startsWith(' ') && parts.length) {
@@ -416,6 +431,24 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
416
431
  visit(0, value.length);
417
432
  }
418
433
  check();
434
+ // Trailing-carrier print CSS pulls a last marker up so it stays on the sheet.
435
+ // Keep that paint; put the searchable object after the preceding words so
436
+ // reading order matches the source (the marker itself has no visible ink).
437
+ const pageHeight = origin.height;
438
+ for (const page of pages) {
439
+ let prev = null;
440
+ for (const run of page) {
441
+ if (run.carrier && prev && run.y + 0.25 < prev.y + prev.height * .8) {
442
+ const next = Math.min(pageHeight - 0.5, prev.y + prev.height * .85);
443
+ if (next > run.y) {
444
+ const dy = next - run.y;
445
+ run.y += dy;
446
+ for (const part of run.parts || []) part.y += dy;
447
+ }
448
+ }
449
+ prev = run;
450
+ }
451
+ }
419
452
  return pages.map(page => page.map(({block, carrier, ...run}) => run));
420
453
  }
421
454
 
@@ -450,6 +483,28 @@ function _rapierPrintLinkPages(main, width, height, count) {
450
483
  return {_rapierWillSentinelPlan, _rapierPrintWillAdmit, _rapierPrintWillFont, _rapierPrintWillMetrics, _rapierPrintWillPlace, _rapierPrintWillLost, _rapierPrintWillPageCss, _rapierWritePrintPdf};
451
484
  }
452
485
 
486
+ // Where each scalar of an invisible text run stands, in character slots from the run's left edge: a mark shares its letter's
487
+ // slot, a right-to-left word runs from the right, and so do the words of a right-to-left line (strong letters only; one line).
488
+ function _rapierPrintSlots(text, rtl) {
489
+ const pieces = [];
490
+ let scalars = 0, count = 0;
491
+ for (const {segment} of new Intl.Segmenter(undefined, {granularity: 'grapheme'}).segment(text)) {
492
+ const dir = /[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}]/u.test(segment) ? 'r' : /\p{L}/u.test(segment) ? 'l' : '';
493
+ const cluster = {from: scalars, length: Array.from(segment).length}, last = pieces.at(-1);
494
+ scalars += cluster.length; count++;
495
+ if (last && (!dir || !last.dir || last.dir === dir)) { last.clusters.push(cluster); last.dir ||= dir; }
496
+ else pieces.push({dir, clusters: [cluster]});
497
+ }
498
+ const slots = new Array(scalars);
499
+ let slot = 0;
500
+ for (const piece of rtl ? pieces.reverse() : pieces) {
501
+ for (const cluster of (piece.dir || (rtl ? 'r' : 'l')) === 'r' ? piece.clusters.reverse() : piece.clusters) {
502
+ slots.fill(slot++, cluster.from, cluster.from + cluster.length);
503
+ }
504
+ }
505
+ return {slots, count};
506
+ }
507
+
453
508
  // A standard PDF page tree, image streams, Unicode text and an associated source file.
454
509
  // It accepts already-rendered pages, so there is no second document grammar or layout engine.
455
510
  async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [], canonical, bom = false, filename, title, font, width, height, margins, pixelWidth, pixelHeight, pageNumbers, maxBytes, check}) {
@@ -515,24 +570,31 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
515
570
  const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /' + name + ' /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
516
571
  nativeResources.push(entry.name + ' ' + type + ' 0 R');
517
572
  }
573
+ const sharedImages = new Map();
518
574
  for (let index = 0; index < pages.length; index++) {
519
575
  check();
520
- const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (pages[index].rgb ? 'FlateDecode' : 'DCTDecode'), pages[index].rgb || pages[index]);
576
+ const held = pages[index], flate = !!held?.rgb;
577
+ const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
578
+ pages[index] = null;
521
579
  let commands = 'q\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\n';
522
- const pictures = ['/Im0 ' + picture + ' 0 R'], states = [], rasterCache = new Map();
580
+ const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
523
581
  for (const drawing of vectorPages[index] || []) {
524
582
  commands += `q\n.75 0 0 -.75 ${decimal(margins.left)} ${decimal(height - margins.top)} cm\n0 0 ${pixelWidth} ${pixelHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
525
583
  commands += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
526
584
  for (const piece of drawing.sequence) {
527
585
  if (piece.image) {
528
- let name = rasterCache.get(piece.image);
529
- if (!name) {
586
+ let shared = sharedImages.get(piece.image);
587
+ if (!shared) {
530
588
  const image = piece.image;
531
589
  const mask = stream('/Type /XObject /Subtype /Image /Width ' + image.width + ' /Height ' + image.height + ' /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode', image.alpha);
532
590
  const object = stream('/Type /XObject /Subtype /Image /Width ' + image.width + ' /Height ' + image.height + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /SMask ' + mask + ' 0 R /Filter /FlateDecode', image.rgb);
533
- name = '/Im' + pictures.length; pictures.push(name + ' ' + object + ' 0 R'); rasterCache.set(image, name);
591
+ image.alpha = image.rgb = null;
592
+ const name = '/Im' + (sharedImages.size + 1);
593
+ shared = {name, resource: name + ' ' + object + ' 0 R'};
594
+ sharedImages.set(piece.image, shared);
534
595
  }
535
- commands += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${name} Do\nQ\n`;
596
+ if (!pictures.includes(shared.resource)) pictures.push(shared.resource);
597
+ commands += `q\n${[piece.width, 0, 0, -piece.height, piece.x, piece.y + piece.height].map(decimal).join(' ')} cm\n${shared.name} Do\nQ\n`;
536
598
  } else {
537
599
  const state = '/Gs' + states.length;
538
600
  states.push(state + ' ' + add('<< /Type /ExtGState /ca ' + decimal(piece.fillAlpha) + ' /CA ' + decimal(piece.strokeAlpha) + ' >>') + ' 0 R');
@@ -556,11 +618,12 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
556
618
  commands += `${state} gs\n${part.rgba.slice(0, 3).map(decimal).join(' ')} rg\n${entry.name} ${decimal(part.size * .75)} Tf\n0 Tr\n1 0 0 1 ${decimal(margins.left + part.x * .75)} ${decimal(height - margins.top - part.baseline * .75)} Tm\n<${value}> Tj\n`;
557
619
  } else {
558
620
  const characters = Array.from(part.text); if (!characters.length) continue;
559
- const advance = part.width * .75 / characters.length, size = Math.max(.1, advance * 2), scaleY = Math.max(.1, part.height * .75) / size;
621
+ const {slots, count} = _rapierPrintSlots(part.text, part.rtl);
622
+ const advance = part.width * .75 / count, size = Math.max(.1, advance * 2), scaleY = Math.max(.1, part.height * .75) / size;
560
623
  const baseline = decimal(height - margins.top - (part.y + part.height * .8) * .75);
561
624
  for (let letter = 0; letter < characters.length; letter++) {
562
625
  const value = codes.get(characters[letter].codePointAt(0)).toString(16).padStart(4, '0');
563
- commands += `/F1 ${decimal(size)} Tf\n3 Tr\n1 0 0 ${decimal(scaleY)} ${decimal(margins.left + part.x * .75 + letter * advance)} ${baseline} Tm\n<${value}> Tj\n`;
626
+ commands += `/F1 ${decimal(size)} Tf\n3 Tr\n1 0 0 ${decimal(scaleY)} ${decimal(margins.left + part.x * .75 + slots[letter] * advance)} ${baseline} Tm\n<${value}> Tj\n`;
564
627
  }
565
628
  }
566
629
  }
@@ -580,14 +643,27 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
580
643
  put(2, '<< /Type /Pages /Count ' + kids.length + ' /Kids [' + kids.map(id => id + ' 0 R').join(' ') + '] >>');
581
644
  put(1, '<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
582
645
  const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
583
- const parts = [encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n')], offsets = [0];
584
- let length = parts[0].length;
646
+ const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
647
+ let cursor = header.length;
648
+ for (let id = 1; id < objects.length; id++) {
649
+ offsets[id] = cursor;
650
+ cursor += (id + ' 0 obj\n').length + objects[id].length + 8;
651
+ }
652
+ const xrefAt = cursor;
653
+ const xref = encode('xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R >>\nstartxref\n' + xrefAt + '\n%%EOF\n');
654
+ if (cursor + xref.length > maxBytes) fail('export_too_large');
655
+ const bytes = new Uint8Array(cursor + xref.length);
656
+ bytes.set(header, 0);
657
+ let at = header.length;
585
658
  for (let id = 1; id < objects.length; id++) {
586
- offsets.push(length);
587
- const body = concat([encode(id + ' 0 obj\n'), objects[id], encode('\nendobj\n')]); parts.push(body); length += body.length;
659
+ const prefix = encode(id + ' 0 obj\n');
660
+ bytes.set(prefix, at); at += prefix.length;
661
+ bytes.set(objects[id], at); at += objects[id].length;
662
+ objects[id] = null;
663
+ const suffix = encode('\nendobj\n');
664
+ bytes.set(suffix, at); at += suffix.length;
588
665
  }
589
- const xref = length;
590
- parts.push(encode('xref\n0 ' + objects.length + '\n0000000000 65535 f \n' + offsets.slice(1).map(at => String(at).padStart(10, '0') + ' 00000 n \n').join('') + 'trailer\n<< /Size ' + objects.length + ' /Root 1 0 R /Info ' + info + ' 0 R >>\nstartxref\n' + xref + '\n%%EOF\n'));
591
- const bytes = concat(parts); if (bytes.length > maxBytes) fail('export_too_large'); return bytes;
666
+ bytes.set(xref, at);
667
+ return bytes;
592
668
  }
593
669
  export {createPrintRenderer, applyPrintCapitals, RAPIER_WILL_SENTINEL_MARK as sentinel};
@@ -7,7 +7,7 @@ const RAPIER_SANITIZE_PROFILES = Object.freeze({
7
7
  htmlTags: Object.freeze(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),
8
8
  svgTags: Object.freeze(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),
9
9
  mathTags: Object.freeze(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),
10
- htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","command","commandfor","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),
10
+ htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),
11
11
  svgAttributes: Object.freeze(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dominant-baseline","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","pointer-events","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-orientation","text-rendering","textlength","type","u1","u2","unicode","values","vector-effect","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),
12
12
  mathAttributes: Object.freeze(["accent","accentunder","align","bevelled","close","columnalign","columnlines","columnspacing","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lquote","lspace","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),
13
13
  xmlAttributes: Object.freeze(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),
@@ -430,9 +430,11 @@ function escapeRapierHtmlText(value) {
430
430
  }
431
431
 
432
432
  function _rapierChromeOwnsId(id) {
433
- // Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it).
433
+ // Every id the shell's own markup carries, read once before any document renders (initMarkdownIt primes it),
434
+ // and every rapier- id: the chrome makes its dialogs, overlays, parts and plug-in list later, under that prefix
435
+ // (rapier-anchor- is the renderer's own name for a repeated document anchor, and stays the document's).
434
436
  if (!_rapierSanitizeRuntime.chromeIds) _rapierSanitizeRuntime.chromeIds = new Set(Array.from(typeof document?.querySelectorAll === 'function' ? document.querySelectorAll('[id]') : [], element => element.id));
435
- return _rapierSanitizeRuntime.chromeIds.has(String(id));
437
+ return _rapierSanitizeRuntime.chromeIds.has(String(id)) || /^rapier-(?!anchor-)/.test(id);
436
438
  }
437
439
 
438
440
  function _rapierSafeRasterDataUrl(value) {
@@ -522,6 +524,7 @@ function _rapierVerifyRasterBytes(bytes, mime) {
522
524
  // declaration, doctype or generator/license comment before <svg itself, of whatever length its
523
525
 
524
526
  if (mime === 'image/svg+xml') return /<svg[\s>]/i.test(new TextDecoder('utf-8', {fatal: false}).decode(bytes));
527
+ if (mime === 'image/gif') return bytes.length >= 13 && /^GIF8[79]a$/.test(String.fromCharCode(...bytes.subarray(0, 6)));
525
528
  if (mime === 'image/png') return bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 && bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a;
526
529
  if (mime === 'image/jpeg') return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
527
530
  if (mime === 'image/webp') return bytes.length >= 12 && String.fromCharCode(...bytes.slice(0, 4)) === 'RIFF' && String.fromCharCode(...bytes.slice(8, 12)) === 'WEBP';
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {htmlImageRecord, mountHtmlBlocks} from '../spec/html-blocks.mjs';
2
+ import {htmlImageRecord, htmlRuntimeSource} from '../spec/html-blocks.mjs';
3
3
  import {finish, finishAsync, cloneTree, cleanTree, serializeTree, encodeUtf8Steps, pause} from './render-work.mjs';
4
4
  import {fontFamilyCss} from '../spec/md-marks.mjs';
5
5
  import {readFontAssets, documentFontsCss} from '../spec/document-fonts.mjs';
@@ -68,7 +68,7 @@ export function positionNativeDiagram(drawing) {
68
68
  }
69
69
 
70
70
  function createRenderer(runtime) {
71
- const {RAPIER_COLOR_CLOSE, RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierFormatColorOpen, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, rapierConfirm, sanitizeRapierHtml} = runtime;
71
+ const {RapierLedgerCarried, RapierPageReturnAddress, RapierTextCodec, _rapierArtifactHighlight, _rapierArtifactPreference, _rapierArtifactStyles, _rapierBlobDataUrl, _rapierBuildInterchangeContext, _rapierDocumentNameIsAdmissible, _rapierLedgerParts, _rapierDrawReadSVGRecipe, _rapierDrawShapeProfileFor, _rapierExportFontCss, _rapierFillDiagram, _rapierFinalizeExportCode, _rapierLanguageClass, _rapierLineStartOffsets, _rapierMarkdownEnvironment, _rapierPortableHtml, _rapierPrepareInterchangeContext, _rapierProjectPortableRoot, _rapierProviders, _rapierSourceCharEscaped, _rapierSourceLineSpan, crypto, document, escapeRapierHtmlText, globalThis, sanitizeRapierHtml} = runtime;
72
72
  let md = runtime.md;
73
73
  async function render(source, options = {}) {
74
74
  if (typeof source !== 'string') throw new TypeError('Document source must be a string');
@@ -157,14 +157,16 @@ async function _rapierBuildArtifact(options, providedContext) {
157
157
  styledRoot.setAttribute('lang', language);
158
158
  if (completePage && typeof _rapierFinalizeExportCode === 'function') await _rapierFinalizeExportCode(styledRoot);
159
159
  // _rapierProjectStyledRoot already returns the sanitized projection.
160
- // Print runs shared paragraph typography, but keeps its existing picture layout;
161
- // picture-shape annotations have no consumer in that artifact.
160
+ // Picture-shape annotations have no consumer in a printed artifact.
162
161
  if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
162
+ // Paper keeps each picture's wrap through the same static float layout a page uses.
163
+ if (completePage || opts.print) {
164
+ if (work) await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
165
+ else _rapierSharedPageLayout(styledRoot);
166
+ }
163
167
  if (completePage && !opts.print) {
164
- if (work) {
165
- await finishAsync(_rapierSharedPageLayoutSteps(styledRoot), work);
166
- await finishAsync(_rapierShareCapLongCodeBlocksSteps(styledRoot), work);
167
- } else { _rapierSharedPageLayout(styledRoot); _rapierShareCapLongCodeBlocks(styledRoot); }
168
+ if (work) await finishAsync(_rapierShareCapLongCodeBlocksSteps(styledRoot), work);
169
+ else _rapierShareCapLongCodeBlocks(styledRoot);
168
170
  }
169
171
  if (typeof opts.afterRoot === 'function') await opts.afterRoot(styledRoot);
170
172
  markPrintCapitals(styledRoot);
@@ -185,8 +187,6 @@ async function _rapierBuildArtifact(options, providedContext) {
185
187
  // rules of its own -- Share, for the no-script float it alone writes -- appends them.
186
188
  // Print retains its existing factory resources. HTML adds only the optional small Sans subset below.
187
189
  const includeFonts = !!opts.print;
188
- const zenFontCss = opts.print && styledRoot.querySelector('svg[aria-roledescription="zenuml"]')
189
- ? (_rapierProviders?.mermaid?.exportFontCss?.() || '') : '';
190
190
  const willFont = opts.print && opts.willFont ? opts.willFont : null;
191
191
  const settingsCss = _rapierDocumentSettingsCss(settings);
192
192
  const refined = styledRoot.querySelector('p[data-md-hyphens="auto"],p[data-md-lines],p[data-md-caps="small"],p[data-md-hanging="on"],img[data-rapier-print-lines]');
@@ -227,7 +227,7 @@ async function _rapierBuildArtifact(options, providedContext) {
227
227
  // nonce out of it and be admitted.
228
228
  const layoutScript = opts.scripts !== false ? _rapierArtifactLayoutScript(styledRoot, nonce, {pictures:!opts.print}) : '';
229
229
  const inkScript = !opts.print && opts.scripts !== false ? _rapierArtifactInkScript(styledRoot, nonce) : '';
230
- const htmlScript = opts.scripts !== false && styledRoot.querySelector('[data-rapier-html]') ? '<script nonce="' + nonce + '">(' + mountHtmlBlocks.toString().replace(/<\/script/gi, '<\\/script') + ')(document);</script>' : '';
230
+ const htmlScript = !opts.print && opts.scripts !== false && styledRoot.querySelector('[data-rapier-html],[data-rapier-animation]') ? '<script nonce="' + nonce + '">' + htmlRuntimeSource().replace(/<\/script/gi, '<\\/script') + '</script>' : '';
231
231
  const page = '<!DOCTYPE html>\n'
232
232
  + '<html lang="' + escapeRapierHtmlText(language) + '" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '" data-invert-image-alpha="' + (invertImageAlpha ? 'on' : 'off') + '">\n'
233
233
  + '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
@@ -243,7 +243,7 @@ async function _rapierBuildArtifact(options, providedContext) {
243
243
  + '<meta name="generator" content="Rapier ' + escapeRapierHtmlText(version) + '">\n'
244
244
  + '<title>' + escapeRapierHtmlText(pageTitle) + '</title>\n'
245
245
  + '<style>' + css + '</style>\n</head>\n'
246
- + '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '"' + (settings?.space ? ' data-md-space' : '') + '>'
246
+ + '<body class="' + bodyClass + '"><main class="' + docClass + '" data-md-theme="' + theme + '">'
247
247
  + bodyHtml + '</main>' + carrier + layoutScript + inkScript + htmlScript + '</body>\n</html>';
248
248
 
249
249
  return {
@@ -301,49 +301,6 @@ function* _rapierProjectStyledRootSteps(semanticRoot, options) {
301
301
  if (lang) code.classList.add('language-' + String(lang).replace(/[^a-z0-9_-]/gi, ''));
302
302
  }
303
303
  } else {
304
- if (root.hasAttribute('data-md-space')) {
305
- // One raw HTML token may have several siblings. Its authored heights stay inside one line-sized box.
306
- for (let element = root.firstElementChild; element;) {
307
- const group = element.getAttribute('data-md-html-block');
308
- if (group === null) { element = element.nextElementSibling; continue; }
309
- let first = element;
310
- // Sanitizing a container or repairing a table can leave its text before the first surviving tag.
311
- while (first.previousSibling && first.previousSibling.nodeType !== 1) { first = first.previousSibling; yield; }
312
- const wrap = document.createElement('div');
313
- wrap.className = 'md-space-block';
314
- root.insertBefore(wrap, first);
315
- let node = first;
316
- while (node && (node.nodeType !== 1 || node.getAttribute('data-md-html-block') === group)) {
317
- const next = node.nextSibling;
318
- wrap.appendChild(node);
319
- node = next;
320
- yield;
321
- }
322
- element = wrap.nextElementSibling;
323
- }
324
- // A removed raw container can leave only text. Reserve that surviving content in this projection.
325
- for (let first = root.firstChild; first;) {
326
- if (first.nodeType === 1) { first = first.nextSibling; continue; }
327
- let end = first, hasText = false;
328
- while (end && end.nodeType !== 1) {
329
- if (end.nodeType === 3 && end.nodeValue.trim()) hasText = true;
330
- end = end.nextSibling;
331
- yield;
332
- }
333
- if (hasText) {
334
- const wrap = document.createElement('div');
335
- wrap.className = 'md-space-block';
336
- root.insertBefore(wrap, first);
337
- for (let node = first; node !== end;) {
338
- const next = node.nextSibling;
339
- wrap.appendChild(node);
340
- node = next;
341
- yield;
342
- }
343
- }
344
- first = end;
345
- }
346
- }
347
304
  for (const code of root.querySelectorAll('pre > code')) { yield;
348
305
  if (code.parentElement?.classList.contains('diagram-source') &&
349
306
  code.closest('.diagram-block')?.getAttribute('data-diagram-state') === 'ready') continue;
@@ -465,11 +422,15 @@ function _rapierPaintCode(el, html) {
465
422
 
466
423
  function _rapierPrefixPortableAnchors(root, baseName) { return finish(_rapierPrefixPortableAnchorsSteps(root, baseName)); }
467
424
 
468
- function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
425
+ // The id an anchor carries on a portable page: the document's name, then the anchor with what an id cannot hold made a hyphen.
426
+ function _rapierPortableAnchorId(baseName, id) {
469
427
  const slug = String(baseName || 'document').toLowerCase()
470
428
  .replace(/[^a-z0-9]+/g, '-')
471
429
  .replace(/^-+|-+$/g, '') || 'document';
472
- const prefix = 'rapier-' + slug + '-';
430
+ return 'rapier-' + slug + '-' + (String(id).replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'anchor');
431
+ }
432
+
433
+ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
473
434
  const ids = new Map(), svgIds = new Map();
474
435
  const used = new Set();
475
436
  const scopeOf = element => {
@@ -486,11 +447,10 @@ function* _rapierPrefixPortableAnchorsSteps(root, baseName) {
486
447
  for (const element of root.querySelectorAll('[id]')) { yield;
487
448
  const oldId = element.id;
488
449
  if (!oldId) continue;
489
- const base = oldId.replace(/[^a-z0-9_-]+/gi, '-')
490
- .replace(/^-+|-+$/g, '') || 'anchor';
491
- let nextId = prefix + base;
450
+ const first = _rapierPortableAnchorId(baseName, oldId);
451
+ let nextId = first;
492
452
  let suffix = 2;
493
- while (used.has(nextId)) nextId = prefix + base + '-' + suffix++;
453
+ while (used.has(nextId)) nextId = first + '-' + suffix++;
494
454
  used.add(nextId);
495
455
  // A document link resolves the first matching anchor, as it did before renaming.
496
456
  // SVG references resolve within their own drawing, never the last repeated drawing.
@@ -693,13 +653,14 @@ function _rapierSharedResolve(text, ids, images, definitions) {
693
653
  let broken = false;
694
654
  const src = value => {
695
655
  const found = images.get(value);
696
- if (typeof found !== 'string' || !/^data:image\/(?:png|jpeg|webp|svg\+xml|jxl);base64,[A-Za-z0-9+/]+={0,2}$/i.test(found)) { broken = true; return null; }
656
+ if (typeof found !== 'string' || !/^data:image\/(?:png|jpeg|webp|gif|svg\+xml|jxl);base64,[A-Za-z0-9+/]+={0,2}$/i.test(found)) { broken = true; return null; }
697
657
  return found;
698
658
  };
699
659
  // Authored # characters were encoded. A raw id after ]( can only be a structural
700
660
  // substitution; matching its destination lets nested alt brackets survive unchanged.
701
661
  const inline = new RegExp('(\\]\\([ \\t\\r\\n]*(?:&lt;)?)#' + id + '(?=>?[ \\t\\r\\n]*\\)|>?[ \\t\\r\\n]+["\'(])', 'g');
702
- const definition = new RegExp('(^ {0,3}\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
662
+ // A definition may sit in a quote or a list item: its line starts with that container's markers.
663
+ const definition = new RegExp('(^[ \\t>]*\\[((?:\\\\.|[^\\]\\\\])+)\\]:[ \\t]*(?:(?:\\r\\n|\\r|\\n)[ \\t>]*)?(?:&lt;)?)#' + id + '(?=>?[ \\t]*$|>?[ \\t]+["\'(])', 'gm');
703
664
  const swapInline = (match, lead, value) => { const found = src(value); return found == null ? match : lead + found; };
704
665
  const swapDefinition = (match, lead, label, value) => {
705
666
  if (!definitions.has(md.utils.normalizeReference(_rapierSharedDecode(label)))) return match;
@@ -877,7 +838,7 @@ function* _rapierSharedPageLayoutSteps(root) {
877
838
  // `layout.y` already owns `shape-outside` for its own inset reservation (below); the two are not
878
839
  // combinable (a float has exactly one `shape-outside` value), so a `y`-offset `around` picture
879
840
  // keeps that inset instead of silhouette wrapping -- a documented, narrow degradation.
880
- if (layout.wrap === 'around' && !layout.rotate && !layout.y) {
841
+ if (layout.wrap === 'around' && !layout.rotate && !layout.y && !image.dataset.rapierContour?.endsWith('-box')) {
881
842
  const src = image.getAttribute('src');
882
843
  if (src) {
883
844
  paragraph.style.setProperty('shape-outside', 'url("' + src + '")');
@@ -905,16 +866,12 @@ function* _rapierSharedPageLayoutSteps(root) {
905
866
  for (const {image, paragraph, layout} of positionedAssignments) {
906
867
  yield;
907
868
  paragraph.style.cssText = 'position:relative;height:0;margin:0;padding:0;line-height:0';
908
- const x = layout.x ?? 50, naturalWidth = Number(image.getAttribute('width')) || 0;
909
- if (layout.width != null) {
910
- image.style.width = layout.width + '%';
911
- image.style.left = (x - layout.width / 2) + '%';
912
- } else if (naturalWidth > 0) {
913
- image.style.width = naturalWidth + 'px';
914
- image.style.left = 'calc(' + x + '% - ' + (naturalWidth / 2) + 'px)';
915
- } else {
916
- image.style.left = x + '%';
917
- }
869
+ // Without x the picture starts at the left edge, as everywhere else (layout/model.mjs imageBox).
870
+ const x = layout.x, naturalWidth = Number(image.getAttribute('width')) || 0;
871
+ if (layout.width != null) image.style.width = layout.width + '%';
872
+ else if (naturalWidth > 0) image.style.width = naturalWidth + 'px';
873
+ image.style.left = x == null ? '0' : layout.width != null ? (x - layout.width / 2) + '%'
874
+ : naturalWidth > 0 ? 'calc(' + x + '% - ' + (naturalWidth / 2) + 'px)' : x + '%';
918
875
  image.style.position = 'absolute';
919
876
  image.style.top = (layout.y || 0) + 'em';
920
877
  image.style.margin = '0';
@@ -1567,6 +1524,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1567
1524
  // wrap=around reads data-rapier-occupancy, else alpha. Cache key: src, dimensions, layout.wrap, rotate angle.
1568
1525
  // A turned raster's profile is recomputed here as pictureProfile does, never shipped.
1569
1526
  function profile(image, layout) {
1527
+ if (layout.wrap === 'around' && image.dataset?.rapierContour?.endsWith('-box')) return null;
1570
1528
  const src = image.currentSrc || image.getAttribute('src') || '', rotateRad = (layout.rotate || 0) * Math.PI / 180;
1571
1529
  const cached = profiles.get(image);
1572
1530
  if (cached?.src === src && cached.width === image.naturalWidth && cached.height === image.naturalHeight &&
@@ -1780,8 +1738,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1780
1738
  if (image.hasAttribute('data-rapier-html')) {
1781
1739
  const attrs = JSON.parse(decodeURIComponent(image.getAttribute('data-rapier-html'))).attrs;
1782
1740
  const line = parseFloat(computed.lineHeight) || (parseFloat(computed.fontSize) || 16) * 1.6;
1783
- unrotated.height = !attrs.height || attrs.height === 'auto'
1784
- ? Number(image.dataset.rapierNaturalHeight) || box(image).height : Number(attrs.height) * line;
1741
+ unrotated.height = metadata.objectHeight(unrotated.width, Number(image.dataset.rapierNaturalHeight) || box(image).height, attrs, line);
1785
1742
  }
1786
1743
  // rasterReserved (layout/model.mjs).
1787
1744
  const rasterRad = (layout.rotate || 0) * Math.PI / 180;
@@ -1936,6 +1893,6 @@ function _rapierArtifactTypographyScript(root) {
1936
1893
  '})();';
1937
1894
  }
1938
1895
 
1939
- return {_rapierDataImageDestinationsAsync, _rapierSharedSourceFormsAsync, _rapierPrefixPortableAnchorsSteps, _rapierProjectStyledRootAsync, _rapierBuildArtifact, _rapierBuildPublishedPage, _rapierProjectStyledRoot, _rapierDocxRenderStyles, _rapierPaintCode, _rapierPrefixPortableAnchors, _rapierDocumentSettingsOf, _rapierDocumentSettingsCss, _rapierExportedPageCsp, _rapierArtifactNonce, _rapierAnnotateExportBoxPolygons, _rapierSharedSourceHash, _rapierSharedSourceForms, _rapierSharedSourceCarrier, _rapierSharedResolve, _rapierReadSharedDocument, _rapierSharedPageFallbackCss, _rapierShareWrapKind, _rapierSharePictureParagraph, _rapierSharedPageLayout, _rapierShareCapLongCodeBlocks, _rapierRequireOfflinePageImages, _rapierCssValueUrls, _rapierBuildSharedPage, _rapierImageSources, _rapierImageDestinations, _rapierScanMarkdownImages, _rapierParseMarkdownImageDestination, _rapierImageOpenRunsList, _rapierArtifactFactoryModules, _rapierProjectArtifactLayout, _rapierArtifactLayoutScript, _rapierArtifactInkScript , render};
1896
+ return {_rapierDataImageDestinationsAsync, _rapierSharedSourceFormsAsync, _rapierPrefixPortableAnchorsSteps, _rapierPortableAnchorId, _rapierProjectStyledRootAsync, _rapierBuildArtifact, _rapierBuildPublishedPage, _rapierProjectStyledRoot, _rapierDocxRenderStyles, _rapierPaintCode, _rapierPrefixPortableAnchors, _rapierDocumentSettingsOf, _rapierDocumentSettingsCss, _rapierExportedPageCsp, _rapierArtifactNonce, _rapierAnnotateExportBoxPolygons, _rapierSharedSourceHash, _rapierSharedSourceForms, _rapierSharedSourceCarrier, _rapierSharedResolve, _rapierReadSharedDocument, _rapierSharedPageFallbackCss, _rapierShareWrapKind, _rapierSharePictureParagraph, _rapierSharedPageLayout, _rapierShareCapLongCodeBlocks, _rapierRequireOfflinePageImages, _rapierCssValueUrls, _rapierBuildSharedPage, _rapierImageSources, _rapierImageDestinations, _rapierScanMarkdownImages, _rapierParseMarkdownImageDestination, _rapierImageOpenRunsList, _rapierArtifactFactoryModules, _rapierProjectArtifactLayout, _rapierArtifactLayoutScript, _rapierArtifactInkScript , render};
1940
1897
  }
1941
1898
  export {createRenderer};