rapier-markdown-kit 1.2.43 → 1.2.45

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.
@@ -459,10 +459,215 @@ function initMarkdownIt() {
459
459
  globalThis.RapierImageAssets.configureParser(md, _rapierSplitOpeningFrontmatter);
460
460
  const render = md.renderer.render;
461
461
  md.renderer.render = function (tokens, options, env) {
462
- return render.call(this, _rapierCloseOpenDetails(tokens), options, env);
462
+ return render.call(this, _rapierHideForeignParagraphs(_rapierCloseOpenDetails(tokens)), options, env);
463
463
  };
464
464
  }
465
465
 
466
+ function _rapierHtmlTagStack(html, stack = []) {
467
+ if (!html) return stack;
468
+ const swallow = _rapierHtmlTagStack.swallow || (_rapierHtmlTagStack.swallow = new Set('select template textarea title style xmp plaintext noscript iframe noembed noframes script svg math'.split(' ')));
469
+ const rawTags = _rapierHtmlTagStack.raw || (_rapierHtmlTagStack.raw = new Set('script style textarea title xmp iframe noembed noframes noscript plaintext'.split(' ')));
470
+ let i = 0;
471
+ while (i < html.length) {
472
+ const top = stack.at(-1);
473
+ if (top && top.raw) {
474
+ const closer = new RegExp('</' + top.name + '(?=[\\t\\n\\f\\r />])', 'ig');
475
+ closer.lastIndex = i;
476
+ const found = closer.exec(html);
477
+ if (!found) break;
478
+ i = found.index;
479
+ }
480
+ const open = html.indexOf('<', i);
481
+ if (open < 0) break;
482
+ if (html.startsWith('<!--', open)) {
483
+ const end = html.indexOf('-->', open + 4);
484
+ if (end < 0) break;
485
+ i = end + 3;
486
+ continue;
487
+ }
488
+ if (html.startsWith('<!', open) || html.startsWith('<?', open)) {
489
+ const end = html.indexOf('>', open + 2);
490
+ i = end < 0 ? html.length : end + 1;
491
+ continue;
492
+ }
493
+ const match = /^<\/?([A-Za-z][A-Za-z0-9:-]*)/.exec(html.slice(open));
494
+ if (!match) { i = open + 1; continue; }
495
+ const name = match[1].toLowerCase();
496
+ let end = open + match[0].length, quote = '';
497
+ for (; end < html.length; end++) {
498
+ const character = html[end];
499
+ if (quote) { if (character === quote) quote = ''; }
500
+ else if (character === '"' || character === "'") quote = character;
501
+ else if (character === '>') break;
502
+ }
503
+ if (end >= html.length) break;
504
+ if (html[open + 1] === '/') {
505
+ let at = stack.length - 1;
506
+ while (at >= 0 && stack[at].name !== name && !swallow.has(stack[at].name)) at--;
507
+ if (at >= 0 && stack[at].name === name) stack.length = at;
508
+ } else stack.push({name, at: open, tagEnd: end + 1, raw: rawTags.has(name)});
509
+ i = end + 1;
510
+ }
511
+ return stack;
512
+ }
513
+ function _rapierForeignOpen(stack) {
514
+ for (let index = 0; index < stack.length; index++) {
515
+ const name = stack[index].name;
516
+ if (name === 'svg' || name === 'math') return true;
517
+ }
518
+ return false;
519
+ }
520
+ function _rapierSealBlockHtml(html) {
521
+ if (!html || html.indexOf('<') < 0) return html;
522
+ const stack = _rapierHtmlTagStack(html);
523
+ const swallow = _rapierHtmlTagStack.swallow;
524
+ const foreign = _rapierSealBlockHtml.foreign || (_rapierSealBlockHtml.foreign = new Set(['svg', 'math']));
525
+ const cut = stack.find(frame => swallow.has(frame.name) && (frame.raw || foreign.has(frame.name)));
526
+ if (cut) {
527
+ const wrappers = stack.filter(frame => frame.at < cut.at);
528
+ return html.slice(0, cut.at) + _rapierSealBlockTail(html.slice(cut.at), wrappers);
529
+ }
530
+ const stale = stack.filter(frame => swallow.has(frame.name));
531
+ if (!stale.length) return html;
532
+ let out = html;
533
+ for (const frame of stale.sort((left, right) => right.at - left.at))
534
+ out = out.slice(0, frame.at) + escapeRapierHtmlText(out.slice(frame.at, frame.tagEnd)) + out.slice(frame.tagEnd);
535
+ return out;
536
+ }
537
+
538
+ function _rapierSealBlockTail(tail, wrappers) {
539
+ const open = wrappers.map(frame => frame.name);
540
+ let out = '', i = 0;
541
+ while (i < tail.length) {
542
+ const at = tail.indexOf('<', i);
543
+ if (at < 0) { out += escapeRapierHtmlText(tail.slice(i)); break; }
544
+ out += escapeRapierHtmlText(tail.slice(i, at));
545
+ const match = /^<\/([A-Za-z][A-Za-z0-9:-]*)\s*>/.exec(tail.slice(at));
546
+ if (match && open.at(-1) === match[1].toLowerCase()) {
547
+ out += match[0];
548
+ open.pop();
549
+ i = at + match[0].length;
550
+ continue;
551
+ }
552
+ out += escapeRapierHtmlText(tail[at]);
553
+ i = at + 1;
554
+ }
555
+ return out;
556
+ }
557
+
558
+ function _rapierRenderTokenHtml(tokens, index, env) {
559
+ const token = tokens[index], rule = md.renderer.rules[token.type];
560
+ return token.type === 'inline' ? md.renderer.renderInline(token.children, md.options, env)
561
+ : rule ? rule(tokens, index, md.options, env, md.renderer) : md.renderer.renderToken(tokens, index, md.options);
562
+ }
563
+
564
+ function _rapierTokenBlocks(tokens) {
565
+ const blocks = [];
566
+ let start = 0, depth = 0;
567
+ for (let index = 0; index < tokens.length; index++) {
568
+ depth += tokens[index].nesting || 0;
569
+ if (depth <= 0) { blocks.push([start, index + 1]); start = index + 1; depth = 0; }
570
+ }
571
+ if (start < tokens.length) blocks.push([start, tokens.length]);
572
+ return blocks;
573
+ }
574
+ function _rapierScanHtmlBlocks(tokens, start, end, stack) {
575
+ for (let index = start; index < end; index++) {
576
+ if (tokens[index].type === 'html_block') _rapierHtmlTagStack(tokens[index].content || '', stack);
577
+ }
578
+ return stack;
579
+ }
580
+ function _rapierForeignSealRanges(tokens) {
581
+ const blocks = _rapierTokenBlocks(tokens);
582
+ const ranges = [];
583
+ const scanLater = ([start, end], frames) => {
584
+ if (tokens[start].type !== 'paragraph_open') return _rapierScanHtmlBlocks(tokens, start, end, frames);
585
+ for (const child of tokens[start + 1] && tokens[start + 1].children || [])
586
+ if (child.type === 'html_inline') _rapierHtmlTagStack(child.content || '', frames);
587
+ return frames;
588
+ };
589
+ for (let index = 0; index < blocks.length; index++) {
590
+ const stack = _rapierScanHtmlBlocks(tokens, blocks[index][0], blocks[index][1], []);
591
+ if (!_rapierForeignOpen(stack)) { ranges.push(blocks[index]); continue; }
592
+ const frames = stack.map(frame => ({name: frame.name, raw: frame.raw}));
593
+ let joined = -1;
594
+ for (let later = index + 1; later < blocks.length && later <= index + 512; later++) {
595
+ scanLater(blocks[later], frames);
596
+ if (!_rapierForeignOpen(frames)) { joined = later; break; }
597
+ }
598
+ if (joined < 0) { ranges.push(blocks[index]); continue; }
599
+ const range = [blocks[index][0], blocks[joined][1]];
600
+ for (let later = index + 1; later <= joined; later++) {
601
+ const [start, end] = blocks[later];
602
+ if (tokens[start].type === 'paragraph_open' && tokens[end - 1].type === 'paragraph_close' &&
603
+ (tokens[start + 1] && tokens[start + 1].children || []).some(child => child.type === 'html_inline'))
604
+ (range.hide || (range.hide = new Set())).add(start).add(end - 1);
605
+ }
606
+ ranges.push(range);
607
+ index = joined;
608
+ }
609
+ return ranges;
610
+ }
611
+ function _rapierHideForeignParagraphs(tokens) {
612
+ let out = tokens;
613
+ for (const range of _rapierForeignSealRanges(tokens)) {
614
+ if (!range.hide) continue;
615
+ if (out === tokens) out = tokens.slice();
616
+ for (const index of range.hide) out[index] = Object.assign(Object.create(Object.getPrototypeOf(tokens[index])), tokens[index], {hidden: true});
617
+ }
618
+ return out;
619
+ }
620
+ function _rapierMergeForeignBlocks(blocks, source, parser) {
621
+ const text = String(source || '');
622
+ const out = [];
623
+ const inline = new Map();
624
+ const scanLater = (row, frames) => {
625
+ if (!row) return frames;
626
+ if (row.type === 'html_block') return _rapierHtmlTagStack(String(row.raw || ''), frames);
627
+ if (row.type !== 'paragraph' || String(row.raw || '').indexOf('<') < 0) return frames;
628
+ let children = inline.get(row);
629
+ if (!children) {
630
+ if (!parser && !md) initMarkdownIt();
631
+ const parsed = (parser || md).parseInline(String(row.raw || ''), {});
632
+ children = parsed[0] && parsed[0].children || [];
633
+ inline.set(row, children);
634
+ }
635
+ for (const child of children) if (child.type === 'html_inline') _rapierHtmlTagStack(child.content || '', frames);
636
+ return frames;
637
+ };
638
+ let index = 0;
639
+ while (index < blocks.length) {
640
+ const block = blocks[index];
641
+ const stack = [];
642
+ if (block && block.type === 'html_block') _rapierHtmlTagStack(String(block.raw || ''), stack);
643
+ if (!_rapierForeignOpen(stack)) { out.push(block); index++; continue; }
644
+ const frames = stack.map(frame => ({name: frame.name, raw: frame.raw}));
645
+ let joined = -1;
646
+ for (let later = index + 1; later < blocks.length && later <= index + 512; later++) {
647
+ scanLater(blocks[later], frames);
648
+ if (!_rapierForeignOpen(frames)) { joined = later; break; }
649
+ }
650
+ if (joined < 0) { out.push(block); index++; continue; }
651
+ const start = Number(block._sourceStart), end = Number(blocks[joined]._sourceEnd);
652
+ const raw = Number.isFinite(start) && Number.isFinite(end) && start >= 0 && end >= start && end <= text.length
653
+ ? text.slice(start, end)
654
+ : blocks.slice(index, joined + 1).map(row => String(row && row.raw || '')).join('\n\n');
655
+ out.push({...block, raw, _sourceEnd: Number.isFinite(end) ? end : block._sourceEnd, rendered: null});
656
+ index = joined + 1;
657
+ }
658
+ out._rapierFacts = Array.isArray(blocks && blocks._rapierFacts) ? blocks._rapierFacts.slice() : [];
659
+ return out;
660
+ }
661
+ function _rapierRenderSealedHtml(tokens, env) {
662
+ let html = '';
663
+ for (const range of _rapierForeignSealRanges(tokens)) {
664
+ let block = '';
665
+ for (let index = range[0]; index < range[1]; index++) block += range.hide && range.hide.has(index) ? (tokens[index].nesting < 0 ? '\n' : '') : _rapierRenderTokenHtml(tokens, index, env);
666
+ html += _rapierSealBlockHtml(block);
667
+ }
668
+ return html;
669
+ }
670
+
466
671
  function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
