@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.
Files changed (145) hide show
  1. package/dist/autodoc/markdown-generator.d.ts +6 -0
  2. package/dist/autodoc/markdown-generator.d.ts.map +1 -0
  3. package/dist/autodoc/markdown-generator.js +242 -0
  4. package/dist/autodoc/markdown-generator.js.map +1 -0
  5. package/dist/autodoc/module-resolver.d.ts +8 -0
  6. package/dist/autodoc/module-resolver.d.ts.map +1 -0
  7. package/dist/autodoc/module-resolver.js +218 -0
  8. package/dist/autodoc/module-resolver.js.map +1 -0
  9. package/dist/autodoc/python-docstring.d.ts +7 -0
  10. package/dist/autodoc/python-docstring.d.ts.map +1 -0
  11. package/dist/autodoc/python-docstring.js +324 -0
  12. package/dist/autodoc/python-docstring.js.map +1 -0
  13. package/dist/autodoc/python-parser.d.ts +10 -0
  14. package/dist/autodoc/python-parser.d.ts.map +1 -0
  15. package/dist/autodoc/python-parser.js +541 -0
  16. package/dist/autodoc/python-parser.js.map +1 -0
  17. package/dist/autodoc/tree-builder.d.ts +9 -0
  18. package/dist/autodoc/tree-builder.d.ts.map +1 -0
  19. package/dist/autodoc/tree-builder.js +279 -0
  20. package/dist/autodoc/tree-builder.js.map +1 -0
  21. package/dist/autodoc/types.d.ts +128 -0
  22. package/dist/autodoc/types.d.ts.map +1 -0
  23. package/dist/autodoc/types.js +2 -0
  24. package/dist/autodoc/types.js.map +1 -0
  25. package/dist/cli.d.ts +3 -0
  26. package/dist/cli.d.ts.map +1 -0
  27. package/dist/cli.js +32 -0
  28. package/dist/cli.js.map +1 -0
  29. package/dist/export/to-astro.d.ts +26 -0
  30. package/dist/export/to-astro.d.ts.map +1 -0
  31. package/dist/export/to-astro.js +551 -0
  32. package/dist/export/to-astro.js.map +1 -0
  33. package/dist/export/to-mkdocs-project.d.ts +27 -0
  34. package/dist/export/to-mkdocs-project.d.ts.map +1 -0
  35. package/dist/export/to-mkdocs-project.js +192 -0
  36. package/dist/export/to-mkdocs-project.js.map +1 -0
  37. package/dist/export/to-mkdocs.d.ts +6 -0
  38. package/dist/export/to-mkdocs.d.ts.map +1 -0
  39. package/dist/export/to-mkdocs.js +178 -0
  40. package/dist/export/to-mkdocs.js.map +1 -0
  41. package/dist/importer.d.ts +30 -0
  42. package/dist/importer.d.ts.map +1 -0
  43. package/dist/importer.js +376 -0
  44. package/dist/importer.js.map +1 -0
  45. package/dist/index.d.ts +57 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +170 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/inline-walker.d.ts +17 -0
  50. package/dist/inline-walker.d.ts.map +1 -0
  51. package/dist/inline-walker.js +167 -0
  52. package/dist/inline-walker.js.map +1 -0
  53. package/dist/loader.d.ts +42 -0
  54. package/dist/loader.d.ts.map +1 -0
  55. package/dist/loader.js +765 -0
  56. package/dist/loader.js.map +1 -0
  57. package/dist/parse.d.ts +12 -0
  58. package/dist/parse.d.ts.map +1 -0
  59. package/dist/parse.js +46 -0
  60. package/dist/parse.js.map +1 -0
  61. package/dist/renderers/mermaid.d.ts +9 -0
  62. package/dist/renderers/mermaid.d.ts.map +1 -0
  63. package/dist/renderers/mermaid.js +117 -0
  64. package/dist/renderers/mermaid.js.map +1 -0
  65. package/dist/rules/admonition.d.ts +6 -0
  66. package/dist/rules/admonition.d.ts.map +1 -0
  67. package/dist/rules/admonition.js +73 -0
  68. package/dist/rules/admonition.js.map +1 -0
  69. package/dist/rules/annotations.d.ts +6 -0
  70. package/dist/rules/annotations.d.ts.map +1 -0
  71. package/dist/rules/annotations.js +57 -0
  72. package/dist/rules/annotations.js.map +1 -0
  73. package/dist/rules/autodoc.d.ts +7 -0
  74. package/dist/rules/autodoc.d.ts.map +1 -0
  75. package/dist/rules/autodoc.js +102 -0
  76. package/dist/rules/autodoc.js.map +1 -0
  77. package/dist/rules/blocks.d.ts +6 -0
  78. package/dist/rules/blocks.d.ts.map +1 -0
  79. package/dist/rules/blocks.js +172 -0
  80. package/dist/rules/blocks.js.map +1 -0
  81. package/dist/rules/content-tabs.d.ts +6 -0
  82. package/dist/rules/content-tabs.d.ts.map +1 -0
  83. package/dist/rules/content-tabs.js +67 -0
  84. package/dist/rules/content-tabs.js.map +1 -0
  85. package/dist/rules/docusaurus-admonitions.d.ts +7 -0
  86. package/dist/rules/docusaurus-admonitions.d.ts.map +1 -0
  87. package/dist/rules/docusaurus-admonitions.js +101 -0
  88. package/dist/rules/docusaurus-admonitions.js.map +1 -0
  89. package/dist/rules/file-include.d.ts +28 -0
  90. package/dist/rules/file-include.d.ts.map +1 -0
  91. package/dist/rules/file-include.js +198 -0
  92. package/dist/rules/file-include.js.map +1 -0
  93. package/dist/rules/footnotes.d.ts +6 -0
  94. package/dist/rules/footnotes.d.ts.map +1 -0
  95. package/dist/rules/footnotes.js +161 -0
  96. package/dist/rules/footnotes.js.map +1 -0
  97. package/dist/rules/icon-shortcode.d.ts +30 -0
  98. package/dist/rules/icon-shortcode.d.ts.map +1 -0
  99. package/dist/rules/icon-shortcode.js +169 -0
  100. package/dist/rules/icon-shortcode.js.map +1 -0
  101. package/dist/rules/keys.d.ts +6 -0
  102. package/dist/rules/keys.d.ts.map +1 -0
  103. package/dist/rules/keys.js +30 -0
  104. package/dist/rules/keys.js.map +1 -0
  105. package/dist/rules/link-rewrite.d.ts +7 -0
  106. package/dist/rules/link-rewrite.d.ts.map +1 -0
  107. package/dist/rules/link-rewrite.js +93 -0
  108. package/dist/rules/link-rewrite.js.map +1 -0
  109. package/dist/rules/math.d.ts +14 -0
  110. package/dist/rules/math.d.ts.map +1 -0
  111. package/dist/rules/math.js +114 -0
  112. package/dist/rules/math.js.map +1 -0
  113. package/dist/rules/md-in-html.d.ts +6 -0
  114. package/dist/rules/md-in-html.d.ts.map +1 -0
  115. package/dist/rules/md-in-html.js +135 -0
  116. package/dist/rules/md-in-html.js.map +1 -0
  117. package/dist/rules/snippets.d.ts +10 -0
  118. package/dist/rules/snippets.d.ts.map +1 -0
  119. package/dist/rules/snippets.js +109 -0
  120. package/dist/rules/snippets.js.map +1 -0
  121. package/dist/rules/templates.d.ts +15 -0
  122. package/dist/rules/templates.d.ts.map +1 -0
  123. package/dist/rules/templates.js +105 -0
  124. package/dist/rules/templates.js.map +1 -0
  125. package/dist/rules/variants.d.ts +12 -0
  126. package/dist/rules/variants.d.ts.map +1 -0
  127. package/dist/rules/variants.js +148 -0
  128. package/dist/rules/variants.js.map +1 -0
  129. package/dist/tree.d.ts +2 -0
  130. package/dist/tree.d.ts.map +1 -0
  131. package/dist/tree.js +2 -0
  132. package/dist/tree.js.map +1 -0
  133. package/dist/utils/indent.d.ts +18 -0
  134. package/dist/utils/indent.d.ts.map +1 -0
  135. package/dist/utils/indent.js +56 -0
  136. package/dist/utils/indent.js.map +1 -0
  137. package/dist/utils/types.d.ts +19 -0
  138. package/dist/utils/types.d.ts.map +1 -0
  139. package/dist/utils/types.js +63 -0
  140. package/dist/utils/types.js.map +1 -0
  141. package/dist/walker.d.ts +8 -0
  142. package/dist/walker.d.ts.map +1 -0
  143. package/dist/walker.js +193 -0
  144. package/dist/walker.js.map +1 -0
  145. 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, "&amp;")
403
+ .replace(/</g, "&lt;")
404
+ .replace(/>/g, "&gt;");
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, "&quot;");
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