@dogsbay/format-openknowledge 0.2.0-beta.101
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/README.md +49 -0
- package/dist/capabilities.d.ts +14 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +141 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +94 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/project.d.ts +75 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +174 -0
- package/dist/project.js.map +1 -0
- package/dist/serialize.d.ts +70 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +738 -0
- package/dist/serialize.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
import { walkInline, applyTextFlags, renderLeaf, indent, prefixLines, pickCodeFence, stripHtml, inlineHtmlToMarkdown, headingLevel, headingText, slugifyHeadingText, inlinePlainText, codeSpan, ExportLedger, } from "@dogsbay/serialize-core";
|
|
2
|
+
/** GFM alert types OpenKnowledge renders natively (remark-github-alerts). */
|
|
3
|
+
const VARIANT_TO_ALERT = {
|
|
4
|
+
note: "note",
|
|
5
|
+
info: "note",
|
|
6
|
+
question: "note",
|
|
7
|
+
abstract: "note",
|
|
8
|
+
tip: "tip",
|
|
9
|
+
hint: "tip",
|
|
10
|
+
success: "tip",
|
|
11
|
+
example: "tip",
|
|
12
|
+
important: "important",
|
|
13
|
+
warning: "warning",
|
|
14
|
+
attention: "warning",
|
|
15
|
+
danger: "caution",
|
|
16
|
+
error: "caution",
|
|
17
|
+
failure: "caution",
|
|
18
|
+
bug: "caution",
|
|
19
|
+
caution: "caution",
|
|
20
|
+
};
|
|
21
|
+
export function treeToOpenKnowledge(nodes, options = {}) {
|
|
22
|
+
const ctx = {
|
|
23
|
+
href: options.href ?? defaultHref,
|
|
24
|
+
ledger: options.ledger ?? new ExportLedger(),
|
|
25
|
+
page: options.page ?? "",
|
|
26
|
+
depth: 0,
|
|
27
|
+
};
|
|
28
|
+
return nodes.map((n) => nodeToMdx(n, ctx)).filter(Boolean).join("\n\n");
|
|
29
|
+
}
|
|
30
|
+
/** Root-absolute `.md` shape without page knowledge: `/a/b/` → `/a/b.md`, `#x` kept, external kept. */
|
|
31
|
+
export function defaultHref(href) {
|
|
32
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("#") || href.startsWith("//"))
|
|
33
|
+
return href;
|
|
34
|
+
const [pathPart, frag] = splitFragment(href);
|
|
35
|
+
let p = pathPart.replace(/\/+$/, "");
|
|
36
|
+
if (!p)
|
|
37
|
+
return frag ? `#${frag}` : "/";
|
|
38
|
+
if (!p.startsWith("/"))
|
|
39
|
+
p = `/${p}`;
|
|
40
|
+
if (!/\.[a-z0-9]+$/i.test(p))
|
|
41
|
+
p = `${p}.md`;
|
|
42
|
+
return frag ? `${p}#${frag}` : p;
|
|
43
|
+
}
|
|
44
|
+
export function splitFragment(href) {
|
|
45
|
+
const i = href.indexOf("#");
|
|
46
|
+
return i < 0 ? [href, ""] : [href.slice(0, i), href.slice(i + 1)];
|
|
47
|
+
}
|
|
48
|
+
/* ── MDX text escaping ─────────────────────────────────────────────────── */
|
|
49
|
+
/**
|
|
50
|
+
* MDX treats `{`/`}` as expressions and `<` followed by anything but
|
|
51
|
+
* whitespace as a tag start (`<3`, `<=`, `<-` are all errors); a backslash
|
|
52
|
+
* escape renders literally in both the strict MDX stack and OpenKnowledge's
|
|
53
|
+
* (which restores backslash escapes). Backslashes are escaped FIRST so a
|
|
54
|
+
* literal `\{` in prose does not become `\\{` (= escaped backslash + a LIVE
|
|
55
|
+
* expression). Text starting a paragraph with `import`/`export` is ESM in
|
|
56
|
+
* strict MDX — see `noEsmStart`.
|
|
57
|
+
*/
|
|
58
|
+
export function escapeMdxText(text) {
|
|
59
|
+
return text
|
|
60
|
+
.replace(/\\/g, "\\\\")
|
|
61
|
+
.replace(/[{}]/g, "\\$&")
|
|
62
|
+
// a `<` at the END of a text node is followed by the next inline node (a code span, a link…): escape it too
|
|
63
|
+
.replace(/<(?=\S|$)/g, "\\<");
|
|
64
|
+
}
|
|
65
|
+
/** A paragraph may not begin with `import`/`export` in strict MDX (ESM); a char-ref renders the same letter. */
|
|
66
|
+
function noEsmStart(text) {
|
|
67
|
+
return text.replace(/^(import|export)(?=\s|$)/, (m) => (m[0] === "i" ? "i" + m.slice(1) : "e" + m.slice(1)));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* JSX attribute value. OpenKnowledge does NOT decode HTML entities inside
|
|
71
|
+
* JSX attributes (its `destructureAttrs` copies the raw string; only
|
|
72
|
+
* text/url/title/alt are restored), and JSX string attributes have no
|
|
73
|
+
* backslash escapes. So: a plain `"…"` when the value is simple, otherwise
|
|
74
|
+
* an EXPRESSION holding a JSON string with `{ } < > & "` written as
|
|
75
|
+
* `\uXXXX` — acorn accepts it and OpenKnowledge `JSON.parse`s it back to
|
|
76
|
+
* the exact value (verified against both parsers).
|
|
77
|
+
*/
|
|
78
|
+
function attr(value) {
|
|
79
|
+
const v = value.replace(/\r?\n/g, " ");
|
|
80
|
+
if (!/["\\{}<>&]/.test(v))
|
|
81
|
+
return `"${v}"`;
|
|
82
|
+
const json = JSON.stringify(v).replace(/[{}<>&"]/g, (ch) => (ch === '"' ? '"' : `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`));
|
|
83
|
+
// JSON.stringify keeps the outer quotes; inner quotes are already \"-escaped
|
|
84
|
+
return `{${json}}`;
|
|
85
|
+
}
|
|
86
|
+
/** Fence meta `title="…"` is never entity-decoded: drop what it cannot carry. */
|
|
87
|
+
function metaTitle(value) {
|
|
88
|
+
const clean = value.replace(/[{}"]/g, "").replace(/\r?\n/g, " ").trim();
|
|
89
|
+
return `title="${clean}"`;
|
|
90
|
+
}
|
|
91
|
+
/* ── Block dispatcher ──────────────────────────────────────────────────── */
|
|
92
|
+
function nodeToMdx(node, ctx) {
|
|
93
|
+
switch (node.type) {
|
|
94
|
+
case "frontmatter":
|
|
95
|
+
// the project layer writes page frontmatter from ExportPage; an inline
|
|
96
|
+
// frontmatter node inside a tree is source noise, not content
|
|
97
|
+
return "";
|
|
98
|
+
case "references":
|
|
99
|
+
return referencesToMdx(node, ctx);
|
|
100
|
+
case "heading":
|
|
101
|
+
return headingToMdx(node, ctx);
|
|
102
|
+
case "paragraph":
|
|
103
|
+
return noEsmStart(leaf(node, ctx));
|
|
104
|
+
case "prose":
|
|
105
|
+
return noEsmStart(proseToMdx(node, ctx));
|
|
106
|
+
case "callout":
|
|
107
|
+
return calloutToMdx(node, ctx);
|
|
108
|
+
case "details":
|
|
109
|
+
return detailsToMdx(node, ctx);
|
|
110
|
+
case "tabs":
|
|
111
|
+
return tabsToMdx(node, ctx);
|
|
112
|
+
case "code":
|
|
113
|
+
return codeToMdx(node, ctx);
|
|
114
|
+
case "diagram":
|
|
115
|
+
return diagramToMdx(node);
|
|
116
|
+
case "math-block":
|
|
117
|
+
return `$$\n${String(node.props?.latex ?? "")}\n$$`;
|
|
118
|
+
case "table":
|
|
119
|
+
return tableToMdx(node, ctx);
|
|
120
|
+
case "blockquote":
|
|
121
|
+
return prefixLines(children(node, ctx), "> ");
|
|
122
|
+
case "ordered-list":
|
|
123
|
+
return orderedListToMdx(node, ctx);
|
|
124
|
+
case "unordered-list":
|
|
125
|
+
return unorderedListToMdx(node, ctx);
|
|
126
|
+
case "list-item":
|
|
127
|
+
return listItemToMdx(node, ctx);
|
|
128
|
+
case "deflist":
|
|
129
|
+
return deflistToMdx(node, ctx);
|
|
130
|
+
case "dt":
|
|
131
|
+
return `**${leaf(node, ctx)}**`;
|
|
132
|
+
case "dd":
|
|
133
|
+
return leaf(node, ctx);
|
|
134
|
+
case "steps":
|
|
135
|
+
return stepsToMdx(node, ctx);
|
|
136
|
+
case "step":
|
|
137
|
+
return children(node, ctx);
|
|
138
|
+
case "substeps":
|
|
139
|
+
return children(node, ctx);
|
|
140
|
+
case "annotation-list":
|
|
141
|
+
return children(node, ctx);
|
|
142
|
+
case "link-button": {
|
|
143
|
+
// navigational: a plain link feeds their graph/backlinks (§3.3)
|
|
144
|
+
const href = ctx.href(String(node.props?.href ?? ""));
|
|
145
|
+
const text = children(node, ctx) || leaf(node, ctx) || href;
|
|
146
|
+
ctx.ledger.record({ feature: "link-button", page: ctx.page, action: "degraded", detail: "rendered as a plain markdown link" });
|
|
147
|
+
return `[${text}](${href})`;
|
|
148
|
+
}
|
|
149
|
+
case "hr":
|
|
150
|
+
return "---";
|
|
151
|
+
case "html":
|
|
152
|
+
ctx.ledger.record({ feature: "html", page: ctx.page, action: "degraded", detail: "raw HTML converted to markdown (MDX cannot take unbalanced HTML)" });
|
|
153
|
+
return htmlToMdx(node.html ?? "");
|
|
154
|
+
case "html-container":
|
|
155
|
+
return children(node, ctx);
|
|
156
|
+
case "figure":
|
|
157
|
+
return figureToMdx(node, ctx);
|
|
158
|
+
case "card":
|
|
159
|
+
return cardToMdx(node, ctx);
|
|
160
|
+
case "cards":
|
|
161
|
+
case "grid":
|
|
162
|
+
return cardsToMdx(node, ctx);
|
|
163
|
+
case "youtube":
|
|
164
|
+
return youtubeToMdx(node);
|
|
165
|
+
case "api-class":
|
|
166
|
+
case "api-member":
|
|
167
|
+
return apiSymbolToMdx(node, ctx);
|
|
168
|
+
case "api-doc":
|
|
169
|
+
return apiDocToMdx(node, ctx);
|
|
170
|
+
case "api-params":
|
|
171
|
+
return apiParamsToMdx(node, ctx);
|
|
172
|
+
case "api-source":
|
|
173
|
+
return apiSourceToMdx(node, ctx);
|
|
174
|
+
case "footnote-list":
|
|
175
|
+
return (node.children ?? []).map((c) => nodeToMdx(c, ctx)).join("\n");
|
|
176
|
+
case "footnote-def": {
|
|
177
|
+
const label = String(node.props?.label ?? "1");
|
|
178
|
+
const body = node.inline?.length ? inlineToMdx(node.inline, ctx) : children(node, ctx);
|
|
179
|
+
const [first, ...rest] = body.split("\n");
|
|
180
|
+
return rest.length ? `[^${label}]: ${first}\n${rest.map((l) => (l ? ` ${l}` : l)).join("\n")}` : `[^${label}]: ${first}`;
|
|
181
|
+
}
|
|
182
|
+
case "mdx-raw":
|
|
183
|
+
// an importer's never-drop raw JSX/MDX: OpenKnowledge's wildcard renders it as an editable raw-MDX block
|
|
184
|
+
ctx.ledger.record({ feature: "mdx-raw", page: ctx.page, action: "degraded", detail: `raw MDX <${String(node.props?.component ?? "…")}> passed through to the wildcard` });
|
|
185
|
+
return String(node.props?.source ?? "");
|
|
186
|
+
case "accordion-group":
|
|
187
|
+
return children(node, ctx);
|
|
188
|
+
case "related": {
|
|
189
|
+
const title = String(node.props?.title ?? "").trim();
|
|
190
|
+
const body = children(node, ctx);
|
|
191
|
+
if (!title)
|
|
192
|
+
return body;
|
|
193
|
+
return `**${escapeMdxText(title.replace(/([*\\])/g, "\\$1"))}**\n\n${body}`;
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
ctx.ledger.record({ feature: node.type, page: ctx.page, action: node.children || node.html || node.inline ? "degraded" : "dropped", detail: `no OpenKnowledge mapping for "${node.type}"; children emitted` });
|
|
197
|
+
if (node.html)
|
|
198
|
+
return htmlToMdx(node.html);
|
|
199
|
+
if (node.children)
|
|
200
|
+
return children(node, ctx);
|
|
201
|
+
if (node.inline)
|
|
202
|
+
return inlineToMdx(node.inline, ctx);
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/* ── Inline ─────────────────────────────────────────────────────────────── */
|
|
207
|
+
const inlineEmitters = (ctx) => ({
|
|
208
|
+
text: (node) => applyTextFlags(escapeMdxText(node.text), node, {
|
|
209
|
+
bold: (s) => `**${s}**`,
|
|
210
|
+
italic: (s) => `*${s}*`,
|
|
211
|
+
strike: (s) => `~~${s}~~`,
|
|
212
|
+
}),
|
|
213
|
+
link: (node, c) => {
|
|
214
|
+
const title = node.title ? ` "${node.title.replace(/"/g, '\\"')}"` : "";
|
|
215
|
+
return `[${c.recurse(node.children)}](${urlSafe(ctx.href(node.href))}${title})`;
|
|
216
|
+
},
|
|
217
|
+
image: (node) => `![${(node.alt ?? "").replace(/[[\]]/g, "\\$&")}](${urlSafe(node.src)})`,
|
|
218
|
+
code: (node) => codeSpan(node.text),
|
|
219
|
+
highlight: (node, c) => `==${c.recurse(node.children)}==`,
|
|
220
|
+
"footnote-ref": (node) => `[^${node.label}]`,
|
|
221
|
+
// OpenKnowledge shows a literal `<kbd>` (lowercase tags other than img/video/audio/mark are sentinelled): keys as code spans
|
|
222
|
+
kbd: (node) => {
|
|
223
|
+
ctx.ledger.record({ feature: "kbd", page: ctx.page, action: "degraded", detail: "keys rendered as code spans (no <kbd> in OpenKnowledge)" });
|
|
224
|
+
return node.keys.map((k) => codeSpan(k)).join("+");
|
|
225
|
+
},
|
|
226
|
+
math: (node) => `$${node.latex}$`,
|
|
227
|
+
icon: (node) => {
|
|
228
|
+
ctx.ledger.record({ feature: "icon", page: ctx.page, action: "degraded", detail: `icon ":${node.name}:" rendered as text` });
|
|
229
|
+
return escapeMdxText(`:${node.name}:`);
|
|
230
|
+
},
|
|
231
|
+
"html-inline": (node) => inlineHtmlToMdx(node.html ?? ""),
|
|
232
|
+
break: () => " \n",
|
|
233
|
+
});
|
|
234
|
+
function inlineToMdx(nodes, ctx) {
|
|
235
|
+
return walkInline(pairInlineHtml(nodes, ctx), { emitters: inlineEmitters(ctx) });
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* markdown-it hands an inline `<abbr title="…">ORM</abbr>` over as THREE
|
|
239
|
+
* nodes — html-inline open, text, html-inline close — and stripping each
|
|
240
|
+
* tag alone loses the semantics (43 <abbr> + 50 <dfn> on FastAPI). Pair
|
|
241
|
+
* open/close across the array and rewrite the span: <a href> → link,
|
|
242
|
+
* <b>/<strong> → bold, <i>/<em> → italic, <code> → code span, <abbr>/<dfn
|
|
243
|
+
* title> → "text (title)", <s>/<del> → strike, other known tags → unwrapped.
|
|
244
|
+
* OpenKnowledge shows lowercase tags other than img/video/audio/mark as
|
|
245
|
+
* literal text, so JSX passthrough is not an option here.
|
|
246
|
+
*/
|
|
247
|
+
function pairInlineHtml(nodes, ctx) {
|
|
248
|
+
const out = [];
|
|
249
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
250
|
+
const n = nodes[i];
|
|
251
|
+
if (n.type !== "html-inline") {
|
|
252
|
+
out.push(n);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const open = /^<([a-z][a-z0-9]*)(\s[^>]*)?>$/i.exec((n.html ?? "").trim());
|
|
256
|
+
if (!open) {
|
|
257
|
+
out.push(n);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const tag = open[1].toLowerCase();
|
|
261
|
+
if (!PAIRABLE.has(tag)) {
|
|
262
|
+
out.push(n);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// find the matching close at the same depth
|
|
266
|
+
let depth = 0;
|
|
267
|
+
let close = -1;
|
|
268
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
269
|
+
const m = nodes[j];
|
|
270
|
+
if (m.type !== "html-inline")
|
|
271
|
+
continue;
|
|
272
|
+
const h = (m.html ?? "").trim();
|
|
273
|
+
if (new RegExp(`^<${tag}(\\s[^>]*)?>$`, "i").test(h))
|
|
274
|
+
depth++;
|
|
275
|
+
else if (new RegExp(`^</${tag}\\s*>$`, "i").test(h)) {
|
|
276
|
+
if (depth === 0) {
|
|
277
|
+
close = j;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
depth--;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (close < 0) {
|
|
284
|
+
out.push(n);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const inner = pairInlineHtml(nodes.slice(i + 1, close), ctx);
|
|
288
|
+
const attrs = open[2] ?? "";
|
|
289
|
+
const attrOf = (name) => new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i").exec(attrs)?.slice(1).find((v) => v !== undefined);
|
|
290
|
+
const flag = (f) => inner.map((x) => (x.type === "text" || x.type === "code" ? { ...x, [f]: true } : x));
|
|
291
|
+
switch (tag) {
|
|
292
|
+
case "a": {
|
|
293
|
+
const href = attrOf("href") ?? "";
|
|
294
|
+
out.push({ type: "link", href, children: inner });
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case "b":
|
|
298
|
+
case "strong":
|
|
299
|
+
out.push(...flag("bold"));
|
|
300
|
+
break;
|
|
301
|
+
case "i":
|
|
302
|
+
case "em":
|
|
303
|
+
out.push(...flag("italic"));
|
|
304
|
+
break;
|
|
305
|
+
case "s":
|
|
306
|
+
case "del":
|
|
307
|
+
case "strike":
|
|
308
|
+
out.push(...flag("strikethrough"));
|
|
309
|
+
break;
|
|
310
|
+
case "code":
|
|
311
|
+
case "kbd":
|
|
312
|
+
case "samp":
|
|
313
|
+
case "tt":
|
|
314
|
+
out.push({ type: "code", text: inner.map((x) => inlinePlainText(x, (t) => t)).join("") });
|
|
315
|
+
break;
|
|
316
|
+
case "abbr":
|
|
317
|
+
case "dfn": {
|
|
318
|
+
const title = attrOf("title");
|
|
319
|
+
out.push(...inner);
|
|
320
|
+
if (title) {
|
|
321
|
+
ctx.ledger.record({ feature: "html-inline", page: ctx.page, action: "degraded", detail: `<${tag} title> expanded inline as "text (title)"` });
|
|
322
|
+
out.push({ type: "text", text: ` (${title})` });
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case "mark":
|
|
327
|
+
out.push({ type: "highlight", children: inner });
|
|
328
|
+
break;
|
|
329
|
+
default:
|
|
330
|
+
ctx.ledger.record({ feature: "html-inline", page: ctx.page, action: "degraded", detail: `<${tag}> unwrapped (no OpenKnowledge inline equivalent)` });
|
|
331
|
+
out.push(...inner);
|
|
332
|
+
}
|
|
333
|
+
i = close;
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
const PAIRABLE = new Set(["a", "b", "strong", "i", "em", "s", "del", "strike", "code", "kbd", "samp", "tt", "abbr", "dfn", "mark", "sup", "sub", "span", "u", "small", "cite", "var", "q"]);
|
|
338
|
+
/** A URL in `[](…)` may not contain spaces or unbalanced parens: percent-encode them. */
|
|
339
|
+
function urlSafe(url) {
|
|
340
|
+
return url.replace(/ /g, "%20").replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Inline HTML: convert the unambiguous tags to markdown, strip the rest
|
|
344
|
+
* (MDX would choke on `<br>`). A lone tag-shaped placeholder that is not
|
|
345
|
+
* a known HTML element (`<cluster-name>`, `<your-token>`) is PROSE the
|
|
346
|
+
* writer meant literally — keep it, escaped, instead of stripping it.
|
|
347
|
+
*/
|
|
348
|
+
function inlineHtmlToMdx(html) {
|
|
349
|
+
const placeholder = /^<([a-z][a-z0-9-]*)>$/i.exec(html.trim());
|
|
350
|
+
if (placeholder && !KNOWN_HTML.has(placeholder[1].toLowerCase()))
|
|
351
|
+
return escapeMdxText(html.trim());
|
|
352
|
+
return escapeMdxText(stripHtml(inlineHtmlToMarkdown(html)));
|
|
353
|
+
}
|
|
354
|
+
const KNOWN_HTML = new Set(["a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "del", "dfn", "em", "i", "img", "ins", "kbd", "mark", "q", "s", "samp", "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "p", "div", "pre", "ul", "ol", "li", "table", "tr", "td", "th", "thead", "tbody", "hr", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "details", "summary", "figure", "figcaption", "video", "audio", "source", "iframe", "button", "input", "label", "select", "option", "textarea", "form", "nav", "section", "article", "aside", "header", "footer", "main", "dl", "dt", "dd"]);
|
|
355
|
+
function htmlToMdx(html) {
|
|
356
|
+
const cleaned = html
|
|
357
|
+
.replace(/<(style|script)\b[\s\S]*?<\/\1\s*>/gi, "")
|
|
358
|
+
// block boundaries become whitespace so `</td><td>` cannot glue words
|
|
359
|
+
.replace(/<\/(td|th)\s*>\s*<(td|th)\b/gi, "</$1> | <$2")
|
|
360
|
+
.replace(/<\/(tr|p|div|li|h[1-6]|blockquote|section|article|dd|dt)\s*>/gi, "$&\n")
|
|
361
|
+
.replace(/<br\s*\/?>/gi, "\n");
|
|
362
|
+
const text = escapeMdxText(stripHtml(inlineHtmlToMarkdown(cleaned)).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim());
|
|
363
|
+
// converted HTML yields several paragraphs; each may not start with import/export (ESM in strict MDX)
|
|
364
|
+
return text.split(/\n\n/).map(noEsmStart).join("\n\n");
|
|
365
|
+
}
|
|
366
|
+
function proseToMdx(node, ctx) {
|
|
367
|
+
if (node.inline)
|
|
368
|
+
return inlineToMdx(node.inline, ctx);
|
|
369
|
+
if (node.html)
|
|
370
|
+
return htmlToMdx(node.html);
|
|
371
|
+
return "";
|
|
372
|
+
}
|
|
373
|
+
/* ── Callouts / details / tabs (§3.1) ──────────────────────────────────── */
|
|
374
|
+
function calloutToMdx(node, ctx) {
|
|
375
|
+
const variant = String(node.props?.variant ?? node.props?.type ?? "note").toLowerCase();
|
|
376
|
+
const alert = VARIANT_TO_ALERT[variant] ?? "note";
|
|
377
|
+
if (alert !== variant)
|
|
378
|
+
ctx.ledger.record({ feature: "callout", page: ctx.page, action: "degraded", detail: `variant "${variant}" mapped to "${alert}" (GFM alert enum)` });
|
|
379
|
+
const title = node.props?.title;
|
|
380
|
+
const autoTitle = variant.charAt(0).toUpperCase() + variant.slice(1);
|
|
381
|
+
const inner = children(node, ctx);
|
|
382
|
+
if (title && title !== autoTitle) {
|
|
383
|
+
// a custom title needs the component (GFM alerts have no title slot); a JSX string is not markdown
|
|
384
|
+
return `<Callout type=${attr(alert)} title=${attr(plainTitle(title))}>\n\n${inner}\n\n</Callout>`;
|
|
385
|
+
}
|
|
386
|
+
const opener = `> [!${alert.toUpperCase()}]`;
|
|
387
|
+
if (!inner)
|
|
388
|
+
return opener;
|
|
389
|
+
return `${opener}\n${prefixLines(inner, "> ")}`;
|
|
390
|
+
}
|
|
391
|
+
/** Markdown emphasis/code markers have no meaning inside a JSX string attribute. */
|
|
392
|
+
function plainTitle(title) {
|
|
393
|
+
return title.replace(/\*\*|__|`|~~/g, "").replace(/(^|\s)[*_](\S)/g, "$1$2").replace(/(\S)[*_](\s|$)/g, "$1$2").trim();
|
|
394
|
+
}
|
|
395
|
+
function detailsToMdx(node, ctx) {
|
|
396
|
+
const title = plainTitle(String(node.props?.title ?? "Details"));
|
|
397
|
+
const open = node.props?.open === true || node.props?.open === "" || node.props?.open === "true";
|
|
398
|
+
const variant = node.props?.variant ?? node.props?.type;
|
|
399
|
+
if (variant && variant !== "note")
|
|
400
|
+
ctx.ledger.record({ feature: "details", page: ctx.page, action: "degraded", detail: `fold variant "${String(variant)}" not representable (Accordion has no variant)` });
|
|
401
|
+
const inner = children(node, ctx);
|
|
402
|
+
return `<Accordion title=${attr(title)}${open ? " defaultOpen" : ""}>\n\n${inner}\n\n</Accordion>`;
|
|
403
|
+
}
|
|
404
|
+
function tabsToMdx(node, ctx) {
|
|
405
|
+
const tabs = node.children ?? [];
|
|
406
|
+
const inner = ctx.depth++;
|
|
407
|
+
const body = tabs
|
|
408
|
+
.map((tab) => {
|
|
409
|
+
const label = String(tab.props?.title || tab.props?.label || tab.props?.value || "Tab");
|
|
410
|
+
const content = children(tab, ctx);
|
|
411
|
+
return `<Tab label=${attr(label)}>\n\n${content}\n\n</Tab>`;
|
|
412
|
+
})
|
|
413
|
+
.join("\n\n");
|
|
414
|
+
ctx.depth = inner;
|
|
415
|
+
return `<Tabs>\n\n${body}\n\n</Tabs>`;
|
|
416
|
+
}
|
|
417
|
+
/* ── Code (§2.3: plain fences, never an envelope) ───────────────────────── */
|
|
418
|
+
function codeToMdx(node, ctx) {
|
|
419
|
+
const lang = String(node.props?.lang || node.props?.language || "");
|
|
420
|
+
const code = String(node.props?.code ?? node.html ?? "").replace(/\n$/, "");
|
|
421
|
+
const title = node.props?.title;
|
|
422
|
+
const highlights = node.props?.highlights;
|
|
423
|
+
const annotations = node.props?.annotations;
|
|
424
|
+
const fence = fenceFor(code, ctx);
|
|
425
|
+
const meta = [];
|
|
426
|
+
if (title)
|
|
427
|
+
meta.push(metaTitle(title));
|
|
428
|
+
if (highlights) {
|
|
429
|
+
ctx.ledger.record({ feature: "code", page: ctx.page, action: "degraded", detail: `line highlights "${highlights}" have no OpenKnowledge affordance (block kept)` });
|
|
430
|
+
}
|
|
431
|
+
if (annotations && Object.keys(annotations).length) {
|
|
432
|
+
ctx.ledger.record({ feature: "code", page: ctx.page, action: "degraded", detail: `${Object.keys(annotations).length} code annotation(s) have no OpenKnowledge affordance (block kept)` });
|
|
433
|
+
}
|
|
434
|
+
const info = [lang, ...meta].filter(Boolean).join(" ");
|
|
435
|
+
return `${fence}${info}\n${code}\n${fence}`;
|
|
436
|
+
}
|
|
437
|
+
/** Fence for a code body: longest-run-safe. JSX containers are not fences, so no alternation is needed inside `<Tabs>`. */
|
|
438
|
+
function fenceFor(code, _ctx) {
|
|
439
|
+
return pickCodeFence(code);
|
|
440
|
+
}
|
|
441
|
+
function diagramToMdx(node) {
|
|
442
|
+
const lang = String(node.props?.lang ?? "mermaid");
|
|
443
|
+
const code = String(node.props?.code ?? "").replace(/\n$/, "");
|
|
444
|
+
const f = pickCodeFence(code);
|
|
445
|
+
return `${f}${lang}\n${code}\n${f}`;
|
|
446
|
+
}
|
|
447
|
+
/* ── Tables (GFM) ───────────────────────────────────────────────────────── */
|
|
448
|
+
function tableToMdx(node, ctx) {
|
|
449
|
+
const rows = [];
|
|
450
|
+
let headerCount = 0;
|
|
451
|
+
const cell = (c) => (children(c, ctx) || proseToMdx(c, ctx)).replace(/\n+/g, " ").replace(/\s+/g, " ").replace(/\|/g, "\\|").trim();
|
|
452
|
+
for (const child of node.children ?? []) {
|
|
453
|
+
if ((child.type === "thead" || child.type === "tbody") && child.children) {
|
|
454
|
+
for (const tr of child.children) {
|
|
455
|
+
if (tr.type !== "tr" || !tr.children)
|
|
456
|
+
continue;
|
|
457
|
+
const cells = tr.children.map(cell);
|
|
458
|
+
rows.push(cells);
|
|
459
|
+
if (child.type === "thead")
|
|
460
|
+
headerCount = cells.length;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else if (child.type === "tr" && child.children) {
|
|
464
|
+
const cells = child.children.map(cell);
|
|
465
|
+
rows.push(cells);
|
|
466
|
+
if (headerCount === 0 && child.children.some((c) => c.type === "th"))
|
|
467
|
+
headerCount = cells.length;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!rows.length)
|
|
471
|
+
return "";
|
|
472
|
+
const colCount = Math.max(headerCount, ...rows.map((r) => r.length));
|
|
473
|
+
const widths = Array(colCount).fill(3);
|
|
474
|
+
for (const row of rows)
|
|
475
|
+
row.forEach((c, i) => (widths[i] = Math.max(widths[i], c.length)));
|
|
476
|
+
const lines = [];
|
|
477
|
+
rows.forEach((row, r) => {
|
|
478
|
+
const padded = row.map((c, i) => c.padEnd(widths[i]));
|
|
479
|
+
while (padded.length < colCount)
|
|
480
|
+
padded.push(" ".repeat(widths[padded.length]));
|
|
481
|
+
lines.push(`| ${padded.join(" | ")} |`);
|
|
482
|
+
if (r === 0)
|
|
483
|
+
lines.push(`| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`);
|
|
484
|
+
});
|
|
485
|
+
const caption = node.props?.caption;
|
|
486
|
+
if (caption) {
|
|
487
|
+
ctx.ledger.record({ feature: "table", page: ctx.page, action: "degraded", detail: "caption rendered as italic text below the table" });
|
|
488
|
+
lines.push("", `*${escapeMdxText(caption)}*`);
|
|
489
|
+
}
|
|
490
|
+
return lines.join("\n");
|
|
491
|
+
}
|
|
492
|
+
/* ── Lists ──────────────────────────────────────────────────────────────── */
|
|
493
|
+
function orderedListToMdx(node, ctx) {
|
|
494
|
+
const start = node.props?.start || 1;
|
|
495
|
+
return (node.children ?? [])
|
|
496
|
+
.map((child, i) => {
|
|
497
|
+
const marker = `${start + i}. `;
|
|
498
|
+
const text = nodeToMdx(child, ctx);
|
|
499
|
+
const [first, ...rest] = text.split("\n");
|
|
500
|
+
const pad = " ".repeat(marker.length);
|
|
501
|
+
return rest.length ? `${marker}${first}\n${rest.map((l) => (l ? pad + l : l)).join("\n")}` : `${marker}${first}`;
|
|
502
|
+
})
|
|
503
|
+
.join("\n");
|
|
504
|
+
}
|
|
505
|
+
function unorderedListToMdx(node, ctx) {
|
|
506
|
+
return (node.children ?? [])
|
|
507
|
+
.map((child) => {
|
|
508
|
+
const text = nodeToMdx(child, ctx);
|
|
509
|
+
const [first, ...rest] = text.split("\n");
|
|
510
|
+
return rest.length ? `- ${first}\n${rest.map((l) => (l ? ` ${l}` : l)).join("\n")}` : `- ${first}`;
|
|
511
|
+
})
|
|
512
|
+
.join("\n");
|
|
513
|
+
}
|
|
514
|
+
function listItemToMdx(node, ctx) {
|
|
515
|
+
const parts = [];
|
|
516
|
+
if (node.inline?.length)
|
|
517
|
+
parts.push(inlineToMdx(node.inline, ctx));
|
|
518
|
+
if (node.children)
|
|
519
|
+
parts.push(...node.children.map((c) => nodeToMdx(c, ctx)).filter(Boolean));
|
|
520
|
+
return parts.filter(Boolean).join("\n\n");
|
|
521
|
+
}
|
|
522
|
+
/* ── Definition lists: readable markdown, never a code block ────────────── */
|
|
523
|
+
/**
|
|
524
|
+
* `- **term**` with the definition body nested under it. OpenKnowledge has no
|
|
525
|
+
* `<dl>` component and no `: definition` syntax; a JSX wildcard block would
|
|
526
|
+
* turn PROSE into an opaque raw-MDX box for the corpus's most common
|
|
527
|
+
* construct. A term list keeps it readable, editable and re-importable
|
|
528
|
+
* (as a list — declared `partial`).
|
|
529
|
+
*/
|
|
530
|
+
function deflistToMdx(node, ctx) {
|
|
531
|
+
ctx.ledger.record({ feature: "deflist", page: ctx.page, action: "degraded", detail: "definition list rendered as a bulleted term list (no <dl> in OpenKnowledge)" });
|
|
532
|
+
const items = [];
|
|
533
|
+
let current = null;
|
|
534
|
+
for (const child of node.children ?? []) {
|
|
535
|
+
if (child.type === "dt") {
|
|
536
|
+
if (current)
|
|
537
|
+
items.push(renderTerm(current));
|
|
538
|
+
current = { term: leaf(child, ctx).trim(), defs: [] };
|
|
539
|
+
}
|
|
540
|
+
else if (child.type === "dd") {
|
|
541
|
+
const body = (children(child, ctx) || leaf(child, ctx)).trim();
|
|
542
|
+
if (!current)
|
|
543
|
+
current = { term: "", defs: [] };
|
|
544
|
+
if (body)
|
|
545
|
+
current.defs.push(body);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const other = nodeToMdx(child, ctx).trim();
|
|
549
|
+
if (!other)
|
|
550
|
+
continue;
|
|
551
|
+
if (!current)
|
|
552
|
+
current = { term: "", defs: [] };
|
|
553
|
+
current.defs.push(other);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (current)
|
|
557
|
+
items.push(renderTerm(current));
|
|
558
|
+
return items.join("\n");
|
|
559
|
+
}
|
|
560
|
+
function renderTerm(t) {
|
|
561
|
+
const head = t.term ? `- **${t.term}**` : "-";
|
|
562
|
+
if (!t.defs.length)
|
|
563
|
+
return head;
|
|
564
|
+
return `${head}\n\n${indent(t.defs.join("\n\n"), 2)}`;
|
|
565
|
+
}
|
|
566
|
+
/* ── Steps / cards (wildcard JSX, Fumadocs vocabulary) ─────────────────── */
|
|
567
|
+
function stepsToMdx(node, ctx) {
|
|
568
|
+
ctx.ledger.record({ feature: "steps", page: ctx.page, action: "degraded", detail: "<Steps> lands on OpenKnowledge's wildcard (editable raw MDX, not rendered)" });
|
|
569
|
+
const inner = ctx.depth++;
|
|
570
|
+
const body = (node.children ?? [])
|
|
571
|
+
.map((step) => {
|
|
572
|
+
const title = (step.inline?.length ? step.inline.map((n) => inlinePlainText(n, (t) => t)).join("") : "") || String(step.props?.title ?? "");
|
|
573
|
+
const kids = step.children ?? [];
|
|
574
|
+
const firstText = kids[0] ? headingText(kids[0]).trim() : "";
|
|
575
|
+
const body = title && firstText && firstText === title.trim() ? kids.slice(1) : kids;
|
|
576
|
+
const content = body.map((c) => nodeToMdx(c, ctx)).filter(Boolean).join("\n\n");
|
|
577
|
+
const open = title ? `<Step title=${attr(title)}>` : "<Step>";
|
|
578
|
+
return content ? `${open}\n\n${content}\n\n</Step>` : `${open}</Step>`;
|
|
579
|
+
})
|
|
580
|
+
.join("\n\n");
|
|
581
|
+
ctx.depth = inner;
|
|
582
|
+
return `<Steps>\n\n${body}\n\n</Steps>`;
|
|
583
|
+
}
|
|
584
|
+
function cardToMdx(node, ctx) {
|
|
585
|
+
const title = String(node.props?.title ?? "");
|
|
586
|
+
const href = node.props?.href;
|
|
587
|
+
const icon = node.props?.icon;
|
|
588
|
+
const content = children(node, ctx);
|
|
589
|
+
const props = [title ? `title=${attr(title)}` : "", href ? `href=${attr(ctx.href(href))}` : "", icon ? `icon=${attr(icon)}` : ""].filter(Boolean).join(" ");
|
|
590
|
+
const open = props ? `<Card ${props}>` : "<Card>";
|
|
591
|
+
return content ? `${open}\n\n${content}\n\n</Card>` : `${open}</Card>`;
|
|
592
|
+
}
|
|
593
|
+
function cardsToMdx(node, ctx) {
|
|
594
|
+
ctx.ledger.record({ feature: node.type, page: ctx.page, action: "degraded", detail: "<Cards> lands on OpenKnowledge's wildcard (editable raw MDX, not rendered)" });
|
|
595
|
+
const inner = ctx.depth++;
|
|
596
|
+
const body = (node.children ?? []).map((c) => nodeToMdx(c, ctx)).filter(Boolean).join("\n\n");
|
|
597
|
+
ctx.depth = inner;
|
|
598
|
+
return `<Cards>\n\n${body}\n\n</Cards>`;
|
|
599
|
+
}
|
|
600
|
+
/* ── Media ──────────────────────────────────────────────────────────────── */
|
|
601
|
+
function figureToMdx(node, ctx) {
|
|
602
|
+
const caption = node.props?.caption;
|
|
603
|
+
const findImage = (n) => {
|
|
604
|
+
const hit = (n.inline ?? []).find((i) => i.type === "image");
|
|
605
|
+
if (hit)
|
|
606
|
+
return hit;
|
|
607
|
+
for (const c of n.children ?? []) {
|
|
608
|
+
const h = findImage(c);
|
|
609
|
+
if (h)
|
|
610
|
+
return h;
|
|
611
|
+
}
|
|
612
|
+
if (n.html) {
|
|
613
|
+
const m = /<img\b[^>]*\bsrc="([^"]+)"[^>]*(?:\balt="([^"]*)")?/i.exec(n.html);
|
|
614
|
+
if (m)
|
|
615
|
+
return { src: m[1], alt: m[2] ?? "" };
|
|
616
|
+
}
|
|
617
|
+
return undefined;
|
|
618
|
+
};
|
|
619
|
+
const img = findImage(node);
|
|
620
|
+
if (img?.src && caption) {
|
|
621
|
+
return `<Image src=${attr(img.src)} alt=${attr(img.alt ?? "")} caption=${attr(caption)} />`;
|
|
622
|
+
}
|
|
623
|
+
const inner = children(node, ctx);
|
|
624
|
+
return caption ? `${inner}\n\n*${escapeMdxText(caption)}*` : inner;
|
|
625
|
+
}
|
|
626
|
+
function youtubeToMdx(node) {
|
|
627
|
+
const id = String(node.props?.id || node.props?.videoId || "");
|
|
628
|
+
return `<Video src=${attr(`https://www.youtube.com/watch?v=${id}`)} />`;
|
|
629
|
+
}
|
|
630
|
+
/* ── API nodes (same shape as the Obsidian target: markdown + an HTML-free table) ── */
|
|
631
|
+
function apiSymbolToMdx(node, ctx) {
|
|
632
|
+
const name = String(node.props?.name ?? "");
|
|
633
|
+
const kind = String(node.props?.kind ?? "class");
|
|
634
|
+
const level = node.type === "api-class" ? 3 : 4;
|
|
635
|
+
const sig = String(node.props?.signature ?? "");
|
|
636
|
+
const basesRaw = node.props?.bases;
|
|
637
|
+
const bases = Array.isArray(basesRaw) ? basesRaw.join(", ") : basesRaw;
|
|
638
|
+
const parts = [`${"#".repeat(level)} ${kind} ${codeSpan(name)}`];
|
|
639
|
+
if (bases)
|
|
640
|
+
parts.push(`Bases: ${codeSpan(bases)}`);
|
|
641
|
+
if (sig) {
|
|
642
|
+
const f = pickCodeFence(sig);
|
|
643
|
+
parts.push(`${f}python\n${sig}\n${f}`);
|
|
644
|
+
}
|
|
645
|
+
if (node.children)
|
|
646
|
+
parts.push(children(node, ctx));
|
|
647
|
+
return parts.filter(Boolean).join("\n\n");
|
|
648
|
+
}
|
|
649
|
+
function apiParamsToMdx(node, ctx) {
|
|
650
|
+
const params = (node.props?.params ?? []);
|
|
651
|
+
if (!params.length)
|
|
652
|
+
return "";
|
|
653
|
+
ctx.ledger.record({ feature: "api-params", page: ctx.page, action: "degraded", detail: "parameter table flattened to a GFM table (one line per cell)" });
|
|
654
|
+
const rows = params.map((p) => {
|
|
655
|
+
const meta = [p.type ? `type: \`${p.type}\`` : "", p.default != null ? `default: \`${String(p.default)}\`` : "", p.required ? "required" : ""].filter(Boolean).join(" · ");
|
|
656
|
+
const desc = [escapeMdxText(String(p.doc ?? p.description ?? "")).replace(/\s+/g, " ").trim(), meta].filter(Boolean).join(" — ");
|
|
657
|
+
return `| \`${p.name}\` | ${desc.replace(/\|/g, "\\|")} |`;
|
|
658
|
+
});
|
|
659
|
+
return ["| Parameter | Description |", "| --- | --- |", ...rows].join("\n");
|
|
660
|
+
}
|
|
661
|
+
function apiDocToMdx(node, ctx) {
|
|
662
|
+
const html = String(node.props?.html ?? "");
|
|
663
|
+
return html ? htmlToMdx(html) : children(node, ctx);
|
|
664
|
+
}
|
|
665
|
+
function apiSourceToMdx(node, ctx) {
|
|
666
|
+
const code = String(node.props?.code ?? "");
|
|
667
|
+
const file = String(node.props?.file ?? "");
|
|
668
|
+
const line = node.props?.line;
|
|
669
|
+
const parts = [];
|
|
670
|
+
if (file)
|
|
671
|
+
parts.push(`*Source: \`${line ? `${file}:${line}` : file}\`*`);
|
|
672
|
+
if (code) {
|
|
673
|
+
const body = code.replace(/\n$/, "");
|
|
674
|
+
const f = pickCodeFence(body);
|
|
675
|
+
parts.push(`${f}python\n${body}\n${f}`);
|
|
676
|
+
}
|
|
677
|
+
else if (node.children?.length)
|
|
678
|
+
parts.push(children(node, ctx));
|
|
679
|
+
return parts.join("\n\n");
|
|
680
|
+
}
|
|
681
|
+
/* ── Headings / references ──────────────────────────────────────────────── */
|
|
682
|
+
/**
|
|
683
|
+
* The headings a page WILL emit, in emit order, as OpenKnowledge will see
|
|
684
|
+
* their text — headings nested in callouts/tabs/lists included, and the
|
|
685
|
+
* `### class \`name\`` lines api-class/api-member emit (whose textContent
|
|
686
|
+
* is "class name"). Anchors must be computed from THIS, not from an
|
|
687
|
+
* importer's `page.headings` (top-level only, importer-defined text).
|
|
688
|
+
*/
|
|
689
|
+
export function collectEmittedHeadings(nodes) {
|
|
690
|
+
const out = [];
|
|
691
|
+
const walk = (n) => {
|
|
692
|
+
if (n.type === "heading") {
|
|
693
|
+
out.push({ slug: String(n.props?.slug ?? ""), text: headingText(n) });
|
|
694
|
+
}
|
|
695
|
+
else if (n.type === "api-class" || n.type === "api-member") {
|
|
696
|
+
const name = String(n.props?.name ?? "");
|
|
697
|
+
const kind = String(n.props?.kind ?? "class");
|
|
698
|
+
// the Dogsbay id an importer's TOC gives an api symbol is the slug of its bare NAME
|
|
699
|
+
// (mkdocs `Heading.text = name`); the emitted heading text is `kind name`
|
|
700
|
+
out.push({ slug: String(n.props?.slug || slugifyHeadingText(name)), text: `${kind} ${name}` });
|
|
701
|
+
}
|
|
702
|
+
// a `frontmatter` node has no headings; a `code` node's text is not a heading
|
|
703
|
+
if (n.children && n.type !== "code" && n.type !== "html")
|
|
704
|
+
n.children.forEach(walk);
|
|
705
|
+
};
|
|
706
|
+
nodes.forEach(walk);
|
|
707
|
+
return out;
|
|
708
|
+
}
|
|
709
|
+
function headingToMdx(node, ctx) {
|
|
710
|
+
const hashes = "#".repeat(headingLevel(node));
|
|
711
|
+
// three shapes: flat `inline` (dogsbay-md), `children:[prose{inline}]` (mkdocs/html importers), `props.text`
|
|
712
|
+
const text = node.inline?.length
|
|
713
|
+
? inlineToMdx(node.inline, ctx)
|
|
714
|
+
: node.children?.length
|
|
715
|
+
? node.children.map((c) => (c.inline?.length ? inlineToMdx(c.inline, ctx) : c.html ? htmlToMdx(c.html) : "")).join("").replace(/\n+/g, " ").trim() || escapeMdxText(headingText(node))
|
|
716
|
+
: escapeMdxText(headingText(node));
|
|
717
|
+
// NO `^anchor`, no `{#id}`: OpenKnowledge computes anchors from the text
|
|
718
|
+
return `${hashes} ${text}`;
|
|
719
|
+
}
|
|
720
|
+
function referencesToMdx(node, ctx) {
|
|
721
|
+
const entries = (node.props?.entries ?? []);
|
|
722
|
+
return entries.map((e) => `[${e.label}]: ${ctx.href(e.href)}${e.title ? ` "${e.title}"` : ""}`).join("\n");
|
|
723
|
+
}
|
|
724
|
+
/* ── Utilities ──────────────────────────────────────────────────────────── */
|
|
725
|
+
function children(node, ctx) {
|
|
726
|
+
if (!node.children?.length)
|
|
727
|
+
return "";
|
|
728
|
+
return node.children.map((c) => nodeToMdx(c, ctx)).filter(Boolean).join("\n\n");
|
|
729
|
+
}
|
|
730
|
+
function leaf(node, ctx) {
|
|
731
|
+
return renderLeaf(node, {
|
|
732
|
+
inline: (nodes) => inlineToMdx(nodes, ctx),
|
|
733
|
+
children: (nodes) => nodes.map((c) => nodeToMdx(c, ctx)).filter(Boolean).join("\n\n"),
|
|
734
|
+
html: htmlToMdx,
|
|
735
|
+
join: "\n\n",
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=serialize.js.map
|