@dogsbay/format-mkdocs 0.2.0-beta.13 → 0.2.0-beta.131

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