postext 1.33.11 → 1.34.0

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 (140) hide show
  1. package/README.md +2 -2
  2. package/dist/__tests__/bundle/designOverflowPin.test.js +1 -1
  3. package/dist/__tests__/bundle/gridBreakRulesPin.test.js +2 -2
  4. package/dist/__tests__/bundle/shapingLanguagePin.test.js +3 -3
  5. package/dist/__tests__/bundle/taskBoxPin.test.js +1 -1
  6. package/dist/__tests__/bundle.test.js +1 -1
  7. package/dist/__tests__/codeBlocks.test.js +1 -1
  8. package/dist/__tests__/composed/corpus.test.d.ts +2 -0
  9. package/dist/__tests__/composed/corpus.test.d.ts.map +1 -0
  10. package/dist/__tests__/composed/corpus.test.js +33 -0
  11. package/dist/__tests__/composed/corpus.test.js.map +1 -0
  12. package/dist/__tests__/composed/slotChecks.test.d.ts +2 -0
  13. package/dist/__tests__/composed/slotChecks.test.d.ts.map +1 -0
  14. package/dist/__tests__/composed/slotChecks.test.js +148 -0
  15. package/dist/__tests__/composed/slotChecks.test.js.map +1 -0
  16. package/dist/__tests__/composedTemplates.test.d.ts +2 -0
  17. package/dist/__tests__/composedTemplates.test.d.ts.map +1 -0
  18. package/dist/__tests__/composedTemplates.test.js +349 -0
  19. package/dist/__tests__/composedTemplates.test.js.map +1 -0
  20. package/dist/__tests__/contentInConfig.test.d.ts +2 -0
  21. package/dist/__tests__/contentInConfig.test.d.ts.map +1 -0
  22. package/dist/__tests__/contentInConfig.test.js +201 -0
  23. package/dist/__tests__/contentInConfig.test.js.map +1 -0
  24. package/dist/__tests__/defaults/stripRoundTrip.test.js +3 -1
  25. package/dist/__tests__/defaults/stripRoundTrip.test.js.map +1 -1
  26. package/dist/__tests__/exports.test.js +22 -0
  27. package/dist/__tests__/exports.test.js.map +1 -1
  28. package/dist/__tests__/hardBreaks.test.js +1 -1
  29. package/dist/__tests__/headingInlineMarks.test.js +1 -1
  30. package/dist/__tests__/mathSizeMigration.test.js +2 -2
  31. package/dist/__tests__/parse/composedPages.test.d.ts +2 -0
  32. package/dist/__tests__/parse/composedPages.test.d.ts.map +1 -0
  33. package/dist/__tests__/parse/composedPages.test.js +323 -0
  34. package/dist/__tests__/parse/composedPages.test.js.map +1 -0
  35. package/dist/__tests__/parse/containers.test.js +2 -2
  36. package/dist/__tests__/parse/containers.test.js.map +1 -1
  37. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +1 -1
  38. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +1 -1
  39. package/dist/__tests__/pipeline/colonListRoom.test.js +1 -1
  40. package/dist/__tests__/pipeline/compoundBreaks.test.js +1 -1
  41. package/dist/__tests__/pipeline/dashBreaks.test.js +1 -1
  42. package/dist/__tests__/pipeline/headingSplitOrphans.test.js +1 -1
  43. package/dist/__tests__/pipeline/inlineFigureSpace.test.js +1 -1
  44. package/dist/__tests__/pipeline/paragraphContainerTail.test.js +1 -1
  45. package/dist/__tests__/pipeline/raggedOptimal.test.js +1 -1
  46. package/dist/__tests__/tocGroups.test.d.ts +2 -0
  47. package/dist/__tests__/tocGroups.test.d.ts.map +1 -0
  48. package/dist/__tests__/tocGroups.test.js +213 -0
  49. package/dist/__tests__/tocGroups.test.js.map +1 -0
  50. package/dist/__tests__/verseLines.test.js +1 -1
  51. package/dist/bundle/configVersion.d.ts +16 -5
  52. package/dist/bundle/configVersion.d.ts.map +1 -1
  53. package/dist/bundle/configVersion.js +17 -4
  54. package/dist/bundle/configVersion.js.map +1 -1
  55. package/dist/bundle/types.d.ts +4 -3
  56. package/dist/bundle/types.d.ts.map +1 -1
  57. package/dist/composed/document.d.ts +43 -0
  58. package/dist/composed/document.d.ts.map +1 -0
  59. package/dist/composed/document.js +88 -0
  60. package/dist/composed/document.js.map +1 -0
  61. package/dist/composed/slotChecks.d.ts +29 -0
  62. package/dist/composed/slotChecks.d.ts.map +1 -0
  63. package/dist/composed/slotChecks.js +109 -0
  64. package/dist/composed/slotChecks.js.map +1 -0
  65. package/dist/composed/templates.d.ts +133 -0
  66. package/dist/composed/templates.d.ts.map +1 -0
  67. package/dist/composed/templates.js +702 -0
  68. package/dist/composed/templates.js.map +1 -0
  69. package/dist/composed/types.d.ts +404 -0
  70. package/dist/composed/types.d.ts.map +1 -0
  71. package/dist/composed/types.js +2 -0
  72. package/dist/composed/types.js.map +1 -0
  73. package/dist/composed/validate.d.ts +26 -0
  74. package/dist/composed/validate.d.ts.map +1 -0
  75. package/dist/composed/validate.js +276 -0
  76. package/dist/composed/validate.js.map +1 -0
  77. package/dist/configWarnings.d.ts +19 -3
  78. package/dist/configWarnings.d.ts.map +1 -1
  79. package/dist/configWarnings.js +170 -40
  80. package/dist/configWarnings.js.map +1 -1
  81. package/dist/defaults/checks.d.ts +7 -0
  82. package/dist/defaults/checks.d.ts.map +1 -0
  83. package/dist/defaults/checks.js +49 -0
  84. package/dist/defaults/checks.js.map +1 -0
  85. package/dist/defaults/index.d.ts +1 -0
  86. package/dist/defaults/index.d.ts.map +1 -1
  87. package/dist/defaults/index.js +26 -0
  88. package/dist/defaults/index.js.map +1 -1
  89. package/dist/defaults/toc.d.ts +17 -2
  90. package/dist/defaults/toc.d.ts.map +1 -1
  91. package/dist/defaults/toc.js +53 -1
  92. package/dist/defaults/toc.js.map +1 -1
  93. package/dist/design/slots.d.ts +29 -0
  94. package/dist/design/slots.d.ts.map +1 -0
  95. package/dist/design/slots.js +48 -0
  96. package/dist/design/slots.js.map +1 -0
  97. package/dist/index.d.ts +17 -3
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +10 -1
  100. package/dist/index.js.map +1 -1
  101. package/dist/parse/blockParser.d.ts.map +1 -1
  102. package/dist/parse/blockParser.js +295 -36
  103. package/dist/parse/blockParser.js.map +1 -1
  104. package/dist/parse/index.d.ts +1 -1
  105. package/dist/parse/index.d.ts.map +1 -1
  106. package/dist/parse/index.js.map +1 -1
  107. package/dist/parse/indexMarks.d.ts.map +1 -1
  108. package/dist/parse/indexMarks.js +24 -0
  109. package/dist/parse/indexMarks.js.map +1 -1
  110. package/dist/parse/types.d.ts +95 -2
  111. package/dist/parse/types.d.ts.map +1 -1
  112. package/dist/pipeline/build.d.ts.map +1 -1
  113. package/dist/pipeline/build.js +178 -3
  114. package/dist/pipeline/build.js.map +1 -1
  115. package/dist/pipeline/config.d.ts.map +1 -1
  116. package/dist/pipeline/config.js +17 -2
  117. package/dist/pipeline/config.js.map +1 -1
  118. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  119. package/dist/pipeline/contentWarnings.js +73 -0
  120. package/dist/pipeline/contentWarnings.js.map +1 -1
  121. package/dist/pipeline/placeholders.d.ts +7 -0
  122. package/dist/pipeline/placeholders.d.ts.map +1 -1
  123. package/dist/pipeline/placeholders.js +41 -0
  124. package/dist/pipeline/placeholders.js.map +1 -1
  125. package/dist/pipeline/textWrap.js +1 -1
  126. package/dist/pipeline/textWrap.js.map +1 -1
  127. package/dist/pipeline/toc.d.ts +11 -1
  128. package/dist/pipeline/toc.d.ts.map +1 -1
  129. package/dist/pipeline/toc.js +147 -8
  130. package/dist/pipeline/toc.js.map +1 -1
  131. package/dist/types.d.ts +99 -0
  132. package/dist/types.d.ts.map +1 -1
  133. package/dist/util/closestKey.d.ts +4 -0
  134. package/dist/util/closestKey.d.ts.map +1 -0
  135. package/dist/util/closestKey.js +40 -0
  136. package/dist/util/closestKey.js.map +1 -0
  137. package/dist/vdt.d.ts +125 -7
  138. package/dist/vdt.d.ts.map +1 -1
  139. package/dist/vdt.js.map +1 -1
  140. package/package.json +1 -1
