@dogsbay/format-docusaurus 0.2.0-beta.78

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.
@@ -0,0 +1,765 @@
1
+ /**
2
+ * Generic Docusaurus MDX parser.
3
+ *
4
+ * Reads a standard Docusaurus `.mdx`/`.md` body and produces TreeNode[].
5
+ * Handles only built-in Docusaurus syntax:
6
+ * - :::admonitions (note/tip/info/warning/danger/caution) with optional title
7
+ * - <Tabs> / <TabItem value label>
8
+ * - <details> / <summary>
9
+ * - <CodeBlock> and fenced code with metastrings (title/{ranges}/showLineNumbers)
10
+ * - <DocCardList> / <DocCard> (theme built-ins → placeholder + warning)
11
+ * - import/export MDX statements (stripped)
12
+ *
13
+ * Site-specific components, variable substitution, and partial includes are
14
+ * the job of an injected `DocusaurusAdapter` — never this file.
15
+ */
16
+ import { posix } from "node:path";
17
+ import MarkdownIt from "markdown-it";
18
+ import mdxJsx, { normalizeJsxLines } from "@dogsbay/markdown-it-mdx-jsx";
19
+ import { extractFrontmatter } from "./frontmatter.js";
20
+ import { parseCodeMeta } from "./code-meta.js";
21
+ // ── Standard Docusaurus components ──────────────────────
22
+ const DOCUSAURUS_COMPONENTS = {
23
+ Tabs: { type: "tabs", container: true },
24
+ TabItem: {
25
+ type: "tab",
26
+ container: true,
27
+ propsFromAttrs: (attrs) => ({ title: attrs.label || attrs.value || "" }),
28
+ },
29
+ // Docusaurus admonitions are converted to <Admonition> before parsing.
30
+ Admonition: {
31
+ type: "callout",
32
+ container: true,
33
+ propsFromAttrs: (attrs) => ({
34
+ variant: mapAdmonitionType(attrs.type || "note"),
35
+ title: attrs.title || undefined,
36
+ }),
37
+ },
38
+ // Native collapsible. <details>/<summary> are lowercase HTML tags, which the
39
+ // mdx-jsx plugin (uppercase-only) ignores, so they are rewritten to these
40
+ // capitalized component names before parsing (see rewriteNativeHtml).
41
+ Details: { type: "details", container: true },
42
+ Summary: { type: "summary", container: true },
43
+ // Docusaurus theme code component.
44
+ CodeBlock: {
45
+ type: "code",
46
+ container: true,
47
+ propsFromAttrs: (attrs) => ({
48
+ lang: attrs.language || "plaintext",
49
+ title: attrs.title,
50
+ }),
51
+ },
52
+ // Theme card components — auto-generated from the sidebar at runtime, so we
53
+ // can't reproduce them statically. Emit a placeholder and warn (see D2).
54
+ DocCardList: { type: "__doc-card-list", container: true },
55
+ DocCard: { type: "__doc-card-list", container: true },
56
+ };
57
+ const CORE_SELF_CLOSING = ["DocCardList", "DocCard"];
58
+ /** Map a Docusaurus admonition type to a Dogsbay callout variant. */
59
+ function mapAdmonitionType(type) {
60
+ const map = {
61
+ note: "note",
62
+ tip: "tip",
63
+ info: "info",
64
+ warning: "warning",
65
+ danger: "danger",
66
+ caution: "warning",
67
+ important: "warning",
68
+ };
69
+ return map[type.toLowerCase()] ?? "note";
70
+ }
71
+ function buildComponentMap(adapter) {
72
+ const map = { ...DOCUSAURUS_COMPONENTS };
73
+ const selfClosing = [...CORE_SELF_CLOSING];
74
+ if (adapter?.components) {
75
+ Object.assign(map, adapter.components);
76
+ }
77
+ for (const tag of adapter?.selfClosingTags ?? []) {
78
+ selfClosing.push(tag);
79
+ }
80
+ return { components: Object.keys(map), selfClosing, map };
81
+ }
82
+ // ── Pre-processing ──────────────────────────────────────
83
+ function applyInlineTransforms(source, adapter) {
84
+ if (!adapter?.inlineTransforms)
85
+ return source;
86
+ let result = source;
87
+ for (const [regex, replacement] of adapter.inlineTransforms) {
88
+ result = result.replace(regex, replacement);
89
+ }
90
+ return result;
91
+ }
92
+ /**
93
+ * Strip top-level `export ...` MDX statements (the mdx-jsx plugin already
94
+ * strips `import`). Fence-aware so `export` lines inside code blocks survive.
95
+ */
96
+ function stripExports(source) {
97
+ const lines = source.split("\n");
98
+ const out = [];
99
+ let inFence = false;
100
+ let fenceMarker = "";
101
+ for (const line of lines) {
102
+ const fence = line.match(/^(\s*)(```+|~~~+)/);
103
+ if (fence) {
104
+ const marker = fence[2];
105
+ if (!inFence) {
106
+ inFence = true;
107
+ fenceMarker = marker[0];
108
+ }
109
+ else if (marker[0] === fenceMarker) {
110
+ inFence = false;
111
+ }
112
+ out.push(line);
113
+ continue;
114
+ }
115
+ if (!inFence && /^export\s+(const|let|var|default|function|async|\{)/.test(line)) {
116
+ continue;
117
+ }
118
+ out.push(line);
119
+ }
120
+ return out.join("\n");
121
+ }
122
+ /**
123
+ * Rewrite native lowercase `<details>`/`<summary>` HTML tags to capitalized
124
+ * component tags so the uppercase-only mdx-jsx plugin tokenizes them. Fence-aware
125
+ * so literal `<details>` inside a code block is left intact.
126
+ */
127
+ function rewriteNativeHtml(source) {
128
+ const lines = source.split("\n");
129
+ const out = [];
130
+ let inFence = false;
131
+ let fenceMarker = "";
132
+ for (const line of lines) {
133
+ const fence = line.match(/^(\s*)(```+|~~~+)/);
134
+ if (fence) {
135
+ const marker = fence[2][0];
136
+ if (!inFence) {
137
+ inFence = true;
138
+ fenceMarker = marker;
139
+ }
140
+ else if (marker === fenceMarker) {
141
+ inFence = false;
142
+ }
143
+ out.push(line);
144
+ continue;
145
+ }
146
+ if (inFence) {
147
+ out.push(line);
148
+ continue;
149
+ }
150
+ out.push(line
151
+ .replace(/<details(\s[^>]*)?>/g, "<Details>")
152
+ .replace(/<\/details>/g, "</Details>")
153
+ .replace(/<summary(\s[^>]*)?>/g, "<Summary>")
154
+ .replace(/<\/summary>/g, "</Summary>"));
155
+ }
156
+ return out.join("\n");
157
+ }
158
+ /**
159
+ * Convert Docusaurus `:::type[Title]` / `:::type Title` admonition directives
160
+ * to `<Admonition type="..." title="...">` so the JSX pipeline handles them.
161
+ * Supports 3+ colons (nested admonitions use more colons).
162
+ */
163
+ function convertAdmonitions(source) {
164
+ const lines = source.split("\n");
165
+ const result = [];
166
+ const open = [];
167
+ for (const line of lines) {
168
+ const openMatch = line.match(/^(\s*)(:{3,})(note|tip|info|warning|danger|caution|important)(?:\[([^\]]*)\]|\s+(.+?))?\s*$/);
169
+ const closeMatch = line.match(/^(\s*)(:{3,})\s*$/);
170
+ if (openMatch) {
171
+ const indent = openMatch[1] || "";
172
+ const colons = openMatch[2];
173
+ const type = openMatch[3];
174
+ const title = (openMatch[4] ?? openMatch[5] ?? "").trim();
175
+ const titleAttr = title ? ` title="${title.replace(/"/g, "&quot;")}"` : "";
176
+ result.push(`${indent}<Admonition type="${type}"${titleAttr}>`);
177
+ open.push({ indent, colons });
178
+ continue;
179
+ }
180
+ if (closeMatch && open.length > 0 && closeMatch[2] === open[open.length - 1].colons) {
181
+ const { indent } = open.pop();
182
+ result.push(`${indent}</Admonition>`);
183
+ continue;
184
+ }
185
+ result.push(line);
186
+ }
187
+ return result.join("\n");
188
+ }
189
+ // ── Token → TreeNode conversion ─────────────────────────
190
+ function tokensToTree(tokens, ctx) {
191
+ const { componentMap } = ctx;
192
+ const tree = [];
193
+ let i = 0;
194
+ while (i < tokens.length) {
195
+ const token = tokens[i];
196
+ // ── JSX self-closing ──
197
+ if (token.type === "jsx_self_closing") {
198
+ const mapping = componentMap[token.tag];
199
+ if (mapping && !mapping.strip) {
200
+ if (mapping.type === "__doc-card-list") {
201
+ tree.push(ctx.docCardListCards ?? docCardListPlaceholder(ctx));
202
+ }
203
+ else if (mapping.type === "__placeholder") {
204
+ tree.push(componentPlaceholder(ctx, token.tag));
205
+ }
206
+ else {
207
+ const attrs = token.meta?.attrs || {};
208
+ const props = mapping.propsFromAttrs?.(attrs) || {};
209
+ tree.push({ type: mapping.type, props, children: [] });
210
+ }
211
+ }
212
+ i++;
213
+ continue;
214
+ }
215
+ // ── JSX open ──
216
+ if (token.type === "jsx_open") {
217
+ const tag = token.tag;
218
+ const mapping = componentMap[tag];
219
+ const children = [];
220
+ let depth = 1;
221
+ i++;
222
+ while (i < tokens.length && depth > 0) {
223
+ if (tokens[i].type === "jsx_open" && tokens[i].tag === tag)
224
+ depth++;
225
+ else if (tokens[i].type === "jsx_close" && tokens[i].tag === tag) {
226
+ depth--;
227
+ if (depth === 0) {
228
+ i++;
229
+ break;
230
+ }
231
+ }
232
+ children.push(tokens[i]);
233
+ i++;
234
+ }
235
+ if (mapping) {
236
+ if (mapping.type === "__doc-card-list") {
237
+ tree.push(ctx.docCardListCards ?? docCardListPlaceholder(ctx));
238
+ }
239
+ else if (mapping.type === "__placeholder") {
240
+ tree.push(componentPlaceholder(ctx, tag));
241
+ }
242
+ else if (mapping.strip) {
243
+ if (mapping.container && children.length > 0) {
244
+ tree.push(...tokensToTree(children, ctx));
245
+ }
246
+ }
247
+ else if (mapping.type === "code") {
248
+ // <CodeBlock>...</CodeBlock>: inner content is the code text.
249
+ const attrs = token.meta?.attrs || {};
250
+ const props = mapping.propsFromAttrs?.(attrs) || {};
251
+ const code = children
252
+ .filter((t) => t.type === "inline" || t.type === "fence" || t.type === "code_block")
253
+ .map((t) => t.content)
254
+ .join("");
255
+ tree.push({ type: "code", props: { ...props, code: code.replace(/\n$/, "") } });
256
+ }
257
+ else {
258
+ const attrs = token.meta?.attrs || {};
259
+ const props = mapping.propsFromAttrs?.(attrs) || {};
260
+ tree.push({ type: mapping.type, props, children: tokensToTree(children, ctx) });
261
+ }
262
+ }
263
+ else {
264
+ // Unknown component — keep inner content, drop the wrapper.
265
+ tree.push(...tokensToTree(children, ctx));
266
+ }
267
+ continue;
268
+ }
269
+ if (token.type === "jsx_close") {
270
+ i++;
271
+ continue;
272
+ }
273
+ // ── Standard markdown ──
274
+ if (token.type === "heading_open") {
275
+ const level = parseInt(token.tag.slice(1), 10);
276
+ const inline = tokens[i + 1];
277
+ const text = inline?.content || "";
278
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
279
+ tree.push({
280
+ type: "heading",
281
+ props: { level, text, slug },
282
+ inline: inline ? inlineTokensToInline(inline.children || []) : [],
283
+ });
284
+ i += 3;
285
+ continue;
286
+ }
287
+ if (token.type === "paragraph_open") {
288
+ const inline = tokens[i + 1];
289
+ if (inline?.children?.length) {
290
+ tree.push({
291
+ type: "paragraph",
292
+ children: [{ type: "prose", inline: inlineTokensToInline(inline.children) }],
293
+ });
294
+ }
295
+ i += 3;
296
+ continue;
297
+ }
298
+ if (token.type === "fence") {
299
+ const meta = parseCodeMeta(token.info || "");
300
+ const props = {
301
+ code: token.content.replace(/\n$/, ""),
302
+ lang: meta.lang,
303
+ };
304
+ if (meta.title)
305
+ props.title = meta.title;
306
+ if (meta.highlights)
307
+ props.highlights = meta.highlights;
308
+ if (meta.showLineNumbers)
309
+ props.showLineNumbers = true;
310
+ tree.push({ type: "code", props });
311
+ i++;
312
+ continue;
313
+ }
314
+ if (token.type === "code_block") {
315
+ tree.push({
316
+ type: "code",
317
+ props: { code: token.content.replace(/\n$/, ""), lang: "plaintext" },
318
+ });
319
+ i++;
320
+ continue;
321
+ }
322
+ if (token.type === "bullet_list_open" || token.type === "ordered_list_open") {
323
+ const listType = token.type === "ordered_list_open" ? "ordered-list" : "unordered-list";
324
+ const start = token.type === "ordered_list_open"
325
+ ? parseInt(token.attrGet("start") || "1", 10)
326
+ : undefined;
327
+ const items = [];
328
+ i++;
329
+ while (i < tokens.length &&
330
+ tokens[i].type !== "bullet_list_close" &&
331
+ tokens[i].type !== "ordered_list_close") {
332
+ if (tokens[i].type === "list_item_open") {
333
+ i++;
334
+ const itemTokens = [];
335
+ let liDepth = 1;
336
+ while (i < tokens.length && liDepth > 0) {
337
+ if (tokens[i].type === "list_item_open")
338
+ liDepth++;
339
+ if (tokens[i].type === "list_item_close") {
340
+ liDepth--;
341
+ if (liDepth === 0)
342
+ break;
343
+ }
344
+ itemTokens.push(tokens[i]);
345
+ i++;
346
+ }
347
+ items.push({ type: "list-item", children: tokensToTree(itemTokens, ctx) });
348
+ i++;
349
+ }
350
+ else {
351
+ i++;
352
+ }
353
+ }
354
+ i++;
355
+ const listProps = start && start !== 1 ? { start } : undefined;
356
+ tree.push({ type: listType, props: listProps, children: items });
357
+ continue;
358
+ }
359
+ if (token.type === "table_open") {
360
+ const { node, endIndex } = parseTable(tokens, i);
361
+ tree.push(node);
362
+ i = endIndex;
363
+ continue;
364
+ }
365
+ if (token.type === "hr") {
366
+ tree.push({ type: "thematic-break" });
367
+ i++;
368
+ continue;
369
+ }
370
+ if (token.type === "blockquote_open") {
371
+ const children = [];
372
+ let depth = 1;
373
+ i++;
374
+ const inner = [];
375
+ while (i < tokens.length && depth > 0) {
376
+ if (tokens[i].type === "blockquote_open")
377
+ depth++;
378
+ else if (tokens[i].type === "blockquote_close") {
379
+ depth--;
380
+ if (depth === 0)
381
+ break;
382
+ }
383
+ inner.push(tokens[i]);
384
+ i++;
385
+ }
386
+ i++;
387
+ children.push(...tokensToTree(inner, ctx));
388
+ tree.push({ type: "blockquote", children });
389
+ continue;
390
+ }
391
+ if (token.type === "html_block") {
392
+ const trimmed = token.content.trim();
393
+ if (trimmed.match(/^<[A-Z]/)) {
394
+ i++;
395
+ continue;
396
+ }
397
+ tree.push({ type: "html", html: token.content });
398
+ i++;
399
+ continue;
400
+ }
401
+ i++;
402
+ }
403
+ return tree;
404
+ }
405
+ function docCardListPlaceholder(ctx) {
406
+ ctx.onWarn("<DocCardList>/<DocCard> is auto-generated from the sidebar and cannot be reproduced statically — emitted a placeholder.");
407
+ return {
408
+ type: "callout",
409
+ props: { variant: "info" },
410
+ children: [
411
+ {
412
+ type: "paragraph",
413
+ children: [
414
+ {
415
+ type: "prose",
416
+ inline: [
417
+ {
418
+ type: "text",
419
+ text: "This section originally rendered an auto-generated card list of its sub-pages. Use the sidebar to navigate.",
420
+ },
421
+ ],
422
+ },
423
+ ],
424
+ },
425
+ ],
426
+ meta: { docusaurus: "DocCardList" },
427
+ };
428
+ }
429
+ /**
430
+ * Placeholder for a data-driven component an adapter can't reproduce statically
431
+ * (e.g. a React component that builds tables/steps from JSON). Emitting a note
432
+ * instead of stripping keeps containers (tabs, list items) non-empty — an empty
433
+ * tab otherwise loses its label — and flags the omission to the reader.
434
+ */
435
+ function componentPlaceholder(ctx, name) {
436
+ ctx.onWarn(`<${name}> is a data-driven component and couldn't be reproduced from source — emitted a placeholder.`);
437
+ return {
438
+ type: "callout",
439
+ props: { variant: "info" },
440
+ children: [
441
+ {
442
+ type: "paragraph",
443
+ children: [
444
+ {
445
+ type: "prose",
446
+ inline: [
447
+ { type: "text", text: "Content generated by the " },
448
+ { type: "code", text: name },
449
+ { type: "text", text: " component was not reproduced in this import." },
450
+ ],
451
+ },
452
+ ],
453
+ },
454
+ ],
455
+ meta: { docusaurus: name },
456
+ };
457
+ }
458
+ function parseTable(tokens, start) {
459
+ const rows = [];
460
+ let i = start + 1;
461
+ let inHead = false;
462
+ while (i < tokens.length && tokens[i].type !== "table_close") {
463
+ if (tokens[i].type === "thead_open") {
464
+ inHead = true;
465
+ i++;
466
+ continue;
467
+ }
468
+ if (tokens[i].type === "thead_close") {
469
+ inHead = false;
470
+ i++;
471
+ continue;
472
+ }
473
+ if (tokens[i].type === "tbody_open" || tokens[i].type === "tbody_close") {
474
+ i++;
475
+ continue;
476
+ }
477
+ if (tokens[i].type === "tr_open") {
478
+ const cells = [];
479
+ i++;
480
+ while (i < tokens.length && tokens[i].type !== "tr_close") {
481
+ if (tokens[i].type === "th_open" || tokens[i].type === "td_open") {
482
+ const cellType = inHead ? "th" : "td";
483
+ const inline = tokens[i + 1];
484
+ const inlineNodes = inline?.type === "inline" ? inlineTokensToInline(inline.children || []) : [];
485
+ cells.push({ type: cellType, children: [{ type: "prose", inline: inlineNodes }] });
486
+ i += 3;
487
+ }
488
+ else {
489
+ i++;
490
+ }
491
+ }
492
+ rows.push({ type: "tr", children: cells });
493
+ i++;
494
+ continue;
495
+ }
496
+ i++;
497
+ }
498
+ return { node: { type: "table", children: rows }, endIndex: i + 1 };
499
+ }
500
+ // ── Inline conversion ───────────────────────────────────
501
+ function inlineTokensToInline(tokens) {
502
+ const result = [];
503
+ let bold = false;
504
+ let italic = false;
505
+ let inLink = false;
506
+ let linkHref = "";
507
+ let linkChildren = [];
508
+ const push = (node) => {
509
+ if (inLink)
510
+ linkChildren.push(node);
511
+ else
512
+ result.push(node);
513
+ };
514
+ for (const token of tokens) {
515
+ if (token.type === "text") {
516
+ push({ type: "text", text: token.content, bold, italic });
517
+ }
518
+ else if (token.type === "code_inline") {
519
+ push({ type: "code", text: token.content });
520
+ }
521
+ else if (token.type === "softbreak" || token.type === "hardbreak") {
522
+ push({ type: "text", text: "\n" });
523
+ }
524
+ else if (token.type === "strong_open") {
525
+ bold = true;
526
+ }
527
+ else if (token.type === "strong_close") {
528
+ bold = false;
529
+ }
530
+ else if (token.type === "em_open") {
531
+ italic = true;
532
+ }
533
+ else if (token.type === "em_close") {
534
+ italic = false;
535
+ }
536
+ else if (token.type === "link_open") {
537
+ linkHref = token.attrGet("href") || "";
538
+ linkChildren = [];
539
+ inLink = true;
540
+ }
541
+ else if (token.type === "link_close") {
542
+ if (inLink) {
543
+ result.push({ type: "link", href: linkHref, children: linkChildren });
544
+ inLink = false;
545
+ linkHref = "";
546
+ linkChildren = [];
547
+ }
548
+ }
549
+ else if (token.type === "image") {
550
+ push({
551
+ type: "image",
552
+ src: token.attrGet("src") || "",
553
+ alt: token.content || token.attrGet("alt") || "",
554
+ });
555
+ }
556
+ else if (token.type === "html_inline") {
557
+ push({ type: "text", text: token.content });
558
+ }
559
+ // Inline JSX tokens (jsx_inline_*) are intentionally skipped.
560
+ }
561
+ return result;
562
+ }
563
+ // ── Post-processing ─────────────────────────────────────
564
+ /** Hoist a <details>'s <summary> child into props.title. */
565
+ function hoistDetailsSummary(nodes) {
566
+ for (const node of nodes) {
567
+ if (node.children)
568
+ node.children = hoistDetailsSummary(node.children);
569
+ if (node.type === "details" && node.children) {
570
+ const idx = node.children.findIndex((c) => c.type === "summary");
571
+ if (idx !== -1) {
572
+ const summary = node.children[idx];
573
+ node.props = { ...node.props, title: summaryText(summary) };
574
+ node.children.splice(idx, 1);
575
+ }
576
+ }
577
+ }
578
+ return nodes;
579
+ }
580
+ function summaryText(node) {
581
+ const parts = [];
582
+ const walk = (n) => {
583
+ if (n.inline) {
584
+ for (const inl of n.inline)
585
+ if (inl.type === "text" || inl.type === "code")
586
+ parts.push(inl.text);
587
+ }
588
+ if (n.children)
589
+ n.children.forEach(walk);
590
+ };
591
+ walk(node);
592
+ return parts.join("").trim();
593
+ }
594
+ /**
595
+ * Rewrite internal doc links to served URLs.
596
+ *
597
+ * Docusaurus authors cross-references as links to source files — relative
598
+ * (`../foo/bar.mdx`) or root-absolute (`/calico/foo`). The browser can't follow
599
+ * a `.mdx` path, and relative depths don't match the served (trailing-slash) URL
600
+ * structure, so Docusaurus resolves these to absolute URLs at build time. We do
601
+ * the same: strip `.mdx`/`.md` (+ `/index`), resolve relatives against the
602
+ * current page's slug, and emit a root-absolute `<hrefPrefix>/<slug>` href.
603
+ *
604
+ * `currentSlug` is the importing file's slug (e.g.
605
+ * "getting-started/kubernetes/managed-public-cloud/eks") — relatives resolve
606
+ * against its directory.
607
+ */
608
+ function rewriteLinks(nodes, routeBasePath, hrefPrefix, currentSlug) {
609
+ const base = "/" + routeBasePath.replace(/^\/|\/$/g, "");
610
+ const prefix = hrefPrefix.replace(/\/$/, "");
611
+ const currentDir = posix.dirname("/" + currentSlug).replace(/^\//, "");
612
+ const cleanSlug = (p) => p.replace(/\.mdx?$/, "").replace(/\/index$/, "");
613
+ const fix = (href) => {
614
+ if (/^[a-z]+:\/\//i.test(href) || href.startsWith("#") || href.startsWith("mailto:")) {
615
+ return href;
616
+ }
617
+ // Preserve a trailing #anchor / ?query.
618
+ const split = href.match(/^([^#?]*)([#?].*)?$/);
619
+ const path = split?.[1] ?? href;
620
+ const suffix = split?.[2] ?? "";
621
+ if (!path)
622
+ return href; // bare "#anchor" already handled above
623
+ const isDoc = /\.mdx?$/.test(path);
624
+ if (path.startsWith("/")) {
625
+ // Root-absolute. Map the instance's routeBasePath onto hrefPrefix; strip
626
+ // doc extensions either way (non-routeBasePath links are cross-product —
627
+ // left as-is beyond ext stripping, like Starlight).
628
+ let p = path;
629
+ if (base !== "/" && (p === base || p.startsWith(base + "/"))) {
630
+ p = prefix + cleanSlug(p.slice(base.length));
631
+ }
632
+ else if (isDoc) {
633
+ p = cleanSlug(p);
634
+ }
635
+ else {
636
+ return href;
637
+ }
638
+ return p + suffix;
639
+ }
640
+ if (isDoc) {
641
+ // Relative doc link → resolve against the current page's dir → absolute.
642
+ const resolved = cleanSlug(posix.normalize(posix.join(currentDir, path)));
643
+ return `${prefix}/${resolved}${suffix}`;
644
+ }
645
+ return href; // relative non-doc (rare) — leave untouched
646
+ };
647
+ const walkInline = (inl) => {
648
+ for (const node of inl) {
649
+ if (node.type === "link") {
650
+ node.href = fix(node.href);
651
+ walkInline(node.children);
652
+ }
653
+ }
654
+ };
655
+ const walk = (n) => {
656
+ if (n.inline)
657
+ walkInline(n.inline);
658
+ if (n.children)
659
+ n.children.forEach(walk);
660
+ };
661
+ nodes.forEach(walk);
662
+ return nodes;
663
+ }
664
+ /**
665
+ * Rewrite root-absolute image srcs onto the Dogsbay `_assets` convention.
666
+ * Docusaurus serves `static/` at the site root, so `static/img/x` is referenced
667
+ * as `/img/x`; mapped to `/_assets/img/x` (docs/images.md) so it gets correct
668
+ * basePath prefixing and the copied `static/` tree under `_assets/` resolves.
669
+ */
670
+ function rewriteImageSrcs(nodes, assetPrefix) {
671
+ const prefix = assetPrefix.replace(/\/$/, "");
672
+ const fix = (src) => {
673
+ if (!src.startsWith("/") || src.startsWith("//"))
674
+ return src; // relative/protocol-relative/external
675
+ if (src.startsWith(prefix + "/"))
676
+ return src; // already mounted
677
+ return prefix + src;
678
+ };
679
+ const walkInline = (inl) => {
680
+ for (const node of inl) {
681
+ if (node.type === "image")
682
+ node.src = fix(node.src);
683
+ else if (node.type === "link")
684
+ walkInline(node.children);
685
+ }
686
+ };
687
+ const walk = (n) => {
688
+ if (n.inline)
689
+ walkInline(n.inline);
690
+ if (n.children)
691
+ n.children.forEach(walk);
692
+ };
693
+ nodes.forEach(walk);
694
+ return nodes;
695
+ }
696
+ /**
697
+ * Resolve `card` nodes that carry a `docId` (e.g. from `<DocCardLink docId=…>`):
698
+ * fill in `href` and `title` from the target doc via the injected resolver, so
699
+ * the card isn't emitted empty. An explicit title/href already on the node wins.
700
+ */
701
+ function resolveCardDocs(nodes, resolveDoc) {
702
+ const walk = (n) => {
703
+ if (n.type === "card" && typeof n.props?.docId === "string") {
704
+ const r = resolveDoc(n.props.docId);
705
+ if (r) {
706
+ if (!n.props.href)
707
+ n.props.href = r.href;
708
+ if (!n.props.title)
709
+ n.props.title = r.title;
710
+ }
711
+ delete n.props.docId;
712
+ }
713
+ n.children?.forEach(walk);
714
+ };
715
+ nodes.forEach(walk);
716
+ return nodes;
717
+ }
718
+ /** Build a `cards` grid TreeNode from a sidebar category's child nav items. */
719
+ function navItemsToCards(items) {
720
+ const cards = items
721
+ .filter((it) => it.href || (it.children && it.children.length))
722
+ .map((it) => ({
723
+ type: "card",
724
+ props: { title: it.label, href: it.href ?? it.children?.[0]?.href },
725
+ }));
726
+ return { type: "cards", children: cards };
727
+ }
728
+ export function docusaurusToTree(source, options = {}) {
729
+ const adapter = options.adapter;
730
+ const onWarn = options.onWarn ?? (() => { });
731
+ const { components, selfClosing, map: componentMap } = buildComponentMap(adapter);
732
+ const { body } = extractFrontmatter(source);
733
+ // Adapter preprocessing runs first: $[var] substitution, partial inlining, etc.
734
+ let cleaned = adapter?.preprocess
735
+ ? adapter.preprocess(body, options.preprocessContext ?? {})
736
+ : body;
737
+ cleaned = cleaned.replace(/\t/g, " ");
738
+ cleaned = applyInlineTransforms(cleaned, adapter);
739
+ cleaned = stripExports(cleaned);
740
+ cleaned = convertAdmonitions(cleaned);
741
+ cleaned = rewriteNativeHtml(cleaned);
742
+ cleaned = normalizeJsxLines(cleaned);
743
+ const md = new MarkdownIt({ html: true, linkify: true }).use(mdxJsx, {
744
+ components,
745
+ selfClosing,
746
+ });
747
+ const tokens = md.parse(cleaned, {});
748
+ // If this page is a category index, build the DocCardList grid from its
749
+ // sidebar children up front so tokensToTree can emit it in place.
750
+ const cardListItems = options.resolveDocCardList?.(options.currentSlug ?? "");
751
+ const docCardListCards = cardListItems && cardListItems.length > 0 ? navItemsToCards(cardListItems) : undefined;
752
+ const ctx = { componentMap, onWarn, docCardListCards };
753
+ let tree = tokensToTree(tokens, ctx);
754
+ tree = hoistDetailsSummary(tree);
755
+ if (options.routeBasePath) {
756
+ tree = rewriteLinks(tree, options.routeBasePath, options.hrefPrefix ?? "/docs", options.currentSlug ?? "");
757
+ }
758
+ if (options.assetPrefix) {
759
+ tree = rewriteImageSrcs(tree, options.assetPrefix);
760
+ }
761
+ if (options.resolveDoc) {
762
+ tree = resolveCardDocs(tree, options.resolveDoc);
763
+ }
764
+ return tree;
765
+ }