@dogsbay/format-mkdocs 0.2.0-beta.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.
- package/dist/autodoc/markdown-generator.d.ts +6 -0
- package/dist/autodoc/markdown-generator.d.ts.map +1 -0
- package/dist/autodoc/markdown-generator.js +242 -0
- package/dist/autodoc/markdown-generator.js.map +1 -0
- package/dist/autodoc/module-resolver.d.ts +8 -0
- package/dist/autodoc/module-resolver.d.ts.map +1 -0
- package/dist/autodoc/module-resolver.js +218 -0
- package/dist/autodoc/module-resolver.js.map +1 -0
- package/dist/autodoc/python-docstring.d.ts +7 -0
- package/dist/autodoc/python-docstring.d.ts.map +1 -0
- package/dist/autodoc/python-docstring.js +324 -0
- package/dist/autodoc/python-docstring.js.map +1 -0
- package/dist/autodoc/python-parser.d.ts +10 -0
- package/dist/autodoc/python-parser.d.ts.map +1 -0
- package/dist/autodoc/python-parser.js +541 -0
- package/dist/autodoc/python-parser.js.map +1 -0
- package/dist/autodoc/tree-builder.d.ts +9 -0
- package/dist/autodoc/tree-builder.d.ts.map +1 -0
- package/dist/autodoc/tree-builder.js +279 -0
- package/dist/autodoc/tree-builder.js.map +1 -0
- package/dist/autodoc/types.d.ts +128 -0
- package/dist/autodoc/types.d.ts.map +1 -0
- package/dist/autodoc/types.js +2 -0
- package/dist/autodoc/types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +32 -0
- package/dist/cli.js.map +1 -0
- package/dist/export/to-astro.d.ts +26 -0
- package/dist/export/to-astro.d.ts.map +1 -0
- package/dist/export/to-astro.js +551 -0
- package/dist/export/to-astro.js.map +1 -0
- package/dist/export/to-mkdocs-project.d.ts +27 -0
- package/dist/export/to-mkdocs-project.d.ts.map +1 -0
- package/dist/export/to-mkdocs-project.js +192 -0
- package/dist/export/to-mkdocs-project.js.map +1 -0
- package/dist/export/to-mkdocs.d.ts +6 -0
- package/dist/export/to-mkdocs.d.ts.map +1 -0
- package/dist/export/to-mkdocs.js +178 -0
- package/dist/export/to-mkdocs.js.map +1 -0
- package/dist/importer.d.ts +30 -0
- package/dist/importer.d.ts.map +1 -0
- package/dist/importer.js +376 -0
- package/dist/importer.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +170 -0
- package/dist/index.js.map +1 -0
- package/dist/inline-walker.d.ts +17 -0
- package/dist/inline-walker.d.ts.map +1 -0
- package/dist/inline-walker.js +167 -0
- package/dist/inline-walker.js.map +1 -0
- package/dist/loader.d.ts +42 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +765 -0
- package/dist/loader.js.map +1 -0
- package/dist/parse.d.ts +12 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +46 -0
- package/dist/parse.js.map +1 -0
- package/dist/renderers/mermaid.d.ts +9 -0
- package/dist/renderers/mermaid.d.ts.map +1 -0
- package/dist/renderers/mermaid.js +117 -0
- package/dist/renderers/mermaid.js.map +1 -0
- package/dist/rules/admonition.d.ts +6 -0
- package/dist/rules/admonition.d.ts.map +1 -0
- package/dist/rules/admonition.js +73 -0
- package/dist/rules/admonition.js.map +1 -0
- package/dist/rules/annotations.d.ts +6 -0
- package/dist/rules/annotations.d.ts.map +1 -0
- package/dist/rules/annotations.js +57 -0
- package/dist/rules/annotations.js.map +1 -0
- package/dist/rules/autodoc.d.ts +7 -0
- package/dist/rules/autodoc.d.ts.map +1 -0
- package/dist/rules/autodoc.js +102 -0
- package/dist/rules/autodoc.js.map +1 -0
- package/dist/rules/blocks.d.ts +6 -0
- package/dist/rules/blocks.d.ts.map +1 -0
- package/dist/rules/blocks.js +172 -0
- package/dist/rules/blocks.js.map +1 -0
- package/dist/rules/content-tabs.d.ts +6 -0
- package/dist/rules/content-tabs.d.ts.map +1 -0
- package/dist/rules/content-tabs.js +67 -0
- package/dist/rules/content-tabs.js.map +1 -0
- package/dist/rules/docusaurus-admonitions.d.ts +7 -0
- package/dist/rules/docusaurus-admonitions.d.ts.map +1 -0
- package/dist/rules/docusaurus-admonitions.js +101 -0
- package/dist/rules/docusaurus-admonitions.js.map +1 -0
- package/dist/rules/file-include.d.ts +28 -0
- package/dist/rules/file-include.d.ts.map +1 -0
- package/dist/rules/file-include.js +198 -0
- package/dist/rules/file-include.js.map +1 -0
- package/dist/rules/footnotes.d.ts +6 -0
- package/dist/rules/footnotes.d.ts.map +1 -0
- package/dist/rules/footnotes.js +161 -0
- package/dist/rules/footnotes.js.map +1 -0
- package/dist/rules/icon-shortcode.d.ts +30 -0
- package/dist/rules/icon-shortcode.d.ts.map +1 -0
- package/dist/rules/icon-shortcode.js +169 -0
- package/dist/rules/icon-shortcode.js.map +1 -0
- package/dist/rules/keys.d.ts +6 -0
- package/dist/rules/keys.d.ts.map +1 -0
- package/dist/rules/keys.js +30 -0
- package/dist/rules/keys.js.map +1 -0
- package/dist/rules/link-rewrite.d.ts +7 -0
- package/dist/rules/link-rewrite.d.ts.map +1 -0
- package/dist/rules/link-rewrite.js +93 -0
- package/dist/rules/link-rewrite.js.map +1 -0
- package/dist/rules/math.d.ts +14 -0
- package/dist/rules/math.d.ts.map +1 -0
- package/dist/rules/math.js +114 -0
- package/dist/rules/math.js.map +1 -0
- package/dist/rules/md-in-html.d.ts +6 -0
- package/dist/rules/md-in-html.d.ts.map +1 -0
- package/dist/rules/md-in-html.js +135 -0
- package/dist/rules/md-in-html.js.map +1 -0
- package/dist/rules/snippets.d.ts +10 -0
- package/dist/rules/snippets.d.ts.map +1 -0
- package/dist/rules/snippets.js +109 -0
- package/dist/rules/snippets.js.map +1 -0
- package/dist/rules/templates.d.ts +15 -0
- package/dist/rules/templates.d.ts.map +1 -0
- package/dist/rules/templates.js +105 -0
- package/dist/rules/templates.js.map +1 -0
- package/dist/rules/variants.d.ts +12 -0
- package/dist/rules/variants.d.ts.map +1 -0
- package/dist/rules/variants.js +148 -0
- package/dist/rules/variants.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +2 -0
- package/dist/tree.js.map +1 -0
- package/dist/utils/indent.d.ts +18 -0
- package/dist/utils/indent.d.ts.map +1 -0
- package/dist/utils/indent.js +56 -0
- package/dist/utils/indent.js.map +1 -0
- package/dist/utils/types.d.ts +19 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +63 -0
- package/dist/utils/types.js.map +1 -0
- package/dist/walker.d.ts +8 -0
- package/dist/walker.d.ts.map +1 -0
- package/dist/walker.js +193 -0
- package/dist/walker.js.map +1 -0
- package/package.json +71 -0
package/dist/loader.js
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
import { parseToTree } from "./parse.js";
|
|
2
|
+
import { walkInlineTokens } from "./inline-walker.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parse a MkDocs markdown string into a TreeNode[] + headings.
|
|
5
|
+
* Adapts tree node shapes to match base-astro's ContentRenderer expectations.
|
|
6
|
+
*
|
|
7
|
+
* ContentRenderer handles: prose, heading, code, callout, details, tabs, tab,
|
|
8
|
+
* table, deflist, dt, dd, steps, step, substeps.
|
|
9
|
+
*
|
|
10
|
+
* It does NOT handle: paragraph, ordered-list, unordered-list, list-item,
|
|
11
|
+
* blockquote, html, annotation-list, hr. These must be collapsed into prose
|
|
12
|
+
* nodes with pre-rendered HTML.
|
|
13
|
+
*/
|
|
14
|
+
export async function parseMkdocsMarkdown(source, md, options) {
|
|
15
|
+
const { tree, env } = parseToTree(md, source, options?.env);
|
|
16
|
+
// Process autodoc directives (::: module.Class) — convert to API TreeNodes
|
|
17
|
+
if (options?.autodoc) {
|
|
18
|
+
await processAutodocDirectives(tree, options.autodoc, md);
|
|
19
|
+
}
|
|
20
|
+
// Parse MkDocs attr_list syntax { #id .class data-attr="val" } into node props
|
|
21
|
+
parseAttrLists(tree);
|
|
22
|
+
// Ensure all <img> in html nodes have alt attributes (a11y)
|
|
23
|
+
ensureImgAlt(tree);
|
|
24
|
+
// Strip <script> tags from html nodes (MkDocs demo scripts don't work in our renderer)
|
|
25
|
+
stripScriptTags(tree);
|
|
26
|
+
// Parse inline content in props (tab labels, heading text, etc.)
|
|
27
|
+
parsePropsInline(tree, md);
|
|
28
|
+
// Render inline markdown to HTML in prose nodes (only needed for collapse mode)
|
|
29
|
+
if (options?.collapse !== false) {
|
|
30
|
+
renderInlineContent(tree, md, env);
|
|
31
|
+
}
|
|
32
|
+
// Expand footnote references with rendered content
|
|
33
|
+
if (env.footnotes) {
|
|
34
|
+
expandFootnotes(tree, md, env);
|
|
35
|
+
}
|
|
36
|
+
// Convert standalone YouTube links into youtube nodes (opt-in)
|
|
37
|
+
if (options?.youtubeEmbed) {
|
|
38
|
+
liftYouTubeEmbeds(tree);
|
|
39
|
+
}
|
|
40
|
+
// Render diagram code blocks (mermaid etc.) to SVG (opt-in)
|
|
41
|
+
if (options?.diagrams) {
|
|
42
|
+
await renderDiagrams(tree);
|
|
43
|
+
}
|
|
44
|
+
// Convert api-params HTML comments into structured TreeNodes
|
|
45
|
+
convertApiParams(tree, md);
|
|
46
|
+
// Extract headings (needs raw text before any collapsing)
|
|
47
|
+
const headings = extractHeadings(tree);
|
|
48
|
+
if (options?.collapse !== false) {
|
|
49
|
+
// Collapse unsupported block types into prose nodes
|
|
50
|
+
collapseForRenderer(tree);
|
|
51
|
+
}
|
|
52
|
+
// Adapt remaining structured nodes for ContentRenderer
|
|
53
|
+
adaptTree(tree);
|
|
54
|
+
return { tree, headings };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convert <!-- api-params:JSON --> HTML comments into api-params TreeNodes.
|
|
58
|
+
* The JSON encodes parameter data from the autodoc markdown generator.
|
|
59
|
+
* Each param's doc field is rendered as markdown HTML for rich display.
|
|
60
|
+
*/
|
|
61
|
+
const API_PARAMS_RE = /^<!--\s*api-params:(.*)\s*-->$/;
|
|
62
|
+
function convertApiParams(nodes, md) {
|
|
63
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
64
|
+
const node = nodes[i];
|
|
65
|
+
if (node.type === "html" && node.html) {
|
|
66
|
+
const match = API_PARAMS_RE.exec(node.html.trim());
|
|
67
|
+
if (match) {
|
|
68
|
+
try {
|
|
69
|
+
const params = JSON.parse(match[1]);
|
|
70
|
+
// Render each param's doc as markdown HTML
|
|
71
|
+
const renderedParams = params.map((p) => ({
|
|
72
|
+
...p,
|
|
73
|
+
docHtml: p.doc ? md.render(p.doc) : null,
|
|
74
|
+
}));
|
|
75
|
+
nodes[i] = {
|
|
76
|
+
type: "api-params",
|
|
77
|
+
props: { params: renderedParams },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Invalid JSON — leave as HTML
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (node.children)
|
|
86
|
+
convertApiParams(node.children, md);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Process ::: autodoc directives in the tree.
|
|
91
|
+
* After md.parse(), ::: directives appear as paragraph nodes containing
|
|
92
|
+
* text like "::: fastapi.FastAPI". We find these and replace with API TreeNodes.
|
|
93
|
+
*/
|
|
94
|
+
const DIRECTIVE_RE = /^:::\s+(\S+)\s*$/;
|
|
95
|
+
async function processAutodocDirectives(nodes, options, md) {
|
|
96
|
+
const { resolveIdentifier } = await import("./autodoc/module-resolver.js");
|
|
97
|
+
const { symbolToTreeNode } = await import("./autodoc/tree-builder.js");
|
|
98
|
+
const YAML = (await import("yaml")).default;
|
|
99
|
+
// Defaults match mkdocstrings Python handler defaults.
|
|
100
|
+
// Projects override these via mkdocs.yml handler options (passed as globalOptions).
|
|
101
|
+
const globalOpts = {
|
|
102
|
+
showRootHeading: false,
|
|
103
|
+
showRootFullPath: true,
|
|
104
|
+
showIfNoDocstring: false,
|
|
105
|
+
showBases: true,
|
|
106
|
+
showSource: true,
|
|
107
|
+
showSignature: true,
|
|
108
|
+
mergeInitIntoClass: false,
|
|
109
|
+
separateSignature: false,
|
|
110
|
+
unwrapAnnotated: false,
|
|
111
|
+
groupByCategory: true,
|
|
112
|
+
inheritedMembers: false,
|
|
113
|
+
filters: ["!^_[^_]"],
|
|
114
|
+
membersOrder: "alphabetical",
|
|
115
|
+
headingLevel: 2,
|
|
116
|
+
...options.globalOptions,
|
|
117
|
+
};
|
|
118
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
119
|
+
const node = nodes[i];
|
|
120
|
+
// Check if this is a paragraph containing a ::: directive
|
|
121
|
+
if (node.type === "paragraph" && node.children) {
|
|
122
|
+
const proseChild = node.children.find((c) => c.type === "prose" && c.html);
|
|
123
|
+
if (proseChild?.html) {
|
|
124
|
+
const fullText = proseChild.html.trim();
|
|
125
|
+
// The first line should be "::: identifier"
|
|
126
|
+
const firstLine = fullText.split("\n")[0].trim();
|
|
127
|
+
const match = DIRECTIVE_RE.exec(firstLine);
|
|
128
|
+
if (match) {
|
|
129
|
+
const identifier = match[1];
|
|
130
|
+
// Extract YAML options from remaining lines (indented text after :::)
|
|
131
|
+
let directiveOpts = { ...globalOpts };
|
|
132
|
+
const remainingLines = fullText.split("\n").slice(1).join("\n").trim();
|
|
133
|
+
if (remainingLines) {
|
|
134
|
+
try {
|
|
135
|
+
const parsed = YAML.parse(remainingLines);
|
|
136
|
+
if (parsed?.options) {
|
|
137
|
+
const opts = parsed.options;
|
|
138
|
+
directiveOpts = {
|
|
139
|
+
...globalOpts,
|
|
140
|
+
members: opts.members ?? directiveOpts.members,
|
|
141
|
+
showRootHeading: opts.show_root_heading ?? directiveOpts.showRootHeading,
|
|
142
|
+
showRootFullPath: opts.show_root_full_path ?? directiveOpts.showRootFullPath,
|
|
143
|
+
showIfNoDocstring: opts.show_if_no_docstring ?? directiveOpts.showIfNoDocstring,
|
|
144
|
+
showBases: opts.show_bases ?? directiveOpts.showBases,
|
|
145
|
+
showSource: opts.show_source ?? directiveOpts.showSource,
|
|
146
|
+
showSignature: opts.show_signature ?? directiveOpts.showSignature,
|
|
147
|
+
inheritedMembers: opts.inherited_members ?? directiveOpts.inheritedMembers,
|
|
148
|
+
mergeInitIntoClass: opts.merge_init_into_class ?? directiveOpts.mergeInitIntoClass,
|
|
149
|
+
separateSignature: opts.separate_signature ?? directiveOpts.separateSignature,
|
|
150
|
+
unwrapAnnotated: opts.unwrap_annotated ?? directiveOpts.unwrapAnnotated,
|
|
151
|
+
groupByCategory: opts.group_by_category ?? directiveOpts.groupByCategory,
|
|
152
|
+
filters: opts.filters ?? directiveOpts.filters,
|
|
153
|
+
membersOrder: opts.members_order ?? directiveOpts.membersOrder,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch { /* not valid YAML */ }
|
|
158
|
+
}
|
|
159
|
+
// Resolve and replace
|
|
160
|
+
try {
|
|
161
|
+
const symbol = await resolveIdentifier(identifier, options.sourceRoot);
|
|
162
|
+
if (symbol) {
|
|
163
|
+
const apiNode = symbolToTreeNode(symbol, directiveOpts, md, identifier, options.sourceRoot);
|
|
164
|
+
nodes[i] = apiNode;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch { /* leave as-is */ }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (node.children) {
|
|
172
|
+
await processAutodocDirectives(node.children, options, md);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Diagram languages that should be rendered as SVG instead of code blocks.
|
|
178
|
+
*/
|
|
179
|
+
const DIAGRAM_LANGS = new Set(["mermaid"]);
|
|
180
|
+
/**
|
|
181
|
+
* Render diagram code blocks to SVG.
|
|
182
|
+
* Transforms { type: "code", props: { lang: "mermaid", code: "..." } }
|
|
183
|
+
* into { type: "diagram", props: { lang: "mermaid", code: "...", svg: "..." } }.
|
|
184
|
+
* Source code is preserved for round-trip export and search.
|
|
185
|
+
*/
|
|
186
|
+
async function renderDiagrams(nodes) {
|
|
187
|
+
let renderMermaidSvg;
|
|
188
|
+
try {
|
|
189
|
+
const mod = await import("./renderers/mermaid.js");
|
|
190
|
+
renderMermaidSvg = mod.renderMermaidSvg;
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.warn("[diagrams] could not load mermaid renderer:", e);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
async function walk(nodes) {
|
|
197
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
198
|
+
const node = nodes[i];
|
|
199
|
+
if (node.type === "code" &&
|
|
200
|
+
DIAGRAM_LANGS.has((node.props?.lang || "").toLowerCase())) {
|
|
201
|
+
const code = node.props.code;
|
|
202
|
+
const lang = node.props.lang;
|
|
203
|
+
try {
|
|
204
|
+
const svg = await renderMermaidSvg(code);
|
|
205
|
+
if (svg) {
|
|
206
|
+
nodes[i] = {
|
|
207
|
+
type: "diagram",
|
|
208
|
+
props: { lang, code, svg },
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Leave as code block on render failure
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (node.children)
|
|
217
|
+
await walk(node.children);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
await walk(nodes);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Extract heading info for table of contents.
|
|
224
|
+
*/
|
|
225
|
+
function extractHeadings(tree) {
|
|
226
|
+
const headings = [];
|
|
227
|
+
const seen = new Map();
|
|
228
|
+
function walk(nodes) {
|
|
229
|
+
for (const node of nodes) {
|
|
230
|
+
if (node.type === "heading") {
|
|
231
|
+
const level = node.props?.level ?? 1;
|
|
232
|
+
const text = extractText(node);
|
|
233
|
+
// Use explicit { #id } from attr_list if set, otherwise auto-generate
|
|
234
|
+
let slug = node.props?.slug;
|
|
235
|
+
if (!slug) {
|
|
236
|
+
const baseSlug = slugify(text);
|
|
237
|
+
const count = seen.get(baseSlug) ?? 0;
|
|
238
|
+
slug = count === 0 ? baseSlug : `${baseSlug}-${count}`;
|
|
239
|
+
seen.set(baseSlug, count + 1);
|
|
240
|
+
}
|
|
241
|
+
// Add slug and text to heading node props
|
|
242
|
+
if (!node.props)
|
|
243
|
+
node.props = {};
|
|
244
|
+
node.props.slug = slug;
|
|
245
|
+
node.props.text = text;
|
|
246
|
+
headings.push({ depth: level, slug, text });
|
|
247
|
+
}
|
|
248
|
+
// API symbols generate TOC entries with kind badges
|
|
249
|
+
if (node.type === "api-class" || node.type === "api-member") {
|
|
250
|
+
const name = node.props?.name || "";
|
|
251
|
+
const kind = node.props?.kind || "";
|
|
252
|
+
const level = node.type === "api-class" ? 2 : 3;
|
|
253
|
+
const baseSlug = slugify(name);
|
|
254
|
+
const count = seen.get(baseSlug) ?? 0;
|
|
255
|
+
const slug = count === 0 ? baseSlug : `${baseSlug}-${count}`;
|
|
256
|
+
seen.set(baseSlug, count + 1);
|
|
257
|
+
headings.push({ depth: level, slug, text: name, kind });
|
|
258
|
+
}
|
|
259
|
+
if (node.children)
|
|
260
|
+
walk(node.children);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
walk(tree);
|
|
264
|
+
return headings;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Render inline markdown content to HTML in all prose nodes.
|
|
268
|
+
* The walker stores raw markdown in prose.html — this converts
|
|
269
|
+
* **bold**, *italic*, `code`, [links](url), etc. to HTML.
|
|
270
|
+
*/
|
|
271
|
+
function renderInlineContent(nodes, md, env) {
|
|
272
|
+
for (const node of nodes) {
|
|
273
|
+
if (node.type === "prose" && node.html) {
|
|
274
|
+
node.html = md.renderInline(node.html, env);
|
|
275
|
+
}
|
|
276
|
+
if (node.children)
|
|
277
|
+
renderInlineContent(node.children, md, env);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Node types that ContentRenderer handles natively.
|
|
282
|
+
* Everything else gets collapsed to prose with rendered HTML.
|
|
283
|
+
*/
|
|
284
|
+
const RENDERER_TYPES = new Set([
|
|
285
|
+
"prose", "heading", "code", "diagram", "api-class", "api-member", "api-doc", "api-params", "api-source", "callout", "details",
|
|
286
|
+
"tabs", "tab", "table", "thead", "tbody", "tr", "th", "td",
|
|
287
|
+
"deflist", "dt", "dd",
|
|
288
|
+
"steps", "step", "substeps",
|
|
289
|
+
"paragraph", "ordered-list", "unordered-list", "list-item",
|
|
290
|
+
"blockquote", "hr", "html", "html-container", "annotation-list",
|
|
291
|
+
"deflist", "dt", "dd", "youtube", "footnote-list", "footnote-def", "math-block",
|
|
292
|
+
]);
|
|
293
|
+
/**
|
|
294
|
+
* Collapse block types that ContentRenderer doesn't handle
|
|
295
|
+
* (paragraph, lists, blockquote, hr, html, annotation-list)
|
|
296
|
+
* into prose nodes with pre-rendered HTML.
|
|
297
|
+
*
|
|
298
|
+
* Then merge consecutive prose nodes into a single prose block
|
|
299
|
+
* so Tailwind Typography's .prose class can style them properly
|
|
300
|
+
* (spacing between paragraphs, heading sizes, list bullets, etc.)
|
|
301
|
+
*/
|
|
302
|
+
function collapseForRenderer(nodes) {
|
|
303
|
+
// Step 1: recurse into structured containers, collapse others to prose
|
|
304
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
305
|
+
const node = nodes[i];
|
|
306
|
+
if (RENDERER_TYPES.has(node.type) && node.children) {
|
|
307
|
+
collapseForRenderer(node.children);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!RENDERER_TYPES.has(node.type)) {
|
|
311
|
+
const html = nodeToHtml(node);
|
|
312
|
+
nodes[i] = { type: "prose", html };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Step 2: merge consecutive prose nodes (if any remain after collapsing)
|
|
316
|
+
mergeConsecutiveProse(nodes);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Merge runs of consecutive prose nodes into a single prose node.
|
|
320
|
+
* Also absorbs heading nodes into prose runs so typography styles
|
|
321
|
+
* apply consistent heading sizes and margins.
|
|
322
|
+
*/
|
|
323
|
+
function mergeConsecutiveProse(nodes) {
|
|
324
|
+
let i = 0;
|
|
325
|
+
while (i < nodes.length) {
|
|
326
|
+
if (nodes[i].type === "prose") {
|
|
327
|
+
// Collect consecutive prose + heading nodes
|
|
328
|
+
const parts = [nodes[i].html ?? ""];
|
|
329
|
+
let j = i + 1;
|
|
330
|
+
while (j < nodes.length && (nodes[j].type === "prose" || nodes[j].type === "heading")) {
|
|
331
|
+
if (nodes[j].type === "heading") {
|
|
332
|
+
const level = nodes[j].props?.level ?? 2;
|
|
333
|
+
const slug = nodes[j].props?.slug ?? "";
|
|
334
|
+
const text = nodes[j].props?.text ?? "";
|
|
335
|
+
const innerHtml = nodes[j].children?.map(nodeToHtml).join("") ?? text;
|
|
336
|
+
parts.push(`<h${level} id="${slug}">` +
|
|
337
|
+
`<a href="#${slug}" class="heading-anchor" aria-label="Link to ${text}">` +
|
|
338
|
+
`<span aria-hidden="true">#</span></a>` +
|
|
339
|
+
`${innerHtml}</h${level}>`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
parts.push(nodes[j].html ?? "");
|
|
343
|
+
}
|
|
344
|
+
j++;
|
|
345
|
+
}
|
|
346
|
+
if (j > i + 1) {
|
|
347
|
+
// Replace the run with a single merged prose node
|
|
348
|
+
nodes.splice(i, j - i, { type: "prose", html: parts.join("\n") });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
i++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Render a tree node (and its children) to HTML string.
|
|
356
|
+
*/
|
|
357
|
+
function nodeToHtml(node) {
|
|
358
|
+
switch (node.type) {
|
|
359
|
+
case "paragraph":
|
|
360
|
+
return `<p>${childrenToHtml(node)}</p>`;
|
|
361
|
+
case "prose":
|
|
362
|
+
return node.html ?? "";
|
|
363
|
+
case "ordered-list":
|
|
364
|
+
return `<ol>${listItemsToHtml(node, "ol")}</ol>`;
|
|
365
|
+
case "unordered-list":
|
|
366
|
+
return `<ul>${listItemsToHtml(node, "ul")}</ul>`;
|
|
367
|
+
case "list-item":
|
|
368
|
+
return `<li>${childrenToHtml(node)}</li>`;
|
|
369
|
+
case "blockquote":
|
|
370
|
+
return `<blockquote>${childrenToHtml(node)}</blockquote>`;
|
|
371
|
+
case "hr":
|
|
372
|
+
return "<hr>";
|
|
373
|
+
case "html":
|
|
374
|
+
return node.html ?? "";
|
|
375
|
+
case "annotation-list":
|
|
376
|
+
return `<div class="annotation-list">${childrenToHtml(node)}</div>`;
|
|
377
|
+
case "heading": {
|
|
378
|
+
const level = node.props?.level ?? 2;
|
|
379
|
+
return `<h${level}>${childrenToHtml(node)}</h${level}>`;
|
|
380
|
+
}
|
|
381
|
+
case "code": {
|
|
382
|
+
const lang = node.props?.lang ?? "";
|
|
383
|
+
const code = node.props?.code ?? "";
|
|
384
|
+
return `<pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>`;
|
|
385
|
+
}
|
|
386
|
+
default:
|
|
387
|
+
return childrenToHtml(node);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function childrenToHtml(node) {
|
|
391
|
+
if (!node.children)
|
|
392
|
+
return node.html ?? "";
|
|
393
|
+
return node.children.map(nodeToHtml).join("");
|
|
394
|
+
}
|
|
395
|
+
function listItemsToHtml(node, _tag) {
|
|
396
|
+
if (!node.children)
|
|
397
|
+
return "";
|
|
398
|
+
return node.children.map(nodeToHtml).join("");
|
|
399
|
+
}
|
|
400
|
+
function escapeHtml(s) {
|
|
401
|
+
return s
|
|
402
|
+
.replace(/&/g, "&")
|
|
403
|
+
.replace(/</g, "<")
|
|
404
|
+
.replace(/>/g, ">");
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Adapt tree node shapes so they match what base-astro's
|
|
408
|
+
* ContentRenderer.astro expects.
|
|
409
|
+
*/
|
|
410
|
+
function adaptTree(nodes) {
|
|
411
|
+
for (const node of nodes) {
|
|
412
|
+
// ContentRenderer expects tabs children to have props.value and props.label
|
|
413
|
+
// Our walker produces props.title
|
|
414
|
+
if (node.type === "tab") {
|
|
415
|
+
const title = node.props?.title ?? "Tab";
|
|
416
|
+
if (!node.props)
|
|
417
|
+
node.props = {};
|
|
418
|
+
node.props.value = slugify(title);
|
|
419
|
+
node.props.label = title;
|
|
420
|
+
}
|
|
421
|
+
// Details: ContentRenderer expects props.title (we already have this)
|
|
422
|
+
// Callout: ContentRenderer expects props.variant and props.title (we already have these)
|
|
423
|
+
// But it doesn't handle inline — add as class hint in meta if needed
|
|
424
|
+
// Strip frontmatter/references nodes — they're not renderable
|
|
425
|
+
// (handled at tree level, not here)
|
|
426
|
+
if (node.children)
|
|
427
|
+
adaptTree(node.children);
|
|
428
|
+
}
|
|
429
|
+
// Remove non-renderable nodes
|
|
430
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
431
|
+
if (nodes[i].type === "frontmatter" || nodes[i].type === "references") {
|
|
432
|
+
nodes.splice(i, 1);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Detect paragraphs that contain only a YouTube link and convert them
|
|
438
|
+
* into { type: "youtube", props: { id, title } } nodes.
|
|
439
|
+
*
|
|
440
|
+
* Uses structured inline nodes when available, falls back to html regex.
|
|
441
|
+
*/
|
|
442
|
+
const YT_URL_RE = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/;
|
|
443
|
+
const YT_LINK_RE = /<a\s+href="(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)[^"]*"[^>]*>([^<]*)<\/a>/;
|
|
444
|
+
function liftYouTubeEmbeds(nodes) {
|
|
445
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
446
|
+
const node = nodes[i];
|
|
447
|
+
if (node.type === "paragraph" && node.children?.length === 1 && node.children[0].type === "prose") {
|
|
448
|
+
const prose = node.children[0];
|
|
449
|
+
// Structured inline path: check if sole meaningful content is a link to YouTube
|
|
450
|
+
if (prose.inline) {
|
|
451
|
+
const meaningful = prose.inline.filter(n => !(n.type === "text" && n.text.trim() === ""));
|
|
452
|
+
if (meaningful.length === 1 && meaningful[0].type === "link") {
|
|
453
|
+
const link = meaningful[0];
|
|
454
|
+
const m = YT_URL_RE.exec(link.href);
|
|
455
|
+
if (m) {
|
|
456
|
+
const title = extractInlineText(link.children) || "YouTube video";
|
|
457
|
+
nodes[i] = { type: "youtube", props: { id: m[1], title } };
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Legacy html fallback
|
|
463
|
+
if (prose.html) {
|
|
464
|
+
const html = prose.html.trim();
|
|
465
|
+
const m = YT_LINK_RE.exec(html);
|
|
466
|
+
if (m) {
|
|
467
|
+
const stripped = html
|
|
468
|
+
.replace(/<\/?(?:strong|em|b|i)>/g, "")
|
|
469
|
+
.replace(/<a\s[^>]*>[^<]*<\/a>/, "")
|
|
470
|
+
.trim();
|
|
471
|
+
if (stripped === "") {
|
|
472
|
+
nodes[i] = { type: "youtube", props: { id: m[1], title: m[2] || "YouTube video" } };
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (node.children)
|
|
479
|
+
liftYouTubeEmbeds(node.children);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Expand <fn-ref data-label="X"> placeholders in prose HTML
|
|
484
|
+
* with rendered footnote content from env.footnotes.
|
|
485
|
+
*/
|
|
486
|
+
const FN_REF_RE = /<fn-ref data-label="([^"]+)"><\/fn-ref>/g;
|
|
487
|
+
function expandFootnotes(nodes, md, env) {
|
|
488
|
+
const footnotes = env.footnotes;
|
|
489
|
+
if (!footnotes)
|
|
490
|
+
return;
|
|
491
|
+
// Render footnote content markdown to inline nodes (once per label)
|
|
492
|
+
const renderedInline = {};
|
|
493
|
+
const renderedHtml = {};
|
|
494
|
+
for (const [label, content] of Object.entries(footnotes)) {
|
|
495
|
+
renderedHtml[label] = md.renderInline(content, env);
|
|
496
|
+
// Parse footnote content into inline nodes
|
|
497
|
+
const fnTokens = md.parseInline(content, env);
|
|
498
|
+
if (fnTokens.length > 0 && fnTokens[0].children) {
|
|
499
|
+
renderedInline[label] = walkInlineTokens(fnTokens[0].children);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
renderedInline[label] = [{ type: "text", text: content }];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Walk tree: set content on footnote-ref inline nodes, and update html for legacy path
|
|
506
|
+
function walk(nodes) {
|
|
507
|
+
for (const node of nodes) {
|
|
508
|
+
// Structured inline path: set content on footnote-ref nodes
|
|
509
|
+
if (node.inline) {
|
|
510
|
+
for (const span of node.inline) {
|
|
511
|
+
if (span.type === "footnote-ref" && footnotes[span.label]) {
|
|
512
|
+
span.content = renderedHtml[span.label];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Legacy html path
|
|
517
|
+
if (node.html && node.html.includes("<fn-ref")) {
|
|
518
|
+
node.html = node.html.replace(FN_REF_RE, (_match, label) => {
|
|
519
|
+
const content = renderedHtml[label] || label;
|
|
520
|
+
const escaped = content.replace(/"/g, """);
|
|
521
|
+
return `<a data-footnote data-label="${label}" data-content="${escaped}" href="#fn-${label}" id="fnref-${label}" class="no-underline"><sup class="cursor-pointer text-primary hover:underline">[${label}]</sup></a>`;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
if (node.children)
|
|
525
|
+
walk(node.children);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
walk(nodes);
|
|
529
|
+
// Append footnote definitions as structured nodes
|
|
530
|
+
const labels = Object.keys(footnotes);
|
|
531
|
+
if (labels.length > 0) {
|
|
532
|
+
nodes.push({ type: "hr" });
|
|
533
|
+
nodes.push({
|
|
534
|
+
type: "footnote-list",
|
|
535
|
+
children: labels.map((label) => ({
|
|
536
|
+
type: "footnote-def",
|
|
537
|
+
props: { label, id: `fn-${label}`, backrefId: `fnref-${label}` },
|
|
538
|
+
inline: renderedInline[label],
|
|
539
|
+
// Legacy html fallback
|
|
540
|
+
html: renderedHtml[label],
|
|
541
|
+
})),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Parse inline markdown in string props that may contain shortcodes or formatting.
|
|
547
|
+
* Targets tab labels, admonition titles, and similar props that hold raw markdown.
|
|
548
|
+
*/
|
|
549
|
+
function parsePropsInline(nodes, md) {
|
|
550
|
+
for (const node of nodes) {
|
|
551
|
+
// Tab nodes: parse label into inline
|
|
552
|
+
if (node.type === "tab" && (node.props?.label || node.props?.title) && typeof (node.props?.label || node.props?.title) === "string") {
|
|
553
|
+
const label = (node.props.label || node.props.title);
|
|
554
|
+
if (label.includes(":") || label.includes("*") || label.includes("`")) {
|
|
555
|
+
const tokens = md.parseInline(label, {});
|
|
556
|
+
if (tokens.length > 0 && tokens[0].children) {
|
|
557
|
+
node.inline = walkInlineTokens(tokens[0].children);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (node.children)
|
|
562
|
+
parsePropsInline(node.children, md);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Strip <script> tags from html nodes.
|
|
567
|
+
* MkDocs content sometimes contains interactive demo scripts that
|
|
568
|
+
* reference MkDocs-specific DOM elements and cause errors in our renderer.
|
|
569
|
+
*/
|
|
570
|
+
const SCRIPT_TAG_RE = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
|
|
571
|
+
function stripScriptTags(nodes) {
|
|
572
|
+
for (const node of nodes) {
|
|
573
|
+
if (node.html && node.html.includes("<script")) {
|
|
574
|
+
node.html = node.html.replace(SCRIPT_TAG_RE, "");
|
|
575
|
+
}
|
|
576
|
+
if (node.children)
|
|
577
|
+
stripScriptTags(node.children);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Ensure all <img> tags in html nodes have an alt attribute.
|
|
582
|
+
* Derives alt from filename when missing.
|
|
583
|
+
*/
|
|
584
|
+
const IMG_NO_ALT_RE = /<img(?![^>]*\balt\b)([^>]*?)(\s*\/?>)/g;
|
|
585
|
+
function ensureImgAlt(nodes) {
|
|
586
|
+
for (const node of nodes) {
|
|
587
|
+
if (node.html && node.html.includes("<img")) {
|
|
588
|
+
node.html = node.html.replace(IMG_NO_ALT_RE, (_match, attrs, close) => {
|
|
589
|
+
// Try to derive alt from src filename
|
|
590
|
+
const srcMatch = attrs.match(/src="([^"]+)"/);
|
|
591
|
+
let alt = "";
|
|
592
|
+
if (srcMatch) {
|
|
593
|
+
const filename = srcMatch[1].split("/").pop()?.replace(/\.[^.]+$/, "") || "";
|
|
594
|
+
alt = filename.replace(/[-_]/g, " ");
|
|
595
|
+
}
|
|
596
|
+
return `<img alt="${alt}"${attrs}${close}`;
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
if (node.children)
|
|
600
|
+
ensureImgAlt(node.children);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Parse MkDocs attr_list syntax { #id .class key="val" } from content
|
|
605
|
+
* and move parsed attributes into node props.
|
|
606
|
+
*
|
|
607
|
+
* For headings: sets slug/id from { #custom-id }
|
|
608
|
+
* For other nodes: stores in props.attrs for the renderer to apply
|
|
609
|
+
*/
|
|
610
|
+
const ATTR_LIST_TRAILING_RE = /\s*\{([^}]+)\}\s*$/;
|
|
611
|
+
const ATTR_LIST_INLINE_RE = /\s*\{([^}]+)\}/g;
|
|
612
|
+
function parseAttrString(attrStr) {
|
|
613
|
+
const result = {};
|
|
614
|
+
// Parse #id
|
|
615
|
+
const idMatch = attrStr.match(/#([\w-]+)/);
|
|
616
|
+
if (idMatch)
|
|
617
|
+
result.id = idMatch[1];
|
|
618
|
+
// Parse .class (multiple allowed)
|
|
619
|
+
const classMatches = attrStr.matchAll(/\.([\w-]+)/g);
|
|
620
|
+
const classes = [];
|
|
621
|
+
for (const m of classMatches)
|
|
622
|
+
classes.push(m[1]);
|
|
623
|
+
if (classes.length > 0)
|
|
624
|
+
result.classes = classes;
|
|
625
|
+
// Parse key="value" or key=value
|
|
626
|
+
const kvMatches = attrStr.matchAll(/([\w-]+)="([^"]*)"/g);
|
|
627
|
+
for (const m of kvMatches) {
|
|
628
|
+
if (!result.attrs)
|
|
629
|
+
result.attrs = {};
|
|
630
|
+
result.attrs[m[1]] = m[2];
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
function parseAttrLists(nodes) {
|
|
635
|
+
for (const node of nodes) {
|
|
636
|
+
// Handle headings: extract { #id } and set as slug
|
|
637
|
+
if (node.type === "heading" && node.children) {
|
|
638
|
+
for (const child of node.children) {
|
|
639
|
+
if (child.html) {
|
|
640
|
+
const match = ATTR_LIST_TRAILING_RE.exec(child.html);
|
|
641
|
+
if (match) {
|
|
642
|
+
const parsed = parseAttrString(match[1]);
|
|
643
|
+
child.html = child.html.replace(ATTR_LIST_TRAILING_RE, "");
|
|
644
|
+
if (parsed.id) {
|
|
645
|
+
if (!node.props)
|
|
646
|
+
node.props = {};
|
|
647
|
+
node.props.slug = parsed.id;
|
|
648
|
+
}
|
|
649
|
+
if (parsed.classes) {
|
|
650
|
+
if (!node.props)
|
|
651
|
+
node.props = {};
|
|
652
|
+
node.props.class = parsed.classes.join(" ");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (child.inline) {
|
|
657
|
+
const lastText = [...child.inline].reverse().find(n => n.type === "text");
|
|
658
|
+
if (lastText && lastText.type === "text") {
|
|
659
|
+
const match = ATTR_LIST_TRAILING_RE.exec(lastText.text);
|
|
660
|
+
if (match) {
|
|
661
|
+
const parsed = parseAttrString(match[1]);
|
|
662
|
+
lastText.text = lastText.text.replace(ATTR_LIST_TRAILING_RE, "");
|
|
663
|
+
if (parsed.id) {
|
|
664
|
+
if (!node.props)
|
|
665
|
+
node.props = {};
|
|
666
|
+
node.props.slug = parsed.id;
|
|
667
|
+
}
|
|
668
|
+
if (parsed.classes) {
|
|
669
|
+
if (!node.props)
|
|
670
|
+
node.props = {};
|
|
671
|
+
node.props.class = parsed.classes.join(" ");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// Handle prose/other nodes: extract attrs and store in props
|
|
679
|
+
if (node.type !== "heading") {
|
|
680
|
+
if (node.html) {
|
|
681
|
+
const match = ATTR_LIST_TRAILING_RE.exec(node.html);
|
|
682
|
+
if (match) {
|
|
683
|
+
const parsed = parseAttrString(match[1]);
|
|
684
|
+
node.html = node.html.replace(ATTR_LIST_TRAILING_RE, "");
|
|
685
|
+
if (parsed.id || parsed.classes || parsed.attrs) {
|
|
686
|
+
if (!node.props)
|
|
687
|
+
node.props = {};
|
|
688
|
+
if (parsed.id)
|
|
689
|
+
node.props.id = parsed.id;
|
|
690
|
+
if (parsed.classes)
|
|
691
|
+
node.props.class = parsed.classes.join(" ");
|
|
692
|
+
if (parsed.attrs)
|
|
693
|
+
node.props.attrs = parsed.attrs;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (node.inline) {
|
|
698
|
+
for (const span of node.inline) {
|
|
699
|
+
if (span.type === "text") {
|
|
700
|
+
span.text = span.text.replace(ATTR_LIST_INLINE_RE, "");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (node.children)
|
|
706
|
+
parseAttrLists(node.children);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Extract plain text from a tree node.
|
|
711
|
+
* Prefers inline nodes when available, falls back to html stripping.
|
|
712
|
+
*/
|
|
713
|
+
function extractText(node) {
|
|
714
|
+
const parts = [];
|
|
715
|
+
if (node.inline) {
|
|
716
|
+
parts.push(extractInlineText(node.inline));
|
|
717
|
+
}
|
|
718
|
+
else if (node.html) {
|
|
719
|
+
parts.push(node.html.replace(/<[^>]+>/g, ""));
|
|
720
|
+
}
|
|
721
|
+
if (node.children) {
|
|
722
|
+
for (const child of node.children) {
|
|
723
|
+
parts.push(extractText(child));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return parts.filter(Boolean).join(" ").trim();
|
|
727
|
+
}
|
|
728
|
+
function extractInlineText(nodes) {
|
|
729
|
+
const parts = [];
|
|
730
|
+
for (const node of nodes) {
|
|
731
|
+
switch (node.type) {
|
|
732
|
+
case "text":
|
|
733
|
+
parts.push(node.text);
|
|
734
|
+
break;
|
|
735
|
+
case "code":
|
|
736
|
+
parts.push(node.text);
|
|
737
|
+
break;
|
|
738
|
+
case "link":
|
|
739
|
+
parts.push(extractInlineText(node.children));
|
|
740
|
+
break;
|
|
741
|
+
case "image":
|
|
742
|
+
if (node.alt)
|
|
743
|
+
parts.push(node.alt);
|
|
744
|
+
break;
|
|
745
|
+
case "kbd":
|
|
746
|
+
parts.push(node.keys.join("+"));
|
|
747
|
+
break;
|
|
748
|
+
case "html-inline":
|
|
749
|
+
parts.push(node.html.replace(/<[^>]+>/g, ""));
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return parts.join("");
|
|
754
|
+
}
|
|
755
|
+
function slugify(text) {
|
|
756
|
+
return text
|
|
757
|
+
.toLowerCase()
|
|
758
|
+
.replace(/[^\w\s-]/g, "")
|
|
759
|
+
.replace(/\s+/g, "-")
|
|
760
|
+
.replace(/-+/g, "-")
|
|
761
|
+
.replace(/^-+|-+$/g, "")
|
|
762
|
+
.trim()
|
|
763
|
+
|| "tab"; // fallback for titles that are entirely punctuation
|
|
764
|
+
}
|
|
765
|
+
//# sourceMappingURL=loader.js.map
|