rapier-markdown-kit 1.1.90 → 1.1.92

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.
@@ -1,4 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
+ import {finish, finishAsync, cloneTree, cleanTree, serializeTree, htmlBoundary, pause, check} from './render-work.mjs';
2
3
  const RAPIER_TABLE_CAPTION_PREFIX_RE = /^(?:Table:|:) /;
3
4
  const RAPIER_PORTABLE_HIGHLIGHT_HEX = Object.freeze({
4
5
  green: '#35c57a', red: '#f05a62', blue: '#4f91f7', yellow: '#e6b91e', purple: '#9b6dea',
@@ -383,6 +384,56 @@ function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
383
384
  return root;
384
385
  }
385
386
 
387
+ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {}) {
388
+ const work = options.work || options;
389
+ check(work);
390
+ if (metadata.docKind !== 'markdown' || !md) {
391
+ const root = _rapierRenderSemanticRoot(canonical, metadata, options);
392
+ await pause(work, .5);
393
+ return root;
394
+ }
395
+ const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
396
+ const fallbackEnv = options.page ? {rapierPage: true} : {};
397
+ const parsed = options.parsed || (runtime.parseAsync ? await runtime.parseAsync(markdown, options) : {tokens: md.parse(markdown, fallbackEnv), env: fallbackEnv});
398
+ const {tokens, env} = parsed, labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
399
+ const root = document.createElement('div'), part = document.createElement('div'), boundary = htmlBoundary();
400
+ root.setAttribute('data-rapier-semantic-root', 'true');
401
+ let html = '', began = performance.now();
402
+ const append = () => {
403
+ if (md.renderer.rapierTransformHtml) html = md.renderer.rapierTransformHtml(html);
404
+ if (labels?.length) html = _relabelFootnoteHtml(html, labels);
405
+ part.innerHTML = sanitizeRapierHtml(html, options.page ? 'source' : 'export');
406
+ while (part.firstChild) root.appendChild(part.firstChild);
407
+ html = '';
408
+ };
409
+ for (let index = 0; index < tokens.length; index++) {
410
+ const token = tokens[index], rule = md.renderer.rules[token.type];
411
+ let rendered = token.type === 'inline' ? md.renderer.renderInline(token.children, md.options, env)
412
+ : rule ? rule(tokens, index, md.options, env, md.renderer) : md.renderer.renderToken(tokens, index, md.options);
413
+ html += rendered;
414
+ if (boundary(rendered) && html.length >= 16384) append();
415
+ if (performance.now() - began >= 8) {
416
+ await pause(work, .3 + .2 * (index + 1) / tokens.length);
417
+ began = performance.now();
418
+ }
419
+ }
420
+ if (html) append();
421
+ await finishAsync(_rapierSemanticDecorations(root), work);
422
+ await pause(work, .6);
423
+ return root;
424
+ }
425
+
426
+ function* _rapierSemanticDecorations(root) {
427
+ yield* _rapierApplyBlockDirectionSteps(root); yield;
428
+ yield* _rapierAssignHeadingSlugsSteps(root, RAPIER_RENDERED_HEADING_SELECTOR, false); yield;
429
+ yield* _rapierDisambiguateRenderedAnchorsSteps(root); yield;
430
+ for (const code of root.querySelectorAll('pre > code')) { _rapierNormalizeCodeElement(code); yield; }
431
+ yield* _rapierMarkTableCaptionsSteps(root); yield;
432
+ yield* _rapierMarkFigureCaptionsSteps(root); yield;
433
+ yield* _rapierMarkListContinuationsSteps(Array.from(root.children)); yield;
434
+ yield* _rapierDecorateJumpListsSteps(root); yield;
435
+ }
436
+
386
437
  function _rapierInstallSemanticProbe(instance) {
387
438
  if (!instance || instance.__rapierSemanticProbeInstalled) return;
388
439
  instance.__rapierSemanticProbeInstalled = true;
@@ -584,11 +635,21 @@ function _rapierRawHtmlFragment(source, rewrite, parser) {
584
635
  return out;
585
636
  }
586
637
 
587
- function _rapierApplyBlockDirection(root) {
638
+ function _rapierApplyBlockDirection(root) { return finish(_rapierApplyBlockDirectionSteps(root)); }
639
+
640
+ function* _rapierApplyBlockDirectionSteps(root) {
588
641
  if (!root || typeof root.querySelectorAll !== 'function') return;
589
642
  for (const el of root.querySelectorAll(_RAPIER_DIR_BLOCKS)) {
643
+ yield;
590
644
  if (el.closest('pre, code')) continue;
591
- const dir = _rapierFirstStrongDir(el.textContent);
645
+ let dir = 'ltr', found = false;
646
+ const walker = el.ownerDocument.createTreeWalker(el, 4);
647
+ for (let text = walker.nextNode(); text && !found; text = walker.nextNode()) {
648
+ for (const character of text.nodeValue || '') {
649
+ const strong = _rapierBidiStrong(character.codePointAt(0));
650
+ if (strong) { dir = strong === 'R' ? 'rtl' : 'ltr'; found = true; break; }
651
+ }
652
+ }
592
653
  if (el.getAttribute('dir') !== dir) el.setAttribute('dir', dir);
593
654
  }
594
655
  }
@@ -607,35 +668,39 @@ function _rapierFirstStrongDir(text) {
607
668
 
608
669
  const _RAPIER_DIR_BLOCKS = 'p,h1,h2,h3,h4,h5,h6,li,blockquote,ul,ol,dt,dd,td,th';
609
670
 
610
- function _rapierAssignHeadingSlugs(root, selector = RAPIER_RENDERED_HEADING_SELECTOR, reserveExisting = true) {
671
+ function _rapierAssignHeadingSlugs(root, selector, reserveExisting) { return finish(_rapierAssignHeadingSlugsSteps(root, selector, reserveExisting)); }
672
+
673
+ function* _rapierAssignHeadingSlugsSteps(root, selector = RAPIER_RENDERED_HEADING_SELECTOR, reserveExisting = true) {
611
674
  if (!root || !root.querySelectorAll) return;
612
675
  const used = Object.create(null);
613
676
  const headings = Array.from(root.querySelectorAll(selector)).concat(_rapierDormantHeadings(root, selector));
614
677
  const headingSet = new Set(headings);
615
- if (reserveExisting) (root.isConnected ? document : root).querySelectorAll('[id]').forEach(element => {
678
+ if (reserveExisting) for (const element of (root.isConnected ? document : root).querySelectorAll('[id]')) { yield;
616
679
  if (!headingSet.has(element) && element.id) used[element.id] = 0;
617
- });
618
- headings.forEach(heading => {
680
+ }
681
+ for (const heading of headings) { yield;
619
682
  heading.id = _rapierNextHeadingSlug(_rapierHeadingSlugBase(heading.textContent), used);
620
- });
683
+ }
621
684
  }
622
685
 
623
- function _rapierDisambiguateRenderedAnchors(root) {
686
+ function _rapierDisambiguateRenderedAnchors(root) { return finish(_rapierDisambiguateRenderedAnchorsSteps(root)); }
687
+
688
+ function* _rapierDisambiguateRenderedAnchorsSteps(root) {
624
689
  if (!root || !root.querySelectorAll) return;
625
690
  const headings = new Set(root.querySelectorAll(RAPIER_RENDERED_HEADING_SELECTOR));
626
691
  const candidates = Array.from(root.querySelectorAll('[id]'))
627
692
  .filter(element => !headings.has(element));
628
- candidates.forEach(element => {
693
+ for (const element of candidates) { yield;
629
694
  const original = _rapierRenderedAnchorOriginalIds.get(element);
630
695
  if (original) element.id = original;
631
- });
696
+ }
632
697
 
633
698
  const groups = new Map();
634
- candidates.forEach(element => {
635
- if (!element.id) return;
699
+ for (const element of candidates) { yield;
700
+ if (!element.id) continue;
636
701
  if (!groups.has(element.id)) groups.set(element.id, []);
637
702
  groups.get(element.id).push(element);
638
- });
703
+ }
639
704
  const used = new Set(Array.from(headings).map(heading => heading.id).filter(Boolean));
640
705
  const allocate = original => {
641
706
  const compact = String(original || '').replace(/[^a-z0-9_-]+/gi, '-')
@@ -649,7 +714,27 @@ function _rapierDisambiguateRenderedAnchors(root) {
649
714
  return next;
650
715
  };
651
716
 
652
- groups.forEach((elements, original) => {
717
+ const indexes = new Map();
718
+ const add = (map, key, link) => { if (!map.has(key)) map.set(key, new Set()); map.get(key).add(link); };
719
+ for (const selector of ['.footnote-ref a[href^="#"]', '.footnote-backref[href^="#"]']) {
720
+ const index = {hash: new Map(), label: new Map(), order: new Map()};
721
+ for (const link of root.querySelectorAll(selector)) {
722
+ index.order.set(link, index.order.size);
723
+ add(index.hash, _rapierDecodeHash(link.getAttribute('href')), link);
724
+ add(index.label, link.getAttribute('data-footnote-label'), link);
725
+ yield;
726
+ }
727
+ indexes.set(selector, index);
728
+ }
729
+ const moveLink = (link, id) => {
730
+ const before = _rapierDecodeHash(link.getAttribute('href'));
731
+ link.setAttribute('href', '#' + id);
732
+ const after = _rapierDecodeHash(link.getAttribute('href'));
733
+ for (const index of indexes.values()) if (index.order.has(link)) {
734
+ index.hash.get(before)?.delete(link); add(index.hash, after, link);
735
+ }
736
+ };
737
+ for (const [original, elements] of groups.entries()) { yield;
653
738
  const explicitFootnotes = elements.filter(element =>
654
739
  element.classList.contains('footnote-item') && element.hasAttribute('data-footnote-label'));
655
740
  const footnoteRefs = elements.filter(element =>
@@ -662,26 +747,26 @@ function _rapierDisambiguateRenderedAnchors(root) {
662
747
  winner.id = winnerId;
663
748
  if (winnerId !== original) _rapierRenderedAnchorOriginalIds.set(winner, original);
664
749
 
665
- elements.forEach(element => {
666
- if (element === winner) return;
750
+ for (const element of elements) { yield;
751
+ if (element === winner) continue;
667
752
  const alias = allocate(original);
668
753
  element.id = alias;
669
754
  _rapierRenderedAnchorOriginalIds.set(element, original);
670
- });
755
+ }
671
756
 
672
757
  const labels = new Set(elements.map(element => element.getAttribute('data-footnote-label'))
673
758
  .filter(label => label != null));
674
- const matchingHashLinks = selector => Array.from(root.querySelectorAll(selector))
675
- .filter(link => _rapierDecodeHash(link.getAttribute('href')) === original ||
676
- (labels.size && labels.has(link.getAttribute('data-footnote-label'))));
759
+ const matchingHashLinks = selector => {
760
+ const index = indexes.get(selector), links = new Set(index.hash.get(original));
761
+ for (const label of labels) for (const link of index.label.get(label) || []) links.add(link);
762
+ return [...links].sort((a, b) => index.order.get(a) - index.order.get(b));
763
+ };
677
764
  if (elements.some(element => element.classList.contains('footnote-item'))) {
678
- matchingHashLinks('.footnote-ref a[href^="#"]')
679
- .forEach(link => link.setAttribute('href', '#' + winnerId));
765
+ for (const link of matchingHashLinks('.footnote-ref a[href^="#"]')) { yield;moveLink(link, winnerId);}
680
766
  } else if (elements.some(element => element.closest && element.closest('.footnote-ref'))) {
681
- matchingHashLinks('.footnote-backref[href^="#"]')
682
- .forEach(link => link.setAttribute('href', '#' + winnerId));
767
+ for (const link of matchingHashLinks('.footnote-backref[href^="#"]')) { yield;moveLink(link, winnerId);}
683
768
  }
684
- });
769
+ }
685
770
  }
686
771
 
687
772
  function _rapierDecodeHash(hash) {
@@ -695,11 +780,13 @@ function _rapierDecodeHash(hash) {
695
780
  catch (_) { return raw; }
696
781
  }
697
782
 
698
- function _rapierDecorateJumpLists(root) {
699
- root.querySelectorAll('ul').forEach(list => {
700
- if (list.closest('li')) return;
783
+ function _rapierDecorateJumpLists(root) { return finish(_rapierDecorateJumpListsSteps(root)); }
784
+
785
+ function* _rapierDecorateJumpListsSteps(root) {
786
+ for (const list of root.querySelectorAll('ul')) { yield;
787
+ if (list.closest('li')) continue;
701
788
  const items = Array.from(list.children).filter(item => item.tagName === 'LI');
702
- if (items.length < 2 || items.length !== list.children.length) return;
789
+ if (items.length < 2 || items.length !== list.children.length) continue;
703
790
  const isWayfinder = items.every(item => {
704
791
  if (item.querySelector('ul,ol')) return false;
705
792
  const links = Array.from(item.querySelectorAll(':scope > a[href^="#"]'));
@@ -716,7 +803,7 @@ function _rapierDecorateJumpLists(root) {
716
803
  label.classList.add('rapier-jump-label');
717
804
  }
718
805
  }
719
- });
806
+ }
720
807
 
721
808
  const only = root.children && root.children.length === 1 ? root.firstElementChild : null;
722
809
  if (only && only.tagName === 'P' && /^jump\s+to:?$/i.test(String(only.textContent || '').trim())) {
@@ -724,37 +811,43 @@ function _rapierDecorateJumpLists(root) {
724
811
  }
725
812
  }
726
813
 
727
- function _rapierMarkFigureCaptions(root) {
814
+ function _rapierMarkFigureCaptions(root) { return finish(_rapierMarkFigureCaptionsSteps(root)); }
815
+
816
+ function* _rapierMarkFigureCaptionsSteps(root) {
728
817
  // Same path as a table caption: an ordinary paragraph after the picture, still the source line.
729
818
  const figureCaption = /^Figure: /;
730
- root.querySelectorAll('p').forEach(paragraph => {
731
- if (!figureCaption.test(paragraph.textContent || '')) return;
819
+ for (const paragraph of root.querySelectorAll('p')) { yield;
820
+ if (!figureCaption.test(paragraph.textContent || '')) continue;
732
821
  const previous = paragraph.previousElementSibling;
733
- if (!previous || previous.tagName !== 'P') return;
822
+ if (!previous || previous.tagName !== 'P') continue;
734
823
  const images = previous.querySelectorAll('img');
735
- if (images.length !== 1 || previous.textContent.trim()) return;
824
+ if (images.length !== 1 || previous.textContent.trim()) continue;
736
825
  paragraph.classList.add('rapier-figure-caption');
737
- });
826
+ }
738
827
  }
739
828
 
740
- function _rapierMarkListContinuations(blocks) {
829
+ function _rapierMarkListContinuations(blocks) { return finish(_rapierMarkListContinuationsSteps(blocks)); }
830
+
831
+ function* _rapierMarkListContinuationsSteps(blocks) {
741
832
  const isHeading = element => !!element && /^H[1-6]$/.test(element.tagName);
742
- blocks.forEach((element, index) => {
743
- if (!element || element.tagName !== 'OL') return;
833
+ for (const [index, element] of blocks.entries()) { yield;
834
+ if (!element || element.tagName !== 'OL') continue;
744
835
  const start = parseInt(element.getAttribute('start'), 10);
745
836
  const last = (Number.isFinite(start) ? start : 1) + element.querySelectorAll(':scope > li').length - 1;
746
837
  const headings = [];
747
838
  let next = index + 1;
748
839
  while (isHeading(blocks[next])) headings.push(blocks[next++]);
749
840
  const following = blocks[next];
750
- if (!headings.length || !following || following.tagName !== 'OL') return;
751
- if (parseInt(following.getAttribute('start'), 10) !== last + 1) return;
841
+ if (!headings.length || !following || following.tagName !== 'OL') continue;
842
+ if (parseInt(following.getAttribute('start'), 10) !== last + 1) continue;
752
843
  element.setAttribute('data-rapier-list-continues', '');
753
- headings.forEach(heading => heading.setAttribute('data-rapier-list-through', ''));
754
- });
844
+ for (const heading of headings) { yield;heading.setAttribute('data-rapier-list-through', '');}
845
+ }
755
846
  }
756
847
 
757
- function _rapierMarkTableCaptions(root) {
848
+ function _rapierMarkTableCaptions(root) { return finish(_rapierMarkTableCaptionsSteps(root)); }
849
+
850
+ function* _rapierMarkTableCaptionsSteps(root) {
758
851
  // The source convention is untouched: the caption stays an ordinary trailing paragraph
759
852
  // (docs/markdown-standard.md). Only this rendered projection associates it with its table —
760
853
  // via a generated id plus aria-describedby, not a real <caption>, which would have to move the
@@ -762,10 +855,10 @@ function _rapierMarkTableCaptions(root) {
762
855
  // passes (heading slugs, portable export) rely on for this same root.
763
856
  const used = new Set(Array.from(root.querySelectorAll('[id]'), element => element.id));
764
857
  let ordinal = 0;
765
- root.querySelectorAll('table').forEach(table => {
858
+ for (const table of root.querySelectorAll('table')) { yield;
766
859
  const sibling = table.nextElementSibling;
767
- if (!sibling || sibling.tagName !== 'P') return;
768
- if (!RAPIER_TABLE_CAPTION_PREFIX_RE.test(sibling.textContent || '')) return;
860
+ if (!sibling || sibling.tagName !== 'P') continue;
861
+ if (!RAPIER_TABLE_CAPTION_PREFIX_RE.test(sibling.textContent || '')) continue;
769
862
  sibling.classList.add('rapier-table-caption');
770
863
  let id;
771
864
  do { id = 'rapier-table-caption-' + (++ordinal); } while (used.has(id));
@@ -773,7 +866,7 @@ function _rapierMarkTableCaptions(root) {
773
866
  sibling.id = id;
774
867
  const existing = (table.getAttribute('aria-describedby') || '').trim();
775
868
  table.setAttribute('aria-describedby', existing ? existing + ' ' + id : id);
776
- });
869
+ }
777
870
  }
778
871
 
779
872
 
@@ -870,13 +963,15 @@ function _rapierMathSource(node) {
870
963
  return String((placeholder || node).textContent || '').trim();
871
964
  }
872
965
 
873
- function _rapierFlattenPortableDetails(root) {
966
+ function _rapierFlattenPortableDetails(root) { return finish(_rapierFlattenPortableDetailsSteps(root)); }
967
+
968
+ function* _rapierFlattenPortableDetailsSteps(root) {
874
969
  const blockTags = new Set([
875
970
  'ADDRESS','ARTICLE','ASIDE','BLOCKQUOTE','DIV','DL','FIELDSET','FIGURE',
876
971
  'FOOTER','FORM','H1','H2','H3','H4','H5','H6','HEADER','HR','MAIN',
877
972
  'NAV','OL','P','PRE','SECTION','TABLE','UL',
878
973
  ]);
879
- Array.from(root.querySelectorAll('details')).reverse().forEach(details => {
974
+ for (const details of Array.from(root.querySelectorAll('details')).reverse()) { yield;
880
975
  const summary = Array.from(details.children).find(child => child.tagName === 'SUMMARY');
881
976
  const section = document.createElement('section');
882
977
  if (details.id) section.id = details.id;
@@ -891,10 +986,10 @@ function _rapierFlattenPortableDetails(root) {
891
986
  }
892
987
 
893
988
  let paragraph = null;
894
- Array.from(details.childNodes).forEach(child => {
895
- if (child === summary) return;
989
+ for (const child of Array.from(details.childNodes)) { yield;
990
+ if (child === summary) continue;
896
991
  const isText = child.nodeType === Node.TEXT_NODE;
897
- if (isText && !String(child.nodeValue || '').trim()) return;
992
+ if (isText && !String(child.nodeValue || '').trim()) continue;
898
993
  const isBlock = child.nodeType === Node.ELEMENT_NODE && blockTags.has(child.tagName);
899
994
  if (!isBlock) {
900
995
  if (!paragraph) {
@@ -902,16 +997,19 @@ function _rapierFlattenPortableDetails(root) {
902
997
  section.appendChild(paragraph);
903
998
  }
904
999
  paragraph.appendChild(child);
905
- return;
1000
+ continue;
906
1001
  }
907
1002
  paragraph = null;
908
1003
  section.appendChild(child);
909
- });
1004
+ }
910
1005
  details.replaceWith(section);
911
- });
1006
+ }
912
1007
  }
913
1008
 
914
- function _rapierProjectPortableRoot(semanticRoot, options) {
1009
+ function _rapierProjectPortableRoot(semanticRoot, options) { return finish(_rapierProjectPortableRootSteps(semanticRoot, options)); }
1010
+ async function _rapierProjectPortableRootAsync(semanticRoot, options = {}) { return finishAsync(_rapierProjectPortableRootSteps(semanticRoot, options), options.work); }
1011
+
1012
+ function* _rapierProjectPortableRootSteps(semanticRoot, options) {
915
1013
  const opts = options || {};
916
1014
  // This projection is HTML-shaped (width baked into a CSS `style`, `data-*` stripped) for the
917
1015
  // consumers that read HTML -- publishing/fragment export, TXT. DOCX is not an HTML consumer: its
@@ -921,9 +1019,9 @@ function _rapierProjectPortableRoot(semanticRoot, options) {
921
1019
  // the semantic root carries them. Ordinary paragraph/list CSS measurements also reach their
922
1020
  // native reader; no derived private CSS is needed.
923
1021
  const keepLayoutData = !!opts.keepLayoutData;
924
- const root = semanticRoot.cloneNode(true), nativeElements = new Set();
1022
+ const root = yield* cloneTree(semanticRoot), nativeElements = new Set();
925
1023
  // A source soft break is a reading space, never the editor's invisible token.
926
- _rapierResolveSoftBreakTokens(root);
1024
+ yield* _rapierResolveSoftBreakTokensSteps(root);
927
1025
  if (opts.keepDiagrams) for (const figure of root.querySelectorAll('.diagram-block[data-diagram-native]')) {
928
1026
  const svg = figure.querySelector('.diagram-cache > svg.rapier-native-flowchart');
929
1027
  if (figure.getAttribute('data-diagram-state') === 'ready' && svg) {
@@ -933,69 +1031,70 @@ function _rapierProjectPortableRoot(semanticRoot, options) {
933
1031
  }
934
1032
  }
935
1033
 
936
- root.querySelectorAll('blockquote').forEach(blockquote => {
1034
+ for (const blockquote of root.querySelectorAll('blockquote')) { yield;
937
1035
  const info = _rapierCalloutInfo(blockquote);
938
- if (!info) return;
1036
+ if (!info) continue;
939
1037
  if (info.existing) info.existing.remove();
940
1038
  else _rapierRemoveCalloutMarker(info.first);
941
1039
  const label = document.createElement('strong');
942
1040
  label.textContent = info.type.charAt(0) + info.type.slice(1).toLowerCase() + ': ';
943
1041
  info.first.insertBefore(label, info.first.firstChild);
944
- });
1042
+ }
945
1043
 
946
- root.querySelectorAll('input[type="checkbox"]').forEach(input => {
1044
+ for (const input of root.querySelectorAll('input[type="checkbox"]')) { yield;
947
1045
  const nextStartsWithSpace = input.nextSibling
948
1046
  && input.nextSibling.nodeType === Node.TEXT_NODE
949
1047
  && /^\s/.test(input.nextSibling.nodeValue || '');
950
1048
  input.replaceWith(document.createTextNode((input.checked ? '☒' : '☐') + (nextStartsWithSpace ? '' : ' ')));
951
- });
1049
+ }
952
1050
 
953
- root.querySelectorAll('.math-display-wrap').forEach(wrapper => {
1051
+ for (const wrapper of root.querySelectorAll('.math-display-wrap')) { yield;
954
1052
  const source = _rapierMathSource(wrapper);
955
1053
  const p = document.createElement('p');
956
1054
  const code = document.createElement('code');
957
1055
  code.textContent = source || '[equation]';
958
1056
  p.appendChild(code);
959
1057
  wrapper.replaceWith(p);
960
- });
961
- root.querySelectorAll('.math-rendered,.math-placeholder').forEach(math => {
1058
+ }
1059
+ for (const math of root.querySelectorAll('.math-rendered,.math-placeholder')) { yield;
962
1060
  const code = document.createElement('code');
963
1061
  code.textContent = _rapierMathSource(math) || '[equation]';
964
1062
  math.replaceWith(code);
965
- });
966
- root.querySelectorAll('math').forEach(math => {
1063
+ }
1064
+ for (const math of root.querySelectorAll('math')) { yield;
967
1065
  const code = document.createElement('code');
968
1066
  code.textContent = String(math.textContent || '').trim() || '[equation]';
969
1067
  math.replaceWith(code);
970
- });
1068
+ }
971
1069
 
972
- _rapierFlattenPortableDetails(root);
1070
+ yield* _rapierFlattenPortableDetailsSteps(root);
973
1071
 
974
- root.querySelectorAll('section.footnotes').forEach(section => {
975
- section.querySelectorAll('.footnotes-sep').forEach(node => node.remove());
1072
+ for (const section of root.querySelectorAll('section.footnotes')) { yield;
1073
+ for (const node of section.querySelectorAll('.footnotes-sep')) { yield;node.remove();}
976
1074
  if (!section.querySelector(':scope > h1,:scope > h2,:scope > h3')) {
977
1075
  const heading = document.createElement('h2');
978
1076
  heading.textContent = 'Footnotes';
979
1077
  section.insertBefore(heading, section.firstChild);
980
1078
  }
981
- });
1079
+ }
982
1080
 
983
- root.querySelectorAll('.table-scroll-wrap,.math-display-wrap').forEach(_rapierUnwrapElement);
1081
+ for (const item of root.querySelectorAll('.table-scroll-wrap,.math-display-wrap')) { yield; _rapierUnwrapElement(item); }
984
1082
 
985
- root.querySelectorAll('pre > code').forEach(_rapierNormalizeCodeElement);
1083
+ for (const item of root.querySelectorAll('pre > code')) { yield; _rapierNormalizeCodeElement(item); }
986
1084
 
987
- root.querySelectorAll('svg').forEach(svg => {
988
- if (nativeElements.has(svg)) return;
1085
+ for (const svg of root.querySelectorAll('svg')) { yield;
1086
+ if (nativeElements.has(svg)) continue;
989
1087
  const title = svg.getAttribute('aria-label')
990
1088
  || (svg.querySelector('title') && svg.querySelector('title').textContent)
991
1089
  || '';
992
1090
  svg.replaceWith(document.createTextNode(title ? '[Image: ' + title.trim() + ']' : '[Image]'));
993
- });
1091
+ }
994
1092
 
995
- _rapierPrefixPortableAnchors(root, opts.baseName);
1093
+ if (runtime.prefixAnchorsSteps) yield* runtime.prefixAnchorsSteps(root, opts.baseName);
1094
+ else _rapierPrefixPortableAnchors(root, opts.baseName);
996
1095
 
997
- root.querySelectorAll('*').forEach(element => {
998
- if (nativeElements.has(element)) return;
1096
+ for (const element of root.querySelectorAll('*')) { yield;
1097
+ if (nativeElements.has(element)) continue;
999
1098
  const lang = element.tagName === 'CODE' ? _rapierLanguageClass(element) : '';
1000
1099
  // The native note reader needs these transient roles to omit the renderer's
1001
1100
  // separator and return-link spacing before it compares source-owned review notes.
@@ -1017,15 +1116,15 @@ function _rapierProjectPortableRoot(semanticRoot, options) {
1017
1116
  const isTableCaption = element.tagName === 'P' && element.classList.contains('rapier-table-caption');
1018
1117
 
1019
1118
  const isPageBreak = element.hasAttribute('data-md-break');
1020
- Array.from(element.attributes).forEach(attribute => {
1119
+ for (const attribute of Array.from(element.attributes)) { yield;
1021
1120
  const name = attribute.name.toLowerCase();
1022
- if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-layout' || name === 'data-md-align' || name === 'data-rapier-ink')) return;
1121
+ if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-layout' || name === 'data-md-align' || name === 'data-rapier-ink')) continue;
1023
1122
  if (name === 'class' || name === 'style' || name.startsWith('data-')
1024
1123
  || name === 'contenteditable' || name === 'spellcheck'
1025
1124
  || name === 'tabindex' || name === 'disabled' || name === 'aria-disabled') {
1026
1125
  element.removeAttribute(attribute.name);
1027
1126
  }
1028
- });
1127
+ }
1029
1128
  if (footnoteRole) element.className = footnoteRole;
1030
1129
  if (lang) element.className = 'language-' + lang;
1031
1130
  for (const [property, value] of measurements) if (value) element.style.setProperty(property, value);
@@ -1044,16 +1143,15 @@ function _rapierProjectPortableRoot(semanticRoot, options) {
1044
1143
  p.setAttribute('style', 'page-break-before: always');
1045
1144
  element.replaceWith(p);
1046
1145
  }
1047
- });
1146
+ }
1048
1147
 
1049
- const clean = document.createElement('div');
1050
- clean.innerHTML = sanitizeRapierHtml(root.innerHTML, 'export');
1051
- return clean;
1148
+ return yield* cleanTree(root, sanitizeRapierHtml);
1052
1149
  }
1053
1150
 
1054
1151
  function _rapierPortableHtml(root) {
1055
1152
  return sanitizeRapierHtml(root ? root.innerHTML : '', 'export').trim();
1056
1153
  }
1154
+ async function _rapierPortableHtmlAsync(root, work) { return finishAsync(serializeTree(root, sanitizeRapierHtml, true), work); }
1057
1155
 
1058
1156
  function _rapierNormalizeInlineText(value) {
1059
1157
  return String(value || '')
@@ -1072,10 +1170,13 @@ function _rapierPictureWords(alt, src) {
1072
1170
 
1073
1171
  // Held Markdown and HTML pictures keep their addresses as inert data. Text export reads them before
1074
1172
  // the portable projection removes editor attributes, without restoring a fetching image source.
1075
- function _rapierHeldPicturesAsWords(root) {
1076
- const copy = root.cloneNode(true);
1077
- copy.querySelectorAll('span[data-rapier-remote-src],img[data-rapier-remote-src]').forEach(held => held.replaceWith(document.createTextNode(
1078
- _rapierPictureWords(String(held.getAttribute('data-rapier-remote-alt') || '').trim(), String(held.getAttribute('data-rapier-remote-src') || '').trim()))));
1173
+ function _rapierHeldPicturesAsWords(root) { return finish(_rapierHeldPicturesAsWordsSteps(root)); }
1174
+ async function _rapierHeldPicturesAsWordsAsync(root, work) { return finishAsync(_rapierHeldPicturesAsWordsSteps(root), work); }
1175
+
1176
+ function* _rapierHeldPicturesAsWordsSteps(root) {
1177
+ const copy = yield* cloneTree(root);
1178
+ for (const held of copy.querySelectorAll('span[data-rapier-remote-src],img[data-rapier-remote-src]')) { yield;held.replaceWith(document.createTextNode(
1179
+ _rapierPictureWords(String(held.getAttribute('data-rapier-remote-alt') || '').trim(), String(held.getAttribute('data-rapier-remote-src') || '').trim())));}
1079
1180
  return copy;
1080
1181
  }
1081
1182
 
@@ -1148,7 +1249,10 @@ function _rapierPortableTableText(table, options) {
1148
1249
  }).filter(Boolean).join('\n');
1149
1250
  }
1150
1251
 
1151
- function _rapierPortablePlainText(root, options) {
1252
+ function _rapierPortablePlainText(root, options) { return finish(_rapierPortablePlainTextSteps(root, options)); }
1253
+ async function _rapierPortablePlainTextAsync(root, options = {}) { return finishAsync(_rapierPortablePlainTextSteps(root, options), options.work); }
1254
+
1255
+ function* _rapierPortablePlainTextSteps(root, options) {
1152
1256
  const opts = options || {};
1153
1257
  const blocks = [];
1154
1258
  const blockTags = new Set([
@@ -1163,7 +1267,7 @@ function _rapierPortablePlainText(root, options) {
1163
1267
  if (value) blocks.push({ text: value, preserve: !!preserve });
1164
1268
  };
1165
1269
 
1166
- const collectChildren = parent => {
1270
+ const collectChildren = function* (parent) {
1167
1271
  let inline = [];
1168
1272
  const flush = () => {
1169
1273
  if (!inline.length) return;
@@ -1174,10 +1278,11 @@ function _rapierPortablePlainText(root, options) {
1174
1278
  const blankText = node => node.nodeType === Node.TEXT_NODE && !String(node.nodeValue || '').trim();
1175
1279
  const children = Array.from(parent.childNodes);
1176
1280
  const inTableRun = index => { for (let at = index - 1; at >= 0; at--) { if (!blankText(children[at])) return tablePart(children[at]); } return false; };
1177
- children.forEach((child, index) => {
1281
+ for (const [index, child] of children.entries()) {
1282
+ yield;
1178
1283
  if (tablePart(child)) {
1179
1284
  // The parts of a table a selection clipped out of it: one block of rows, whichever parts arrived.
1180
- if (inTableRun(index)) return;
1285
+ if (inTableRun(index)) continue;
1181
1286
  flush();
1182
1287
  const parts = [];
1183
1288
  for (let at = index; at < children.length && (tablePart(children[at]) || blankText(children[at])); at++) {
@@ -1185,10 +1290,10 @@ function _rapierPortablePlainText(root, options) {
1185
1290
  }
1186
1291
  pushBlock(parts.filter(Boolean).join('\n'), false);
1187
1292
  } else if (blankText(child) && inTableRun(index)) {
1188
- return;
1293
+ continue;
1189
1294
  } else if (child.nodeType === Node.ELEMENT_NODE && blockTags.has(child.tagName)) {
1190
1295
  flush();
1191
- collectBlock(child);
1296
+ yield* collectBlock(child);
1192
1297
  } else if (opts.visible && child.nodeType === Node.ELEMENT_NODE && child.tagName === 'CODE' && String(child.textContent || '').includes('\n')) {
1193
1298
  // Lines of a code block taken without their block: the code as shown, every space kept.
1194
1299
  flush();
@@ -1196,11 +1301,11 @@ function _rapierPortablePlainText(root, options) {
1196
1301
  } else {
1197
1302
  inline.push(child);
1198
1303
  }
1199
- });
1304
+ }
1200
1305
  flush();
1201
1306
  };
1202
1307
 
1203
- const collectBlock = node => {
1308
+ const collectBlock = function* (node) {
1204
1309
  if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
1205
1310
  const tag = node.tagName;
1206
1311
  if (/^H[1-6]$/.test(tag) || tag === 'P') {
@@ -1230,7 +1335,7 @@ function _rapierPortablePlainText(root, options) {
1230
1335
  }
1231
1336
  if (tag === 'BLOCKQUOTE') {
1232
1337
  const before = blocks.length;
1233
- collectChildren(node);
1338
+ yield* collectChildren(node);
1234
1339
  const quoted = blocks.splice(before).filter(block => !block.blank).map(block => block.text).join('\n\n');
1235
1340
  if (quoted) pushBlock(opts.visible ? quoted : quoted.split('\n').map(line => line ? '> ' + line : '>').join('\n'), true);
1236
1341
  return;
@@ -1251,16 +1356,17 @@ function _rapierPortablePlainText(root, options) {
1251
1356
  if (tag === 'DETAILS') {
1252
1357
  const summary = node.querySelector(':scope > summary');
1253
1358
  if (summary) pushBlock(_rapierNormalizeInlineText(_rapierPortableInlineText(summary, opts)), false);
1254
- Array.from(node.children).filter(child => child !== summary).forEach(collectBlock);
1359
+ for (const child of node.children) if (child !== summary) { yield; yield* collectBlock(child); }
1255
1360
  return;
1256
1361
  }
1257
- collectChildren(node);
1362
+ yield* collectChildren(node);
1258
1363
  };
1259
1364
 
1260
- collectChildren(root);
1365
+ yield* collectChildren(root);
1261
1366
  // Each block stands a blank line from the next, and each empty paragraph between them adds one newline more.
1262
1367
  let joined = '', blanks = 0;
1263
1368
  for (const block of blocks) {
1369
+ yield;
1264
1370
  if (block.blank) { blanks++; continue; }
1265
1371
  joined += (joined ? '\n\n' + '\n'.repeat(blanks) : '') + block.text;
1266
1372
  blanks = 0;
@@ -1277,6 +1383,16 @@ function _rapierPlainTextFile(context) {
1277
1383
  : context.canonical;
1278
1384
  }
1279
1385
 
1386
+ async function _rapierPlainTextFileAsync(context) {
1387
+ if (context.metadata.docKind !== 'markdown') return context.canonical;
1388
+ const work = context.work;
1389
+ const root = await _rapierHeldPicturesAsWordsAsync(context.semanticRoot, work);
1390
+ const portable = await _rapierProjectPortableRootAsync(root, {baseName: context.baseName, work});
1391
+ return _rapierPortablePlainTextAsync(portable, {finalNewline: true, work});
1392
+ }
1393
+
1394
+ async function _rapierCloneRootAsync(root, work) { return finishAsync(cloneTree(root), work); }
1395
+
1280
1396
  function _rapierSourceTokenValue(node) {
1281
1397
  if (!node?.hasAttribute('data-rapier-source') || !node.hasAttribute('data-rapier-visible') ||
1282
1398
  Array.from(node.childNodes).some(child => child.nodeType !== Node.TEXT_NODE)) return null;
@@ -1286,11 +1402,13 @@ function _rapierSourceTokenValue(node) {
1286
1402
  } catch (_) { return null; }
1287
1403
  }
1288
1404
 
1289
- function _rapierResolveSoftBreakTokens(root) {
1290
- root.querySelectorAll('.rapier-source-token--softbreak').forEach(token => {
1405
+ function _rapierResolveSoftBreakTokens(root) { return finish(_rapierResolveSoftBreakTokensSteps(root)); }
1406
+
1407
+ function* _rapierResolveSoftBreakTokensSteps(root) {
1408
+ for (const token of root.querySelectorAll('.rapier-source-token--softbreak')) { yield;
1291
1409
  if (_rapierSourceTokenValue(token) === '\n') token.replaceWith(document.createTextNode(' '));
1292
1410
  else token.replaceWith(...token.childNodes);
1293
- });
1411
+ }
1294
1412
  return root;
1295
1413
  }
1296
1414
 
@@ -1307,6 +1425,6 @@ function _rapierBidiStrong(cp) {
1307
1425
  return null;
1308
1426
  }
1309
1427
 
1310
- return {_rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
1428
+ return {_rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
1311
1429
  }
1312
1430
  export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};