@dogsbay/format-starlight 0.2.0-beta.0

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.js ADDED
@@ -0,0 +1,231 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join, relative, dirname } from "node:path";
3
+ import { parseMeta } from "@dogsbay/types";
4
+ import { starlightToTree as starlightToTreeLegacy, extractFrontmatter } from "./parse.js";
5
+ import { starlightToTree as starlightToTreeMdx } from "./parse-mdx.js";
6
+ import { cloudflareAdapter } from "./adapters/cloudflare.js";
7
+ import { buildNavFromDirectory, buildNavFromConfig } from "./nav.js";
8
+ const ADAPTERS = {
9
+ cloudflare: cloudflareAdapter,
10
+ };
11
+ /**
12
+ * Detect if a directory contains a Starlight docs source.
13
+ *
14
+ * Checks for:
15
+ * 1. src/content/docs/ directory with .mdx files
16
+ * 2. astro.config with @astrojs/starlight
17
+ * 3. .mdx files with Starlight-style frontmatter (sidebar.order)
18
+ */
19
+ function detectStarlightSource(path) {
20
+ // Check for src/content/docs/ pattern
21
+ const docsDir = join(path, "src", "content", "docs");
22
+ if (existsSync(docsDir)) {
23
+ const entries = readdirSync(docsDir, { recursive: true });
24
+ if (entries.some((e) => e.endsWith(".mdx") || e.endsWith(".md"))) {
25
+ return true;
26
+ }
27
+ }
28
+ // Check for astro.config with starlight
29
+ for (const configName of ["astro.config.ts", "astro.config.mjs", "astro.config.js"]) {
30
+ const configPath = join(path, configName);
31
+ if (existsSync(configPath)) {
32
+ const content = readFileSync(configPath, "utf-8");
33
+ if (content.includes("@astrojs/starlight"))
34
+ return true;
35
+ }
36
+ }
37
+ // Check if the path itself is a docs directory with .mdx files
38
+ if (existsSync(path)) {
39
+ try {
40
+ const entries = readdirSync(path);
41
+ const hasMdx = entries.some((e) => e.endsWith(".mdx") || e.endsWith(".md"));
42
+ // Check a sample file for Starlight frontmatter
43
+ if (hasMdx) {
44
+ const sample = entries.find((e) => e.endsWith(".mdx") || e.endsWith(".md"));
45
+ if (sample) {
46
+ const content = readFileSync(join(path, sample), "utf-8");
47
+ if (content.includes("sidebar:") && content.includes("order:"))
48
+ return true;
49
+ }
50
+ }
51
+ }
52
+ catch {
53
+ // Not a directory or can't read
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+ /**
59
+ * Walk a directory tree and collect all .mdx/.md files.
60
+ */
61
+ function walkMdxFiles(dir, rootDir) {
62
+ const results = [];
63
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
64
+ const fullPath = join(dir, entry.name);
65
+ if (entry.isDirectory()) {
66
+ results.push(...walkMdxFiles(fullPath, rootDir));
67
+ }
68
+ else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
69
+ const rel = relative(rootDir, fullPath)
70
+ .replace(/\\/g, "/")
71
+ .replace(/\.mdx?$/, "")
72
+ .replace(/\/index$/, "");
73
+ results.push({ filePath: fullPath, slug: rel || "index" });
74
+ }
75
+ }
76
+ return results;
77
+ }
78
+ /**
79
+ * Import a Starlight docs source directory.
80
+ */
81
+ async function importStarlight(source, opts) {
82
+ // Determine the docs directory
83
+ let docsDir = source;
84
+ const srcContentDocs = join(source, "src", "content", "docs");
85
+ if (existsSync(srcContentDocs)) {
86
+ docsDir = srcContentDocs;
87
+ }
88
+ // Determine href prefix. Defaults to "/docs" but the CLI threads
89
+ // `site.basePath` (normalized) through opts.hrefPrefix so a site
90
+ // configured for a different prefix (or root-served, "") gets nav
91
+ // hrefs matching where pages will actually be emitted.
92
+ const hrefPrefix = typeof opts.hrefPrefix === "string" ? opts.hrefPrefix : "/docs";
93
+ // Build nav
94
+ let nav = null;
95
+ // Try Mode 1: explicit config
96
+ for (const configName of ["astro.config.ts", "astro.config.mjs", "astro.config.js"]) {
97
+ const configPath = join(source, configName);
98
+ if (existsSync(configPath)) {
99
+ nav = buildNavFromConfig(configPath, docsDir, hrefPrefix);
100
+ if (nav)
101
+ break;
102
+ }
103
+ }
104
+ // Fall back to Mode 2: directory scan
105
+ if (!nav) {
106
+ nav = buildNavFromDirectory(docsDir, hrefPrefix);
107
+ }
108
+ // Resolve adapter
109
+ const adapterName = opts.adapter;
110
+ const adapter = adapterName ? ADAPTERS[adapterName] : undefined;
111
+ if (adapterName && !adapter) {
112
+ console.warn(`Warning: Unknown adapter "${adapterName}". Available: ${Object.keys(ADAPTERS).join(", ")}`);
113
+ }
114
+ // Auto-detect partials directory
115
+ // Walk up from docsDir to find src/content/partials/
116
+ let partialsDir;
117
+ let searchDir = docsDir;
118
+ for (let depth = 0; depth < 10; depth++) {
119
+ const candidate = join(searchDir, "src", "content", "partials");
120
+ if (existsSync(candidate)) {
121
+ partialsDir = candidate;
122
+ break;
123
+ }
124
+ // Also check if partials is a sibling of our current path
125
+ const sibling = join(dirname(searchDir), "partials");
126
+ if (existsSync(sibling)) {
127
+ partialsDir = sibling;
128
+ break;
129
+ }
130
+ const parent = dirname(searchDir);
131
+ if (parent === searchDir)
132
+ break;
133
+ searchDir = parent;
134
+ }
135
+ // Allow explicit override via CLI option
136
+ if (opts.partialsDir) {
137
+ partialsDir = opts.partialsDir;
138
+ }
139
+ // Parse all pages
140
+ const mdxFiles = walkMdxFiles(docsDir, docsDir);
141
+ const pages = [];
142
+ // Custom taxonomy names from dogsbay.config.yml's `taxonomies:` block
143
+ // (threaded through cli's import-content.ts).
144
+ const taxonomyNames = Array.isArray(opts.taxonomyNames)
145
+ ? opts.taxonomyNames
146
+ : undefined;
147
+ for (const { filePath, slug } of mdxFiles) {
148
+ const content = readFileSync(filePath, "utf-8");
149
+ const { frontmatter, raw, body } = extractFrontmatter(content);
150
+ // Use new MDX parser (markdown-it-mdx-jsx plugin) by default
151
+ // Fall back to legacy regex parser with --legacy-parser flag
152
+ const useLegacy = opts.legacyParser === true;
153
+ const starlightToTree = useLegacy ? starlightToTreeLegacy : starlightToTreeMdx;
154
+ const tree = starlightToTree(body, { adapter, partialsDir });
155
+ // Extract headings from tree
156
+ const headings = tree
157
+ .filter((n) => n.type === "heading")
158
+ .map((n) => ({
159
+ depth: n.props?.level,
160
+ text: n.props?.text,
161
+ slug: n.props?.slug,
162
+ }));
163
+ // Check for redirect pages (adapter-configurable frontmatter field)
164
+ let redirect;
165
+ if (adapter?.redirectField) {
166
+ const redirectMatch = content.match(new RegExp(`^${adapter.redirectField}:\\s*(.+)$`, "m"));
167
+ if (redirectMatch) {
168
+ redirect = redirectMatch[1].trim().replace(/^["']|["']$/g, "");
169
+ }
170
+ }
171
+ // Generate visible content for redirect pages with empty body
172
+ if (redirect && tree.length === 0) {
173
+ tree.push({
174
+ type: "callout",
175
+ props: { variant: "info" },
176
+ children: [
177
+ {
178
+ type: "paragraph",
179
+ children: [
180
+ {
181
+ type: "prose",
182
+ inline: [
183
+ { type: "text", text: "This page redirects to ", bold: false, italic: false },
184
+ {
185
+ type: "link",
186
+ href: redirect,
187
+ children: [
188
+ { type: "text", text: frontmatter.title || slug, bold: false, italic: false },
189
+ ],
190
+ },
191
+ { type: "text", text: ".", bold: false, italic: false },
192
+ ],
193
+ },
194
+ ],
195
+ },
196
+ ],
197
+ });
198
+ }
199
+ const title = frontmatter.title || slug;
200
+ const meta = parseMeta(raw, { slug, taxonomyNames });
201
+ pages.push({
202
+ slug,
203
+ title,
204
+ tree,
205
+ headings,
206
+ redirect,
207
+ frontmatter: raw,
208
+ meta,
209
+ });
210
+ }
211
+ // Set site name from opts or infer
212
+ if (!opts.siteName && pages.length > 0) {
213
+ const indexPage = pages.find((p) => p.slug === "index" || p.slug === "");
214
+ if (indexPage) {
215
+ opts.siteName = indexPage.title;
216
+ }
217
+ }
218
+ return { pages, nav };
219
+ }
220
+ export const plugin = {
221
+ name: "starlight",
222
+ canImport: true,
223
+ canExport: false,
224
+ detectSource: detectStarlightSource,
225
+ exportOptions: [
226
+ { flags: "--adapter <name>", description: "Site adapter for custom components (e.g. cloudflare)" },
227
+ ],
228
+ async import(source, opts) {
229
+ return importStarlight(source, opts);
230
+ },
231
+ };
@@ -0,0 +1,6 @@
1
+ export { starlightToTree as starlightToTreeLegacy, extractFrontmatter } from "./parse.js";
2
+ export { starlightToTree } from "./parse-mdx.js";
3
+ export type { ParseOptions } from "./parse-mdx.js";
4
+ export { buildNavFromDirectory, buildNavFromConfig } from "./nav.js";
5
+ export type { ComponentMapping, SiteAdapter } from "./adapter.js";
6
+ export { cloudflareAdapter } from "./adapters/cloudflare.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { starlightToTree as starlightToTreeLegacy, extractFrontmatter } from "./parse.js";
2
+ export { starlightToTree } from "./parse-mdx.js";
3
+ export { buildNavFromDirectory, buildNavFromConfig } from "./nav.js";
4
+ export { cloudflareAdapter } from "./adapters/cloudflare.js";
package/dist/nav.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Nav builder for Starlight sites.
3
+ *
4
+ * Two modes:
5
+ * 1. buildNavFromDirectory — Scan filesystem + frontmatter sidebar.order/label (Mode 2)
6
+ * 2. buildNavFromConfig — Parse explicit sidebar config from astro.config.ts (Mode 1)
7
+ *
8
+ * Both produce the same NavItem[] output.
9
+ */
10
+ import type { NavItem } from "@dogsbay/types";
11
+ /**
12
+ * Build nav from directory structure and frontmatter sidebar.order.
13
+ *
14
+ * Scans the docs directory, reads frontmatter from each .mdx/.md file,
15
+ * and builds a hierarchical nav sorted by sidebar.order.
16
+ *
17
+ * @param docsDir - The directory containing .mdx source files (e.g. src/content/docs/ddos-protection/)
18
+ * @param hrefPrefix - Prefix for href values (e.g. "/docs/ddos-protection")
19
+ */
20
+ export declare function buildNavFromDirectory(docsDir: string, hrefPrefix?: string): NavItem[];
21
+ /**
22
+ * Attempt to extract sidebar config from astro.config.ts.
23
+ *
24
+ * This is a best-effort parser — it handles static sidebar arrays
25
+ * but cannot resolve dynamic expressions (e.g. Cloudflare's autogenSections()).
26
+ * Returns null if the sidebar can't be parsed statically.
27
+ *
28
+ * @param configPath - Path to astro.config.ts or astro.config.mjs
29
+ * @param docsDir - Docs directory for resolving autogenerate directives
30
+ * @param hrefPrefix - Prefix for href values
31
+ */
32
+ export declare function buildNavFromConfig(configPath: string, docsDir: string, hrefPrefix?: string): NavItem[] | null;
package/dist/nav.js ADDED
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Nav builder for Starlight sites.
3
+ *
4
+ * Two modes:
5
+ * 1. buildNavFromDirectory — Scan filesystem + frontmatter sidebar.order/label (Mode 2)
6
+ * 2. buildNavFromConfig — Parse explicit sidebar config from astro.config.ts (Mode 1)
7
+ *
8
+ * Both produce the same NavItem[] output.
9
+ */
10
+ import { readdirSync, readFileSync } from "node:fs";
11
+ import { join, relative } from "node:path";
12
+ import { extractFrontmatter } from "./parse.js";
13
+ /**
14
+ * Build nav from directory structure and frontmatter sidebar.order.
15
+ *
16
+ * Scans the docs directory, reads frontmatter from each .mdx/.md file,
17
+ * and builds a hierarchical nav sorted by sidebar.order.
18
+ *
19
+ * @param docsDir - The directory containing .mdx source files (e.g. src/content/docs/ddos-protection/)
20
+ * @param hrefPrefix - Prefix for href values (e.g. "/docs/ddos-protection")
21
+ */
22
+ export function buildNavFromDirectory(docsDir, hrefPrefix = "/docs") {
23
+ const pages = collectPages(docsDir, docsDir);
24
+ return buildHierarchy(pages, hrefPrefix);
25
+ }
26
+ function collectPages(dir, rootDir) {
27
+ const pages = [];
28
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
29
+ const fullPath = join(dir, entry.name);
30
+ if (entry.isDirectory()) {
31
+ pages.push(...collectPages(fullPath, rootDir));
32
+ }
33
+ else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
34
+ const content = readFileSync(fullPath, "utf-8");
35
+ const { frontmatter } = extractFrontmatter(content);
36
+ if (frontmatter.sidebarHidden)
37
+ continue;
38
+ // Build slug from file path relative to root dir
39
+ let slug = relative(rootDir, fullPath)
40
+ .replace(/\\/g, "/")
41
+ .replace(/\.mdx?$/, "")
42
+ .replace(/\/index$/, "");
43
+ pages.push({
44
+ title: frontmatter.title,
45
+ sidebarLabel: frontmatter.sidebarLabel,
46
+ sidebarOrder: frontmatter.sidebarOrder,
47
+ sidebarHidden: frontmatter.sidebarHidden,
48
+ hideIndex: frontmatter.hideIndex,
49
+ slug,
50
+ filePath: fullPath,
51
+ });
52
+ }
53
+ }
54
+ return pages;
55
+ }
56
+ function buildHierarchy(pages, hrefPrefix) {
57
+ // Determine which slugs are directory index pages (i.e. there are other pages under that path)
58
+ const allSlugs = new Set(pages.map((p) => p.slug));
59
+ const directorySlugs = new Set();
60
+ for (const page of pages) {
61
+ const parts = page.slug.split("/");
62
+ if (parts.length > 1) {
63
+ // Mark all ancestor directories
64
+ for (let i = 1; i < parts.length; i++) {
65
+ directorySlugs.add(parts.slice(0, i).join("/"));
66
+ }
67
+ }
68
+ }
69
+ // Group pages by their top-level directory
70
+ const groups = new Map();
71
+ const topLevel = [];
72
+ for (const page of pages) {
73
+ const parts = page.slug.split("/");
74
+ if (parts.length > 1) {
75
+ // Has parent directory — group under top-level dir
76
+ const group = parts[0];
77
+ if (!groups.has(group))
78
+ groups.set(group, []);
79
+ groups.get(group).push(page);
80
+ }
81
+ else if (directorySlugs.has(page.slug)) {
82
+ // This is an index page for a directory — it becomes the group header, not a top-level item
83
+ const group = page.slug;
84
+ if (!groups.has(group))
85
+ groups.set(group, []);
86
+ groups.get(group).push(page);
87
+ }
88
+ else {
89
+ // True top-level leaf page
90
+ topLevel.push(page);
91
+ }
92
+ }
93
+ const entries = [];
94
+ // Top-level leaf pages
95
+ for (const page of topLevel) {
96
+ entries.push({
97
+ order: page.sidebarOrder ?? 999,
98
+ item: pageToNavItem(page, hrefPrefix),
99
+ });
100
+ }
101
+ // Groups
102
+ for (const [groupName, groupPages] of groups) {
103
+ const indexPage = groupPages.find((p) => p.slug === groupName);
104
+ // Group label uses title, not sidebarLabel.
105
+ const label = indexPage?.title || slugToLabel(groupName);
106
+ const children = buildGroupChildren(groupPages, groupName, hrefPrefix);
107
+ if (children.length > 0) {
108
+ const href = indexPage ? `${hrefPrefix}/${indexPage.slug}` : undefined;
109
+ entries.push({
110
+ order: indexPage?.sidebarOrder ?? 999,
111
+ item: { label, href, children },
112
+ });
113
+ }
114
+ }
115
+ // Sort by order, then alphabetically
116
+ entries.sort((a, b) => {
117
+ if (a.order !== b.order)
118
+ return a.order - b.order;
119
+ return (a.item.label || "").localeCompare(b.item.label || "");
120
+ });
121
+ const nav = entries.map((e) => e.item);
122
+ return nav;
123
+ }
124
+ function buildGroupChildren(pages, groupName, hrefPrefix) {
125
+ // Separate into sub-groups and leaf pages
126
+ const subGroups = new Map();
127
+ const leaves = [];
128
+ // Find the index page for this group
129
+ const indexPage = pages.find((p) => p.slug === groupName);
130
+ // First pass: collect sub-group directories
131
+ for (const page of pages) {
132
+ if (page.slug === groupName)
133
+ continue;
134
+ const rel = page.slug.startsWith(groupName + "/")
135
+ ? page.slug.slice(groupName.length + 1)
136
+ : page.slug;
137
+ const parts = rel.split("/");
138
+ if (parts.length > 1) {
139
+ const subGroup = parts[0];
140
+ const key = `${groupName}/${subGroup}`;
141
+ if (!subGroups.has(key))
142
+ subGroups.set(key, []);
143
+ subGroups.get(key).push(page);
144
+ }
145
+ }
146
+ // Second pass: collect leaf pages, but skip those that are index pages of sub-groups
147
+ for (const page of pages) {
148
+ if (page.slug === groupName)
149
+ continue;
150
+ const rel = page.slug.startsWith(groupName + "/")
151
+ ? page.slug.slice(groupName.length + 1)
152
+ : page.slug;
153
+ const parts = rel.split("/");
154
+ if (parts.length === 1) {
155
+ // Check if this leaf's slug matches a sub-group directory
156
+ const potentialSubGroup = `${groupName}/${rel}`;
157
+ if (subGroups.has(potentialSubGroup)) {
158
+ // This is the index page for a sub-group — add it to that sub-group's pages
159
+ subGroups.get(potentialSubGroup).push(page);
160
+ }
161
+ else {
162
+ leaves.push(page);
163
+ }
164
+ }
165
+ }
166
+ const children = [];
167
+ // Add index page as first child of the group — unless hideIndex is set.
168
+ // Starlight's sidebar.group.hideIndex suppresses the index page from children.
169
+ // When shown: "Overview" if title matches group label, else the page's own title.
170
+ if (indexPage && !indexPage.hideIndex) {
171
+ const indexTitle = indexPage.sidebarLabel || indexPage.title || "";
172
+ const normalize = (s) => s.toLowerCase().replace(/[\s-]+/g, "");
173
+ const groupSlug = groupName.split("/").pop() || groupName;
174
+ const isGenericIndex = normalize(indexTitle) === normalize(groupSlug) ||
175
+ normalize(indexTitle) === normalize(slugToLabel(groupSlug));
176
+ children.push({
177
+ label: isGenericIndex ? "Overview" : indexTitle,
178
+ href: `${hrefPrefix}/${indexPage.slug}`,
179
+ });
180
+ }
181
+ const entries = [];
182
+ // Leaves
183
+ for (const page of leaves) {
184
+ entries.push({
185
+ order: page.sidebarOrder ?? 999,
186
+ item: pageToNavItem(page, hrefPrefix),
187
+ });
188
+ }
189
+ // Sub-groups
190
+ for (const [subGroupPath, subPages] of subGroups) {
191
+ const subGroupSlug = subGroupPath.split("/").pop();
192
+ const subIndex = subPages.find((p) => p.slug === subGroupPath);
193
+ const label = subIndex?.title || slugToLabel(subGroupSlug);
194
+ const order = subIndex?.sidebarOrder ?? 999;
195
+ const subChildren = buildGroupChildren(subPages, subGroupPath, hrefPrefix);
196
+ if (subChildren.length > 0) {
197
+ const href = subIndex ? `${hrefPrefix}/${subIndex.slug}` : undefined;
198
+ entries.push({ order, item: { label, href, children: subChildren } });
199
+ }
200
+ }
201
+ // Sort by order, then alphabetically
202
+ entries.sort((a, b) => {
203
+ if (a.order !== b.order)
204
+ return a.order - b.order;
205
+ return (a.item.label || "").localeCompare(b.item.label || "");
206
+ });
207
+ for (const entry of entries) {
208
+ children.push(entry.item);
209
+ }
210
+ return children;
211
+ }
212
+ function pageToNavItem(page, hrefPrefix) {
213
+ const label = page.sidebarLabel || page.title || slugToLabel(page.slug);
214
+ const href = `${hrefPrefix}/${page.slug}`;
215
+ return { label, href };
216
+ }
217
+ function sortPages(pages) {
218
+ pages.sort((a, b) => {
219
+ const orderA = a.sidebarOrder ?? 999;
220
+ const orderB = b.sidebarOrder ?? 999;
221
+ if (orderA !== orderB)
222
+ return orderA - orderB;
223
+ return a.title.localeCompare(b.title);
224
+ });
225
+ }
226
+ function slugToLabel(slug) {
227
+ const name = slug.split("/").pop() || slug;
228
+ return name
229
+ .split("-")
230
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
231
+ .join(" ");
232
+ }
233
+ // ── Mode 1: Build nav from astro.config sidebar ─────────
234
+ /**
235
+ * Attempt to extract sidebar config from astro.config.ts.
236
+ *
237
+ * This is a best-effort parser — it handles static sidebar arrays
238
+ * but cannot resolve dynamic expressions (e.g. Cloudflare's autogenSections()).
239
+ * Returns null if the sidebar can't be parsed statically.
240
+ *
241
+ * @param configPath - Path to astro.config.ts or astro.config.mjs
242
+ * @param docsDir - Docs directory for resolving autogenerate directives
243
+ * @param hrefPrefix - Prefix for href values
244
+ */
245
+ export function buildNavFromConfig(configPath, docsDir, hrefPrefix = "/docs") {
246
+ let content;
247
+ try {
248
+ content = readFileSync(configPath, "utf-8");
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ // Find the sidebar array — look for sidebar: [ ... ]
254
+ const sidebarMatch = content.match(/sidebar:\s*\[/);
255
+ if (!sidebarMatch)
256
+ return null;
257
+ // Extract the array by tracking bracket depth
258
+ const start = sidebarMatch.index + sidebarMatch[0].length - 1;
259
+ let depth = 0;
260
+ let end = start;
261
+ for (let i = start; i < content.length; i++) {
262
+ if (content[i] === "[")
263
+ depth++;
264
+ else if (content[i] === "]") {
265
+ depth--;
266
+ if (depth === 0) {
267
+ end = i + 1;
268
+ break;
269
+ }
270
+ }
271
+ }
272
+ const sidebarStr = content.slice(start, end);
273
+ // Try to parse as JSON5-like (remove trailing commas, handle unquoted keys)
274
+ let sidebarData;
275
+ try {
276
+ const jsonLike = sidebarStr
277
+ .replace(/(\w+):/g, '"$1":') // quote keys
278
+ .replace(/'/g, '"') // single to double quotes
279
+ .replace(/,\s*([}\]])/g, "$1") // trailing commas
280
+ .replace(/\/\/.*/g, ""); // strip comments
281
+ sidebarData = JSON.parse(jsonLike);
282
+ }
283
+ catch {
284
+ // Config is too dynamic to parse statically
285
+ return null;
286
+ }
287
+ return sidebarDataToNav(sidebarData, docsDir, hrefPrefix);
288
+ }
289
+ function sidebarDataToNav(items, docsDir, hrefPrefix) {
290
+ const nav = [];
291
+ for (const item of items) {
292
+ if (typeof item === "string") {
293
+ // Slug shorthand
294
+ nav.push({ label: slugToLabel(item), href: `${hrefPrefix}/${item}` });
295
+ }
296
+ else if (typeof item === "object" && item !== null) {
297
+ const obj = item;
298
+ if (obj.slug) {
299
+ // Explicit slug reference
300
+ const slug = obj.slug;
301
+ const label = obj.label || slugToLabel(slug);
302
+ nav.push({ label, href: `${hrefPrefix}/${slug}` });
303
+ }
304
+ else if (obj.link) {
305
+ // URL link
306
+ const label = obj.label || "";
307
+ nav.push({ label, href: obj.link });
308
+ }
309
+ else if (obj.items) {
310
+ // Group with nested items
311
+ const label = obj.label || "";
312
+ const children = sidebarDataToNav(obj.items, docsDir, hrefPrefix);
313
+ nav.push({ label, children });
314
+ }
315
+ else if (obj.autogenerate) {
316
+ // Autogenerate from directory
317
+ const label = obj.label || "";
318
+ const dir = obj.autogenerate.directory;
319
+ const fullDir = join(docsDir, dir);
320
+ try {
321
+ const children = buildNavFromDirectory(fullDir, `${hrefPrefix}/${dir}`);
322
+ nav.push({ label, children });
323
+ }
324
+ catch {
325
+ // Directory doesn't exist or can't be read
326
+ nav.push({ label, children: [] });
327
+ }
328
+ }
329
+ }
330
+ }
331
+ return nav;
332
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Starlight MDX parser — v2 using markdown-it-mdx-jsx plugin.
3
+ *
4
+ * Replaces the regex-based segmentMdx approach with proper JSX token handling.
5
+ * The markdown-it-mdx-jsx plugin emits jsx_open/jsx_close/jsx_self_closing
6
+ * tokens that are handled alongside standard markdown tokens.
7
+ *
8
+ * Keeps:
9
+ * - Directive conversion (:::note → <Aside>)
10
+ * - Inline transforms (adapter.inlineTransforms)
11
+ * - Steps unwrapping (steps > ordered-list > list-item → steps > step)
12
+ * - Card grouping (consecutive cards → cards container)
13
+ * - The adapter pattern for site-specific component mappings
14
+ */
15
+ import type { TreeNode } from "@dogsbay/types";
16
+ import type { SiteAdapter } from "./adapter.js";
17
+ export interface ParseOptions {
18
+ adapter?: SiteAdapter;
19
+ /** Directory containing partial MDX files for <Render> resolution */
20
+ partialsDir?: string;
21
+ }
22
+ export declare function starlightToTree(source: string, options?: ParseOptions): TreeNode[];