@@ -5,7 +5,7 @@
5
5
  * positions back into the original markdown.
6
6
  */
7
7
  import { attachEquationAnchors } from './equationLabels';
8
- import { parseAttrBlobStrict, parseDirectiveAttrs } from './attrs';
8
+ import { parseAttrBlobStrict, parseDirectiveAttrs, scanDirectiveAttrs } from './attrs';
9
9
  import { extractInlineMath, fixMathSourceMap, injectMathSpans } from './inlineMath';
10
10
  import { BREAK_PLACEHOLDER, TITLE_BREAK_RE, extractInlineChips, extractInlineFootnotes, extractInlineRefs, injectFootnoteSpans, extractInlineSwatches, injectChipSpans, injectRefSpans, injectSwatchSpans, parseInlineFormatting, protectCodeSpans, replaceBodyBreaks, BODY_BREAK_MARK, bodyBreakSpans, titleBreakIndices, trimSpans, extractInlineTabs, injectTabSpans } from './inlineFormatting';
11
11
  import { buildBlockMapping } from './sourceMapping';
@@ -24,12 +24,23 @@ const HEADING_RE = /^(#{1,6})\s+(.+)$/;
24
24
  const DIRECTIVE_RE = /^:::\s*([a-z][a-z0-9-]*)\s*(?:\{([^}]*)\})?\s*$/;
