@mirrordown/core 0.1.0 → 0.1.1
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/CHANGELOG.md +17 -0
- package/dist/index.d.mts +68 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +621 -169
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -133,6 +133,8 @@ const ICONS = {
|
|
|
133
133
|
};
|
|
134
134
|
// src/github-alerts/index.ts
|
|
135
135
|
const MARKER_RE = /^\[!([^\]]+)\]([+-])?/;
|
|
136
|
+
/** 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. */
|
|
137
|
+
const isAlertMarkerLine = (line) => MARKER_RE.test(line.replace(/^[ >]*/, ``));
|
|
136
138
|
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
137
139
|
/** Resolve alert keywords, titles and icons the same way in every engine. */
|
|
138
140
|
const createAlertResolver = (options = {}) => {
|
|
@@ -244,9 +246,9 @@ const INLINE_PARENTS = /* @__PURE__ */ new Set([
|
|
|
244
246
|
`subscript`,
|
|
245
247
|
`superscript`
|
|
246
248
|
]);
|
|
247
|
-
const WHITESPACE$
|
|
249
|
+
const WHITESPACE$3 = /^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/u;
|
|
248
250
|
const PUNCTUATION$1 = /^[\p{P}\p{S}]$/u;
|
|
249
|
-
const classify$1 = (char) => WHITESPACE$
|
|
251
|
+
const classify$1 = (char) => WHITESPACE$3.test(char) ? `space` : PUNCTUATION$1.test(char) ? `punct` : `other`;
|
|
250
252
|
/**
|
|
251
253
|
* 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
254
|
*/
|
|
@@ -484,8 +486,8 @@ const tokenType = (type) => type;
|
|
|
484
486
|
const temporary = (type) => tokenType(`${type}SequenceTemporary`);
|
|
485
487
|
const scriptOf = (tokenType) => Object.values(SCRIPTS).find(({ type }) => temporary(type) === tokenType);
|
|
486
488
|
const PUNCTUATION = /\p{P}|\p{S}/u;
|
|
487
|
-
const WHITESPACE$
|
|
488
|
-
const classify = (code) => code === null || code < 0 || code === 32 || WHITESPACE$
|
|
489
|
+
const WHITESPACE$2 = /\s/;
|
|
490
|
+
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
491
|
const splice = (list, start, remove, items) => {
|
|
490
492
|
list.splice(start, remove);
|
|
491
493
|
for (let i = 0; i < items.length; i += 1e4) list.splice(start + i, 0, ...items.slice(i, i + 1e4));
|
|
@@ -603,156 +605,6 @@ const scriptSyntax = (marker) => {
|
|
|
603
605
|
insideSpan: { null: [construct] }
|
|
604
606
|
};
|
|
605
607
|
};
|
|
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 `!` 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
608
|
// src/cut-attributes.ts
|
|
757
609
|
/** Paragraphs recognition rebuilt from lines, as it does where a tab or step header lazily continued them. */
|
|
758
610
|
const rebuiltParagraphs = /* @__PURE__ */ new WeakSet();
|
|
@@ -955,6 +807,492 @@ const rebuildLazyLines = (lines, level) => {
|
|
|
955
807
|
}
|
|
956
808
|
return blocks;
|
|
957
809
|
};
|
|
810
|
+
/** 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. */
|
|
811
|
+
const MARKED_BLOCKS = /* @__PURE__ */ new Set([
|
|
812
|
+
`blockquote`,
|
|
813
|
+
`alert`,
|
|
814
|
+
`tabsItem`,
|
|
815
|
+
`stepsItem`
|
|
816
|
+
]);
|
|
817
|
+
/** Blocks a line can lazily continue into by its indentation rather than its markers: a list item or a definition. */
|
|
818
|
+
const INDENTED_BLOCKS = /* @__PURE__ */ new Set([
|
|
819
|
+
`listItem`,
|
|
820
|
+
`defListDescription`,
|
|
821
|
+
`descriptionDetails`
|
|
822
|
+
]);
|
|
823
|
+
/**
|
|
824
|
+
* 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.
|
|
825
|
+
*/
|
|
826
|
+
const splitAtOpeningLine = (para, sourceLines, quotes, opens) => {
|
|
827
|
+
const start = para.position?.start.line;
|
|
828
|
+
if (start === void 0) return null;
|
|
829
|
+
const lines = paraToLines(para, sourceLines);
|
|
830
|
+
const at = lines.findIndex((line, index) => {
|
|
831
|
+
const first = line[0];
|
|
832
|
+
return index > 0 && first?.type === `text` && opens(first.value) && markerCount(sourceLines[start - 1 + index] ?? ``) === quotes;
|
|
833
|
+
});
|
|
834
|
+
if (at === -1) return null;
|
|
835
|
+
const head = linesToParagraph(lines.slice(0, at));
|
|
836
|
+
const tail = linesToParagraph(lines.slice(at));
|
|
837
|
+
if (!head || !tail) return null;
|
|
838
|
+
if (para.data) tail.data = para.data;
|
|
839
|
+
return [head, tail];
|
|
840
|
+
};
|
|
841
|
+
const lastParagraph = (node) => {
|
|
842
|
+
const last = node?.children?.at(-1);
|
|
843
|
+
if (!last) return void 0;
|
|
844
|
+
if (last.type === `paragraph`) return last;
|
|
845
|
+
return lastParagraph(last);
|
|
846
|
+
};
|
|
847
|
+
/**
|
|
848
|
+
* 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.
|
|
849
|
+
*/
|
|
850
|
+
const endGroupsAtMarkers = (root, sourceLines, listType) => {
|
|
851
|
+
const visit = (node, quotes) => {
|
|
852
|
+
const children = node.children;
|
|
853
|
+
if (!children) return;
|
|
854
|
+
for (let i = 0; i < children.length; i++) {
|
|
855
|
+
const child = children[i];
|
|
856
|
+
visit(child, quotes + (MARKED_BLOCKS.has(child.type) ? 1 : 0));
|
|
857
|
+
if (child.type !== listType) continue;
|
|
858
|
+
const para = lastParagraph(child.children?.at(-1));
|
|
859
|
+
const start = para?.position?.start.line;
|
|
860
|
+
if (!para || start === void 0) continue;
|
|
861
|
+
const lines = paraToLines(para, sourceLines);
|
|
862
|
+
const outside = lines.findIndex((_, index) => index > 0 && markerCount(sourceLines[start - 1 + index] ?? ``) < quotes + 1);
|
|
863
|
+
if (outside === -1) continue;
|
|
864
|
+
const kept = linesToParagraph(lines.slice(0, outside));
|
|
865
|
+
const moved = linesToParagraph(lines.slice(outside));
|
|
866
|
+
if (!kept || !moved) continue;
|
|
867
|
+
para.children = kept.children;
|
|
868
|
+
const data = para.data;
|
|
869
|
+
if (data?.hProperties) {
|
|
870
|
+
const { hProperties, ...rest } = data;
|
|
871
|
+
moved.data = { hProperties };
|
|
872
|
+
para.data = rest;
|
|
873
|
+
}
|
|
874
|
+
children.splice(i + 1, 0, moved);
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
visit(root, 0);
|
|
878
|
+
};
|
|
879
|
+
// src/line-containers.ts
|
|
880
|
+
/**
|
|
881
|
+
* How many of `containers`, outermost first, a line is written in. Tabs count as one column.
|
|
882
|
+
*/
|
|
883
|
+
const containersKept = (line, containers) => {
|
|
884
|
+
const prefix = /^[ \t>]*/.exec(line)?.[0] ?? ``;
|
|
885
|
+
const markers = prefix.split(`>`).length - 1;
|
|
886
|
+
let kept = 0;
|
|
887
|
+
for (const container of containers) {
|
|
888
|
+
if (container.kind === `quote` ? markers < container.markers : prefix.length < container.column) break;
|
|
889
|
+
kept++;
|
|
890
|
+
}
|
|
891
|
+
return kept;
|
|
892
|
+
};
|
|
893
|
+
/**
|
|
894
|
+
* 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.
|
|
895
|
+
*/
|
|
896
|
+
const itemColumns = (line) => {
|
|
897
|
+
const columns = [];
|
|
898
|
+
let pos = 0;
|
|
899
|
+
for (;;) {
|
|
900
|
+
while (line[pos] === ` ` || line[pos] === `\t`) pos++;
|
|
901
|
+
if (line[pos] === `>`) {
|
|
902
|
+
pos++;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
const marker = /^(?:[-+*]|\d{1,9}[.)])(?=[ \t]|$)/.exec(line.slice(pos));
|
|
906
|
+
if (!marker) return columns;
|
|
907
|
+
const end = pos + marker[0].length;
|
|
908
|
+
let spaces = 0;
|
|
909
|
+
while (line[end + spaces] === ` ` || line[end + spaces] === `\t`) spaces++;
|
|
910
|
+
const column = end + spaces === line.length || spaces > 4 ? end + 1 : end + spaces;
|
|
911
|
+
columns.push(column);
|
|
912
|
+
if (spaces > 4) return columns;
|
|
913
|
+
pos = column;
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
// src/caption.ts
|
|
917
|
+
/** The class of the `<figure>` a caption wraps its block in. */
|
|
918
|
+
const CAPTION_CLASS = "markdown-caption";
|
|
919
|
+
/** 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. */
|
|
920
|
+
const isCaptionLine = (line) => /^~:[ \t]+\S/.test(line.replace(/^[ \t>]*/, ``));
|
|
921
|
+
/** Whether text starts with a caption marker, as a parser gives a caption line's text (inline markdown may follow in other nodes). */
|
|
922
|
+
const startsCaption = (text) => /^~:(?:[ \t]|$)/.test(text);
|
|
923
|
+
/** The text after a caption marker. */
|
|
924
|
+
const stripCaptionMarker = (text) => text.replace(/^~:[ \t]*/, ``);
|
|
925
|
+
/**
|
|
926
|
+
* 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).
|
|
927
|
+
*/
|
|
928
|
+
const UNCAPTIONED = /* @__PURE__ */ new Set([
|
|
929
|
+
`heading`,
|
|
930
|
+
`thematicBreak`,
|
|
931
|
+
`html`,
|
|
932
|
+
`definition`,
|
|
933
|
+
`footnoteDefinition`,
|
|
934
|
+
`yaml`,
|
|
935
|
+
`toml`,
|
|
936
|
+
`figure`,
|
|
937
|
+
`alertTitle`,
|
|
938
|
+
`stepsTitle`,
|
|
939
|
+
`tabsLabel`,
|
|
940
|
+
`tabsLabels`,
|
|
941
|
+
`tabsInput`,
|
|
942
|
+
`defListTerm`,
|
|
943
|
+
`descriptionTerm`
|
|
944
|
+
]);
|
|
945
|
+
/** The properties of a caption's `<figure>`: its class, then the attributes written at the end of the caption (a class joins it). */
|
|
946
|
+
const figureProperties = (author) => {
|
|
947
|
+
const { class: extraClass, className: extraClassName, ...rest } = author ?? {};
|
|
948
|
+
const extra = [extraClass, extraClassName].flat().flatMap((value) => typeof value === `string` ? value.split(` `) : []).filter((value) => value !== ``);
|
|
949
|
+
return {
|
|
950
|
+
className: [CAPTION_CLASS, ...extra],
|
|
951
|
+
...rest
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
const WHITESPACE$1 = /^\s*$/;
|
|
955
|
+
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`));
|
|
956
|
+
const figure = (content, caption, author) => ({
|
|
957
|
+
type: `figure`,
|
|
958
|
+
data: {
|
|
959
|
+
hName: `figure`,
|
|
960
|
+
hProperties: figureProperties(author)
|
|
961
|
+
},
|
|
962
|
+
children: [...content, {
|
|
963
|
+
type: `figcaption`,
|
|
964
|
+
data: { hName: `figcaption` },
|
|
965
|
+
children: caption
|
|
966
|
+
}]
|
|
967
|
+
});
|
|
968
|
+
const captionContent = (lines) => {
|
|
969
|
+
const first = lines[0]?.[0];
|
|
970
|
+
if (first?.type === `text`) {
|
|
971
|
+
const value = stripCaptionMarker(first.value);
|
|
972
|
+
lines[0] = value ? [{
|
|
973
|
+
...first,
|
|
974
|
+
value
|
|
975
|
+
}, ...lines[0].slice(1)] : lines[0].slice(1);
|
|
976
|
+
}
|
|
977
|
+
return linesToParagraph(lines)?.children ?? [];
|
|
978
|
+
};
|
|
979
|
+
const takeProperties = (node) => {
|
|
980
|
+
const data = node.data;
|
|
981
|
+
if (!data?.hProperties) return void 0;
|
|
982
|
+
const { hProperties, ...rest } = data;
|
|
983
|
+
node.data = rest;
|
|
984
|
+
return hProperties;
|
|
985
|
+
};
|
|
986
|
+
/**
|
|
987
|
+
* 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.
|
|
988
|
+
*/
|
|
989
|
+
const processCaptions = (root, source) => {
|
|
990
|
+
if (!source.includes(`~:`)) return;
|
|
991
|
+
const sourceLines = source.split(/\r?\n/);
|
|
992
|
+
const parents = /* @__PURE__ */ new Map();
|
|
993
|
+
const paragraphs = [];
|
|
994
|
+
const tables = [];
|
|
995
|
+
const walk = (node) => {
|
|
996
|
+
if (node.type === `paragraph`) paragraphs.push(node);
|
|
997
|
+
else if (node.type === `table`) tables.push(node);
|
|
998
|
+
for (const child of node.children ?? []) {
|
|
999
|
+
parents.set(child, node);
|
|
1000
|
+
walk(child);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
walk(root);
|
|
1004
|
+
const replace = (node, replacement) => {
|
|
1005
|
+
const parent = parents.get(node);
|
|
1006
|
+
const siblings = parent?.children;
|
|
1007
|
+
if (!parent || !siblings) return;
|
|
1008
|
+
const index = siblings.indexOf(node);
|
|
1009
|
+
if (index === -1) return;
|
|
1010
|
+
if (replacement) {
|
|
1011
|
+
siblings.splice(index, 1, replacement);
|
|
1012
|
+
parents.set(replacement, parent);
|
|
1013
|
+
parents.set(node, replacement);
|
|
1014
|
+
} else siblings.splice(index, 1);
|
|
1015
|
+
};
|
|
1016
|
+
const captioned = (node) => parents.get(node)?.type === `figure`;
|
|
1017
|
+
const ancestorsOf = (node) => {
|
|
1018
|
+
const ancestors = [];
|
|
1019
|
+
for (let p = parents.get(node); p; p = parents.get(p)) ancestors.unshift(p);
|
|
1020
|
+
return ancestors;
|
|
1021
|
+
};
|
|
1022
|
+
const containersOf = (ancestors) => {
|
|
1023
|
+
const containers = [];
|
|
1024
|
+
const nearest = (index, type) => ancestors.slice(0, index).reverse().find((a) => a.type === type);
|
|
1025
|
+
let quotes = 0;
|
|
1026
|
+
for (const [index, ancestor] of ancestors.entries()) {
|
|
1027
|
+
const quote = (target) => {
|
|
1028
|
+
containers.push({
|
|
1029
|
+
target: target ?? ancestor,
|
|
1030
|
+
line: {
|
|
1031
|
+
kind: `quote`,
|
|
1032
|
+
markers: ++quotes
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
};
|
|
1036
|
+
switch (ancestor.type) {
|
|
1037
|
+
case `blockquote`:
|
|
1038
|
+
case `alert`:
|
|
1039
|
+
quote(ancestor);
|
|
1040
|
+
break;
|
|
1041
|
+
case `tabsItem`:
|
|
1042
|
+
case `tabsPanel`:
|
|
1043
|
+
quote(nearest(index, `tabsList`));
|
|
1044
|
+
break;
|
|
1045
|
+
case `stepsItem`:
|
|
1046
|
+
if (!(ancestor.children ?? []).some((c) => c.type === `stepsBody`)) quote(nearest(index, `stepsList`));
|
|
1047
|
+
break;
|
|
1048
|
+
case `stepsBody`:
|
|
1049
|
+
quote(nearest(index, `stepsList`));
|
|
1050
|
+
break;
|
|
1051
|
+
case `listItem`:
|
|
1052
|
+
case `defListDescription`:
|
|
1053
|
+
case `descriptionDetails`: {
|
|
1054
|
+
const column = ancestor.children?.[0]?.position?.start.column;
|
|
1055
|
+
if (column === void 0) return void 0;
|
|
1056
|
+
containers.push({
|
|
1057
|
+
target: ancestors[index - 1] ?? ancestor,
|
|
1058
|
+
line: {
|
|
1059
|
+
kind: `item`,
|
|
1060
|
+
column: column - 1
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return containers;
|
|
1068
|
+
};
|
|
1069
|
+
for (const table of tables) {
|
|
1070
|
+
const rows = table.children ?? [];
|
|
1071
|
+
const first = rows.findIndex((row, index) => {
|
|
1072
|
+
const line = row.position?.start.line;
|
|
1073
|
+
const text = row.children?.[0]?.children?.[0];
|
|
1074
|
+
return index > 0 && line !== void 0 && isCaptionLine(sourceLines[line - 1] ?? ``) && text?.type === `text` && startsCaption(text.value ?? ``);
|
|
1075
|
+
});
|
|
1076
|
+
if (first === -1) continue;
|
|
1077
|
+
const captionRows = rows.slice(first);
|
|
1078
|
+
const lastCell = captionRows.at(-1)?.children?.[0];
|
|
1079
|
+
const author = lastCell ? takeProperties(lastCell) : void 0;
|
|
1080
|
+
const caption = captionContent(captionRows.map((row) => row.children?.[0]?.children ?? []));
|
|
1081
|
+
table.children = rows.slice(0, first);
|
|
1082
|
+
replace(table, figure([table], caption, author));
|
|
1083
|
+
}
|
|
1084
|
+
for (const para of paragraphs.reverse()) {
|
|
1085
|
+
const first = para.position?.start.line;
|
|
1086
|
+
if (first === void 0) continue;
|
|
1087
|
+
const lines = paraToLines(para, sourceLines);
|
|
1088
|
+
const parent = parents.get(para);
|
|
1089
|
+
const start = (parent?.type === `alert` || parent?.type === `blockquote` && parent.data?.alertType !== void 0) && isAlertMarkerLine(sourceLines[first - 1] ?? ``) ? first + 1 : first;
|
|
1090
|
+
const isCaptionAt = (index) => {
|
|
1091
|
+
const text = lines[index]?.[0];
|
|
1092
|
+
return isCaptionLine(sourceLines[start + index - 1] ?? ``) && text?.type === `text` && startsCaption(text.value);
|
|
1093
|
+
};
|
|
1094
|
+
if (isCaptionAt(0)) {
|
|
1095
|
+
const siblings = parents.get(para)?.children ?? [];
|
|
1096
|
+
const before = siblings[siblings.indexOf(para) - 1];
|
|
1097
|
+
const above = sourceLines[start - 2];
|
|
1098
|
+
if (!before || UNCAPTIONED.has(before.type) || above === void 0 || above.replace(/^[ \t>]*/, ``) === ``) continue;
|
|
1099
|
+
const author = takeProperties(para);
|
|
1100
|
+
replace(para, null);
|
|
1101
|
+
replace(before, figure([before], captionContent(lines), author));
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
let containers;
|
|
1105
|
+
const segments = [];
|
|
1106
|
+
for (let index = 1; index < lines.length; index++) {
|
|
1107
|
+
if (!isCaptionAt(index)) continue;
|
|
1108
|
+
containers ??= containersOf(ancestorsOf(para));
|
|
1109
|
+
if (!containers) break;
|
|
1110
|
+
const kept = containersKept(sourceLines[start + index - 1] ?? ``, containers.map((c) => c.line));
|
|
1111
|
+
const previous = segments.at(-1);
|
|
1112
|
+
if (previous && kept >= previous.kept) continue;
|
|
1113
|
+
const target = containers[kept]?.target ?? para;
|
|
1114
|
+
if (captioned(target)) continue;
|
|
1115
|
+
segments.push({
|
|
1116
|
+
index,
|
|
1117
|
+
kept,
|
|
1118
|
+
target
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
if (segments.length === 0) continue;
|
|
1122
|
+
const head = lines.slice(0, segments[0].index);
|
|
1123
|
+
const lastLine = head.at(-1);
|
|
1124
|
+
if (lastLine?.at(-1)?.type === `break`) head[head.length - 1] = lastLine.slice(0, -1);
|
|
1125
|
+
para.children = linesToParagraph(head)?.children ?? [];
|
|
1126
|
+
const author = takeProperties(para);
|
|
1127
|
+
for (const [i, segment] of segments.entries()) {
|
|
1128
|
+
const caption = captionContent(lines.slice(segment.index, segments[i + 1]?.index));
|
|
1129
|
+
const properties = i === segments.length - 1 ? author : void 0;
|
|
1130
|
+
const { target } = segment;
|
|
1131
|
+
if (target === para && onlyImages(para.children ?? [])) replace(para, figure(para.children ?? [], caption, properties));
|
|
1132
|
+
else replace(target, figure([target], caption, properties));
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
// src/hast.ts
|
|
1137
|
+
const WHITESPACE = /^[ \t\n\f\r]*$/;
|
|
1138
|
+
const isWhitespaceText = (node) => node.type === `text` && WHITESPACE.test(node.value ?? ``);
|
|
1139
|
+
/** A `<p>` with no children, or only whitespace text: squeeze-paragraphs removes it. */
|
|
1140
|
+
const isEmptyParagraph = (node) => node.tagName === `p` && (node.children ?? []).every(isWhitespaceText);
|
|
1141
|
+
/**
|
|
1142
|
+
* 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.
|
|
1143
|
+
*/
|
|
1144
|
+
const isImageParagraph = (node) => {
|
|
1145
|
+
const scan = (parent, inLink) => {
|
|
1146
|
+
let result = `empty`;
|
|
1147
|
+
for (const child of parent.children ?? []) {
|
|
1148
|
+
if (isWhitespaceText(child)) continue;
|
|
1149
|
+
if (child.type !== `element`) return `other`;
|
|
1150
|
+
if (child.tagName === `img`) {
|
|
1151
|
+
result = `image`;
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (!inLink && (child.tagName === `a` || child.tagName === `button`)) {
|
|
1155
|
+
const inner = scan(child, true);
|
|
1156
|
+
if (inner === `other`) return `other`;
|
|
1157
|
+
if (inner === `image`) result = `image`;
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
return `other`;
|
|
1161
|
+
}
|
|
1162
|
+
return result;
|
|
1163
|
+
};
|
|
1164
|
+
return node.tagName === `p` && scan(node, false) === `image`;
|
|
1165
|
+
};
|
|
1166
|
+
/** The rank of an `h1`–`h6` element, or `undefined`. */
|
|
1167
|
+
const headingRank = (node) => {
|
|
1168
|
+
if (node.type !== `element`) return void 0;
|
|
1169
|
+
const match = /^h([1-6])$/i.exec(node.tagName ?? ``);
|
|
1170
|
+
return match ? Number(match[1]) : void 0;
|
|
1171
|
+
};
|
|
1172
|
+
/**
|
|
1173
|
+
* Wrap heading-delimited runs of `nodes` into `<section data-depth>` elements, nesting deeper headings inside shallower ones. Content before the first heading stays unwrapped.
|
|
1174
|
+
*/
|
|
1175
|
+
const sectionize = (nodes, section, floor = 0) => {
|
|
1176
|
+
let minRank = Infinity;
|
|
1177
|
+
for (const node of nodes) {
|
|
1178
|
+
const rank = headingRank(node);
|
|
1179
|
+
if (rank !== void 0 && rank > floor && rank < minRank) minRank = rank;
|
|
1180
|
+
}
|
|
1181
|
+
if (minRank === Infinity) return [...nodes];
|
|
1182
|
+
const result = [];
|
|
1183
|
+
let i = 0;
|
|
1184
|
+
for (; i < nodes.length; i++) {
|
|
1185
|
+
const node = nodes[i];
|
|
1186
|
+
if (!node || headingRank(node) === minRank) break;
|
|
1187
|
+
result.push(node);
|
|
1188
|
+
}
|
|
1189
|
+
while (i < nodes.length) {
|
|
1190
|
+
const start = i;
|
|
1191
|
+
for (i++; i < nodes.length; i++) {
|
|
1192
|
+
const node = nodes[i];
|
|
1193
|
+
if (!node || headingRank(node) === minRank) break;
|
|
1194
|
+
}
|
|
1195
|
+
result.push(section(minRank, sectionize(nodes.slice(start, i), section, minRank)));
|
|
1196
|
+
}
|
|
1197
|
+
return result;
|
|
1198
|
+
};
|
|
1199
|
+
/**
|
|
1200
|
+
* 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 `!` leaves behind.
|
|
1201
|
+
*/
|
|
1202
|
+
const LIGHTBOX_MARKER = /(?:^|\s)!$/;
|
|
1203
|
+
/**
|
|
1204
|
+
* 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.
|
|
1205
|
+
*/
|
|
1206
|
+
const lightboxId = (src) => {
|
|
1207
|
+
let hash = 2166136261;
|
|
1208
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
1209
|
+
hash ^= src.charCodeAt(i);
|
|
1210
|
+
hash = Math.imul(hash, 16777619);
|
|
1211
|
+
}
|
|
1212
|
+
return `markdown-lightbox-${(hash >>> 0).toString(36)}`;
|
|
1213
|
+
};
|
|
1214
|
+
/** The button that replaces an opted-in image and opens its dialog. */
|
|
1215
|
+
const lightboxTrigger = (id, image) => ({
|
|
1216
|
+
type: `element`,
|
|
1217
|
+
tagName: `button`,
|
|
1218
|
+
properties: {
|
|
1219
|
+
type: `button`,
|
|
1220
|
+
className: [`markdown-lightbox-trigger`],
|
|
1221
|
+
command: `show-modal`,
|
|
1222
|
+
commandfor: id
|
|
1223
|
+
},
|
|
1224
|
+
children: [image]
|
|
1225
|
+
});
|
|
1226
|
+
/** The modal dialog, appended at the document end, showing the image full size, and its caption under it when one is given. */
|
|
1227
|
+
const lightboxDialog = (id, image, caption) => ({
|
|
1228
|
+
type: `element`,
|
|
1229
|
+
tagName: `dialog`,
|
|
1230
|
+
properties: {
|
|
1231
|
+
id,
|
|
1232
|
+
className: [`markdown-lightbox`],
|
|
1233
|
+
closedby: `any`
|
|
1234
|
+
},
|
|
1235
|
+
children: [{
|
|
1236
|
+
type: `element`,
|
|
1237
|
+
tagName: `button`,
|
|
1238
|
+
properties: {
|
|
1239
|
+
type: `button`,
|
|
1240
|
+
className: [`markdown-lightbox-close`],
|
|
1241
|
+
command: `close`,
|
|
1242
|
+
commandfor: id,
|
|
1243
|
+
ariaLabel: `Close image`
|
|
1244
|
+
},
|
|
1245
|
+
children: [image]
|
|
1246
|
+
}, ...caption ? [{
|
|
1247
|
+
type: `element`,
|
|
1248
|
+
tagName: `p`,
|
|
1249
|
+
properties: { className: [`markdown-lightbox-caption`] },
|
|
1250
|
+
children: caption
|
|
1251
|
+
}] : []]
|
|
1252
|
+
});
|
|
1253
|
+
/** 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. */
|
|
1254
|
+
const isCaptionFigure = (node) => {
|
|
1255
|
+
const className = node?.properties?.className;
|
|
1256
|
+
return node?.tagName === `figure` && Array.isArray(className) && className.includes("markdown-caption");
|
|
1257
|
+
};
|
|
1258
|
+
// src/abbr.ts
|
|
1259
|
+
const DEFINITION = /^\*\[([^[\]]+)\]:\s*(.+)$/;
|
|
1260
|
+
const REGEX_SPECIAL = /[.*+?^${}()|[\]\\]/g;
|
|
1261
|
+
/**
|
|
1262
|
+
* 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).
|
|
1263
|
+
*/
|
|
1264
|
+
const parseAbbreviations = (text) => {
|
|
1265
|
+
const definitions = [];
|
|
1266
|
+
for (const line of text.split(`\n`)) {
|
|
1267
|
+
const match = DEFINITION.exec(line);
|
|
1268
|
+
if (!match) return void 0;
|
|
1269
|
+
definitions.push([match[1] ?? ``, (match[2] ?? ``).trim()]);
|
|
1270
|
+
}
|
|
1271
|
+
return definitions;
|
|
1272
|
+
};
|
|
1273
|
+
/**
|
|
1274
|
+
* 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.
|
|
1275
|
+
*/
|
|
1276
|
+
const createAbbreviationScan = (abbreviations) => {
|
|
1277
|
+
const terms = [...abbreviations.keys()].sort((a, b) => b.length - a.length).map((term) => term.replace(REGEX_SPECIAL, `\\$&`));
|
|
1278
|
+
const pattern = new RegExp(`\\b(${terms.join(`|`)})\\b`, `g`);
|
|
1279
|
+
return (value) => {
|
|
1280
|
+
const parts = [];
|
|
1281
|
+
let last = 0;
|
|
1282
|
+
for (const match of value.matchAll(pattern)) {
|
|
1283
|
+
const term = match[1] ?? ``;
|
|
1284
|
+
if (match.index > last) parts.push({ text: value.slice(last, match.index) });
|
|
1285
|
+
parts.push({
|
|
1286
|
+
term,
|
|
1287
|
+
title: abbreviations.get(term) ?? ``
|
|
1288
|
+
});
|
|
1289
|
+
last = match.index + term.length;
|
|
1290
|
+
}
|
|
1291
|
+
if (parts.length === 0) return void 0;
|
|
1292
|
+
if (last < value.length) parts.push({ text: value.slice(last) });
|
|
1293
|
+
return parts;
|
|
1294
|
+
};
|
|
1295
|
+
};
|
|
958
1296
|
// src/steps.ts
|
|
959
1297
|
const STEP_HEADER_RE = /^(@{1,6})(\d+)\. ?(.*)/s;
|
|
960
1298
|
const parseHeaderPrefix = (text) => {
|
|
@@ -1111,8 +1449,10 @@ const splitBlockquote$1 = (bq, counters, currentDepth, sourceLines) => {
|
|
|
1111
1449
|
}
|
|
1112
1450
|
let bodyLines = [];
|
|
1113
1451
|
const lines = paraToLines(child, sourceLines);
|
|
1452
|
+
const first = child.position?.start.line;
|
|
1453
|
+
const outsideBody = (index) => !sourceLines || base === void 0 || first === void 0 || markerCount(sourceLines[first - 1 + index] ?? "") < base;
|
|
1114
1454
|
for (const [index, lineSegs] of lines.entries()) {
|
|
1115
|
-
const header = stepHeader(lineSegs);
|
|
1455
|
+
const header = outsideBody(index) ? stepHeader(lineSegs) : null;
|
|
1116
1456
|
if (header) {
|
|
1117
1457
|
const bodyPara = linesToParagraph(bodyLines);
|
|
1118
1458
|
if (bodyPara) currentSegment.body.push(bodyPara);
|
|
@@ -1168,12 +1508,19 @@ const buildTree$1 = (steps, targetDepth, start, end) => {
|
|
|
1168
1508
|
data: { attrsRole: "container" }
|
|
1169
1509
|
};
|
|
1170
1510
|
};
|
|
1171
|
-
const processStepsInChildren = (children, containerClass, sourceLines) => {
|
|
1511
|
+
const processStepsInChildren = (children, containerClass, sourceLines, quotes = 0, parentType = "root") => {
|
|
1172
1512
|
const newChildren = [];
|
|
1173
1513
|
let i = 0;
|
|
1174
1514
|
while (i < children.length) {
|
|
1175
1515
|
const node = children[i];
|
|
1176
1516
|
if (!node) break;
|
|
1517
|
+
if (node.type === "paragraph" && sourceLines && !INDENTED_BLOCKS.has(parentType)) {
|
|
1518
|
+
const split = splitAtOpeningLine(node, sourceLines, quotes, (text) => parseHeaderPrefix(text)?.depth === 1);
|
|
1519
|
+
if (split) {
|
|
1520
|
+
children.splice(i, 1, ...split);
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1177
1524
|
if (node.type === "paragraph") {
|
|
1178
1525
|
const counters = /* @__PURE__ */ new Map();
|
|
1179
1526
|
const headerPara = extractHeaderPara$1(node, counters);
|
|
@@ -1189,6 +1536,8 @@ const processStepsInChildren = (children, containerClass, sourceLines) => {
|
|
|
1189
1536
|
const sibling = children[j];
|
|
1190
1537
|
if (!sibling) break;
|
|
1191
1538
|
if (sibling.type === "blockquote") {
|
|
1539
|
+
const line = sibling.position?.start.line;
|
|
1540
|
+
if (sourceLines && line !== void 0 && isAlertMarkerLine(sourceLines[line - 1] ?? "")) break;
|
|
1192
1541
|
const segments = splitBlockquote$1(sibling, counters, currentDepth, sourceLines);
|
|
1193
1542
|
const firstSeg = segments[0];
|
|
1194
1543
|
if (firstSeg?.header === null) rawSteps[currentStepIdx]?.body.push(...firstSeg.body);
|
|
@@ -1221,6 +1570,7 @@ const processStepsInChildren = (children, containerClass, sourceLines) => {
|
|
|
1221
1570
|
}
|
|
1222
1571
|
const list = buildTree$1(rawSteps, 1, 0, rawSteps.length);
|
|
1223
1572
|
list.containerClass = containerClass;
|
|
1573
|
+
if (parentType === "stepsItem") list.inBody = true;
|
|
1224
1574
|
newChildren.push(list);
|
|
1225
1575
|
children.splice(j, 0, ...splitAtGroupStarts$1(rest, restLine));
|
|
1226
1576
|
i = j;
|
|
@@ -1237,13 +1587,15 @@ const processStepsInChildren = (children, containerClass, sourceLines) => {
|
|
|
1237
1587
|
*/
|
|
1238
1588
|
const processSteps = (root, containerClass, source) => {
|
|
1239
1589
|
const sourceLines = source?.split(/\r?\n/);
|
|
1240
|
-
const walk = (node) => {
|
|
1590
|
+
const walk = (node, quotes) => {
|
|
1241
1591
|
if (!Array.isArray(node.children) || node.children.length === 0) return;
|
|
1242
|
-
|
|
1243
|
-
|
|
1592
|
+
const inner = quotes + (MARKED_BLOCKS.has(node.type ?? "") ? 1 : 0);
|
|
1593
|
+
processStepsInChildren(node.children, containerClass, sourceLines, inner, node.type);
|
|
1594
|
+
for (const child of node.children) walk(child, inner);
|
|
1244
1595
|
};
|
|
1245
1596
|
processStepsInChildren(root.children, containerClass, sourceLines);
|
|
1246
|
-
for (const child of root.children) walk(child);
|
|
1597
|
+
for (const child of root.children) walk(child, 0);
|
|
1598
|
+
if (sourceLines) endGroupsAtMarkers(root, sourceLines, `stepsList`);
|
|
1247
1599
|
};
|
|
1248
1600
|
// src/tabs.ts
|
|
1249
1601
|
const TAB_HEADER_RE = /^(%{1,6})(\+)? (.*)/;
|
|
@@ -1399,11 +1751,13 @@ const splitBlockquote = (bq, explicit, sourceLines) => {
|
|
|
1399
1751
|
explicit,
|
|
1400
1752
|
lines: []
|
|
1401
1753
|
};
|
|
1754
|
+
const base = sourceLines && !explicit ? blockquoteMarkers(bq, sourceLines) : void 0;
|
|
1402
1755
|
const splitLines = (lines, markersOf, para) => {
|
|
1403
1756
|
let bodyLines = [];
|
|
1404
1757
|
for (const [index, lineSegs] of lines.entries()) {
|
|
1405
1758
|
const first = lineSegs[0];
|
|
1406
|
-
|
|
1759
|
+
const outsidePanel = !markersOf || base === void 0 || markersOf(index) < base;
|
|
1760
|
+
if (first?.type === "text" && outsidePanel) {
|
|
1407
1761
|
const header = parseTabHeader(first.value);
|
|
1408
1762
|
if (header) {
|
|
1409
1763
|
if (bodyLines.length > 0) {
|
|
@@ -1568,12 +1922,19 @@ const buildTree = (rawTabs, targetDepth, start, end, containerClass) => {
|
|
|
1568
1922
|
}
|
|
1569
1923
|
};
|
|
1570
1924
|
};
|
|
1571
|
-
const processTabsInChildren = (children, containerClass, checkBlankLines = true, sourceLines) => {
|
|
1925
|
+
const processTabsInChildren = (children, containerClass, checkBlankLines = true, sourceLines, quotes = 0, parentType = "root") => {
|
|
1572
1926
|
const newChildren = [];
|
|
1573
1927
|
let i = 0;
|
|
1574
1928
|
while (i < children.length) {
|
|
1575
1929
|
const node = children[i];
|
|
1576
1930
|
if (!node) break;
|
|
1931
|
+
if (node.type === "paragraph" && sourceLines && !INDENTED_BLOCKS.has(parentType)) {
|
|
1932
|
+
const split = splitAtOpeningLine(node, sourceLines, quotes, (text) => parseTabHeader(text)?.depth === 1);
|
|
1933
|
+
if (split) {
|
|
1934
|
+
children.splice(i, 1, ...split);
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1577
1938
|
if (node.type === "paragraph") {
|
|
1578
1939
|
const headerPara = extractHeaderPara(node);
|
|
1579
1940
|
if (headerPara) {
|
|
@@ -1719,13 +2080,15 @@ const processTabsInChildren = (children, containerClass, checkBlankLines = true,
|
|
|
1719
2080
|
*/
|
|
1720
2081
|
const processTabs = (root, containerClass, source) => {
|
|
1721
2082
|
const sourceLines = source?.split(/\r?\n/);
|
|
1722
|
-
const walk = (node) => {
|
|
2083
|
+
const walk = (node, quotes) => {
|
|
1723
2084
|
if (!Array.isArray(node.children) || node.children.length === 0) return;
|
|
1724
|
-
|
|
1725
|
-
|
|
2085
|
+
const inner = quotes + (MARKED_BLOCKS.has(node.type ?? "") ? 1 : 0);
|
|
2086
|
+
processTabsInChildren(node.children, containerClass, false, sourceLines, inner, node.type);
|
|
2087
|
+
for (const child of node.children) walk(child, inner);
|
|
1726
2088
|
};
|
|
1727
2089
|
processTabsInChildren(root.children, containerClass, true, sourceLines);
|
|
1728
|
-
for (const child of root.children) walk(child);
|
|
2090
|
+
for (const child of root.children) walk(child, 0);
|
|
2091
|
+
if (sourceLines) endGroupsAtMarkers(root, sourceLines, `tabsList`);
|
|
1729
2092
|
};
|
|
1730
2093
|
// src/editable.ts
|
|
1731
2094
|
const editableTree = (root, build, setChildren, setData) => {
|
|
@@ -1765,7 +2128,7 @@ const editableTree = (root, build, setChildren, setData) => {
|
|
|
1765
2128
|
};
|
|
1766
2129
|
};
|
|
1767
2130
|
// src/inline-svg.ts
|
|
1768
|
-
const hash = (str) => {
|
|
2131
|
+
const hash$1 = (str) => {
|
|
1769
2132
|
let h = 2166136261;
|
|
1770
2133
|
for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), 16777619) >>> 0;
|
|
1771
2134
|
return h.toString(36);
|
|
@@ -1773,7 +2136,7 @@ const hash = (str) => {
|
|
|
1773
2136
|
/**
|
|
1774
2137
|
* The id of a deduplicated SVG's sprite: a hash of its path relative to the document, with `/` separators, so the same document renders the same ids on any machine or OS (an absolute path would leak the build machine into the HTML).
|
|
1775
2138
|
*/
|
|
1776
|
-
const svgSpriteId = (relativePath) => `svg-sprite-${hash(relativePath)}`;
|
|
2139
|
+
const svgSpriteId = (relativePath) => `svg-sprite-${hash$1(relativePath)}`;
|
|
1777
2140
|
const extractSvgParts = (content) => {
|
|
1778
2141
|
const openTag = /<svg([^>]*?)>/i.exec(content)?.[1] ?? ``;
|
|
1779
2142
|
return {
|
|
@@ -1813,6 +2176,95 @@ const inlineSvgMarkup = (content, alts, relativePath, limits) => {
|
|
|
1813
2176
|
return index === 0 ? sprite + use : use;
|
|
1814
2177
|
});
|
|
1815
2178
|
};
|
|
1816
|
-
|
|
2179
|
+
// src/mermaid-pele.ts
|
|
2180
|
+
/** The class a fenced `mermaid` block's `<code>` takes. */
|
|
2181
|
+
const MERMAID_LANGUAGE_CLASS = `language-mermaid`;
|
|
2182
|
+
/** The class a `<pre>` holding a diagram written as HTML takes, as Mermaid reads it in the browser. */
|
|
2183
|
+
const MERMAID_PRE_CLASS = `mermaid`;
|
|
2184
|
+
const hash = (str) => {
|
|
2185
|
+
let h = 2166136261;
|
|
2186
|
+
for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), 16777619) >>> 0;
|
|
2187
|
+
return h.toString(36);
|
|
2188
|
+
};
|
|
2189
|
+
/** A diagram's source as Pele reads it: line endings normalized, as markdown-it already has them. */
|
|
2190
|
+
const mermaidSource = (text) => text.replace(/\r\n?/g, `\n`);
|
|
2191
|
+
/**
|
|
2192
|
+
* A document's id prefixes, one per diagram in document order: `prefix`, then a hash of the diagram's source, so a diagram's ids don't change when others are edited, with `-2`, `-3`… for repeats of one diagram. Pele namespaces every id it writes (an accessible title, a gradient) under it. Create one per document.
|
|
2193
|
+
*/
|
|
2194
|
+
const mermaidIdPrefixes = (prefix) => {
|
|
2195
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2196
|
+
return (source) => {
|
|
2197
|
+
const id = `${prefix}-${hash(source)}`;
|
|
2198
|
+
const count = (seen.get(id) ?? 0) + 1;
|
|
2199
|
+
seen.set(id, count);
|
|
2200
|
+
return count === 1 ? id : `${id}-${count}`;
|
|
2201
|
+
};
|
|
2202
|
+
};
|
|
2203
|
+
const escapeAttribute = (value) => value.replace(/&/g, `&`).replace(/"/g, `"`);
|
|
2204
|
+
const classes = (value) => value.split(/\s+/).filter((name) => name !== `` && name !== MERMAID_LANGUAGE_CLASS && name !== MERMAID_PRE_CLASS);
|
|
2205
|
+
/**
|
|
2206
|
+
* Pele's `<svg>` with the attributes the diagram's element had (from an attribute block such as `{#id .class}`): classes are added after Pele's, a style after Pele's (whose theme variables the diagram needs), and anything else is set, replacing Pele's value. The class that marked the element as a diagram is dropped.
|
|
2207
|
+
*/
|
|
2208
|
+
const mermaidSvg = (svg, attributes) => {
|
|
2209
|
+
const open = /^<svg\b[^>]*>/.exec(svg)?.[0];
|
|
2210
|
+
if (!open || attributes.length === 0) return svg;
|
|
2211
|
+
const own = [...open.slice(4, -1).matchAll(/\s([^\s=]+)="([^"]*)"/g)].map(([, name = ``, value = ``]) => [name, value]);
|
|
2212
|
+
const merged = new Map(own);
|
|
2213
|
+
for (const [name, value] of attributes) {
|
|
2214
|
+
const escaped = escapeAttribute(value);
|
|
2215
|
+
const current = merged.get(name);
|
|
2216
|
+
if (name === `class`) {
|
|
2217
|
+
const added = classes(value).map(escapeAttribute).join(` `);
|
|
2218
|
+
if (added) merged.set(name, current ? `${current} ${added}` : added);
|
|
2219
|
+
} else if (name === `style` && current) merged.set(name, `${current.replace(/;$/, ``)};${escaped}`);
|
|
2220
|
+
else merged.set(name, escaped);
|
|
2221
|
+
}
|
|
2222
|
+
return `<svg${[...merged].map(([name, value]) => ` ${name}="${value}"`).join(``)}>${svg.slice(open.length)}`;
|
|
2223
|
+
};
|
|
2224
|
+
/** A hast element's properties as attributes; `className` becomes `class`, and other names are kept as the attribute plugins write them. */
|
|
2225
|
+
const hastAttributes = (properties) => {
|
|
2226
|
+
const text = (value) => typeof value === `string` ? value : typeof value === `number` ? String(value) : value === true ? `` : Array.isArray(value) ? value.map((item) => text(item) ?? ``).join(` `) : void 0;
|
|
2227
|
+
const attributes = [];
|
|
2228
|
+
for (const [key, value] of Object.entries(properties ?? {})) {
|
|
2229
|
+
const written = text(value);
|
|
2230
|
+
if (written !== void 0) attributes.push([key === `className` ? `class` : key, written]);
|
|
2231
|
+
}
|
|
2232
|
+
return attributes;
|
|
2233
|
+
};
|
|
2234
|
+
const hasClass = (node, name) => {
|
|
2235
|
+
const value = node.properties?.className;
|
|
2236
|
+
return Array.isArray(value) ? value.includes(name) : typeof value === `string` && value.split(/\s+/).includes(name);
|
|
2237
|
+
};
|
|
2238
|
+
const textOf = (node) => node.type === `text` ? node.value ?? `` : (node.children ?? []).map(textOf).join(``);
|
|
2239
|
+
/**
|
|
2240
|
+
* The diagram a `<pre>` holds, as rehype-mermaid finds them: a `<pre>` whose only content (besides whitespace) is a `<code class="language-mermaid">`, which is how a fenced `mermaid` block renders, or a `<pre class="mermaid">`. `element` is the one whose attributes the `<svg>` takes. `undefined` for anything else.
|
|
2241
|
+
*/
|
|
2242
|
+
const mermaidDiagram = (pre) => {
|
|
2243
|
+
if (pre.tagName !== `pre`) return void 0;
|
|
2244
|
+
if (hasClass(pre, MERMAID_PRE_CLASS)) return {
|
|
2245
|
+
source: mermaidSource(textOf(pre)),
|
|
2246
|
+
element: pre
|
|
2247
|
+
};
|
|
2248
|
+
let code;
|
|
2249
|
+
for (const child of pre.children ?? []) {
|
|
2250
|
+
if (child.type === `text` && /^\s*$/.test(child.value ?? ``)) continue;
|
|
2251
|
+
if (code || child.tagName !== `code`) return void 0;
|
|
2252
|
+
code = child;
|
|
2253
|
+
}
|
|
2254
|
+
return code && hasClass(code, MERMAID_LANGUAGE_CLASS) ? {
|
|
2255
|
+
source: mermaidSource(textOf(code)),
|
|
2256
|
+
element: code
|
|
2257
|
+
} : void 0;
|
|
2258
|
+
};
|
|
2259
|
+
/**
|
|
2260
|
+
* The error a diagram that failed to render throws: Pele's message, at the document line Pele located the problem on (counting from `line`, the line the diagram's element starts on), or at `line` itself when Pele has no location. The original error is the `cause`.
|
|
2261
|
+
*/
|
|
2262
|
+
const mermaidFailure = (error, line) => {
|
|
2263
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2264
|
+
const within = error instanceof Error && `line` in error && typeof error.line === `number` && error.line > 0 ? error.line : 0;
|
|
2265
|
+
const at = line === void 0 ? `` : ` on line ${String(line + within)}`;
|
|
2266
|
+
return new Error(`Could not render the mermaid diagram${at}: ${reason}`, { cause: error });
|
|
2267
|
+
};
|
|
2268
|
+
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
2269
|
|
|
1818
2270
|
//# sourceMappingURL=index.mjs.map
|