@dogsbay/format-mkdocs 0.2.0-beta.12 → 0.2.0-beta.121

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 (49) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +113 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/export/to-astro.d.ts.map +1 -1
  5. package/dist/export/to-astro.js +10 -1
  6. package/dist/export/to-astro.js.map +1 -1
  7. package/dist/export/to-mkdocs-project.d.ts +12 -0
  8. package/dist/export/to-mkdocs-project.d.ts.map +1 -1
  9. package/dist/export/to-mkdocs-project.js +279 -63
  10. package/dist/export/to-mkdocs-project.js.map +1 -1
  11. package/dist/export/to-mkdocs.d.ts +53 -3
  12. package/dist/export/to-mkdocs.d.ts.map +1 -1
  13. package/dist/export/to-mkdocs.js +884 -102
  14. package/dist/export/to-mkdocs.js.map +1 -1
  15. package/dist/index.d.ts +20 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +39 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/loader.js +1 -2
  20. package/dist/loader.js.map +1 -1
  21. package/dist/rules/autodoc.d.ts +1 -1
  22. package/dist/rules/autodoc.d.ts.map +1 -1
  23. package/dist/rules/autodoc.js +1 -2
  24. package/dist/rules/autodoc.js.map +1 -1
  25. package/package.json +13 -12
  26. package/dist/autodoc/markdown-generator.d.ts +0 -6
  27. package/dist/autodoc/markdown-generator.d.ts.map +0 -1
  28. package/dist/autodoc/markdown-generator.js +0 -242
  29. package/dist/autodoc/markdown-generator.js.map +0 -1
  30. package/dist/autodoc/module-resolver.d.ts +0 -8
  31. package/dist/autodoc/module-resolver.d.ts.map +0 -1
  32. package/dist/autodoc/module-resolver.js +0 -218
  33. package/dist/autodoc/module-resolver.js.map +0 -1
  34. package/dist/autodoc/python-docstring.d.ts +0 -7
  35. package/dist/autodoc/python-docstring.d.ts.map +0 -1
  36. package/dist/autodoc/python-docstring.js +0 -324
  37. package/dist/autodoc/python-docstring.js.map +0 -1
  38. package/dist/autodoc/python-parser.d.ts +0 -10
  39. package/dist/autodoc/python-parser.d.ts.map +0 -1
  40. package/dist/autodoc/python-parser.js +0 -541
  41. package/dist/autodoc/python-parser.js.map +0 -1
  42. package/dist/autodoc/tree-builder.d.ts +0 -9
  43. package/dist/autodoc/tree-builder.d.ts.map +0 -1
  44. package/dist/autodoc/tree-builder.js +0 -279
  45. package/dist/autodoc/tree-builder.js.map +0 -1
  46. package/dist/autodoc/types.d.ts +0 -128
  47. package/dist/autodoc/types.d.ts.map +0 -1
  48. package/dist/autodoc/types.js +0 -2
  49. package/dist/autodoc/types.js.map +0 -1
@@ -1,15 +1,39 @@
1
+ import { walkInline, applyTextFlags, renderLeaf, indent, prefixLines, pickCodeFence, codeSpan, stripHtml, inlineHtmlToMarkdown, renderUnknownNode, listStart, calloutVariant, leafHtml, isSafeHref, markdownDestination, markdownLatex, safeAnchorId, footnoteLabel, } from "@dogsbay/serialize-core";
2
+ // Module-level, saved and restored around each treeToMkdocs call. Correct
3
+ // for synchronous nesting only: nothing in this module may `await`, or one
4
+ // page's context bleeds into the next.
5
+ let ctx = {
6
+ pageSlug: "",
7
+ };
1
8
  /**
2
- * Convert a TreeNode[] back to MkDocs-compatible Markdown.
9
+ * Convert a TreeNode[] to MkDocs-compatible Markdown.
3
10
  */
