@mirrordown/core 0.1.0 → 0.1.2

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.
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { a as mermaidSource, i as mermaidIdPrefixes, n as mermaidDiagram, o as mermaidSvg, r as mermaidFailure, t as hastAttributes } from "./mermaid-pele-BMyyDozN.mjs";
1
2
  // src/escapes.ts
2
3
  const ASCII_PUNCTUATION = /^[!-/:-@[-`{-~]$/;
3
4
  const ENTITY = /^&(?:#\d+|#x[\da-f]+|[a-z][\da-z]*);/i;
@@ -133,6 +134,8 @@ const ICONS = {
133
134
  };
134
135
  // src/github-alerts/index.ts
135
136
  const MARKER_RE = /^\[!([^\]]+)\]([+-])?/;
137
+ /** Whether a source line opens an alert (`> [!NOTE]`), after its `>` markers and indentation, whether or not the alerts plugin reads it: a block that takes the quote after it as more of its content (a step body) leaves an alert be. */
138
+ const isAlertMarkerLine = (line) => MARKER_RE.test(line.replace(/^[ >]*/, ``));
136
139
  const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
137
140
  /** Resolve alert keywords, titles and icons the same way in every engine. */
138
141
  const createAlertResolver = (options = {}) => {
@@ -244,9 +247,9 @@ const INLINE_PARENTS = /* @__PURE__ */ new Set([
244
247
  `subscript`,
245
248
  `superscript`
246
249
  ]);
247
- const WHITESPACE$2 = /^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/u;
250
+ const WHITESPACE$3 = /^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/u;
248
251
  const PUNCTUATION$1 = /^[\p{P}\p{S}]$/u;
249
- const classify$1 = (char) => WHITESPACE$2.test(char) ? `space` : PUNCTUATION$1.test(char) ? `punct` : `other`;
252
+ const classify$1 = (char) => WHITESPACE$3.test(char) ? `space` : PUNCTUATION$1.test(char) ? `punct` : `other`;
250
253
  /**
251
254
  * markdown-it's `balance_pairs` for a single marker. Every delimiter has `length: 0`, as in markdown-it-mark, so the emphasis "rule of 3" never applies.
252
255
  */
@@ -484,8 +487,8 @@ const tokenType = (type) => type;
484
487
  const temporary = (type) => tokenType(`${type}SequenceTemporary`);
485
488
  const scriptOf = (tokenType) => Object.values(SCRIPTS).find(({ type }) => temporary(type) === tokenType);
486
489
  const PUNCTUATION = /\p{P}|\p{S}/u;
487
- const WHITESPACE$1 = /\s/;
488
- const classify = (code) => code === null || code < 0 || code === 32 || WHITESPACE$1.test(String.fromCharCode(code)) ? 1 : PUNCTUATION.test(String.fromCharCode(code)) ? 2 : void 0;
490
+ const WHITESPACE$2 = /\s/;
491
+ const classify = (code) => code === null || code < 0 || code === 32 || WHITESPACE$2.test(String.fromCharCode(code)) ? 1 : PUNCTUATION.test(String.fromCharCode(code)) ? 2 : void 0;
489
492
  const splice = (list, start, remove, items) => {
490
493
  list.splice(start, remove);
491
494
  for (let i = 0; i < items.length; i += 1e4) list.splice(start + i, 0, ...items.slice(i, i + 1e4));
@@ -603,156 +606,6 @@ const scriptSyntax = (marker) => {
603
606
  insideSpan: { null: [construct] }
604
607
  };
605
608
  };
606
- // src/hast.ts
607
- const WHITESPACE = /^[ \t\n\f\r]*$/;
608
- const isWhitespaceText = (node) => node.type === `text` && WHITESPACE.test(node.value ?? ``);
609
- /** A `<p>` with no children, or only whitespace text: squeeze-paragraphs removes it. */
610
- const isEmptyParagraph = (node) => node.tagName === `p` && (node.children ?? []).every(isWhitespaceText);
611
- /**
612
- * A `<p>` whose content is only images (each possibly inside a link or a lightbox button) and whitespace: unwrap-images replaces it with its children.
613
- */
614
- const isImageParagraph = (node) => {
615
- const scan = (parent, inLink) => {
616
- let result = `empty`;
617
- for (const child of parent.children ?? []) {
618
- if (isWhitespaceText(child)) continue;
619
- if (child.type !== `element`) return `other`;
620
- if (child.tagName === `img`) {
621
- result = `image`;
622
- continue;
623
- }
624
- if (!inLink && (child.tagName === `a` || child.tagName === `button`)) {
625
- const inner = scan(child, true);
626
- if (inner === `other`) return `other`;
627
- if (inner === `image`) result = `image`;
628
- continue;
629
- }
630
- return `other`;
631
- }
632
- return result;
633
- };
634
- return node.tagName === `p` && scan(node, false) === `image`;
635
- };
636
- /** The rank of an `h1`–`h6` element, or `undefined`. */
637
- const headingRank = (node) => {
638
- if (node.type !== `element`) return void 0;
639
- const match = /^h([1-6])$/i.exec(node.tagName ?? ``);
640
- return match ? Number(match[1]) : void 0;
641
- };
642
- /**
643
- * Wrap heading-delimited runs of `nodes` into `<section data-depth>` elements, nesting deeper headings inside shallower ones. Content before the first heading stays unwrapped.
644
- */
645
- const sectionize = (nodes, section, floor = 0) => {
646
- let minRank = Infinity;
647
- for (const node of nodes) {
648
- const rank = headingRank(node);
649
- if (rank !== void 0 && rank > floor && rank < minRank) minRank = rank;
650
- }
651
- if (minRank === Infinity) return [...nodes];
652
- const result = [];
653
- let i = 0;
654
- for (; i < nodes.length; i++) {
655
- const node = nodes[i];
656
- if (!node || headingRank(node) === minRank) break;
657
- result.push(node);
658
- }
659
- while (i < nodes.length) {
660
- const start = i;
661
- for (i++; i < nodes.length; i++) {
662
- const node = nodes[i];
663
- if (!node || headingRank(node) === minRank) break;
664
- }
665
- result.push(section(minRank, sectionize(nodes.slice(start, i), section, minRank)));
666
- }
667
- return result;
668
- };
669
- /**
670
- * The lightbox opt-in: an image directly after a text run that ends in a `!` at a boundary (start of the run or after whitespace), which is what `!![alt](src)` leaves behind.
671
- */
672
- const LIGHTBOX_MARKER = /(?:^|\s)!$/;
673
- /**
674
- * A short, stable lightbox id from an image src (FNV-1a, base36): the same image always gets the same id, so repeated references share one dialog.
675
- */
676
- const lightboxId = (src) => {
677
- let hash = 2166136261;
678
- for (let i = 0; i < src.length; i += 1) {
679
- hash ^= src.charCodeAt(i);
680
- hash = Math.imul(hash, 16777619);
681
- }
682
- return `markdown-lightbox-${(hash >>> 0).toString(36)}`;
683
- };
684
- /** The button that replaces an opted-in image and opens its dialog. */
685
- const lightboxTrigger = (id, image) => ({
686
- type: `element`,
687
- tagName: `button`,
688
- properties: {
689
- type: `button`,
690
- className: [`markdown-lightbox-trigger`],
691
- command: `show-modal`,
692
- commandfor: id
693
- },
694
- children: [image]
695
- });
696
- /** The modal dialog, appended at the document end, showing the image full size. */
697
- const lightboxDialog = (id, image) => ({
698
- type: `element`,
699
- tagName: `dialog`,
700
- properties: {
701
- id,
702
- className: [`markdown-lightbox`],
703
- closedby: `any`
704
- },
705
- children: [{
706
- type: `element`,
707
- tagName: `button`,
708
- properties: {
709
- type: `button`,
710
- className: [`markdown-lightbox-close`],
711
- command: `close`,
712
- commandfor: id,
713
- ariaLabel: `Close image`
714
- },
715
- children: [image]
716
- }]
717
- });
718
- // src/abbr.ts
719
- const DEFINITION = /^\*\[([^[\]]+)\]:\s*(.+)$/;
720
- const REGEX_SPECIAL = /[.*+?^${}()|[\]\\]/g;
721
- /**
722
- * The `[term, title]` pairs a paragraph's text defines, when every line of it is a `*[term]: title` definition; otherwise `undefined` (the paragraph is ordinary text).
723
- */
724
- const parseAbbreviations = (text) => {
725
- const definitions = [];
726
- for (const line of text.split(`\n`)) {
727
- const match = DEFINITION.exec(line);
728
- if (!match) return void 0;
729
- definitions.push([match[1] ?? ``, (match[2] ?? ``).trim()]);
730
- }
731
- return definitions;
732
- };
733
- /**
734
- * Build the scan for a document's abbreviations. Terms match on word boundaries, longest first; the scan returns the pieces of a text value, or `undefined` when it holds no term.
735
- */
736
- const createAbbreviationScan = (abbreviations) => {
737
- const terms = [...abbreviations.keys()].sort((a, b) => b.length - a.length).map((term) => term.replace(REGEX_SPECIAL, `\\$&`));
738
- const pattern = new RegExp(`\\b(${terms.join(`|`)})\\b`, `g`);
739
- return (value) => {
740
- const parts = [];
741
- let last = 0;
742
- for (const match of value.matchAll(pattern)) {
743
- const term = match[1] ?? ``;
744
- if (match.index > last) parts.push({ text: value.slice(last, match.index) });
745
- parts.push({
746
- term,
747
- title: abbreviations.get(term) ?? ``
748
- });
749
- last = match.index + term.length;
750
- }
751
- if (parts.length === 0) return void 0;
752
- if (last < value.length) parts.push({ text: value.slice(last) });
753
- return parts;
754
- };
755
- };
756
609
  // src/cut-attributes.ts
757
610
  /** Paragraphs recognition rebuilt from lines, as it does where a tab or step header lazily continued them. */
758
611
  const rebuiltParagraphs = /* @__PURE__ */ new WeakSet();
@@ -955,6 +808,492 @@ const rebuildLazyLines = (lines, level) => {
955
808
  }
956
809
  return blocks;
957
810
  };
811
+ /** Blocks whose lines carry one more `>` marker than the block around them: a blockquote (or an alert, Sätteri's node for one) and a tab panel or step body. */
812
+ const MARKED_BLOCKS = /* @__PURE__ */ new Set([
813
+ `blockquote`,
814
+ `alert`,
815
+ `tabsItem`,
816
+ `stepsItem`
817
+ ]);
818
+ /** Blocks a line can lazily continue into by its indentation rather than its markers: a list item or a definition. */
819
+ const INDENTED_BLOCKS = /* @__PURE__ */ new Set([
820
+ `listItem`,
821
+ `defListDescription`,
822
+ `descriptionDetails`
823
+ ]);
824
+ /**
825
+ * A paragraph split before its first line (after its first) that `opens` a group, written at the paragraph's level (`quotes` `>` markers), as markdown-it's block rules interrupt a paragraph; `null` when there's none. Attributes ending the paragraph go with the part they end.
826
+ */
827
+ const splitAtOpeningLine = (para, sourceLines, quotes, opens) => {
828
+ const start = para.position?.start.line;
829
+ if (start === void 0) return null;
830
+ const lines = paraToLines(para, sourceLines);
831
+ const at = lines.findIndex((line, index) => {
832
+ const first = line[0];
833
+ return index > 0 && first?.type === `text` && opens(first.value) && markerCount(sourceLines[start - 1 + index] ?? ``) === quotes;
834
+ });
835
+ if (at === -1) return null;
836
+ const head = linesToParagraph(lines.slice(0, at));
837
+ const tail = linesToParagraph(lines.slice(at));
838
+ if (!head || !tail) return null;
839
+ if (para.data) tail.data = para.data;
840
+ return [head, tail];
841
+ };
842
+ const lastParagraph = (node) => {
843
+ const last = node?.children?.at(-1);
844
+ if (!last) return void 0;
845
+ if (last.type === `paragraph`) return last;
846
+ return lastParagraph(last);
847
+ };
848
+ /**
849
+ * End each group of `listType` (tab groups or steps) where its last panel's `>` markers stop, as markdown-it reads a panel: a line CommonMark lazily continued into the panel's last paragraph, written with fewer markers than the panel's own lines, starts a paragraph after the group, with the lines after it. Groups nested in a panel are ended first, so a line that leaves an outer panel too moves on past its group. The paragraph's attributes (Sätteri reads them off its end) go with its last line.
850
+ */
851
+ const endGroupsAtMarkers = (root, sourceLines, listType) => {
852
+ const visit = (node, quotes) => {
853
+ const children = node.children;
854
+ if (!children) return;
855
+ for (let i = 0; i < children.length; i++) {
856
+ const child = children[i];
857
+ visit(child, quotes + (MARKED_BLOCKS.has(child.type) ? 1 : 0));
858
+ if (child.type !== listType) continue;
859
+ const para = lastParagraph(child.children?.at(-1));
860
+ const start = para?.position?.start.line;
861
+ if (!para || start === void 0) continue;
862
+ const lines = paraToLines(para, sourceLines);
863
+ const outside = lines.findIndex((_, index) => index > 0 && markerCount(sourceLines[start - 1 + index] ?? ``) < quotes + 1);
864
+ if (outside === -1) continue;
865
+ const kept = linesToParagraph(lines.slice(0, outside));
866
+ const moved = linesToParagraph(lines.slice(outside));
867
+ if (!kept || !moved) continue;
868
+ para.children = kept.children;
869
+ const data = para.data;
870
+ if (data?.hProperties) {
871
+ const { hProperties, ...rest } = data;
872
+ moved.data = { hProperties };
873
+ para.data = rest;
874
+ }
875
+ children.splice(i + 1, 0, moved);
876
+ }
877
+ };
878
+ visit(root, 0);
879
+ };
880
+ // src/line-containers.ts
881
+ /**
882
+ * How many of `containers`, outermost first, a line is written in. Tabs count as one column.
883
+ */
884
+ const containersKept = (line, containers) => {
885
+ const prefix = /^[ \t>]*/.exec(line)?.[0] ?? ``;
886
+ const markers = prefix.split(`>`).length - 1;
887
+ let kept = 0;
888
+ for (const container of containers) {
889
+ if (container.kind === `quote` ? markers < container.markers : prefix.length < container.column) break;
890
+ kept++;
891
+ }
892
+ return kept;
893
+ };
894
+ /**
895
+ * The columns the content of the list items opened on a line starts at, in the order they open, as CommonMark reads a list marker: up to four spaces after it belong to the marker, more start indented content one column after.
896
+ */
897
+ const itemColumns = (line) => {
898
+ const columns = [];
899
+ let pos = 0;
900
+ for (;;) {
901
+ while (line[pos] === ` ` || line[pos] === `\t`) pos++;
902
+ if (line[pos] === `>`) {
903
+ pos++;
904
+ continue;
905
+ }
906
+ const marker = /^(?:[-+*]|\d{1,9}[.)])(?=[ \t]|$)/.exec(line.slice(pos));
907
+ if (!marker) return columns;
908
+ const end = pos + marker[0].length;
909
+ let spaces = 0;
910
+ while (line[end + spaces] === ` ` || line[end + spaces] === `\t`) spaces++;
911
+ const column = end + spaces === line.length || spaces > 4 ? end + 1 : end + spaces;
912
+ columns.push(column);
913
+ if (spaces > 4) return columns;
914
+ pos = column;
915
+ }
916
+ };
917
+ // src/caption.ts
918
+ /** The class of the `<figure>` a caption wraps its block in. */
919
+ const CAPTION_CLASS = "markdown-caption";
920
+ /** Whether a source line is a caption line: `~:`, whitespace and the caption's first word after its `>` markers and indentation. An escaped `\~:` isn't one, nor is a marker with nothing after it. */
921
+ const isCaptionLine = (line) => /^~:[ \t]+\S/.test(line.replace(/^[ \t>]*/, ``));
922
+ /** Whether text starts with a caption marker, as a parser gives a caption line's text (inline markdown may follow in other nodes). */
923
+ const startsCaption = (text) => /^~:(?:[ \t]|$)/.test(text);
924
+ /** The text after a caption marker. */
925
+ const stripCaptionMarker = (text) => text.replace(/^~:[ \t]*/, ``);
926
+ /**
927
+ * Blocks a caption doesn't attach to, by mdast (and custom node) type: a caption after them stays text. Headings, thematic breaks, raw HTML, definitions and front matter, another caption's figure, and the parts of containers that aren't blocks of their own (titles, labels, terms).
928
+ */
929
+ const UNCAPTIONED = /* @__PURE__ */ new Set([
930
+ `heading`,
931
+ `thematicBreak`,
932
+ `html`,
933
+ `definition`,
934
+ `footnoteDefinition`,
935
+ `yaml`,
936
+ `toml`,
937
+ `figure`,
938
+ `alertTitle`,
939
+ `stepsTitle`,
940
+ `tabsLabel`,
941
+ `tabsLabels`,
942
+ `tabsInput`,
943
+ `defListTerm`,
944
+ `descriptionTerm`
945
+ ]);
946
+ /** The properties of a caption's `<figure>`: its class, then the attributes written at the end of the caption (a class joins it). */
947
+ const figureProperties = (author) => {
948
+ const { class: extraClass, className: extraClassName, ...rest } = author ?? {};
949
+ const extra = [extraClass, extraClassName].flat().flatMap((value) => typeof value === `string` ? value.split(` `) : []).filter((value) => value !== ``);
950
+ return {
951
+ className: [CAPTION_CLASS, ...extra],
952
+ ...rest
953
+ };
954
+ };
955
+ const WHITESPACE$1 = /^\s*$/;
956
+ const onlyImages = (children) => children.some((child) => child.type !== `text`) && children.every((child, index) => child.type === `image` || child.type === `text` && (WHITESPACE$1.test(child.value ?? ``) || /^\s*!$/.test(child.value ?? ``) && children[index + 1]?.type === `image`) || child.type === `link` && (child.children ?? []).length > 0 && (child.children ?? []).every((inner) => inner.type === `image`));
957
+ const figure = (content, caption, author) => ({
958
+ type: `figure`,
959
+ data: {
960
+ hName: `figure`,
961
+ hProperties: figureProperties(author)
962
+ },
963
+ children: [...content, {
964
+ type: `figcaption`,
965
+ data: { hName: `figcaption` },
966
+ children: caption
967
+ }]
968
+ });
969
+ const captionContent = (lines) => {
970
+ const first = lines[0]?.[0];
971
+ if (first?.type === `text`) {
972
+ const value = stripCaptionMarker(first.value);
973
+ lines[0] = value ? [{
974
+ ...first,
975
+ value
976
+ }, ...lines[0].slice(1)] : lines[0].slice(1);
977
+ }
978
+ return linesToParagraph(lines)?.children ?? [];
979
+ };
980
+ const takeProperties = (node) => {
981
+ const data = node.data;
982
+ if (!data?.hProperties) return void 0;
983
+ const { hProperties, ...rest } = data;
984
+ node.data = rest;
985
+ return hProperties;
986
+ };
987
+ /**
988
+ * Wrap each captioned block of an mdast tree, with its caption, in a `figure` node (`hName: "figure"`, class `markdown-caption`) whose last child is a `figcaption` node. `source` is the document the tree was parsed from: caption lines are found, and placed, by their source lines.
989
+ */
990
+ const processCaptions = (root, source) => {
991
+ if (!source.includes(`~:`)) return;
992
+ const sourceLines = source.split(/\r?\n/);
993
+ const parents = /* @__PURE__ */ new Map();
994
+ const paragraphs = [];
995
+ const tables = [];
996
+ const walk = (node) => {
997
+ if (node.type === `paragraph`) paragraphs.push(node);
998
+ else if (node.type === `table`) tables.push(node);
999
+ for (const child of node.children ?? []) {
1000
+ parents.set(child, node);
1001
+ walk(child);
1002
+ }
1003
+ };
1004
+ walk(root);
1005
+ const replace = (node, replacement) => {
1006
+ const parent = parents.get(node);
1007
+ const siblings = parent?.children;
1008
+ if (!parent || !siblings) return;
1009
+ const index = siblings.indexOf(node);
1010
+ if (index === -1) return;
1011
+ if (replacement) {
1012
+ siblings.splice(index, 1, replacement);
1013
+ parents.set(replacement, parent);
1014
+ parents.set(node, replacement);
1015
+ } else siblings.splice(index, 1);
1016
+ };
1017
+ const captioned = (node) => parents.get(node)?.type === `figure`;
1018
+ const ancestorsOf = (node) => {
1019
+ const ancestors = [];
1020
+ for (let p = parents.get(node); p; p = parents.get(p)) ancestors.unshift(p);
1021
+ return ancestors;
1022
+ };
1023
+ const containersOf = (ancestors) => {
1024
+ const containers = [];
1025
+ const nearest = (index, type) => ancestors.slice(0, index).reverse().find((a) => a.type === type);
1026
+ let quotes = 0;
1027
+ for (const [index, ancestor] of ancestors.entries()) {
1028
+ const quote = (target) => {
1029
+ containers.push({
1030
+ target: target ?? ancestor,
1031
+ line: {
1032
+ kind: `quote`,
1033
+ markers: ++quotes
1034
+ }
1035
+ });
1036
+ };
1037
+ switch (ancestor.type) {
1038
+ case `blockquote`:
1039
+ case `alert`:
1040
+ quote(ancestor);
1041
+ break;
1042
+ case `tabsItem`:
1043
+ case `tabsPanel`:
1044
+ quote(nearest(index, `tabsList`));
1045
+ break;
1046
+ case `stepsItem`:
1047
+ if (!(ancestor.children ?? []).some((c) => c.type === `stepsBody`)) quote(nearest(index, `stepsList`));
1048
+ break;
1049
+ case `stepsBody`:
1050
+ quote(nearest(index, `stepsList`));
1051
+ break;
1052
+ case `listItem`:
1053
+ case `defListDescription`:
1054
+ case `descriptionDetails`: {
1055
+ const column = ancestor.children?.[0]?.position?.start.column;
1056
+ if (column === void 0) return void 0;
1057
+ containers.push({
1058
+ target: ancestors[index - 1] ?? ancestor,
1059
+ line: {
1060
+ kind: `item`,
1061
+ column: column - 1
1062
+ }
1063
+ });
1064
+ break;
1065
+ }
1066
+ }
1067
+ }
1068
+ return containers;
1069
+ };
1070
+ for (const table of tables) {
1071
+ const rows = table.children ?? [];
1072
+ const first = rows.findIndex((row, index) => {
1073
+ const line = row.position?.start.line;
1074
+ const text = row.children?.[0]?.children?.[0];
1075
+ return index > 0 && line !== void 0 && isCaptionLine(sourceLines[line - 1] ?? ``) && text?.type === `text` && startsCaption(text.value ?? ``);
1076
+ });
1077
+ if (first === -1) continue;
1078
+ const captionRows = rows.slice(first);
1079
+ const lastCell = captionRows.at(-1)?.children?.[0];
1080
+ const author = lastCell ? takeProperties(lastCell) : void 0;
1081
+ const caption = captionContent(captionRows.map((row) => row.children?.[0]?.children ?? []));
1082
+ table.children = rows.slice(0, first);
1083
+ replace(table, figure([table], caption, author));
1084
+ }
1085
+ for (const para of paragraphs.reverse()) {
1086
+ const first = para.position?.start.line;
1087
+ if (first === void 0) continue;
1088
+ const lines = paraToLines(para, sourceLines);
1089
+ const parent = parents.get(para);
1090
+ const start = (parent?.type === `alert` || parent?.type === `blockquote` && parent.data?.alertType !== void 0) && isAlertMarkerLine(sourceLines[first - 1] ?? ``) ? first + 1 : first;
1091
+ const isCaptionAt = (index) => {
1092
+ const text = lines[index]?.[0];
1093
+ return isCaptionLine(sourceLines[start + index - 1] ?? ``) && text?.type === `text` && startsCaption(text.value);
1094
+ };
1095
+ if (isCaptionAt(0)) {
1096
+ const siblings = parents.get(para)?.children ?? [];
1097
+ const before = siblings[siblings.indexOf(para) - 1];
1098
+ const above = sourceLines[start - 2];
1099
+ if (!before || UNCAPTIONED.has(before.type) || above === void 0 || above.replace(/^[ \t>]*/, ``) === ``) continue;
1100
+ const author = takeProperties(para);
1101
+ replace(para, null);
1102
+ replace(before, figure([before], captionContent(lines), author));
1103
+ continue;
1104
+ }
1105
+ let containers;
1106
+ const segments = [];
1107
+ for (let index = 1; index < lines.length; index++) {
1108
+ if (!isCaptionAt(index)) continue;
1109
+ containers ??= containersOf(ancestorsOf(para));
1110
+ if (!containers) break;
1111
+ const kept = containersKept(sourceLines[start + index - 1] ?? ``, containers.map((c) => c.line));
1112
+ const previous = segments.at(-1);
1113
+ if (previous && kept >= previous.kept) continue;
1114
+ const target = containers[kept]?.target ?? para;
1115
+ if (captioned(target)) continue;
1116
+ segments.push({
1117
+ index,
1118
+ kept,
1119
+ target
1120
+ });
1121
+ }
1122
+ if (segments.length === 0) continue;
1123
+ const head = lines.slice(0, segments[0].index);
1124
+ const lastLine = head.at(-1);
1125
+ if (lastLine?.at(-1)?.type === `break`) head[head.length - 1] = lastLine.slice(0, -1);
1126
+ para.children = linesToParagraph(head)?.children ?? [];
1127
+ const author = takeProperties(para);
1128
+ for (const [i, segment] of segments.entries()) {
1129
+ const caption = captionContent(lines.slice(segment.index, segments[i + 1]?.index));
1130
+ const properties = i === segments.length - 1 ? author : void 0;
1131
+ const { target } = segment;
1132
+ if (target === para && onlyImages(para.children ?? [])) replace(para, figure(para.children ?? [], caption, properties));
1133
+ else replace(target, figure([target], caption, properties));
1134
+ }
1135
+ }
1136
+ };
1137
+ // src/hast.ts
1138
+ const WHITESPACE = /^[ \t\n\f\r]*$/;
1139
+ const isWhitespaceText = (node) => node.type === `text` && WHITESPACE.test(node.value ?? ``);
1140
+ /** A `<p>` with no children, or only whitespace text: squeeze-paragraphs removes it. */
1141
+ const isEmptyParagraph = (node) => node.tagName === `p` && (node.children ?? []).every(isWhitespaceText);
1142
+ /**
1143
+ * A `<p>` whose content is only images (each possibly inside a link or a lightbox button) and whitespace: unwrap-images replaces it with its children.
1144
+ */
1145
+ const isImageParagraph = (node) => {
1146
+ const scan = (parent, inLink) => {
1147
+ let result = `empty`;
1148
+ for (const child of parent.children ?? []) {
1149
+ if (isWhitespaceText(child)) continue;
1150
+ if (child.type !== `element`) return `other`;
1151
+ if (child.tagName === `img`) {
1152
+ result = `image`;
1153
+ continue;
1154
+ }
1155
+ if (!inLink && (child.tagName === `a` || child.tagName === `button`)) {
1156
+ const inner = scan(child, true);
1157
+ if (inner === `other`) return `other`;
1158
+ if (inner === `image`) result = `image`;
1159
+ continue;
1160
+ }
1161
+ return `other`;
1162
+ }
1163
+ return result;
1164
+ };
1165
+ return node.tagName === `p` && scan(node, false) === `image`;
1166
+ };
1167
+ /** The rank of an `h1`–`h6` element, or `undefined`. */
1168
+ const headingRank = (node) => {
1169
+ if (node.type !== `element`) return void 0;
1170
+ const match = /^h([1-6])$/i.exec(node.tagName ?? ``);
1171
+ return match ? Number(match[1]) : void 0;
1172
+ };
1173
+ /**
1174
+ * Wrap heading-delimited runs of `nodes` into `<section data-depth>` elements, nesting deeper headings inside shallower ones. Content before the first heading stays unwrapped.
1175
+ */
1176
+ const sectionize = (nodes, section, floor = 0) => {
1177
+ let minRank = Infinity;
1178
+ for (const node of nodes) {
1179
+ const rank = headingRank(node);
1180
+ if (rank !== void 0 && rank > floor && rank < minRank) minRank = rank;
1181
+ }
1182
+ if (minRank === Infinity) return [...nodes];
1183
+ const result = [];
1184
+ let i = 0;
1185
+ for (; i < nodes.length; i++) {
1186
+ const node = nodes[i];
1187
+ if (!node || headingRank(node) === minRank) break;
1188
+ result.push(node);
1189
+ }
1190
+ while (i < nodes.length) {
1191
+ const start = i;
1192
+ for (i++; i < nodes.length; i++) {
1193
+ const node = nodes[i];
1194
+ if (!node || headingRank(node) === minRank) break;
1195
+ }
1196
+ result.push(section(minRank, sectionize(nodes.slice(start, i), section, minRank)));
1197
+ }
1198
+ return result;
1199
+ };
1200
+ /**
1201
+ * The lightbox opt-in: an image directly after a text run that ends in a `!` at a boundary (start of the run or after whitespace), which is what `!![alt](src)` leaves behind.
1202
+ */
1203
+ const LIGHTBOX_MARKER = /(?:^|\s)!$/;
1204
+ /**
1205
+ * A short, stable lightbox id from an image src (FNV-1a, base36): the same image always gets the same id, so repeated references share one dialog.
1206
+ */
1207
+ const lightboxId = (src) => {
1208
+ let hash = 2166136261;
1209
+ for (let i = 0; i < src.length; i += 1) {
1210
+ hash ^= src.charCodeAt(i);
1211
+ hash = Math.imul(hash, 16777619);
1212
+ }
1213
+ return `markdown-lightbox-${(hash >>> 0).toString(36)}`;
1214
+ };
1215
+ /** The button that replaces an opted-in image and opens its dialog. */
1216
+ const lightboxTrigger = (id, image) => ({
1217
+ type: `element`,
1218
+ tagName: `button`,
1219
+ properties: {
1220
+ type: `button`,
1221
+ className: [`markdown-lightbox-trigger`],
1222
+ command: `show-modal`,
1223
+ commandfor: id
1224
+ },
1225
+ children: [image]
1226
+ });
1227
+ /** The modal dialog, appended at the document end, showing the image full size, and its caption under it when one is given. */
1228
+ const lightboxDialog = (id, image, caption) => ({
1229
+ type: `element`,
1230
+ tagName: `dialog`,
1231
+ properties: {
1232
+ id,
1233
+ className: [`markdown-lightbox`],
1234
+ closedby: `any`
1235
+ },
1236
+ children: [{
1237
+ type: `element`,
1238
+ tagName: `button`,
1239
+ properties: {
1240
+ type: `button`,
1241
+ className: [`markdown-lightbox-close`],
1242
+ command: `close`,
1243
+ commandfor: id,
1244
+ ariaLabel: `Close image`
1245
+ },
1246
+ children: [image]
1247
+ }, ...caption ? [{
1248
+ type: `element`,
1249
+ tagName: `p`,
1250
+ properties: { className: [`markdown-lightbox-caption`] },
1251
+ children: caption
1252
+ }] : []]
1253
+ });
1254
+ /** Whether a node is a caption's figure (`<figure class="markdown-caption">`, as the caption plugins make): an image that is its content shows the figure's caption in its lightbox dialog, with the `caption` option. */
1255
+ const isCaptionFigure = (node) => {
1256
+ const className = node?.properties?.className;
1257
+ return node?.tagName === `figure` && Array.isArray(className) && className.includes("markdown-caption");
1258
+ };
1259
+ // src/abbr.ts
1260
+ const DEFINITION = /^\*\[([^[\]]+)\]:\s*(.+)$/;
1261
+ const REGEX_SPECIAL = /[.*+?^${}()|[\]\\]/g;
1262
+ /**
1263
+ * The `[term, title]` pairs a paragraph's text defines, when every line of it is a `*[term]: title` definition; otherwise `undefined` (the paragraph is ordinary text).
1264
+ */
1265
+ const parseAbbreviations = (text) => {
1266
+ const definitions = [];
1267
+ for (const line of text.split(`\n`)) {
1268
+ const match = DEFINITION.exec(line);
1269
+ if (!match) return void 0;
1270
+ definitions.push([match[1] ?? ``, (match[2] ?? ``).trim()]);
1271
+ }
1272
+ return definitions;
1273
+ };
1274
+ /**
1275
+ * Build the scan for a document's abbreviations. Terms match on word boundaries, longest first; the scan returns the pieces of a text value, or `undefined` when it holds no term.
1276
+ */
1277
+ const createAbbreviationScan = (abbreviations) => {
1278
+ const terms = [...abbreviations.keys()].sort((a, b) => b.length - a.length).map((term) => term.replace(REGEX_SPECIAL, `\\$&`));
1279
+ const pattern = new RegExp(`\\b(${terms.join(`|`)})\\b`, `g`);
1280
+ return (value) => {
1281
+ const parts = [];
1282
+ let last = 0;
1283
+ for (const match of value.matchAll(pattern)) {
1284
+ const term = match[1] ?? ``;
1285
+ if (match.index > last) parts.push({ text: value.slice(last, match.index) });
1286
+ parts.push({
1287
+ term,
1288
+ title: abbreviations.get(term) ?? ``
1289
+ });
1290
+ last = match.index + term.length;
1291
+ }
1292
+ if (parts.length === 0) return void 0;
1293
+ if (last < value.length) parts.push({ text: value.slice(last) });
1294
+ return parts;
1295
+ };
1296
+ };
958
1297
  // src/steps.ts
959
1298
  const STEP_HEADER_RE = /^(@{1,6})(\d+)\. ?(.*)/s;
960
1299
  const parseHeaderPrefix = (text) => {
@@ -1111,8 +1450,10 @@ const splitBlockquote$1 = (bq, counters, currentDepth, sourceLines) => {
1111
1450
  }
1112
1451
  let bodyLines = [];
1113
1452
  const lines = paraToLines(child, sourceLines);
1453
+ const first = child.position?.start.line;
1454
+ const outsideBody = (index) => !sourceLines || base === void 0 || first === void 0 || markerCount(sourceLines[first - 1 + index] ?? "") < base;
1114
1455
  for (const [index, lineSegs] of lines.entries()) {
1115
- const header = stepHeader(lineSegs);
1456
+ const header = outsideBody(index) ? stepHeader(lineSegs) : null;
1116
1457
  if (header) {
1117
1458
  const bodyPara = linesToParagraph(bodyLines);
1118
1459
  if (bodyPara) currentSegment.body.push(bodyPara);
@@ -1168,12 +1509,19 @@ const buildTree$1 = (steps, targetDepth, start, end) => {
1168
1509
  data: { attrsRole: "container" }
1169
1510
  };
1170
1511
  };
1171
- const processStepsInChildren = (children, containerClass, sourceLines) => {
1512
+ const processStepsInChildren = (children, containerClass, sourceLines, quotes = 0, parentType = "root") => {
1172
1513
  const newChildren = [];
1173
1514
  let i = 0;
1174
1515
  while (i < children.length) {
1175
1516
  const node = children[i];
1176
1517
  if (!node) break;
1518
+ if (node.type === "paragraph" && sourceLines && !INDENTED_BLOCKS.has(parentType)) {
1519
+ const split = splitAtOpeningLine(node, sourceLines, quotes, (text) => parseHeaderPrefix(text)?.depth === 1);
1520
+ if (split) {
1521
+ children.splice(i, 1, ...split);
1522
+ continue;
1523
+ }
1524
+ }
1177
1525
  if (node.type === "paragraph") {
1178
1526
  const counters = /* @__PURE__ */ new Map();
1179
1527
  const headerPara = extractHeaderPara$1(node, counters);
@@ -1189,6 +1537,8 @@ const processStepsInChildren = (children, containerClass, sourceLines) => {
1189
1537
  const sibling = children[j];
1190
1538
  if (!sibling) break;
1191
1539
  if (sibling.type === "blockquote") {
1540
+ const line = sibling.position?.start.line;
1541
+ if (sourceLines && line !== void 0 && isAlertMarkerLine(sourceLines[line - 1] ?? "")) break;
1192
1542
  const segments = splitBlockquote$1(sibling, counters, currentDepth, sourceLines);
1193
1543
  const firstSeg = segments[0];
1194
1544
  if (firstSeg?.header === null) rawSteps[currentStepIdx]?.body.push(...firstSeg.body);
@@ -1221,6 +1571,7 @@ const processStepsInChildren = (children, containerClass, sourceLines) => {
1221
1571
  }
1222
1572
  const list = buildTree$1(rawSteps, 1, 0, rawSteps.length);
1223
1573
  list.containerClass = containerClass;
1574
+ if (parentType === "stepsItem") list.inBody = true;
1224
1575
  newChildren.push(list);
1225
1576
  children.splice(j, 0, ...splitAtGroupStarts$1(rest, restLine));
1226
1577
  i = j;
@@ -1237,13 +1588,15 @@ const processStepsInChildren = (children, containerClass, sourceLines) => {
1237
1588
  */
1238
1589
  const processSteps = (root, containerClass, source) => {
1239
1590
  const sourceLines = source?.split(/\r?\n/);
1240
- const walk = (node) => {
1591
+ const walk = (node, quotes) => {
1241
1592
  if (!Array.isArray(node.children) || node.children.length === 0) return;
1242
- processStepsInChildren(node.children, containerClass, sourceLines);
1243
- for (const child of node.children) walk(child);
1593
+ const inner = quotes + (MARKED_BLOCKS.has(node.type ?? "") ? 1 : 0);
1594
+ processStepsInChildren(node.children, containerClass, sourceLines, inner, node.type);
1595
+ for (const child of node.children) walk(child, inner);
1244
1596
  };
1245
1597
  processStepsInChildren(root.children, containerClass, sourceLines);
1246
- for (const child of root.children) walk(child);
1598
+ for (const child of root.children) walk(child, 0);
1599
+ if (sourceLines) endGroupsAtMarkers(root, sourceLines, `stepsList`);
1247
1600
  };
1248
1601
  // src/tabs.ts
1249
1602
  const TAB_HEADER_RE = /^(%{1,6})(\+)? (.*)/;
@@ -1399,11 +1752,13 @@ const splitBlockquote = (bq, explicit, sourceLines) => {
1399
1752
  explicit,
1400
1753
  lines: []
1401
1754
  };
1755
+ const base = sourceLines && !explicit ? blockquoteMarkers(bq, sourceLines) : void 0;
1402
1756
  const splitLines = (lines, markersOf, para) => {
1403
1757
  let bodyLines = [];
1404
1758
  for (const [index, lineSegs] of lines.entries()) {
1405
1759
  const first = lineSegs[0];
1406
- if (first?.type === "text") {
1760
+ const outsidePanel = !markersOf || base === void 0 || markersOf(index) < base;
1761
+ if (first?.type === "text" && outsidePanel) {
1407
1762
  const header = parseTabHeader(first.value);
1408
1763
  if (header) {
1409
1764
  if (bodyLines.length > 0) {
@@ -1568,12 +1923,19 @@ const buildTree = (rawTabs, targetDepth, start, end, containerClass) => {
1568
1923
  }
1569
1924
  };
1570
1925
  };
1571
- const processTabsInChildren = (children, containerClass, checkBlankLines = true, sourceLines) => {
1926
+ const processTabsInChildren = (children, containerClass, checkBlankLines = true, sourceLines, quotes = 0, parentType = "root") => {
1572
1927
  const newChildren = [];
1573
1928
  let i = 0;
1574
1929
  while (i < children.length) {
1575
1930
  const node = children[i];
1576
1931
  if (!node) break;
1932
+ if (node.type === "paragraph" && sourceLines && !INDENTED_BLOCKS.has(parentType)) {
1933
+ const split = splitAtOpeningLine(node, sourceLines, quotes, (text) => parseTabHeader(text)?.depth === 1);
1934
+ if (split) {
1935
+ children.splice(i, 1, ...split);
1936
+ continue;
1937
+ }
1938
+ }
1577
1939
  if (node.type === "paragraph") {
1578
1940
  const headerPara = extractHeaderPara(node);
1579
1941
  if (headerPara) {
@@ -1719,13 +2081,15 @@ const processTabsInChildren = (children, containerClass, checkBlankLines = true,
1719
2081
  */
1720
2082
  const processTabs = (root, containerClass, source) => {
1721
2083
  const sourceLines = source?.split(/\r?\n/);
1722
- const walk = (node) => {
2084
+ const walk = (node, quotes) => {
1723
2085
  if (!Array.isArray(node.children) || node.children.length === 0) return;
1724
- processTabsInChildren(node.children, containerClass, false, sourceLines);
1725
- for (const child of node.children) walk(child);
2086
+ const inner = quotes + (MARKED_BLOCKS.has(node.type ?? "") ? 1 : 0);
2087
+ processTabsInChildren(node.children, containerClass, false, sourceLines, inner, node.type);
2088
+ for (const child of node.children) walk(child, inner);
1726
2089
  };
1727
2090
  processTabsInChildren(root.children, containerClass, true, sourceLines);
1728
- for (const child of root.children) walk(child);
2091
+ for (const child of root.children) walk(child, 0);
2092
+ if (sourceLines) endGroupsAtMarkers(root, sourceLines, `tabsList`);
1729
2093
  };
1730
2094
  // src/editable.ts
1731
2095
  const editableTree = (root, build, setChildren, setData) => {
@@ -1813,6 +2177,6 @@ const inlineSvgMarkup = (content, alts, relativePath, limits) => {
1813
2177
  return index === 0 ? sprite + use : use;
1814
2178
  });
1815
2179
  };
1816
- export { ALERT_ALIASES, DEFAULT_TITLE, ICONS, LIGHTBOX_MARKER, createAbbreviationScan, createAlertResolver, createDelimitedTransform, createKbdTransform, editableTree, escapedIndices, headingRank, inlineSvgMarkup, isEmptyParagraph, isImageParagraph, lightboxDialog, lightboxId, lightboxTrigger, maskEscaped, maskTreeEscapes, parseAbbreviations, placeCutAttributes, plainText, processSteps, processStepsInChildren, processTabs, processTabsInChildren, scriptSyntax, sectionize, splitAlertParagraph, stripAttrs, svgSpriteId, tabLabelText, tabsBlockId, unmaskEscaped };
2180
+ export { ALERT_ALIASES, CAPTION_CLASS, DEFAULT_TITLE, ICONS, LIGHTBOX_MARKER, containersKept, createAbbreviationScan, createAlertResolver, createDelimitedTransform, createKbdTransform, editableTree, escapedIndices, figureProperties, hastAttributes, headingRank, inlineSvgMarkup, isAlertMarkerLine, isCaptionFigure, isCaptionLine, isEmptyParagraph, isImageParagraph, itemColumns, lightboxDialog, lightboxId, lightboxTrigger, maskEscaped, maskTreeEscapes, mermaidDiagram, mermaidFailure, mermaidIdPrefixes, mermaidSource, mermaidSvg, parseAbbreviations, placeCutAttributes, plainText, processCaptions, processSteps, processStepsInChildren, processTabs, processTabsInChildren, scriptSyntax, sectionize, splitAlertParagraph, startsCaption, stripAttrs, stripCaptionMarker, svgSpriteId, tabLabelText, tabsBlockId, unmaskEscaped };
1817
2181
 
1818
2182
  //# sourceMappingURL=index.mjs.map