@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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +113 -4
- package/dist/cli.js.map +1 -1
- package/dist/export/to-astro.d.ts.map +1 -1
- package/dist/export/to-astro.js +10 -1
- package/dist/export/to-astro.js.map +1 -1
- package/dist/export/to-mkdocs-project.d.ts +12 -0
- package/dist/export/to-mkdocs-project.d.ts.map +1 -1
- package/dist/export/to-mkdocs-project.js +279 -63
- package/dist/export/to-mkdocs-project.js.map +1 -1
- package/dist/export/to-mkdocs.d.ts +53 -3
- package/dist/export/to-mkdocs.d.ts.map +1 -1
- package/dist/export/to-mkdocs.js +884 -102
- package/dist/export/to-mkdocs.js.map +1 -1
- package/dist/index.d.ts +20 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -3
- package/dist/index.js.map +1 -1
- package/dist/loader.js +1 -2
- package/dist/loader.js.map +1 -1
- package/dist/rules/autodoc.d.ts +1 -1
- package/dist/rules/autodoc.d.ts.map +1 -1
- package/dist/rules/autodoc.js +1 -2
- package/dist/rules/autodoc.js.map +1 -1
- package/package.json +13 -12
- package/dist/autodoc/markdown-generator.d.ts +0 -6
- package/dist/autodoc/markdown-generator.d.ts.map +0 -1
- package/dist/autodoc/markdown-generator.js +0 -242
- package/dist/autodoc/markdown-generator.js.map +0 -1
- package/dist/autodoc/module-resolver.d.ts +0 -8
- package/dist/autodoc/module-resolver.d.ts.map +0 -1
- package/dist/autodoc/module-resolver.js +0 -218
- package/dist/autodoc/module-resolver.js.map +0 -1
- package/dist/autodoc/python-docstring.d.ts +0 -7
- package/dist/autodoc/python-docstring.d.ts.map +0 -1
- package/dist/autodoc/python-docstring.js +0 -324
- package/dist/autodoc/python-docstring.js.map +0 -1
- package/dist/autodoc/python-parser.d.ts +0 -10
- package/dist/autodoc/python-parser.d.ts.map +0 -1
- package/dist/autodoc/python-parser.js +0 -541
- package/dist/autodoc/python-parser.js.map +0 -1
- package/dist/autodoc/tree-builder.d.ts +0 -9
- package/dist/autodoc/tree-builder.d.ts.map +0 -1
- package/dist/autodoc/tree-builder.js +0 -279
- package/dist/autodoc/tree-builder.js.map +0 -1
- package/dist/autodoc/types.d.ts +0 -128
- package/dist/autodoc/types.d.ts.map +0 -1
- package/dist/autodoc/types.js +0 -2
- package/dist/autodoc/types.js.map +0 -1
package/dist/export/to-mkdocs.js
CHANGED
|
@@ -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[]
|
|
9
|
+
* Convert a TreeNode[] to MkDocs-compatible Markdown.
|
|
3
10
|
*/
|
|
4
|
-
export function treeToMkdocs(nodes) {
|
|
5
|
-
|
|
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 "
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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 "
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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 `)}${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, "&")
|
|
221
|
+
.replace(/<(?=[A-Za-z/!?])/g, "<")
|
|
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 `?${block.slice(1)}`;
|
|
245
|
+
if (/^===/.test(first))
|
|
246
|
+
return `=${block.slice(1)}`;
|
|
247
|
+
if (/^:\s{3}/.test(first))
|
|
248
|
+
return `:${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, """)}"` : "";
|
|
276
|
+
}
|
|
277
|
+
/** Text for a raw HTML element (a figcaption, an attribute). */
|
|
278
|
+
function htmlText(s) {
|
|
279
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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, """)}"`);
|
|
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 `<` 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, "<");
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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, """)}"`;
|
|
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
|
|
67
|
-
const title = node.props?.title;
|
|
552
|
+
const type = admonitionType(node);
|
|
68
553
|
const open = node.props?.open;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
563
|
+
return node.children.map(tabToMkdocs).filter(Boolean).join("\n\n");
|
|
81
564
|
}
|
|
82
565
|
function tabToMkdocs(node) {
|
|
83
|
-
const title = node.props?.title
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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, """)}"`;
|
|
572
|
+
return inner ? `${head}\n\n${indent(inner, 4)}` : head;
|
|
88
573
|
}
|
|
574
|
+
// ── Code ────────────────────────────────────────────────
|
|
89
575
|
function codeToMkdocs(node) {
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
101
|
-
const
|
|
102
|
-
return
|
|
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
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
.
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
912
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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(" · ")}</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
|
|
172
|
-
const
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|