4
- export function treeToMkdocs(nodes) {
5
- return nodes.map(nodeToMkdocs).join("\n\n");
11
+ export function treeToMkdocs(nodes, options = {}) {
12
+ const prev = ctx;
13
+ ctx = { pageSlug: "", ...options };
14
+ try {
15
+ return joinBlocks(nodes);
16
+ }
17
+ finally {
18
+ ctx = prev;
19
+ }
20
+ }
21
+ function report(feature, detail, action = "degraded") {
22
+ ctx.ledger?.record({ feature, detail, action });
6
23
  }
24
+ // ── Block dispatcher ────────────────────────────────────
7
25
  function nodeToMkdocs(node) {
8
26
  switch (node.type) {
9
27
  case "frontmatter":
10
28
  return `---\n${node.props?.content ?? ""}\n---`;
11
29
  case "references":
12
30
  return referencesToMkdocs(node);
31
+ case "heading":
32
+ return headingToMkdocs(node);
33
+ case "paragraph":
34
+ return escapeBlockStart(leafContent(node));
35
+ case "prose":
36
+ return proseToMkdocs(node);
13
37
  case "callout":
14
38
  return calloutToMkdocs(node);
15
39
  case "details":
@@ -22,157 +46,915 @@ function nodeToMkdocs(node) {
22
46
  return codeToMkdocs(node);
23
47
  case "diagram":
24
48
  return diagramToMkdocs(node);
25
- case "annotation-list":
26
- return annotationListToMkdocs(node);
27
- case "heading":
28
- return headingToMkdocs(node);
29
- case "paragraph":
30
- return childrenToMkdocs(node);
31
- case "prose":
32
- return node.html ?? "";
49
+ case "math-block":
50
+ case "math":
51
+ return mathBlockToMkdocs(node);
52
+ case "table":
53
+ return tableToMkdocs(node);
33
54
  case "blockquote":
34
- return blockquoteToMkdocs(node);
55
+ return prefixLines(childrenToMkdocs(node), "> ");
35
56
  case "ordered-list":
36
57
  return orderedListToMkdocs(node);
37
58
  case "unordered-list":
38
59
  return unorderedListToMkdocs(node);
39
60
  case "list-item":
61
+ return listItemToMkdocs(node);
62
+ case "deflist":
63
+ return deflistToMkdocs(node);
64
+ case "dt":
65
+ return escapeBlockStart(leafContent(node));
66
+ case "dd":
67
+ return escapeBlockStart(leafContent(node));
68
+ case "steps":
69
+ case "substeps":
70
+ return stepsToMkdocs(node);
71
+ case "step":
72
+ // A step reached outside stepsToMkdocs: its title is its inline text.
73
+ return [escapeBlockStart(leafContent(node)), childrenToMkdocs(node)].filter(Boolean).join("\n\n");
74
+ case "annotation-list":
40
75
  return childrenToMkdocs(node);
41
- case "html-container":
42
- return htmlContainerToMkdocs(node);
76
+ case "link-button": {
77
+ const raw = node.props?.href ?? "";
78
+ const href = raw && isSafeHref(raw) ? mkdocsHref(raw) : "";
79
+ if (raw && !href)
80
+ report("link-button", `unsafe link target dropped (${raw.slice(0, 40)})`);
81
+ const text = leafContent(node) || escapeProse(href);
82
+ return href ? `[${text}](${markdownDestination(href)}){ .md-button .md-button--primary }` : text;
83
+ }
43
84
  case "hr":
44
85
  return "---";
45
86
  case "html":
46
- return node.html ?? "";
87
+ return relativizeHtmlAssets(node.html ?? "");
88
+ case "html-container":
89
+ return htmlContainerToMkdocs(node);
90
+ case "figure": {
91
+ const caption = node.props?.caption;
92
+ const inner = childrenToMkdocs(node);
93
+ if (!caption)
94
+ return inner;
95
+ return `<figure markdown>\n\n${inner}\n\n<figcaption>${htmlText(caption)}</figcaption>\n</figure>`;
96
+ }
97
+ case "card":
98
+ return cardToMkdocs(node);
99
+ case "cards":
100
+ case "grid":
101
+ return cardsToMkdocs(node);
102
+ case "link-card":
103
+ return cardToMkdocs(node);
104
+ case "youtube":
105
+ return youtubeToMkdocs(node);
106
+ case "api-class":
107
+ case "api-member":
108
+ return apiSymbolToMkdocs(node);
109
+ case "api-doc":
110
+ return apiDocToMkdocs(node);
111
+ case "api-params":
112
+ return apiParamsToMkdocs(node);
113
+ case "api-source":
114
+ return apiSourceToMkdocs(node);
115
+ case "footnote-list":
116
+ return (node.children ?? []).map(nodeToMkdocs).filter(Boolean).join("\n");
117
+ case "footnote-def":
118
+ return footnoteDefToMkdocs(node);
119
+ case "related": {
120
+ // "Additional resources" — the title lives in props; children are the
121
+ // link list. Dropping the label floats the links with no context.
122
+ const title = (node.props?.title ?? "").trim();
123
+ const body = childrenToMkdocs(node);
124
+ if (!title)
125
+ return body;
126
+ return `**${escapeProse(title)}**\n\n${body}`;
127
+ }
128
+ case "include": {
129
+ // Includes are realized upstream (convert / site build). One that
130
+ // reaches the serializer is unresolved: emit the snippet form so the
131
+ // reference is visible in the output rather than vanishing, and say so.
132
+ const src = node.props?.src ?? node.props?.path ?? "";
133
+ report("include", `unrealized include ${src} emitted as a --8<-- snippet`);
134
+ return `--8<-- "${src}"`;
135
+ }
47
136
  default:
48
- return node.html ?? childrenToMkdocs(node);
137
+ // `renderChildren` is what keeps an unknown CONTAINER's content: without
138
+ // it the html policy returns a comment for a node that has children and
139
+ // no html, and the ledger says "no content to preserve" — false.
140
+ return renderUnknownNode(node, {
141
+ policy: "html",
142
+ renderChildren: (kids) => joinBlocks(kids),
143
+ report: (type, detail) => report(`node:${type}`, detail ?? ""),
144
+ join: "\n\n",
145
+ });
49
146
  }
50
147
  }
51
- function calloutToMkdocs(node) {
52
- const variant = node.props?.variant ?? "note";
148
+ // ── Inline rendering ────────────────────────────────────
149
+ /**
150
+ * MkDocs inline syntax as emitters over the core's shared walk.
151
+ *
152
+ * Exhaustive on purpose: an omitted key routes through the core's
153
+ * plain-text degradation, which is the right default for a NEW variant and
154
+ * the wrong behaviour for one PyMdown genuinely supports.
155
+ */
156
+ const MKDOCS_INLINE = {
157
+ text: (node) => applyTextFlags(escapeProse(node.text), node, {
158
+ bold: (s) => `**${s}**`,
159
+ italic: (s) => `*${s}*`,
160
+ strike: (s) => `~~${s}~~`,
161
+ }),
162
+ link: (node, c) => {
163
+ // Python-Markdown does not filter URL schemes.
164
+ if (!isSafeHref(node.href)) {
165
+ c.report("link", `unsafe link target dropped (${node.href.slice(0, 40)})`);
166
+ return c.recurse(node.children);
167
+ }
168
+ return `[${c.recurse(node.children)}](${markdownDestination(mkdocsHref(node.href))}${linkTitle(node.title)})${attrSuffix(node.attrs)}`;
169
+ },
170
+ image: (node, c) => {
171
+ if (!isSafeHref(node.src, "image")) {
172
+ c.report("image", `unsafe image source dropped (${node.src.slice(0, 40)})`);
173
+ return escapeProse(node.alt ?? "");
174
+ }
175
+ const alt = (node.alt ?? "").replace(/\]/g, "\\]");
176
+ const title = linkTitle(node.title);
177
+ const attrs = { ...(node.attrs ?? {}) };
178
+ if (node.width)
179
+ attrs.width = String(node.width);
180
+ if (node.height)
181
+ attrs.height = String(node.height);
182
+ return `![${alt}](${markdownDestination(mkdocsAssetPath(node.src))}${title})${attrSuffix(attrs)}`;
183
+ },
184
+ code: (node) => codeSpan(node.text),
185
+ highlight: (node, c) => `==${c.recurse(node.children)}==`,
186
+ "footnote-ref": (node) => `[^${footnoteLabel(node.label)}]`,
187
+ // pymdownx.keys: `++ctrl+alt+del++`
188
+ // Key names only: anything else in a key was inline HTML to Python-Markdown.
189
+ // A key name as pymdownx.keys knows it; anything else as a quoted key
190
+ // (its text, escaped by the extension), never as inline HTML.
191
+ kbd: (node) => `++${node.keys
192
+ .map((k) => {
193
+ const name = k.toLowerCase().replace(/\s+/g, "-");
194
+ return /^[a-z0-9-]+$/.test(name) ? name : `"${k.replace(/["\\\r\n]/g, "")}"`;
195
+ })
196
+ .join("+")}++`,
197
+ math: (node) => `$${markdownLatex(node.latex)}$`,
198
+ icon: (node) => `:${node.name.replace(/[^A-Za-z0-9_-]/g, "-")}:`,
199
+ "html-inline": (node) => relativizeHtmlAssets(node.html ?? ""),
200
+ // A two-space hard break inside an indented block is fragile; `<br>` is not.
201
+ break: () => "<br>",
202
+ };
203
+ /**
204
+ * Escape prose for Python-Markdown.
205
+ *
206
+ * `<cluster-name>` in running text is an HTML tag to Python-Markdown and the
207
+ * placeholder disappears from the rendered page. `\<` is NOT an escape in
208
+ * this dialect (its ESCAPED_CHARS has no `<`), so the entity is the only
209
+ * form that survives. Emphasis and code markers are backslash-escaped so a
210
+ * literal `*` or `_` in prose cannot open a span.
211
+ */
212
+ function escapeProse(text) {
213
+ // Emoji shortcodes (`:man_raising_hand:`) must keep their underscores.
214
+ const parts = text.split(/(:[a-z0-9_+-]+:)/i);
215
+ return parts
216
+ .map((part, i) => {
217
+ if (i % 2 === 1)
218
+ return part;
219
+ return part
220
+ .replace(/&(?![a-zA-Z#][a-zA-Z0-9]*;)/g, "&amp;")
221
+ .replace(/<(?=[A-Za-z/!?])/g, "&lt;")
222
+ .replace(/([*`[\]])/g, "\\$1")
223
+ // Intraword `_` is literal to Python-Markdown (snake_case is safe);
224
+ // only an underscore at a word edge can open or close emphasis.
225
+ .replace(/(^|[^A-Za-z0-9])_|_(?=[^A-Za-z0-9]|$)/g, (m) => m.replace("_", "\\_"))
226
+ .replace(/~~/g, "\\~\\~");
227
+ })
228
+ .join("");
229
+ }
230
+ /**
231
+ * A block of prose that BEGINS with something Python-Markdown reads as
232
+ * structure — `# `, `> `, `- `, `1. `, `!!! `, `=== `, a def_list `: ` —
233
+ * becomes that structure. Neutralise the first character of the first line.
234
+ */
235
+ function escapeBlockStart(block) {
236
+ if (!block)
237
+ return block;
238
+ const first = block.split("\n", 1)[0];
239
+ if (/^(#{1,6}\s|>|[-+]\s|\d+[.)]\s|---|\+\+\+)/.test(first))
240
+ return `\\${block}`;
241
+ if (/^!!!/.test(first))
242
+ return `\\${block}`;
243
+ if (/^\?\?\?/.test(first))
244
+ return `&#63;${block.slice(1)}`;
245
+ if (/^===/.test(first))
246
+ return `&#61;${block.slice(1)}`;
247
+ if (/^:\s{3}/.test(first))
248
+ return `&#58;${block.slice(1)}`;
249
+ return block;
250
+ }
251
+ /**
252
+ * Two adjacent lists — or two adjacent tab groups — are ONE list / one tab
253
+ * set to Python-Markdown (`1. a` then `- b` merge into a single `<ol>`, and
254
+ * `===!` is not a separator in pymdownx 10). An empty HTML comment between
255
+ * them is invisible and keeps them apart.
256
+ */
257
+ const LIST_TYPES = new Set(["ordered-list", "unordered-list"]);
258
+ function joinBlocks(nodes) {
259
+ const out = [];
260
+ let prev;
261
+ for (const node of nodes) {
262
+ const text = nodeToMkdocs(node);
263
+ if (!text)
264
+ continue;
265
+ if (prev && ((LIST_TYPES.has(prev.type) && LIST_TYPES.has(node.type)) || (prev.type === "tabs" && node.type === "tabs"))) {
266
+ out.push("<!-- -->");
267
+ }
268
+ out.push(text);
269
+ prev = node;
270
+ }
271
+ return out.join("\n\n");
272
+ }
273
+ /** A link title for Python-Markdown: one line, `"` as an entity. */
274
+ function linkTitle(title) {
275
+ return title ? ` "${escapeProse(title.replace(/\s*[\r\n]+\s*/g, " ")).replace(/"/g, "&quot;")}"` : "";
276
+ }
277
+ /** Text for a raw HTML element (a figcaption, an attribute). */
278
+ function htmlText(s) {
279
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
280
+ }
281
+ function inlineToMkdocs(nodes) {
282
+ return walkInline(groupFormatRuns(nodes), {
283
+ emitters: MKDOCS_INLINE,
284
+ // Inline nodes without their own emitter fall back to plain text.
285
+ escape: escapeProse,
286
+ report: (feature, detail) => report(feature, detail ?? ""),
287
+ });
288
+ }
289
+ /**
290
+ * Wrap CONSECUTIVE nodes sharing the same bold/italic/strike flags in one
291
+ * mark pair instead of per-node wrapping. The parser splits a bold run at a
292
+ * code span (`**formatted `code` text**` arrives as three flagged nodes);
293
+ * per-node emission produced `**formatted**** ****text**` — an empty
294
+ * `** **` pair Python-Markdown renders as literal asterisks. Same fix as
295
+ * format-obsidian's groupFormatRuns.
296
+ */
297
+ function groupFormatRuns(nodes) {
298
+ const flagsOf = (n) => {
299
+ const f = n;
300
+ return `${f.bold ? "b" : ""}${f.italic ? "i" : ""}${f.strikethrough ? "s" : ""}`;
301
+ };
302
+ const flaggable = (n) => n.type === "text" || n.type === "code";
303
+ const out = [];
304
+ let i = 0;
305
+ while (i < nodes.length) {
306
+ const key = flagsOf(nodes[i]);
307
+ if (!key || !flaggable(nodes[i])) {
308
+ out.push(nodes[i]);
309
+ i++;
310
+ continue;
311
+ }
312
+ let j = i;
313
+ while (j < nodes.length && flaggable(nodes[j]) && flagsOf(nodes[j]) === key)
314
+ j++;
315
+ if (j === i + 1 && nodes[i].type === "text") {
316
+ out.push(nodes[i]);
317
+ i = j;
318
+ continue;
319
+ }
320
+ const run = nodes.slice(i, j).map((n) => {
321
+ const clone = { ...n };
322
+ delete clone.bold;
323
+ delete clone.italic;
324
+ delete clone.strikethrough;
325
+ return clone;
326
+ });
327
+ let inner = walkInline(run, { emitters: MKDOCS_INLINE, escape: escapeProse });
328
+ const lead = inner.match(/^\s*/)[0];
329
+ const tail = inner.match(/\s*$/)[0];
330
+ inner = inner.slice(lead.length, inner.length - tail.length || undefined);
331
+ if (inner) {
332
+ let open = "";
333
+ let close = "";
334
+ if (key.includes("s")) {
335
+ open += "~~";
336
+ close = "~~" + close;
337
+ }
338
+ if (key.includes("i")) {
339
+ open += "*";
340
+ close = "*" + close;
341
+ }
342
+ if (key.includes("b")) {
343
+ open += "**";
344
+ close = "**" + close;
345
+ }
346
+ // Already-serialized markdown riding the html-inline pass-through.
347
+ out.push({ type: "html-inline", html: lead + open + inner + close + tail });
348
+ }
349
+ else if (lead) {
350
+ out.push({ type: "text", text: lead });
351
+ }
352
+ i = j;
353
+ }
354
+ return out;
355
+ }
356
+ /** `{ .class #id key="value" }` for attr_list, or "" when there is nothing. */
357
+ function attrSuffix(attrs) {
358
+ if (!attrs)
359
+ return "";
360
+ const parts = [];
361
+ if (attrs.id)
362
+ parts.push(`#${attrs.id}`);
363
+ if (attrs.class)
364
+ parts.push(...attrs.class.split(/\s+/).filter(Boolean).map((cls) => `.${cls}`));
365
+ for (const [k, v] of Object.entries(attrs)) {
366
+ if (k === "id" || k === "class" || v === undefined)
367
+ continue;
368
+ parts.push(`${k}="${v.replace(/"/g, "&quot;")}"`);
369
+ }
370
+ return parts.length ? `{ ${parts.join(" ")} }` : "";
371
+ }
372
+ // ── Links and assets ────────────────────────────────────
373
+ /** Directory of the current page's emitted file, as path segments. */
374
+ function pageDirSegments() {
375
+ const parts = ctx.pageSlug.split("/").filter(Boolean);
376
+ parts.pop();
377
+ return parts;
378
+ }
379
+ /** `target` (site-rooted, no leading slash) relative to the current page's directory. */
380
+ function relativeToPage(target) {
381
+ const from = pageDirSegments();
382
+ const to = target.split("/").filter(Boolean);
383
+ let i = 0;
384
+ while (i < from.length && i < to.length && from[i] === to[i])
385
+ i++;
386
+ const up = from.slice(i).map(() => "..");
387
+ const rel = [...up, ...to.slice(i)].join("/");
388
+ return rel || ".";
389
+ }
390
+ /**
391
+ * Site-rooted page links → relative `.md` links, which MkDocs resolves,
392
+ * validates, and rewrites for `use_directory_urls`. Only rewritten when the
393
+ * target is a page in this export; everything else passes through so an
394
+ * asset or external route is never turned into a dead `.md`.
395
+ */
396
+ export function mkdocsHref(href) {
397
+ if (!href || /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("#") || href.startsWith("//")) {
398
+ return href;
399
+ }
400
+ if (!href.startsWith("/"))
401
+ return href;
402
+ const hashAt = href.indexOf("#");
403
+ const path = (hashAt === -1 ? href : href.slice(0, hashAt)).replace(/^\/+|\/+$/g, "");
404
+ const hash = hashAt === -1 ? "" : href.slice(hashAt);
405
+ if (!ctx.slugs || !ctx.pageSlug)
406
+ return href;
407
+ // `/rest_api/objects/index#frag` names the page emitted as `rest_api/objects.md`
408
+ // — 76 of 81 links MkDocs reported "left as is" on the OpenShift subset were
409
+ // this shape. Try the stripped form too.
410
+ const candidates = [
411
+ path,
412
+ path ? `${path}/index` : "index",
413
+ path.replace(/\.md$/, ""),
414
+ path.replace(/\/index$/, ""),
415
+ ];
416
+ const slug = candidates.find((c) => ctx.slugs.has(c));
417
+ if (!slug)
418
+ return href;
419
+ return `${relativeToPage(`${slug}.md`)}${hash}`;
420
+ }
421
+ /** `/images/x.png` → `../images/x.png` relative to the emitted file. */
422
+ function mkdocsAssetPath(src) {
423
+ if (!src || /^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//") || !src.startsWith("/")) {
424
+ return src;
425
+ }
426
+ if (!ctx.pageSlug)
427
+ return src;
428
+ return relativeToPage(src.replace(/^\/+/, ""));
429
+ }
430
+ /**
431
+ * Adapt raw HTML for MkDocs: relative asset paths, and the AsciiDoc
432
+ * converter's admonition-in-a-table-cell convention rewritten to Material's
433
+ * own admonition markup.
434
+ *
435
+ * `adoc2md` emits an admonition inside a table cell as
436
+ * `<dl class="db-admonition db-admonition-important"><dt>Important</dt><dd>…</dd></dl>`,
437
+ * because a `!!!` block cannot live inside a raw HTML cell. The Astro
438
+ * scaffold styles that class; MkDocs has never heard of it, so the 354 such
439
+ * blocks in the OpenShift corpus rendered as plain definition lists — the
440
+ * word "Important" as a term, the warning as its definition, no box. Content
441
+ * intact, signal gone. Material's admonition HTML is stable and documented,
442
+ * so emit that instead.
443
+ */
444
+ function relativizeHtmlAssets(html) {
445
+ let out = html;
446
+ if (out.includes('db-admonition')) {
447
+ out = out.replace(/<dl class="db-admonition(?:\s+db-admonition-([a-z]+))?"[^>]*>\s*<dt>([^<]*)<\/dt>\s*<dd>([\s\S]*?)<\/dd>\s*<\/dl>/g, (_m, kind, title, body) => {
448
+ const raw = (kind || title || "note").toLowerCase();
449
+ const type = ADMONITION_ALIASES[raw] ?? raw;
450
+ const inner = /^\s*<(p|ul|ol|pre|div|table)\b/.test(body) ? body : `<p>${body}</p>`;
451
+ return `<div class="admonition ${type}"><p class="admonition-title">${title.trim() || type}</p>${inner}</div>`;
452
+ });
453
+ }
454
+ if (!ctx.pageSlug)
455
+ return out;
456
+ // src / poster / href-to-a-file, either quote style; srcset entries too.
457
+ out = out.replace(/(\s(?:src|poster|href)=)(["'])(\/[^"']+)\2/g, (m, a, q, path) => {
458
+ if (a.startsWith(" href=") && !/\.[a-z0-9]{2,5}$/i.test(path.split(/[?#]/)[0]))
459
+ return m;
460
+ return `${a}${q}${mkdocsAssetPath(path)}${q}`;
461
+ });
462
+ out = out.replace(/(\ssrcset=)(["'])([^"']+)\2/g, (_m, a, q, list) => `${a}${q}${list.split(",").map((e) => e.trim().replace(/^\/\S+/, (u) => mkdocsAssetPath(u))).join(", ")}${q}`);
463
+ return out;
464
+ }
465
+ // ── Prose ───────────────────────────────────────────────
466
+ function proseToMkdocs(node) {
467
+ if (node.inline)
468
+ return inlineToMkdocs(node.inline);
469
+ if (node.html)
470
+ return htmlToMkdocs(node.html);
471
+ return "";
472
+ }
473
+ /**
474
+ * Text-bearing content across all three TreeNode shapes (flat `inline`,
475
+ * wrapped `prose` child, html-only), via the core's `renderLeaf`.
476
+ */
477
+ function leafContent(node) {
478
+ return renderLeaf(node, {
479
+ inline: inlineToMkdocs,
480
+ children: (children) => children.map(nodeToMkdocs).filter(Boolean).join("\n\n"),
481
+ html: htmlToMkdocs,
482
+ join: "\n\n",
483
+ });
484
+ }
485
+ function htmlToMkdocs(html) {
486
+ // stripHtml decodes `&lt;` back to `<`, which would reintroduce the exact
487
+ // tag-swallow escapeProse exists to prevent. Re-escape after stripping.
488
+ return stripHtml(inlineHtmlToMarkdown(relativizeHtmlAssets(html), { flatten: true })).replace(/<(?=[A-Za-z/!?])/g, "&lt;");
489
+ }
490
+ function childrenToMkdocs(node) {
491
+ if (!node.children || node.children.length === 0)
492
+ return "";
493
+ return joinBlocks(node.children);
494
+ }
495
+ // ── Headings ────────────────────────────────────────────
496
+ function headingToMkdocs(node) {
497
+ const level = node.props?.level ?? 1;
498
+ const hashes = "#".repeat(Math.min(Math.max(level, 1), 6));
499
+ const text = leafContent(node) || (node.props?.text ?? "");
500
+ // The authored id first (`{id="…"}`), then the parser's slug. Emitted via
501
+ // attr_list so cross-references written against the source ids resolve.
502
+ const id = safeAnchorId((node.props?.id || node.props?.slug || "").trim());
503
+ const suffix = id ? ` { #${id} }` : "";
504
+ return `${hashes} ${text.replace(/\n+/g, " ")}${suffix}`;
505
+ }
506
+ // ── Admonitions ─────────────────────────────────────────
507
+ /** Material's admonition vocabulary; anything else keeps its name. */
508
+ const ADMONITION_ALIASES = {
509
+ caution: "warning",
510
+ important: "warning",
511
+ hint: "tip",
512
+ attention: "warning",
513
+ error: "danger",
514
+ };
515
+ function rawVariant(node) {
516
+ return (calloutVariant(node) || "note").toLowerCase();
517
+ }
518
+ function admonitionType(node) {
519
+ const raw = rawVariant(node);
520
+ return ADMONITION_ALIASES[raw] ?? raw;
521
+ }
522
+ function admonitionTitle(node, type) {
53
523
  const title = node.props?.title;
54
- const inline = node.props?.inline;
55
- let opener = "!!!";
56
- let line = `${opener} ${variant}`;
57
- if (inline)
58
- line += ` ${inline}`;
59
- line += titleSuffix(variant, title);
60
- const inner = childrenToMkdocs(node);
61
- if (!inner)
62
- return line;
63
- return `${line}\n\n${indent(inner, 4)}`;
524
+ if (title === null)
525
+ return ' ""';
526
+ if (title === undefined)
527
+ return "";
528
+ const raw = rawVariant(node);
529
+ const autoTitle = raw.charAt(0).toUpperCase() + raw.slice(1);
530
+ // An aliased type would otherwise show the alias's auto-title ("Warning"
531
+ // for an `important`), so keep the original word as an explicit title.
532
+ if (title === autoTitle && raw === type)
533
+ return "";
534
+ // Python-Markdown runs inline patterns over the title: raw HTML there was live.
535
+ return ` "${escapeProse(title.replace(/[\r\n]+/g, " ")).replace(/"/g, "&quot;")}"`;
536
+ }
537
+ function calloutToMkdocs(node) {
538
+ const type = admonitionType(node);
539
+ const inlineMod = node.props?.inline;
540
+ let line = `!!! ${type}`;
541
+ if (inlineMod)
542
+ line += ` ${inlineMod}`;
543
+ line += admonitionTitle(node, type);
544
+ const inner = bodyContent(node);
545
+ return inner ? `${line}\n\n${indent(inner, 4)}` : line;
546
+ }
547
+ /** Block children when present; an html-only body otherwise, so it is not lost. */
548
+ function bodyContent(node) {
549
+ return childrenToMkdocs(node) || (node.children?.length ? "" : leafContent(node));
64
550
  }
65
551
  function detailsToMkdocs(node) {
66
- const variant = node.props?.variant ?? "note";
67
- const title = node.props?.title;
552
+ const type = admonitionType(node);
68
553
  const open = node.props?.open;
69
- const opener = open ? "???+" : "???";
70
- let line = `${opener} ${variant}`;
71
- line += titleSuffix(variant, title);
72
- const inner = childrenToMkdocs(node);
73
- if (!inner)
74
- return line;
75
- return `${line}\n\n${indent(inner, 4)}`;
554
+ let line = `${open ? "???+" : "???"} ${type}`;
555
+ line += admonitionTitle(node, type);
556
+ const inner = bodyContent(node);
557
+ return inner ? `${line}\n\n${indent(inner, 4)}` : line;
76
558
  }
559
+ // ── Tabs ────────────────────────────────────────────────
77
560
  function tabsToMkdocs(node) {
78
561
  if (!node.children)
79
562
  return "";
80
- return node.children.map(nodeToMkdocs).join("\n\n");
563
+ return node.children.map(tabToMkdocs).filter(Boolean).join("\n\n");
81
564
  }
82
565
  function tabToMkdocs(node) {
83
- const title = node.props?.title ?? "Tab";
84
- const inner = childrenToMkdocs(node);
85
- if (!inner)
86
- return `=== "${title}"`;
87
- return `=== "${title}"\n\n${indent(inner, 4)}`;
566
+ const title = node.props?.title ||
567
+ node.props?.label ||
568
+ node.props?.value ||
569
+ "Tab";
570
+ const inner = bodyContent(node);
571
+ const head = `=== "${escapeProse(title.replace(/[\r\n]+/g, " ")).replace(/"/g, "&quot;")}"`;
572
+ return inner ? `${head}\n\n${indent(inner, 4)}` : head;
88
573
  }
574
+ // ── Code ────────────────────────────────────────────────
89
575
  function codeToMkdocs(node) {
90
- const lang = node.props?.lang ?? "";
91
- const code = (node.props?.code ?? "").replace(/\n$/, "");
92
- const fence = node.props?.fence ?? "```";
93
- return `${fence}${lang}\n${code}\n${fence}`;
576
+ // Parser: `props.language` + `node.html`; importers: `props.lang` + `props.code`.
577
+ const lang = node.props?.lang || node.props?.language || "";
578
+ const code = (node.props?.code ?? node.html ?? "").replace(/\n$/, "");
579
+ // One line: a newline in the title ended the fence's opening line.
580
+ const title = node.props?.title?.replace(/[\r\n]+/g, " ").replace(/`/g, "'");
581
+ const highlights = node.props?.highlights;
582
+ const fence = pickCodeFence(code);
583
+ const meta = [];
584
+ // superfences writes the title into the page's HTML unescaped.
585
+ if (title)
586
+ meta.push(`title="${htmlText(title)}"`);
587
+ if (highlights)
588
+ meta.push(`hl_lines="${highlights.replace(/[^0-9 ,-]/g, "").replace(/,/g, " ")}"`);
589
+ const info = [lang || (meta.length ? "text" : ""), ...meta].filter(Boolean).join(" ");
590
+ return `${fence}${info}\n${code}\n${fence}`;
94
591
  }
95
592
  function diagramToMkdocs(node) {
96
593
  const lang = node.props?.lang ?? "mermaid";
97
- const code = (node.props?.code ?? "").replace(/\n$/, "");
98
- return `\`\`\`${lang}\n${code}\n\`\`\``;
594
+ const code = (node.props?.code ?? node.html ?? "").replace(/\n$/, "");
595
+ const fence = pickCodeFence(code);
596
+ return `${fence}${lang.replace(/[^\w+#.-]/g, "")}\n${code}\n${fence}`;
99
597
  }
100
- function referencesToMkdocs(node) {
101
- const entries = (node.props?.entries ?? []);
102
- return entries
103
- .map((e) => {
104
- let line = `[${e.label}]: ${e.href}`;
105
- if (e.title)
106
- line += ` "${e.title}"`;
107
- return line;
108
- })
109
- .join("\n");
598
+ function mathBlockToMkdocs(node) {
599
+ const latex = (node.props?.latex ?? node.props?.content ?? node.html ?? "").trim();
600
+ return `$$\n${markdownLatex(latex)}\n$$`;
110
601
  }
111
- function annotationListToMkdocs(node) {
112
- return childrenToMkdocs(node);
602
+ function collectRows(node) {
603
+ const out = { header: [], body: [] };
604
+ for (const child of node.children ?? []) {
605
+ if (child.type === "thead") {
606
+ for (const tr of child.children ?? [])
607
+ if (tr.type === "tr")
608
+ out.header.push(tr.children ?? []);
609
+ }
610
+ else if (child.type === "tbody") {
611
+ for (const tr of child.children ?? [])
612
+ if (tr.type === "tr")
613
+ out.body.push(tr.children ?? []);
614
+ }
615
+ else if (child.type === "tr") {
616
+ const cells = child.children ?? [];
617
+ if (out.body.length === 0 && out.header.length === 0 && cells.some((c) => c.type === "th")) {
618
+ out.header.push(cells);
619
+ }
620
+ else {
621
+ out.body.push(cells);
622
+ }
623
+ }
624
+ }
625
+ return out;
113
626
  }
114
- function headingToMkdocs(node) {
115
- const level = node.props?.level ?? 1;
116
- const hashes = "#".repeat(level);
117
- const text = childrenToMkdocs(node);
118
- return `${hashes} ${text}`;
627
+ const BLOCK_HTML = /<(ul|ol|p|pre|table|dl|div|blockquote|h[1-6])\b/i;
628
+ /** Does this cell hold something a single pipe-table line cannot? */
629
+ function isBlockCell(cell) {
630
+ const kids = cell.children ?? [];
631
+ if (kids.length > 1)
632
+ return true;
633
+ if (kids.some((k) => !["paragraph", "prose"].includes(k.type)))
634
+ return true;
635
+ if (cell.props?.colspan > 1 || cell.props?.rowspan > 1)
636
+ return true;
637
+ // An html-only cell carrying block markup: stripHtml would glue its words.
638
+ const html = leafHtml(cell);
639
+ return !!html && BLOCK_HTML.test(html);
119
640
  }
120
- function blockquoteToMkdocs(node) {
121
- const inner = childrenToMkdocs(node);
122
- return inner
123
- .split("\n")
124
- .map((line) => (line ? `> ${line}` : ">"))
125
- .join("\n");
641
+ function hasBlockCells(rows) {
642
+ return rows.some((row) => row.some(isBlockCell));
643
+ }
644
+ /**
645
+ * GFM pipe table when every cell is a single line of inline content;
646
+ * otherwise an HTML table whose cells are processed as Markdown
647
+ * (`md_in_html`), so a list or code block inside a cell survives instead of
648
+ * being flattened onto one line.
649
+ */
650
+ function tableToMkdocs(node) {
651
+ const rows = collectRows(node);
652
+ const all = [...rows.header, ...rows.body];
653
+ if (all.length === 0)
654
+ return "";
655
+ if (hasBlockCells(all))
656
+ return htmlTableToMkdocs(rows);
657
+ const text = (cells) => cells.map((c) => (leafContent(c) || "").replace(/\s*\n\s*/g, " ").replace(/\|/g, "\\|").trim());
658
+ const header = rows.header.length ? text(rows.header[0]) : text(all[0]);
659
+ const body = (rows.header.length ? rows.body : all.slice(1)).map(text);
660
+ const cols = Math.max(header.length, ...body.map((r) => r.length));
661
+ const widths = Array.from({ length: cols }, (_, i) => Math.max(3, header[i]?.length ?? 0, ...body.map((r) => r[i]?.length ?? 0)));
662
+ const line = (cells) => `| ${Array.from({ length: cols }, (_, i) => (cells[i] ?? "").padEnd(widths[i])).join(" | ")} |`;
663
+ return [
664
+ line(header),
665
+ `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`,
666
+ ...body.map(line),
667
+ ].join("\n");
668
+ }
669
+ /**
670
+ * md_in_html descends only into elements that THEMSELVES carry a `markdown`
671
+ * attribute, so `<table markdown>` with bare `<tbody>`/`<tr>` never reaches
672
+ * the cell: `<td markdown="block">` was emitted and its list rendered as the
673
+ * literal text `- a - b`. Every wrapper on the way down carries the
674
+ * attribute. Cells with block content are `markdown="block"`; one-line cells
675
+ * are `markdown="span"` so a header is not wrapped in `<p>`. A cell whose
676
+ * content is block HTML from the importer is passed through raw.
677
+ */
678
+ function htmlTableToMkdocs(rows) {
679
+ const cell = (c, tag) => {
680
+ const attrs = [];
681
+ const colspan = c.props?.colspan;
682
+ const rowspan = c.props?.rowspan;
683
+ if (colspan && colspan > 1)
684
+ attrs.push(` colspan="${colspan}"`);
685
+ if (rowspan && rowspan > 1)
686
+ attrs.push(` rowspan="${rowspan}"`);
687
+ const html = leafHtml(c);
688
+ if (!c.children?.length && !c.inline?.length && html && BLOCK_HTML.test(html)) {
689
+ return `<${tag}${attrs.join("")}>${relativizeHtmlAssets(html)}</${tag}>`;
690
+ }
691
+ const block = isBlockCell(c);
692
+ const body = block ? (childrenToMkdocs(c) || leafContent(c)) : (leafContent(c) || childrenToMkdocs(c)).replace(/\s*\n\s*/g, " ");
693
+ return block
694
+ ? `<${tag} markdown="block"${attrs.join("")}>\n${body}\n</${tag}>`
695
+ : `<${tag} markdown="span"${attrs.join("")}>${body}</${tag}>`;
696
+ };
697
+ const tr = (cells, tag) => `<tr markdown>\n${cells.map((c) => cell(c, c.type === "th" ? "th" : tag)).join("\n")}\n</tr>`;
698
+ const parts = ["<table markdown>"];
699
+ if (rows.header.length)
700
+ parts.push(`<thead markdown>\n${rows.header.map((r) => tr(r, "th")).join("\n")}\n</thead>`);
701
+ if (rows.body.length)
702
+ parts.push(`<tbody markdown>\n${rows.body.map((r) => tr(r, "td")).join("\n")}\n</tbody>`);
703
+ parts.push("</table>");
704
+ return parts.join("\n");
705
+ }
706
+ // ── Lists ───────────────────────────────────────────────
707
+ /** Python-Markdown needs FOUR-space continuation for nested block content. */
708
+ function listItemBlock(marker, text) {
709
+ const lines = text.split("\n");
710
+ const pad = " ".repeat(Math.max(4, marker.length));
711
+ // A fence on the marker line is not a fence to the superfences
712
+ // preprocessor — the whole block collapsed to an inline code span. Start a
713
+ // fence-first (or table-first) item on the line AFTER the marker.
714
+ if (/^(`{3,}|~{3,}|<table\b|<div\b|\|)/.test(lines[0] ?? "")) {
715
+ // A bare marker renders as a paragraph containing "-"; an empty HTML
716
+ // comment is invisible and gives the item a first line to hang on.
717
+ return [`${marker}<!-- -->`, "", ...lines.map((l) => (l ? `${pad}${l}` : l))].join("\n");
718
+ }
719
+ const first = `${marker}${lines[0] ?? ""}`;
720
+ const rest = lines.slice(1).map((l) => (l ? `${pad}${l}` : l));
721
+ return [first, ...rest].join("\n");
126
722
  }
127
723
  function orderedListToMkdocs(node) {
128
724
  if (!node.children)
129
725
  return "";
130
- return node.children
131
- .map((child, i) => {
132
- const text = nodeToMkdocs(child);
133
- return `${i + 1}. ${text}`;
134
- })
135
- .join("\n");
726
+ const start = listStart(node) ?? 1;
727
+ const items = node.children.map((child, i) => listItemBlock(`${start + i}. `, nodeToMkdocs(child)));
728
+ return items.join(itemSeparator(node));
136
729
  }
137
730
  function unorderedListToMkdocs(node) {
138
731
  if (!node.children)
139
732
  return "";
140
- return node.children
141
- .map((child) => {
142
- const text = nodeToMkdocs(child);
143
- return `- ${text}`;
144
- })
733
+ const items = node.children.map((child) => listItemBlock("- ", nodeToMkdocs(child)));
734
+ return items.join(itemSeparator(node));
735
+ }
736
+ /**
737
+ * Loose lists need a blank line between items. Loose means an item carries a
738
+ * block beyond its own text — NOT merely "has children": the wrapped shape
739
+ * (every HTML-derived importer, the mkdocs walker) puts the item text in a
740
+ * single paragraph child, and treating that as loose put a `<p>` inside
741
+ * every `<li>` on a mkdocs → mkdocs round trip.
742
+ */
743
+ function itemSeparator(list) {
744
+ const loose = (list.children ?? []).some((li) => {
745
+ const kids = li.children ?? [];
746
+ if (kids.length === 0)
747
+ return false;
748
+ if (li.inline?.length)
749
+ return true;
750
+ return kids.length > 1 || !["paragraph", "prose"].includes(kids[0].type);
751
+ });
752
+ return loose ? "\n\n" : "\n";
753
+ }
754
+ function listItemToMkdocs(node) {
755
+ const parts = [];
756
+ const checked = node.props?.checked;
757
+ const box = checked === undefined ? "" : checked ? "[x] " : "[ ] ";
758
+ if (node.inline && node.inline.length > 0) {
759
+ parts.push(box + escapeBlockStart(inlineToMkdocs(node.inline)));
760
+ }
761
+ if (node.children) {
762
+ const kids = node.children.map(nodeToMkdocs).filter(Boolean);
763
+ // A leading paragraph child IS the item text: it goes on the marker line,
764
+ // and the checkbox goes with it — `- [ ]` alone on the marker line renders
765
+ // as the literal characters.
766
+ if (parts.length === 0 && kids.length && node.children[0].type === "paragraph") {
767
+ parts.push(box + kids[0]);
768
+ parts.push(...joinBlocksRendered(node.children.slice(1), kids.slice(1)));
769
+ }
770
+ else {
771
+ parts.push(...joinBlocksRendered(node.children, kids));
772
+ }
773
+ }
774
+ else if (box && parts.length === 0) {
775
+ parts.push(box.trimEnd());
776
+ }
777
+ return parts.filter(Boolean).join("\n\n");
778
+ }
779
+ /** Already-rendered siblings, with the adjacent-list separator applied. */
780
+ function joinBlocksRendered(nodes, rendered) {
781
+ const out = [];
782
+ let prev;
783
+ nodes.forEach((n, i) => {
784
+ const text = rendered[i];
785
+ if (!text)
786
+ return;
787
+ if (prev && ((LIST_TYPES.has(prev.type) && LIST_TYPES.has(n.type)) || (prev.type === "tabs" && n.type === "tabs")))
788
+ out.push("<!-- -->");
789
+ out.push(text);
790
+ prev = n;
791
+ });
792
+ return out;
793
+ }
794
+ // ── Definition lists (native: def_list) ─────────────────
795
+ function deflistToMkdocs(node) {
796
+ if (!node.children?.length)
797
+ return "";
798
+ const out = [];
799
+ for (const child of node.children) {
800
+ if (child.type === "dt") {
801
+ out.push(escapeBlockStart(leafContent(child).replace(/\n+/g, " ").trim()));
802
+ continue;
803
+ }
804
+ if (child.type === "dd") {
805
+ const body = (childrenToMkdocs(child) || leafContent(child)).trim();
806
+ const lines = body.split("\n");
807
+ const first = `: ${lines[0] ?? ""}`;
808
+ const rest = lines.slice(1).map((l) => (l ? ` ${l}` : l));
809
+ out.push([first, ...rest].join("\n"));
810
+ continue;
811
+ }
812
+ const other = nodeToMkdocs(child).trim();
813
+ if (other)
814
+ out.push(other);
815
+ }
816
+ // Term / definition pairs are adjacent; a blank line separates entries.
817
+ return out
818
+ .map((block, i) => (block.startsWith(": ") || i === 0 ? block : `\n${block}`))
145
819
  .join("\n");
146
820
  }
821
+ // ── Steps → ordered list ────────────────────────────────
822
+ function stepsToMkdocs(node) {
823
+ if (!node.children)
824
+ return "";
825
+ report("steps", "steps rendered as an ordered list (MkDocs has no steps component)");
826
+ const items = node.children.map((child, i) => {
827
+ const title = (child.inline && child.inline.length ? inlineToMkdocs(child.inline) : "") ||
828
+ child.props?.title ||
829
+ "";
830
+ const body = childrenToMkdocs(child);
831
+ const text = title ? (body ? `**${title}**\n\n${body}` : `**${title}**`) : body;
832
+ return listItemBlock(`${i + 1}. `, text);
833
+ });
834
+ return items.join("\n\n");
835
+ }
836
+ // ── Cards (Material grid cards) ─────────────────────────
837
+ function cardToMkdocs(node) {
838
+ const title = escapeProse(node.props?.title || "");
839
+ const raw = node.props?.href;
840
+ const href = raw && isSafeHref(raw) ? raw : undefined;
841
+ if (raw && !href)
842
+ report("card", `unsafe link target dropped (${raw.slice(0, 40)})`);
843
+ const body = childrenToMkdocs(node) || escapeProse(node.props?.description || "");
844
+ const parts = [];
845
+ if (title)
846
+ parts.push(`**${title}**`);
847
+ if (body)
848
+ parts.push(body);
849
+ if (href)
850
+ parts.push(`[:octicons-arrow-right-24: ${title || "Open"}](${markdownDestination(mkdocsHref(href))})`);
851
+ return parts.join("\n\n");
852
+ }
853
+ function cardsToMkdocs(node) {
854
+ if (!node.children?.length)
855
+ return "";
856
+ const items = node.children.map((c) => listItemBlock("- ", nodeToMkdocs(c)));
857
+ return `<div class="grid cards" markdown>\n\n${items.join("\n\n")}\n\n</div>`;
858
+ }
859
+ // ── Misc blocks ─────────────────────────────────────────
147
860
  function htmlContainerToMkdocs(node) {
148
861
  const tag = node.props?.tag ?? "div";
149
862
  const attrs = node.props?.attrs ?? {};
150
863
  const attrStr = Object.entries(attrs)
151
- .map(([k, v]) => (v ? ` ${k}="${v}"` : ` ${k}`))
864
+ .filter(([k]) => /^[A-Za-z_:][\w:.-]*$/.test(k) && !/^on/i.test(k))
865
+ .map(([k, v]) => (v ? ` ${k}="${htmlText(v)}"` : ` ${k}`))
152
866
  .join("");
153
867
  const inner = childrenToMkdocs(node);
154
868
  return `<${tag}${attrStr} markdown>\n\n${inner}\n\n</${tag}>`;
155
869
  }
156
- function childrenToMkdocs(node) {
157
- if (!node.children || node.children.length === 0)
158
- return "";
159
- return node.children.map(nodeToMkdocs).join("\n\n");
870
+ function referencesToMkdocs(node) {
871
+ const entries = (node.props?.entries ?? []);
872
+ return entries
873
+ .filter((e) => isSafeHref(e.href))
874
+ .map((e) => `[${String(e.label).replace(/[\[\]\n]/g, "")}]: ${markdownDestination(mkdocsHref(e.href))}${linkTitle(e.title)}`)
875
+ .join("\n");
160
876
  }
161
- function titleSuffix(variant, title) {
162
- if (title === null)
163
- return ' ""';
164
- if (title === undefined)
165
- return "";
166
- const autoTitle = variant.charAt(0).toUpperCase() + variant.slice(1);
167
- if (title === autoTitle)
877
+ function youtubeToMkdocs(node) {
878
+ const id = String(node.props?.id || node.props?.videoId || "").replace(/[^\w-]/g, "");
879
+ const title = htmlText(node.props?.title ?? "Video");
880
+ return `<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/${id}" title="${title}" frameborder="0" allowfullscreen></iframe>`;
881
+ }
882
+ function footnoteDefToMkdocs(node) {
883
+ const label = footnoteLabel(String(node.props?.label ?? "1"));
884
+ const content = node.inline && node.inline.length > 0 ? inlineToMkdocs(node.inline) : childrenToMkdocs(node);
885
+ const lines = content.split("\n");
886
+ return [`[^${label}]: ${lines[0] ?? ""}`, ...lines.slice(1).map((l) => (l ? ` ${l}` : l))].join("\n");
887
+ }
888
+ // ── API nodes ───────────────────────────────────────────
889
+ function apiSymbolToMkdocs(node) {
890
+ const name = node.props?.name ?? "";
891
+ const kind = node.props?.kind ?? "class";
892
+ const hashes = "#".repeat(node.type === "api-class" ? 3 : 4);
893
+ const sig = node.props?.signature ?? "";
894
+ const basesRaw = node.props?.bases;
895
+ const bases = Array.isArray(basesRaw) ? basesRaw.join(", ") : basesRaw;
896
+ const parts = [`${hashes} ${escapeProse(kind)} ${codeSpan(name)}`];
897
+ if (bases)
898
+ parts.push(`Bases: ${codeSpan(bases)}`);
899
+ if (sig) {
900
+ const fence = pickCodeFence(sig);
901
+ parts.push(`${fence}python\n${sig}\n${fence}`);
902
+ }
903
+ const inner = childrenToMkdocs(node);
904
+ if (inner)
905
+ parts.push(inner);
906
+ return parts.join("\n\n");
907
+ }
908
+ function apiParamsToMkdocs(node) {
909
+ const params = (node.props?.params ?? []);
910
+ if (params.length === 0)
168
911
  return "";
169
- return ` "${title}"`;
912
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
913
+ const rows = params.map((p) => {
914
+ let desc = p.docHtml || "";
915
+ if (!desc && p.doc)
916
+ desc = `<p>${esc(p.doc)}</p>`;
917
+ if (!desc && p.description)
918
+ desc = `<p>${esc(p.description)}</p>`;
919
+ const meta = [];
920
+ if (p.type)
921
+ meta.push(`<strong>TYPE:</strong> <code>${esc(p.type)}</code>`);
922
+ if (p.default != null)
923
+ meta.push(`<strong>DEFAULT:</strong> <code>${esc(String(p.default))}</code>`);
924
+ if (p.required)
925
+ meta.push("<strong>REQUIRED</strong>");
926
+ const metaHtml = meta.length ? `<p>${meta.join(" &nbsp;·&nbsp; ")}</p>` : "";
927
+ return `<tr><td><code>${esc(p.name)}</code></td><td>${desc}${metaHtml}</td></tr>`;
928
+ });
929
+ return ["<table>", "<thead><tr><th>PARAMETER</th><th>DESCRIPTION</th></tr></thead>", "<tbody>", ...rows, "</tbody>", "</table>"].join("\n");
170
930
  }
171
- function indent(text, spaces) {
172
- const prefix = " ".repeat(spaces);
173
- return text
174
- .split("\n")
175
- .map((line) => (line ? `${prefix}${line}` : line))
176
- .join("\n");
931
+ function apiDocToMkdocs(node) {
932
+ const html = node.props?.html ?? "";
933
+ return html ? htmlToMkdocs(html) : childrenToMkdocs(node);
934
+ }
935
+ function apiSourceToMkdocs(node) {
936
+ const code = node.props?.code ?? "";
937
+ const file = node.props?.file ?? "";
938
+ const line = node.props?.line;
939
+ const parts = [];
940
+ if (file)
941
+ parts.push(`*Source: ${codeSpan(line ? `${file}:${line}` : file)}*`);
942
+ if (code)
943
+ parts.push(`\`\`\`python\n${code.replace(/\n$/, "")}\n\`\`\``);
944
+ else {
945
+ const inner = childrenToMkdocs(node);
946
+ if (inner)
947
+ parts.push(inner);
948
+ }
949
+ return parts.join("\n\n");
950
+ }
951
+ // ── Nav helpers shared with the project emitter ─────────
952
+ /** Normalise an href or slug to the slug form pages are keyed by. */
953
+ export function hrefToSlugKey(href) {
954
+ return href
955
+ .replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, "")
956
+ .replace(/[?#].*$/, "")
957
+ .replace(/^\/+|\/+$/g, "")
958
+ .replace(/\.md$/, "");
177
959
  }
178
960
  //# sourceMappingURL=to-mkdocs.js.map