467
672
  const root = document.createElement('template').content.ownerDocument.createElement('div');
468
673
  root.setAttribute('data-rapier-semantic-root', 'true');
@@ -487,20 +692,21 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
487
692
  if (wikiLinks) env.rapierWikiLinks = wikiLinks;
488
693
  if (options.mathEquations) env.rapierMathEquations = options.mathEquations;
489
694
  const nesting = md && options.maxNesting > 0 ? options.maxNesting : 0;
490
- let cutAt = -1, rendered;
695
+ let cutAt = -1, rendered = '';
491
696
  codeLeft = options.codeBudget >= 0 ? options.codeBudget : Infinity;
492
697
  try {
493
- if (nesting || md && options.closeDetails) {
698
+ if (!md) rendered = '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
699
+ else {
494
700
  const saved = md.options.maxNesting;
495
701
  if (nesting) md.options.maxNesting = nesting;
496
702
  try {
497
- const tokens = md.parse(markdown, env);
703
+ let tokens = md.parse(markdown, env);
498
704
  if (nesting) cutAt = _rapierNestingCutAt(tokens, nesting);
499
- rendered = md.renderer.render(options.closeDetails ? _rapierCloseOpenDetails(tokens) : tokens, md.options, env);
705
+ tokens = _rapierCloseOpenDetails(tokens);
706
+ rendered = _rapierRenderSealedHtml(tokens, env);
707
+ if (md.renderer.rapierTransformHtml) rendered = md.renderer.rapierTransformHtml(rendered);
500
708
  } finally { md.options.maxNesting = saved; }
501
- } else rendered = md
502
- ? md.render(markdown, env)
503
- : '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
709
+ }
504
710
  } finally { codeLeft = Infinity; }