25
25
  /** A bare `:::` line: closes the innermost open container. */
26
26
  const CONTAINER_CLOSE_RE = /^:::\s*$/;
27
+ /** A composed page's opening fence (#854): four or more colons, `page`,
28
+ * its attributes (`::::page{template="…"}`). Three colons stay a comic
29
+ * page (`:::page{split}`). */
30
+ const PAGE_FENCE_RE = /^(:{4,})\s*page\s*(?:\{([^}]*)\})?\s*$/;
31
+ /** A bare fence of four or more colons: it closes the composed page opened
32
+ * with as many (#854). */
33
+ const LONG_CLOSE_RE = /^(:{4,})\s*$/;
34
+ /** A `::label[TEXT]{…}` line of a `:::figure` (#854). */
35
+ const LABEL_LINE_RE = /^::label\[/;
36
+ /** `at="62% 48%"`: a label's place in percents of the picture. */
37
+ const LABEL_AT_PERCENT_RE = /^\s*(-?\d+(?:\.\d+)?)\s*%\s+(-?\d+(?:\.\d+)?)\s*%\s*$/;
27
38
  /** A bayt's hemistichs cut at a `\\` with a space on each side (the
28
39
  * Wikisource markup, #378). */
29
40
  const HEMISTICH_WIKI_RE = /\s\\\\\s/;
30
41
  /** Set of directive names recognized today. Unknown names fall through to
31
42
  * paragraph-parsing and downstream warnings flag them. */
32
- export const KNOWN_DIRECTIVES = new Set(['pagebreak', 'numbering', 'columnbreak', 'space', 'toc', 'index', 'bibliography', 'references', 'verse', 'page', 'strip']);
43
+ export const KNOWN_DIRECTIVES = new Set(['pagebreak', 'numbering', 'columnbreak', 'space', 'toc', 'index', 'bibliography', 'references', 'verse', 'page', 'strip', 'figure']);
33
44
  /** Trailing `{key="value" …}` attribute block on a heading line, e.g.
34
45
  * `# Title {author="I. Zango"}`. The braces must be balanced (no nested
35
46
  * braces) and be the last thing on the line, after a space — or right
@@ -55,7 +66,45 @@ function headingAttrTokens(blob, spaced) {
55
66
  }
56
67
  /** Set of fenced-container names recognized today. A `:::name` line whose
57
68
  * name is a known container opens a block that runs until a bare `:::`. */
58
- export const KNOWN_CONTAINERS = new Set(['callout', 'paragraphs', 'part', 'columns', 'paper']);
69
+ export const KNOWN_CONTAINERS = new Set(['callout', 'paragraphs', 'part', 'columns', 'paper', 'frame']);
70
+ /** Absolute source range of each attribute value of a blob that starts at
71
+ * `blobStart` (a flag: its key). A repeated key keeps the last, as
72
+ * `parseDirectiveAttrs` does. */
73
+ function attrSourcesOf(raw, blobStart) {
74
+ const out = {};
75
+ for (const t of scanDirectiveAttrs(raw)) {
76
+ out[t.key] = t.valueStart !== undefined && t.valueEnd !== undefined
77
+ ? { sourceStart: blobStart + t.valueStart, sourceEnd: blobStart + t.valueEnd }
78
+ : { sourceStart: blobStart + t.start, sourceEnd: blobStart + t.end };
79
+ }
80
+ return out;
81
+ }
82
+ /** The slot a blob's `slot="…"` names (#854), or undefined when it names
83
+ * none (or an empty one). */
84
+ function slotOf(attrs, raw, blobStart) {
85
+ const name = attrs.slot?.trim();
86
+ if (!name)
87
+ return undefined;
88
+ const at = attrSourcesOf(raw, blobStart).slot;
89
+ return { name, sourceStart: at.sourceStart, sourceEnd: at.sourceEnd };
90
+ }
91
+ /** The offset just past the `]` closing the bracket opened right before
92
+ * `from` (nested brackets counted, a backslash escapes), or -1. */
93
+ function closingBracket(text, from) {
94
+ let depth = 1;
95
+ for (let k = from; k < text.length; k++) {
96
+ const c = text[k];
97
+ if (c === '\\') {
98
+ k++;
99
+ continue;
100
+ }
101
+ if (c === '[')
102
+ depth++;
103
+ else if (c === ']' && --depth === 0)
104
+ return k;
105
+ }
106
+ return -1;
107
+ }
59
108
  /** True for lines that end a paragraph run even without a blank line: any
60
109
  * `:::name` directive/container fence and the bare `:::` closing fence. */
61
110
  function isFenceLine(trimmed) {
@@ -599,8 +648,149 @@ function parseBlocks(markdown, options) {
599
648
  // Open fenced containers, innermost last. Each entry remembers what it
600
649
  // needs to emit the matching `containerEnd` (or an `unclosedContainer`
601
650
  // issue pointing at the opening line when EOF arrives first).
651
+ // A composed page's entry (#854) records how many colons opened it: only
652
+ // a bare fence of as many closes it, never a bare `:::`.
602
653
  const containerStack = [];
603
654
  let nextContainerId = 1;
655
+ /** Composed page fences read so far (`ComposedPageFence.fenceIndex`). */
656
+ let pageFences = 0;
657
+ /** Emit the `containerEnd` of the innermost open container (at
658
+ * `srcStart`…`srcEnd`), reporting it as unclosed when `unclosed`. */
659
+ const popContainer = (srcStart, srcEnd, unclosed) => {
660
+ const open = containerStack.pop();
661
+ blocks.push({
662
+ type: 'containerEnd',
663
+ text: '',
664
+ spans: [],
665
+ containerName: open.name,
666
+ containerId: open.id,
667
+ sourceStart: srcStart,
668
+ sourceEnd: srcEnd,
669
+ sourceMap: [],
670
+ });
671
+ if (unclosed) {
672
+ issues.push({
673
+ kind: 'unclosedContainer',
674
+ delimiter: ':::',
675
+ containerName: open.name,
676
+ containerId: open.id,
677
+ sourceStart: open.sourceStart,
678
+ sourceEnd: open.sourceEnd,
679
+ });
680
+ }
681
+ };
682
+ /** Index in `containerStack` of the open composed page, or -1. */
683
+ const openPageAt = () => {
684
+ for (let k = containerStack.length - 1; k >= 0; k--)
685
+ if (containerStack[k].colons !== undefined)
686
+ return k;
687
+ return -1;
688
+ };
689
+ /** Whether line `k` (trimmed `pl`) is a composed page fence: an opening
690
+ * one, or the bare fence that closes the open page. */
691
+ const isPageFenceLine = (pl) => {
692
+ if (PAGE_FENCE_RE.test(pl))
693
+ return true;
694
+ const close = LONG_CLOSE_RE.exec(pl);
695
+ const at = close ? openPageAt() : -1;
696
+ return at >= 0 && containerStack[at].colons === close[1].length;
697
+ };
698
+ /**
699
+ * A `:::figure{resource="…" slot="…"}` whose fence is line `start`
700
+ * (#854): its `::label[…]{…}` lines up to the closing `:::`, as one
701
+ * `resourceBlock`. A line that is neither a label nor blank ends a figure
702
+ * left open (`unclosedContainer`), and is read as usual.
703
+ */
704
+ const parseFigure = (start, attrsRaw) => {
705
+ const attrs = parseDirectiveAttrs(attrsRaw);
706
+ const fenceLine = rawLines[start];
707
+ const blobStart = lineOffsets[start] + fenceLine.indexOf('{') + 1;
708
+ const attrSources = attrsRaw ? attrSourcesOf(attrsRaw, blobStart) : {};
709
+ const labels = [];
710
+ let k = start + 1;
711
+ let last = start;
712
+ let closed = false;
713
+ while (k < rawLines.length) {
714
+ const t = rawLines[k].trim();
715
+ if (CONTAINER_CLOSE_RE.test(t)) {
716
+ closed = true;
717
+ break;
718
+ }
719
+ if (t === '') {
720
+ k++;
721
+ continue;
722
+ }
723
+ const label = LABEL_LINE_RE.test(t) ? parseLabel(k) : undefined;
724
+ if (!label)
725
+ break;
726
+ labels.push(label);
727
+ last = k;
728
+ k++;
729
+ }
730
+ const slot = attrsRaw ? slotOf(attrs, attrsRaw, blobStart) : undefined;
731
+ blocks.push({
732
+ type: 'resourceBlock',
733
+ text: '',
734
+ spans: [],
735
+ resourceId: attrs.resource?.trim() ?? '',
736
+ figure: { attrs, attrSources, labels },
737
+ ...(slot ? { slot } : {}),
738
+ sourceStart: lineOffsets[start],
739
+ sourceEnd: closed ? lineEndOffset(k) : lineEndOffset(last),
740
+ sourceMap: [],
741
+ });
742
+ if (!closed) {
743
+ issues.push({
744
+ kind: 'unclosedContainer',
745
+ delimiter: ':::',
746
+ containerName: 'figure',
747
+ containerId: nextContainerId++,
748
+ sourceStart: lineOffsets[start],
749
+ sourceEnd: lineEndOffset(start),
750
+ });
751
+ }
752
+ return closed ? k + 1 : last + 1;
753
+ };
754
+ /** The `::label[TEXT]{at=… style=…}` on line `k` (#854), or undefined
755
+ * when the line is not one (text after the attributes, an unclosed
756
+ * bracket). */
757
+ const parseLabel = (k) => {
758
+ const raw = rawLines[k];
759
+ const lead = raw.length - raw.trimStart().length;
760
+ const body = raw.trimEnd();
761
+ const open = lead + '::label['.length;
762
+ const close = closingBracket(body, open);
763
+ if (close < 0)
764
+ return undefined;
765
+ const rest = body.slice(close + 1);
766
+ const attrsMatch = /^\s*(?:\{([^}]*)\})?\s*$/.exec(rest);
767
+ if (!attrsMatch)
768
+ return undefined;
769
+ const lineStart = lineOffsets[k];
770
+ const blob = attrsMatch[1] ?? '';
771
+ const blobStart = lineStart + close + 1 + rest.indexOf('{') + 1;
772
+ const attrs = parseDirectiveAttrs(blob);
773
+ const attrSources = blob ? attrSourcesOf(blob, blobStart) : {};
774
+ const textStart = lineStart + open;
775
+ const textEnd = lineStart + close;
776
+ const run = inlineRun(raw.slice(open, close), textStart, textEnd);
777
+ const atRaw = attrs.at ?? '';
778
+ const pct = LABEL_AT_PERCENT_RE.exec(atRaw);
779
+ const at = pct ? { x: Number(pct[1]) / 100, y: Number(pct[2]) / 100 } : { anchor: atRaw.trim() };
780
+ const style = attrs.style?.trim();
781
+ return {
782
+ text: run.text,
783
+ spans: run.spans,
784
+ sourceMap: run.sourceMap,
785
+ at,
786
+ ...(style ? { style } : {}),
787
+ textStart,
788
+ textEnd,
789
+ attrSources,
790
+ sourceStart: lineStart + lead,
791
+ sourceEnd: lineStart + body.length,
792
+ };
793
+ };
604
794
  /** Index of the last source line of the latest display formula that
605
795
  * interrupted a paragraph: a paragraph that starts on the next line
606
796
  * continues the one the formula interrupted (see
@@ -689,7 +879,15 @@ function parseBlocks(markdown, options) {
689
879
  const info = readCodeInfo(open.info);
690
880
  const srcStart = lineOffsets[start];
691
881
  const srcEnd = closed ? lineEndOffset(end) : lineEndOffset(rawLines.length - 1);
692
- blocks.push(codeBlock(start + 1, end, cuts, srcStart, srcEnd, { ...info, info: open.info, fence }));
882
+ const code = codeBlock(start + 1, end, cuts, srcStart, srcEnd, { ...info, info: open.info, fence });
883
+ // `slot="…"` among the fence's attributes (#854).
884
+ if (info.attrs.slot !== undefined) {
885
+ const infoAt = rawLines[start].indexOf(open.info, open.indent + open.marker.length);
886
+ const slot = slotOf(info.attrs, open.info, srcStart + Math.max(0, infoAt));
887
+ if (slot)
888
+ code.slot = slot;
889
+ }
890
+ blocks.push(code);
693
891
  if (!closed) {
694
892
  issues.push({
695
893
  kind: 'unclosedCodeBlock',
@@ -827,20 +1025,57 @@ function parseBlocks(markdown, options) {
827
1025
  i++;
828
1026
  continue;
829
1027
  }
830
- // Container closing fence — a bare `:::` while a container is open pops
831
- // it and emits the matching `containerEnd` marker. With nothing open the
832
- // line is not special and falls through to paragraph parsing so the
833
- // stray fence stays visible instead of vanishing silently.
834
- if (containerStack.length > 0 && CONTAINER_CLOSE_RE.test(trimmed)) {
835
- const open = containerStack.pop();
1028
+ // `::resource{id="…" slot="…"}` (#854): the embed sent to a slot of a
1029
+ // composed page. Only the two keys; anything else stays text as before.
1030
+ if (trimmed.startsWith('::resource') && trimmed.includes('slot')) {
1031
+ const m = /^::resource\s*\{([^}]*)\}\s*$/.exec(trimmed);
1032
+ const tokens = m ? parseAttrBlobStrict(m[1]) : undefined;
1033
+ const id = tokens?.find((t) => t.key === 'id');
1034
+ if (tokens && id && id.quoted && id.value !== '' && tokens.some((t) => t.key === 'slot') && tokens.every((t) => t.key === 'id' || t.key === 'slot')) {
1035
+ const blobStart = lineOffsets[i] + line.indexOf('{') + 1;
1036
+ const slot = slotOf(parseDirectiveAttrs(m[1]), m[1], blobStart);
1037
+ blocks.push({
1038
+ type: 'resourceBlock',
1039
+ text: '',
1040
+ spans: [],
1041
+ resourceId: id.value,
1042
+ ...(slot ? { slot } : {}),
1043
+ sourceStart: lineOffsets[i],
1044
+ sourceEnd: lineEndOffset(i),
1045
+ sourceMap: [],
1046
+ });
1047
+ i++;
1048
+ continue;
1049
+ }
1050
+ }
1051
+ // A composed page (#854): `::::page{template=…}` opens it, a bare fence
1052
+ // of as many colons closes it. A page opened while one is open closes
1053
+ // that one first (reported unclosed), and so does its closing fence any
1054
+ // `:::` container still open inside it.
1055
+ const pageOpen = PAGE_FENCE_RE.exec(trimmed);
1056
+ if (pageOpen) {
836
1057
  const srcStart = lineOffsets[i];
837
1058
  const srcEnd = lineEndOffset(i);
1059
+ const at = openPageAt();
1060
+ if (at >= 0)
1061
+ while (containerStack.length > at)
1062
+ popContainer(srcStart, srcStart, true);
1063
+ const attrsRaw = pageOpen[2] ?? '';
1064
+ const attrs = parseDirectiveAttrs(attrsRaw);
1065
+ const blobStart = srcStart + line.indexOf('{') + 1;
1066
+ const attrSources = attrsRaw ? attrSourcesOf(attrsRaw, blobStart) : {};
1067
+ const id = nextContainerId++;
1068
+ const colons = pageOpen[1].length;
1069
+ containerStack.push({ id, name: 'composedPage', sourceStart: srcStart, sourceEnd: srcEnd, colons });
1070
+ const spread = attrs.spread !== undefined && attrs.spread.trim().toLowerCase() !== 'false';
838
1071
  blocks.push({
839
- type: 'containerEnd',
1072
+ type: 'containerStart',
840
1073
  text: '',
841
1074
  spans: [],
842
- containerName: open.name,
843
- containerId: open.id,
1075
+ containerName: 'composedPage',
1076
+ containerAttrs: attrs,
1077
+ containerId: id,
1078
+ composedPage: { template: attrs.template?.trim() ?? '', spread, attrs, attrSources, fenceIndex: pageFences++, colons },
844
1079
  sourceStart: srcStart,
845
1080
  sourceEnd: srcEnd,
846
1081
  sourceMap: [],
@@ -848,6 +1083,29 @@ function parseBlocks(markdown, options) {
848
1083
  i++;
849
1084
  continue;
850
1085
  }
1086
+ const longClose = LONG_CLOSE_RE.exec(trimmed);
1087
+ if (longClose) {
1088
+ const at = openPageAt();
1089
+ if (at >= 0 && containerStack[at].colons === longClose[1].length) {
1090
+ const srcStart = lineOffsets[i];
1091
+ while (containerStack.length > at + 1)
1092
+ popContainer(srcStart, srcStart, true);
1093
+ popContainer(srcStart, lineEndOffset(i), false);
1094
+ i++;
1095
+ continue;
1096
+ }
1097
+ }
1098
+ // Container closing fence — a bare `:::` while a container is open pops
1099
+ // it and emits the matching `containerEnd` marker. With nothing open the
1100
+ // line is not special and falls through to paragraph parsing so the
1101
+ // stray fence stays visible instead of vanishing silently.
1102
+ // A bare `:::` never closes a composed page (#854): right inside one it
1103
+ // is a stray fence, set as text.
1104
+ if (containerStack.length > 0 && containerStack[containerStack.length - 1].colons === undefined && CONTAINER_CLOSE_RE.test(trimmed)) {
1105
+ popContainer(lineOffsets[i], lineEndOffset(i), false);
1106
+ i++;
1107
+ continue;
1108
+ }
851
1109
  // A comic page (#555) or strip (#566): `:::page{split=…}` … `:::`, its
852
1110
  // panels and script read whole (`parseComicFence`).
853
1111
  const comicMatch = trimmed.match(DIRECTIVE_RE);
@@ -906,6 +1164,11 @@ function parseBlocks(markdown, options) {
906
1164
  i = verse.next;
907
1165
  continue;
908
1166
  }
1167
+ // A figure with labels (#854): `:::figure{resource=…}` … `:::`.
1168
+ if (refsMatch && refsMatch[1] === 'figure') {
1169
+ i = parseFigure(i, refsMatch[2] ?? '');
1170
+ continue;
1171
+ }
909
1172
  // Container opening fence — `:::name` / `:::name{attrs}` whose name is a
910
1173
  // known container. Emits a `containerStart` marker; the blocks that
911
1174
  // follow are parsed as usual until the matching closing fence.
@@ -917,13 +1180,17 @@ function parseBlocks(markdown, options) {
917
1180
  const srcEnd = lineEndOffset(i);
918
1181
  const id = nextContainerId++;
919
1182
  containerStack.push({ id, name, sourceStart: srcStart, sourceEnd: srcEnd });
1183
+ const containerAttrs = parseDirectiveAttrs(attrsRaw);
1184
+ // `:::frame{slot="…"}` (#854), or any container sent to a slot.
1185
+ const slot = containerAttrs.slot !== undefined ? slotOf(containerAttrs, attrsRaw, srcStart + line.indexOf('{') + 1) : undefined;
920
1186
  blocks.push({
921
1187
  type: 'containerStart',
922
1188
  text: '',
923
1189
  spans: [],
924
1190
  containerName: name,
925
- containerAttrs: parseDirectiveAttrs(attrsRaw),
1191
+ containerAttrs,
926
1192
  containerId: id,
1193
+ ...(slot ? { slot } : {}),
927
1194
  sourceStart: srcStart,
928
1195
  sourceEnd: srcEnd,
929
1196
  sourceMap: [],
@@ -968,6 +1235,7 @@ function parseBlocks(markdown, options) {
968
1235
  // (the map is a greedy plain-char → source walk bounded by `srcEnd`).
969
1236
  let attrs;
970
1237
  let attrSources;
1238
+ let headingSlot;
971
1239
  const attrsMatch = headingRawContent.match(HEADING_ATTRS_RE);
972
1240
  const tokens = attrsMatch && attrsMatch.index !== undefined
973
1241
  ? headingAttrTokens(attrsMatch[1], attrsMatch[0][0] !== '{')
@@ -984,6 +1252,15 @@ function parseBlocks(markdown, options) {
984
1252
  attrSources ??= {};
985
1253
  attrSources[t.key] = { start: attrsAbsStart + t.valueStart, end: attrsAbsStart + t.valueEnd };
986
1254
  }
1255
+ // `# Title {slot="titulo"}` (#854).
1256
+ const slotToken = read.filter((t) => t.key === 'slot').pop();
1257
+ if (slotToken && slotToken.value.trim() !== '') {
1258
+ headingSlot = {
1259
+ name: slotToken.value.trim(),
1260
+ sourceStart: attrsAbsStart + (slotToken.valueStart ?? slotToken.start),
1261
+ sourceEnd: attrsAbsStart + (slotToken.valueEnd ?? slotToken.end),
1262
+ };
1263
+ }
987
1264
  headingRawContent = headingRawContent.slice(0, attrsMatch.index);
988
1265
  srcEnd = contentAbsStart + headingRawContent.length;
989
1266
  }
@@ -1011,6 +1288,7 @@ function parseBlocks(markdown, options) {
1011
1288
  ...(attrs ? { attrs } : {}),
1012
1289
  ...(attrSources ? { attrSources } : {}),
1013
1290
  ...(titleBreaks.length > 0 ? { titleBreaks } : {}),
1291
+ ...(headingSlot ? { slot: headingSlot } : {}),
1014
1292
  sourceStart: srcStart,
1015
1293
  sourceEnd: srcEnd,
1016
1294
  sourceMap: mapping.sourceMap,
@@ -1141,7 +1419,7 @@ function parseBlocks(markdown, options) {
1141
1419
  const pl = rawLine.trim();
1142
1420
  if (pl === '' || pl.match(HEADING_RE) || pl.startsWith('>') || rawLine.match(LIST_ITEM_RE))
1143
1421
  break;
1144
- if (i > startIdx && isFenceLine(pl))
1422
+ if (i > startIdx && (isFenceLine(pl) || isPageFenceLine(pl)))
1145
1423
  break;
1146
1424
  // A code fence interrupts the paragraph (#624).
1147
1425
  if (i > startIdx && fences && codeFenceOpen(rawLine))
@@ -1190,27 +1468,8 @@ function parseBlocks(markdown, options) {
1190
1468
  // EOF with containers still open: auto-close innermost-first with
1191
1469
  // zero-length end markers at the end of input, and report each one so the
1192
1470
  // sandbox can point at the fence that never got its `:::`.
1193
- while (containerStack.length > 0) {
1194
- const open = containerStack.pop();
1195
- blocks.push({
1196
- type: 'containerEnd',
1197
- text: '',
1198
- spans: [],
1199
- containerName: open.name,
1200
- containerId: open.id,
1201
- sourceStart: markdown.length,
1202
- sourceEnd: markdown.length,
1203
- sourceMap: [],
1204
- });
1205
- issues.push({
1206
- kind: 'unclosedContainer',
1207
- delimiter: ':::',
1208
- containerName: open.name,
1209
- containerId: open.id,
1210
- sourceStart: open.sourceStart,
1211
- sourceEnd: open.sourceEnd,
1212
- });
1213
- }
1471
+ while (containerStack.length > 0)
1472
+ popContainer(markdown.length, markdown.length, true);
1214
1473
  return { blocks, issues };
1215
1474
  }
1216
1475
  //# sourceMappingURL=blockParser.js.map