505
711
  if (md && env.footnotes && Array.isArray(env.footnotes.list) && env.footnotes.list.length) {
506
712
  const labels = env.footnotes.list.map((item, index) =>
@@ -565,17 +771,22 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
565
771
  while (part.firstChild) root.appendChild(part.firstChild);
566
772
  html = '';
567
773
  };
774
+ const finishBlock = block => {
775
+ const sealed = _rapierSealBlockHtml(block);
776
+ html += sealed;
777
+ if (boundary(sealed) && html.length >= 16384) append();
778
+ };
568
779
  try {
569
- for (let index = 0; index < tokens.length; index++) {
570
- const token = tokens[index], rule = md.renderer.rules[token.type];
571
- let rendered = token.type === 'inline' ? md.renderer.renderInline(token.children, md.options, env)
572
- : rule ? rule(tokens, index, md.options, env, md.renderer) : md.renderer.renderToken(tokens, index, md.options);
573
- html += rendered;
574
- if (boundary(rendered) && html.length >= 16384) append();
575
- if (performance.now() - began >= 8) {
576
- await pause(work, .3 + .2 * (index + 1) / tokens.length);
577
- began = performance.now();
780
+ for (const range of _rapierForeignSealRanges(tokens)) {
781
+ let block = '';
782
+ for (let index = range[0]; index < range[1]; index++) {
783
+ block += range.hide && range.hide.has(index) ? (tokens[index].nesting < 0 ? '\n' : '') : _rapierRenderTokenHtml(tokens, index, env);
784
+ if (performance.now() - began >= 8) {
785
+ await pause(work, .3 + .2 * (index + 1) / tokens.length);
786
+ began = performance.now();
787
+ }
578
788
  }
789
+ finishBlock(block);
579
790
  }
580
791
  if (html) append();
581
792
  if (cutAt >= 0) _rapierAppendNestingCut(root, markdown, cutAt);
@@ -1893,7 +2104,7 @@ function renderWithMathML(source, metadata = {}) {
1893
2104
  return {html, equations};
1894
2105
  }
1895
2106
 
1896
- return {_rapierReadPartDecorations, renderWithMathML, _rapierClipboardPayloadAsync, _rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierInertParse, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierApplyImageGeometry, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
2107
+ return {_rapierReadPartDecorations, renderWithMathML, _rapierClipboardPayloadAsync, _rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierInertParse, _rapierUnwrapElement, _rapierBidiStrong, _rapierSealBlockHtml, _rapierMergeForeignBlocks, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierApplyImageGeometry, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
1897
2108
  }
1898
2109
  export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
1899
2110
 
@@ -69,7 +69,8 @@ function cleanDocumentStyle(tag, value, context, classes = '') {
69
69
  const property = part.slice(0, colon).trim().toLowerCase(), body = part.slice(colon + 1).trim();
70
70
  const diagramProperty = diagram && /^--(?:_[a-z0-9_-]+|pele-[a-z0-9-]+)$/.test(property);
71
71
  if (!DOCUMENT_STYLE_PROPERTIES.has(property) && !/^--md-[a-z0-9-]+$/.test(property) && !diagramProperty && !(diagram && property === 'font')) return;
72
- if (!body || /[\\\u0000-\u0008\u000b\u000e-\u001f{}@]|\/\*|\*\//.test(body)) return;
72
+ if (!body || /[\u0000-\u0008\u000b\u000e-\u001f{}@]|\/\*|\*\//.test(body) ||
73
+ /["'\\]/.test(body.replace(/"(?:[^"\\\n\r\f]|\\["\\])*"|'(?:[^'\\\n\r\f]|\\['\\])*'/g, ''))) return;
73
74
  const withoutLocal = body.replace(/\burl\(\s*(['"]?)#[a-z0-9_.:-]+\1\s*\)/gi, '');
74
75
  if (/(?:url|image|image-set|-webkit-image-set|element|paint|attr|expression)\s*\(/i.test(withoutLocal)) return;
75
76
  kept.push(part.trim());
@@ -304,6 +305,31 @@ function _rapierInstallSanitizeHooks() {
304
305
  if (level) node.setAttribute('data-rapier-word-level', String(level));
305
306
  else if (node.nodeName === 'SPAN' && wordListIsMarker(style)) node.setAttribute('data-rapier-word-marker', '1');
306
307
  });
308
+ DOMPurify.addHook('uponSanitizeElement', node => {
309
+ if (_rapierSanitizeRuntime.context !== 'paste' || !node || node.nodeName !== 'TABLE') return;
310
+ node.removeAttribute('data-rapier-paste-word-header');
311
+ try {
312
+ const own = (node.getAttribute('style') || '') + ' ' + (node.getAttribute('class') || '');
313
+ const word = /mso-yfti-|MsoTable/i.test(own) || !!node.querySelector('[style*="mso-"],[class*="Mso"]');
314
+ if (!word || node.tHead || node.querySelector('th')) return;
315
+ const row = Array.from(node.querySelectorAll('tr')).find(candidate => candidate.closest('table') === node);
316
+ if (!row) return;
317
+ const cells = Array.from(row.children).filter(cell => cell.nodeName === 'TD' || cell.nodeName === 'TH');
318
+ if (!cells.length) return;
319
+ let bold = false;
320
+ for (const cell of cells) {
321
+ const walker = node.ownerDocument.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
322
+ for (let text = walker.nextNode(); text; text = walker.nextNode()) {
323
+ if (!/\S/.test(text.nodeValue || '')) continue;
324
+ const parent = text.parentElement;
325
+ const style = parent?.getAttribute?.('style') || '';
326
+ if (!(parent && (parent.closest('b,strong') || /font-weight\s*:\s*(?:bold|[6-9]00|1000)\b/i.test(style)))) return;
327
+ bold = true;
328
+ }
329
+ }
330
+ if (bold) node.setAttribute('data-rapier-paste-word-header', '');
331
+ } catch (_) {}
332
+ });
307
333
  }
308
334
 
309
335
  function _rapierInstallDocumentSanitizeHooks() {
@@ -414,9 +440,17 @@ function sanitize(value, context, fragment) {
414
440
  if (purifyConfigured.get(DOMPurify) !== options) { DOMPurify.setConfig(options); purifyConfigured.set(DOMPurify, options); }
415
441
  const previous = _rapierSanitizeRuntime.context;
416
442
  _rapierSanitizeRuntime.context = selected;
443
+ ensureScratchParseHook();
444
+ const scratch = [];
445
+ const outerSink = scratchSink;
446
+ scratchSink = scratch;
447
+ let result;
417
448
  try {
418
- return DOMPurify.sanitize(value == null ? '' : String(value));
449
+ result = DOMPurify.sanitize(value == null ? '' : String(value));
450
+ return result;
419
451
  } finally {
452
+ scratchSink = outerSink;
453
+ releaseScratchMedia(scratch, result);
420
454
  if (labels) {
421
455
  DOMPurify.removeHook('uponSanitizeElement', diagramLabelElement);
422
456
  DOMPurify.removeHook('uponSanitizeAttribute', diagramLabelAttribute);
@@ -426,6 +460,37 @@ function sanitize(value, context, fragment) {
426
460
  }
427
461
  }
428
462
 
463
+ let scratchSink = null;
464
+ function ensureScratchParseHook() {
465
+ if (typeof DOMParser !== 'function' || DOMParser.prototype.parseFromString._rapierScratch) return;
466
+ const original = DOMParser.prototype.parseFromString;
467
+ function wrapped(source, type) {
468
+ const doc = original.call(this, source, type);
469
+ scratchSink?.push(doc);
470
+ return doc;
471
+ }
472
+ wrapped._rapierScratch = true;
473
+ DOMParser.prototype.parseFromString = wrapped;
474
+ }
475
+
476
+ function releaseScratchMedia(docs, result) {
477
+ const kept = result && typeof result === 'object' ? result.nodeType === 9 ? result : result.ownerDocument : null;
478
+ for (const doc of docs) {
479
+ if (!doc || doc === kept || !doc.querySelectorAll) continue;
480
+ for (const el of [...doc.querySelectorAll('video, audio')]) releaseMediaElement(el);
481
+ }
482
+ }
483
+ function releaseMediaElement(el) {
484
+ if (!el || (el.nodeName !== 'VIDEO' && el.nodeName !== 'AUDIO')) return;
485
+ try {
486
+ el.pause();
487
+ el.removeAttribute('src');
488
+ for (const source of el.querySelectorAll('source')) source.removeAttribute('src');
489
+ el.load();
490
+ } catch (_) {}
491
+ el.remove();
492
+ }
493
+
429
494
  function escapeRapierHtmlText(value) {
430
495
  return String(value == null ? '' : value)
431
496
  .replace(/&/g, '&amp;')
@@ -725,7 +725,7 @@ function* _rapierSharedPageLayoutSteps(root) {
725
725
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
726
726
  yield;
727
727
  const layout = globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
728
- if (layout?.wrap !== 'around' && layout?.wrap !== 'box') continue;
728
+ if (layout?.wrap !== 'around' && layout?.wrap !== 'beside' && layout?.wrap !== 'box') continue;
729
729
  const paragraph = _rapierSharePictureParagraph(image);
730
730
  if (!paragraph) continue;
731
731
  const owner = globalThis.RapierMarkdownLayout.wrapNeighbour(paragraph, _rapierShareWrapKind);
@@ -1458,6 +1458,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1458
1458
  if (layout.wrap === 'box') {
1459
1459
  value = boxProfile(image);
1460
1460
  if (!value && rotateRad) value = geometry.rasterTiltProfile(image.naturalWidth, image.naturalHeight, rotateRad);
1461
+ } else if (layout.wrap === 'beside') {
1462
+ value = rotateRad ? geometry.rasterTiltProfile(image.naturalWidth, image.naturalHeight, rotateRad) : null;
1461
1463
  } else {
1462
1464
  const occupancy = typeof geometry.parseProfile === 'function' && geometry.parseProfile(image.getAttribute('data-rapier-occupancy'));
1463
1465
  value = occupancy || geometry.alphaProfile(image);
@@ -1481,7 +1483,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1481
1483
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
1482
1484
  if (exclude.has(image) || !image.complete || !image.naturalWidth || !image.naturalHeight) continue;
1483
1485
  const layout = metadata.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
1484
- if (!layout?.rotate || ['around', 'box', 'behind', 'front'].includes(layout.wrap)) continue;
1486
+ if (!layout?.rotate || ['around', 'beside', 'box', 'behind', 'front'].includes(layout.wrap)) continue;
1485
1487
  const width = image.offsetWidth, height = image.offsetHeight;
1486
1488
  if (!(width > 0 && height > 0)) continue;
1487
1489
  const paragraph = image.closest('p');
@@ -1619,7 +1621,7 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1619
1621
  const origin = box(root).top, obstacles = [], anchors = new Map(), wrapped = new Set(), placed = [], positionedImages = new Set();
1620
1622
  for (const element of children) {
1621
1623
  const layout = metadata.parseLayoutAttribute(element.getAttribute('data-md-layout'));
1622
- const image = ['around', 'box', 'behind', 'front'].includes(layout?.wrap) && element.tagName === 'P'
1624
+ const image = ['around', 'beside', 'box', 'behind', 'front'].includes(layout?.wrap) && element.tagName === 'P'
1623
1625
  ? element.querySelector('img[data-rapier-image-layout]') : null;
1624
1626
  if (!image?.complete || !image.naturalWidth || !image.naturalHeight || !imageOnly(element, image)) continue;
1625
1627
 
@@ -1665,7 +1667,9 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, preparation) {
1665
1667
  obstacle.y + obstacle.height > y - room && obstacle.y < y + rectangle.height + room)
1666
1668
  y = obstacle.y + obstacle.height + room;
1667
1669
  }
1668
- obstacles.push(...geometry.linesSlices(geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height, scale), layout, lines, y, rectangle.height));
1670
+ let slices = geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height, scale);
1671
+ if (layout.wrap === 'beside') slices = geometry.besideSlices(slices, bounds.left + inset, bounds.left + inset + width, left, rectangle.width);
1672
+ obstacles.push(...geometry.linesSlices(slices, layout, lines, y, rectangle.height));
1669
1673
  }
1670
1674
  placed.push({source, image, rectangle, left, y, wrap: layout.wrap,
1671
1675
  visualLeft: left + visualDeltaX, visualTop: y + visualDeltaY, visualWidth: fit.fit.width, visualHeight: fit.fit.height,
@@ -1746,7 +1750,7 @@ function _rapierArtifactLayoutScript(root, nonce = '', {pictures = true} = {}) {
1746
1750
  const dependencies = globalThis.RapierArtifactLayoutDependencies;
1747
1751
  const layoutOf = element => globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'));
1748
1752
  const pictureLayoutOf = image => globalThis.RapierMarkdownLayout.parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
1749
- const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
1753
+ const wrapping = pictures && ([...root.querySelectorAll('p[data-md-layout]')].some(element => ['around', 'beside', 'box', 'behind', 'front'].includes(layoutOf(element)?.wrap)) ||
1750
1754
  [...root.querySelectorAll('img[data-rapier-image-layout]')].some(image => pictureLayoutOf(image)?.rotate));
1751
1755
  const typography = _rapierArtifactTypographyScript(root);
1752
1756
  if (!wrapping && !typography) return '';
@@ -309,6 +309,19 @@ export function pictureSlices(profile, x, y, width, height, scale = 1) {
309
309
  return slices;
310
310
  }
311
311
 
312
+ export function besideSlices(slices, columnLeft, columnRight, pictureLeft, pictureWidth) {
313
+ if (!Array.isArray(slices) || !(columnRight > columnLeft) || !(pictureWidth > 0) || !finite(pictureLeft)) return slices;
314
+ const blockLeft = pictureLeft + pictureWidth / 2 <= (columnLeft + columnRight) / 2;
315
+ return slices.map(slice => {
316
+ if (!slice || !finite(slice.x) || !finite(slice.width)) return slice;
317
+ if (blockLeft) {
318
+ const right = slice.x + slice.width;
319
+ return {...slice, x: columnLeft, width: Math.max(0, right - columnLeft)};
320
+ }
321
+ return {...slice, width: Math.max(0, columnRight - slice.x)};
322
+ });
323
+ }
324
+
312
325
  export {slotSequence, columnsFilled};
313
326
  export function slotsForBand(width, obstacles, top, height, minWidth = 40) {
314
327
  if (!finite(width) || width <= 0 || !finite(top) || !finite(height) || height <= 0 ||
@@ -3,7 +3,7 @@
3
3
  export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'marker', 'lock', 'hyphens', 'caps', 'hanging', 'before', 'after', 'line']);
4
4
  export const SPACING_FIELDS = Object.freeze(['before', 'after', 'line']);
5
5
  export const alignments = new Set(['left', 'center', 'right', 'justify']);
6
- export const wraps = new Set(['around', 'box', 'behind', 'front']);
6
+ export const wraps = new Set(['around', 'beside', 'box', 'behind', 'front']);
7
7
  const has = (value, key) => Object.hasOwn(value, key);
8
8
  const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
9
9
  const lineCount = value => Number.isInteger(value) && value >= 1 && value <= 12;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.43",
3
+ "version": "1.2.45",
4
4
  "description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
5
5
  "keywords": [
6
6
  "markdown",