@dogsbay/format-mdx 0.2.0-beta.15 → 0.2.0-beta.150

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,151 @@
1
+ /**
2
+ * Layer C `extract` — map a component whose DATA lives in a separate
3
+ * TS/TSX file (`<Features />` backed by `const grid = […]`;
4
+ * `<CommunityAdaptersTable />` backed by `export const communityAdapters
5
+ * = […]` in lib/) onto real content nodes.
6
+ *
7
+ * Static parsing only (the same balanced scanner as the sidebar
8
+ * extractor) — nothing is executed. Anything that doesn't scan preserves
9
+ * the component raw + ledgered (never guess).
10
+ *
11
+ * Config shape (per-docset `mdx.components`):
12
+ *
13
+ * Features:
14
+ * extract: { file: ../../components/docs/features.tsx, export: grid, as: cards }
15
+ * selfClosing: true
16
+ */
17
+ import { readFileSync } from "node:fs";
18
+ import { captureConstLiteral, displayJsValue } from "./js-scan.js";
19
+ import { parseJsArrayElements, parseJsObjectEntries } from "./fumadocs.js";
20
+ function escapeHtml(s) {
21
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
22
+ }
23
+ function field(obj, ...keys) {
24
+ for (const key of keys) {
25
+ if (obj[key] !== undefined)
26
+ return displayJsValue(obj[key]);
27
+ }
28
+ return undefined;
29
+ }
30
+ /** Display form for a table cell; nested objects prefer name/title/url. */
31
+ function cellValue(raw) {
32
+ const t = raw.trim();
33
+ if (t.startsWith("{")) {
34
+ const entries = parseJsObjectEntries(t);
35
+ if (entries) {
36
+ const o = Object.fromEntries(entries);
37
+ return field(o, "name", "title", "label", "url") ?? "";
38
+ }
39
+ return "";
40
+ }
41
+ if (t.startsWith("[")) {
42
+ const elements = parseJsArrayElements(t);
43
+ return elements ? elements.map(cellValue).join(", ") : "";
44
+ }
45
+ return displayJsValue(t);
46
+ }
47
+ function dataToCards(elements) {
48
+ const cards = [];
49
+ for (const element of elements) {
50
+ const entries = parseJsObjectEntries(element);
51
+ if (!entries)
52
+ return null;
53
+ const o = Object.fromEntries(entries);
54
+ const title = field(o, "title", "name", "label");
55
+ if (!title)
56
+ return null;
57
+ const props = { title };
58
+ const description = field(o, "description");
59
+ const href = field(o, "href", "url", "link");
60
+ if (description)
61
+ props.description = description;
62
+ if (href)
63
+ props.href = href;
64
+ cards.push({ type: "card", props });
65
+ }
66
+ return cards.length > 0 ? { type: "cards", children: cards } : null;
67
+ }
68
+ function dataToTable(elements) {
69
+ const rows = [];
70
+ for (const element of elements) {
71
+ const entries = parseJsObjectEntries(element);
72
+ if (!entries)
73
+ return null;
74
+ rows.push(Object.fromEntries(entries));
75
+ }
76
+ if (rows.length === 0)
77
+ return null;
78
+ // Columns: first row's key order; skip columns whose every value is
79
+ // empty after display conversion (avatars, icon components, …).
80
+ const candidates = Object.keys(rows[0]);
81
+ const columns = candidates.filter((c) => rows.some((r) => r[c] !== undefined && cellValue(r[c]) !== ""));
82
+ if (columns.length === 0)
83
+ return null;
84
+ const cell = (html, header = false) => ({
85
+ type: header ? "th" : "td",
86
+ children: [{ type: "prose", html }],
87
+ });
88
+ const humanize = (k) => k.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
89
+ return {
90
+ type: "table",
91
+ children: [
92
+ {
93
+ type: "thead",
94
+ children: [{
95
+ type: "tr",
96
+ children: columns.map((c) => cell(escapeHtml(humanize(c)), true)),
97
+ }],
98
+ },
99
+ {
100
+ type: "tbody",
101
+ children: rows.map((r) => ({
102
+ type: "tr",
103
+ children: columns.map((c) => {
104
+ const value = r[c] !== undefined ? cellValue(r[c]) : "";
105
+ // Linkify when a sibling `<col>Url` field exists (name/nameUrl)
106
+ const urlSibling = r[`${c}Url`] ?? r[`${c}_url`];
107
+ const text = escapeHtml(value);
108
+ const html = urlSibling && value
109
+ ? `<a href="${escapeHtml(displayJsValue(urlSibling))}">${text}</a>`
110
+ : value.match(/^https?:\/\//)
111
+ ? `<a href="${text}">${text}</a>`
112
+ : text;
113
+ return cell(html);
114
+ }),
115
+ })),
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ /**
121
+ * Build a ComponentMapping for an extract entry. `resolveFile` turns the
122
+ * spec's file into a readable path (source-relative or pre-rebased
123
+ * absolute). The data file is read lazily on first use and cached.
124
+ */
125
+ export function makeExtractMapping(spec, resolveFile) {
126
+ let cached;
127
+ return {
128
+ type: spec.as,
129
+ container: false,
130
+ toNode: () => {
131
+ if (cached === undefined) {
132
+ cached = null;
133
+ try {
134
+ const source = readFileSync(resolveFile(spec.file), "utf-8");
135
+ const literal = captureConstLiteral(source, spec.export);
136
+ const elements = literal ? parseJsArrayElements(literal) : null;
137
+ if (elements) {
138
+ cached = spec.as === "cards" ? dataToCards(elements) : dataToTable(elements);
139
+ }
140
+ }
141
+ catch {
142
+ cached = null;
143
+ }
144
+ }
145
+ if (!cached)
146
+ return "preserve";
147
+ // Fresh copy per use — consumers may mutate trees in post-processing.
148
+ return JSON.parse(JSON.stringify(cached));
149
+ },
150
+ };
151
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Fumadocs adapter — component mappings for the Fumadocs UI vocabulary
3
+ * (fumadocs.dev), plus built-in named handlers for common per-docset
4
+ * components layered on top of Fumadocs (e.g. better-auth's <APIMethod>).
5
+ *
6
+ * Generic builtins already cover Callout/Tabs/Steps/Cards/Accordion-style
7
+ * tags; this adapter adds the Fumadocs-specific pieces: TypeTable,
8
+ * Files/File/Folder trees, Accordions, <Link> unwrapping, Banner,
9
+ * ImageZoom.
10
+ *
11
+ * Everything here is static structural mapping — no JSX execution, ever.
12
+ */
13
+ import type { ComponentMapping, MdxAdapter } from "./adapter.js";
14
+ export declare const apiMethodMapping: ComponentMapping;
15
+ /**
16
+ * Map a `<DatabaseTable name fields={…}>` schema table onto a real table.
17
+ * `fields` is an array of `{name, type, description, isPrimaryKey?,
18
+ * isForeignKey?, isOptional?, isUnique?, references?: {model, field}}` —
19
+ * either inline or (the common shape) a same-file `export const`
20
+ * identifier resolved via ctx.moduleData. Static parsing only; anything
21
+ * that doesn't scan is preserved raw + ledgered.
22
+ */
23
+ export declare const databaseTableMapping: ComponentMapping;
24
+ /**
25
+ * Map a component carrying an inline array of link resources
26
+ * (`resources={[{title, description, href, tags}]}` — better-auth's
27
+ * `<Resource>`) onto a cards grid. The array may also be a same-file
28
+ * `export const` identifier (resolved via ctx.moduleData).
29
+ */
30
+ export declare const resourceCardsMapping: ComponentMapping;
31
+ /** Built-in named handlers resolvable from declarative per-docset config. */
32
+ export declare const MDX_BUILTIN_HANDLERS: Record<string, ComponentMapping>;
33
+ /**
34
+ * Parse the entries of a JS object literal WITHOUT executing it: returns
35
+ * [key, rawValue] pairs at one nesting level. Values are raw source spans
36
+ * (balanced braces/brackets/parens, quote-aware with JSX-text tolerance —
37
+ * see js-scan.ts). Returns null on anything that doesn't scan as
38
+ * `key: value, …`.
39
+ */
40
+ export declare function parseJsObjectEntries(src: string): [string, string][] | null;
41
+ /**
42
+ * Split a JS array literal into top-level element spans (quote/bracket
43
+ * aware with JSX-text tolerance — see js-scan.ts; no execution).
44
+ * Returns null when the text doesn't scan.
45
+ */
46
+ export declare function parseJsArrayElements(src: string): string[] | null;
47
+ export declare const fumadocsAdapter: MdxAdapter;
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Fumadocs adapter — component mappings for the Fumadocs UI vocabulary
3
+ * (fumadocs.dev), plus built-in named handlers for common per-docset
4
+ * components layered on top of Fumadocs (e.g. better-auth's <APIMethod>).
5
+ *
6
+ * Generic builtins already cover Callout/Tabs/Steps/Cards/Accordion-style
7
+ * tags; this adapter adds the Fumadocs-specific pieces: TypeTable,
8
+ * Files/File/Folder trees, Accordions, <Link> unwrapping, Banner,
9
+ * ImageZoom.
10
+ *
11
+ * Everything here is static structural mapping — no JSX execution, ever.
12
+ */
13
+ import { inlineToDogsbayMd } from "@dogsbay/format-dogsbay-md";
14
+ import { parseTsTypeBlock } from "./ts-type-block.js";
15
+ import { splitTopLevel } from "./js-scan.js";
16
+ // ── <APIMethod> → endpoint (built-in named handler) ──────
17
+ /**
18
+ * Map an `<APIMethod path method …>` block onto dogsbay's `endpoint`
19
+ * vocabulary (the format-openapi node rendered by EndpointCard /
20
+ * ParameterTable / ApiLayout and serialized agent-readably by
21
+ * format-dogsbay-md's renderEndpoint).
22
+ *
23
+ * The TS type block inside the fence is parsed with parseTsTypeBlock —
24
+ * the same deterministic line-based grammar better-auth's own component
25
+ * applies client-side. The declared type name becomes `clientMethod`
26
+ * (the authClient call name); parsed properties become parameters /
27
+ * request-body rows. The original fence rides along as a code sample,
28
+ * so nothing is lost even when a nuance doesn't parse. Parse failure →
29
+ * "preserve" (mdx-raw + ledger) — never guess.
30
+ */
31
+ /** "/two-factor/enable" → "twoFactor.enable" (kebab segments → camelCase). */
32
+ function pathToDotNotation(path) {
33
+ return path
34
+ .split("/")
35
+ .filter(Boolean)
36
+ .map((segment) => segment
37
+ .split("-")
38
+ .map((word, i) => i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1))
39
+ .join(""))
40
+ .join(".");
41
+ }
42
+ /**
43
+ * Generate the client call-site example the same way better-auth's own
44
+ * component does at runtime:
45
+ *
46
+ * const { data, error } = await authClient.twoFactor.enable({
47
+ * password: "secure-password", // required
48
+ * });
49
+ *
50
+ * Server-only props are skipped; GET-style calls wrap the props in a
51
+ * `query` object; nested object props open/close by path depth.
52
+ */
53
+ function buildClientCallSample(path, props, opts) {
54
+ const indent = (n) => " ".repeat(n);
55
+ const base = opts.asQuery ? 2 : 1;
56
+ let body = "";
57
+ const visible = props.filter((p) => !p.serverOnly);
58
+ for (let i = 0; i < visible.length; i++) {
59
+ const p = visible[i];
60
+ const comment = p.optional ? "" : " // required";
61
+ if (p.type === "Object") {
62
+ body += `${indent(p.path.length + base)}${p.name}: {${comment}\n`;
63
+ }
64
+ else {
65
+ const value = p.example ? `: ${p.example}` : "";
66
+ body += `${indent(p.path.length + base)}${p.name}${value},${comment}\n`;
67
+ }
68
+ // Close nested objects when depth decreases
69
+ const nextDepth = visible[i + 1]?.path.length ?? 0;
70
+ for (let d = p.path.length + (p.type === "Object" ? 1 : 0); d > nextDepth; d--) {
71
+ body += `${indent(d - 1 + base)}},\n`;
72
+ }
73
+ }
74
+ const args = body
75
+ ? opts.asQuery
76
+ ? `{\n${indent(1)}query: {\n${body}${indent(1)}},\n}`
77
+ : `{\n${body}}`
78
+ : "";
79
+ const assign = opts.noResult ? "" : `const ${opts.resultVariable} = `;
80
+ return `${assign}await authClient.${pathToDotNotation(path)}(${args});`;
81
+ }
82
+ export const apiMethodMapping = {
83
+ type: "endpoint",
84
+ container: true,
85
+ toNode: (attrs, innerSource) => {
86
+ const fence = innerSource.match(/```[^\n]*\n([\s\S]*?)\n[ \t]*```/);
87
+ if (!fence)
88
+ return "preserve";
89
+ const code = fence[1];
90
+ const parsed = parseTsTypeBlock(code);
91
+ if (!parsed)
92
+ return "preserve";
93
+ const method = (attrs.method || "GET").toUpperCase();
94
+ const path = attrs.path || "";
95
+ const notes = [];
96
+ if (attrs.note)
97
+ notes.push(attrs.note);
98
+ if (attrs.requireSession === "true")
99
+ notes.push("Requires an active session.");
100
+ if (attrs.requireBearerToken === "true")
101
+ notes.push("Requires a Bearer token.");
102
+ if (attrs.requireHeaders === "true") {
103
+ notes.push(attrs.headersComment || "Requires request headers to be passed.");
104
+ }
105
+ if (attrs.isServerOnly === "true")
106
+ notes.push("Server-side API only.");
107
+ if (attrs.isClientOnly === "true")
108
+ notes.push("Client SDK only.");
109
+ const asQuery = attrs.forceAsQuery === "true" ||
110
+ attrs.forceAsParam === "true" ||
111
+ (method === "GET" && attrs.forceAsBody !== "true");
112
+ const rows = parsed.props.map((p) => {
113
+ const flags = [];
114
+ if (p.serverOnly)
115
+ flags.push("server-only");
116
+ if (p.clientOnly)
117
+ flags.push("client-only");
118
+ const description = [p.description, flags.length ? `(${flags.join(", ")})` : ""]
119
+ .filter(Boolean)
120
+ .join(" ");
121
+ return {
122
+ name: [...p.path, p.name].join("."),
123
+ type: p.type,
124
+ required: !p.optional,
125
+ nullable: p.nullable || undefined,
126
+ description: description || undefined,
127
+ // Display form: `"secure-password"` (source text) → secure-password
128
+ example: p.example !== undefined ? displayValue(p.example) : undefined,
129
+ };
130
+ });
131
+ const props = {
132
+ method,
133
+ path,
134
+ clientMethod: parsed.name,
135
+ };
136
+ if (notes.length > 0)
137
+ props.description = notes.join(" ");
138
+ if (rows.length > 0) {
139
+ if (asQuery) {
140
+ props.parameters = rows.map((r) => ({
141
+ name: r.name,
142
+ in: "query",
143
+ type: r.type,
144
+ required: r.required,
145
+ description: r.description,
146
+ }));
147
+ }
148
+ else {
149
+ props.requestBody = rows;
150
+ }
151
+ }
152
+ // Generated call-site example — the same snippet better-auth's own
153
+ // component builds at runtime (client SDK method from the path,
154
+ // example values, // required comments, query wrap for GET).
155
+ // Server-only endpoints have no client call; they keep just the
156
+ // type block.
157
+ const samples = [];
158
+ if (attrs.isServerOnly !== "true") {
159
+ samples.push({
160
+ language: "ts",
161
+ label: "Client example",
162
+ code: buildClientCallSample(path, parsed.props, {
163
+ asQuery,
164
+ resultVariable: attrs.resultVariable || "{ data, error }",
165
+ noResult: attrs.noResult === "true",
166
+ }),
167
+ });
168
+ }
169
+ // Full-fidelity fallback channel: the original TS block rides along.
170
+ samples.push({ language: "ts", label: "Types", code });
171
+ props.codeSamples = samples;
172
+ return { type: "endpoint", props };
173
+ },
174
+ };
175
+ // ── <DatabaseTable> → table (built-in named handler) ─────
176
+ /**
177
+ * Map a `<DatabaseTable name fields={…}>` schema table onto a real table.
178
+ * `fields` is an array of `{name, type, description, isPrimaryKey?,
179
+ * isForeignKey?, isOptional?, isUnique?, references?: {model, field}}` —
180
+ * either inline or (the common shape) a same-file `export const`
181
+ * identifier resolved via ctx.moduleData. Static parsing only; anything
182
+ * that doesn't scan is preserved raw + ledgered.
183
+ */
184
+ export const databaseTableMapping = {
185
+ type: "table",
186
+ container: false,
187
+ toNode: (attrs, _inner, ctx) => {
188
+ const fieldsExpr = resolveAttrExpr(attrs.fields, ctx.moduleData);
189
+ if (!fieldsExpr)
190
+ return "preserve";
191
+ const elements = parseJsArrayElements(fieldsExpr);
192
+ if (!elements || elements.length === 0)
193
+ return "preserve";
194
+ const rows = [];
195
+ for (const element of elements) {
196
+ const entries = parseJsObjectEntries(element);
197
+ if (!entries)
198
+ return "preserve";
199
+ const f = Object.fromEntries(entries);
200
+ const name = f.name ? displayValue(f.name) : "";
201
+ const type = f.type ? displayValue(f.type) : "";
202
+ const description = f.description ? displayValue(f.description) : "";
203
+ const marks = [];
204
+ if (f.isPrimaryKey === "true")
205
+ marks.push("PK");
206
+ if (f.isForeignKey === "true")
207
+ marks.push("FK");
208
+ if (f.isUnique === "true")
209
+ marks.push("unique");
210
+ if (f.isOptional === "true")
211
+ marks.push("optional");
212
+ if (f.references) {
213
+ const ref = parseJsObjectEntries(f.references);
214
+ if (ref) {
215
+ const r = Object.fromEntries(ref);
216
+ if (r.model && r.field) {
217
+ marks.push(`→ ${displayValue(r.model)}.${displayValue(r.field)}`);
218
+ }
219
+ }
220
+ }
221
+ rows.push({
222
+ type: "tr",
223
+ children: [
224
+ cell(`<code>${escapeHtml(name)}</code>`),
225
+ cell(type ? `<code>${escapeHtml(type)}</code>` : ""),
226
+ cell(escapeHtml(marks.join(", "))),
227
+ cell(escapeHtml(description)),
228
+ ],
229
+ });
230
+ }
231
+ const node = {
232
+ type: "table",
233
+ children: [
234
+ {
235
+ type: "thead",
236
+ children: [{
237
+ type: "tr",
238
+ children: [cell("Field", true), cell("Type", true), cell("Key", true), cell("Description", true)],
239
+ }],
240
+ },
241
+ { type: "tbody", children: rows },
242
+ ],
243
+ };
244
+ // A caption is inline Markdown; the type table's name is plain text
245
+ if (attrs.name)
246
+ node.props = { caption: inlineToDogsbayMd([{ type: "text", text: String(attrs.name) }]) };
247
+ return node;
248
+ },
249
+ };
250
+ // ── <Resource resources={[…]}> → cards (built-in named handler) ──
251
+ /**
252
+ * Map a component carrying an inline array of link resources
253
+ * (`resources={[{title, description, href, tags}]}` — better-auth's
254
+ * `<Resource>`) onto a cards grid. The array may also be a same-file
255
+ * `export const` identifier (resolved via ctx.moduleData).
256
+ */
257
+ export const resourceCardsMapping = {
258
+ type: "cards",
259
+ container: false,
260
+ toNode: (attrs, _inner, ctx) => {
261
+ const raw = resolveAttrExpr(attrs.resources, ctx.moduleData) ??
262
+ resolveAttrExpr(attrs.items, ctx.moduleData) ??
263
+ resolveAttrExpr(attrs.data, ctx.moduleData);
264
+ if (!raw)
265
+ return "preserve";
266
+ const elements = parseJsArrayElements(raw);
267
+ if (!elements || elements.length === 0)
268
+ return "preserve";
269
+ const cards = [];
270
+ for (const element of elements) {
271
+ const entries = parseJsObjectEntries(element);
272
+ if (!entries)
273
+ return "preserve";
274
+ const o = Object.fromEntries(entries);
275
+ const title = o.title ?? o.name;
276
+ if (!title)
277
+ return "preserve";
278
+ const props = { title: displayValue(title) };
279
+ if (o.description)
280
+ props.description = displayValue(o.description);
281
+ const href = o.href ?? o.url ?? o.link;
282
+ if (href)
283
+ props.href = displayValue(href);
284
+ cards.push({ type: "card", props });
285
+ }
286
+ return { type: "cards", children: cards };
287
+ },
288
+ };
289
+ /** Built-in named handlers resolvable from declarative per-docset config. */
290
+ export const MDX_BUILTIN_HANDLERS = {
291
+ "api-method": apiMethodMapping,
292
+ "database-table": databaseTableMapping,
293
+ "resource-cards": resourceCardsMapping,
294
+ };
295
+ // ── <TypeTable> → table ──────────────────────────────────
296
+ function cell(html, header = false) {
297
+ return { type: header ? "th" : "td", children: [{ type: "prose", html }] };
298
+ }
299
+ function escapeHtml(s) {
300
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
301
+ }
302
+ /**
303
+ * Parse the entries of a JS object literal WITHOUT executing it: returns
304
+ * [key, rawValue] pairs at one nesting level. Values are raw source spans
305
+ * (balanced braces/brackets/parens, quote-aware with JSX-text tolerance —
306
+ * see js-scan.ts). Returns null on anything that doesn't scan as
307
+ * `key: value, …`.
308
+ */
309
+ export function parseJsObjectEntries(src) {
310
+ let s = src.trim();
311
+ if (s.startsWith("{") && s.endsWith("}"))
312
+ s = s.slice(1, -1);
313
+ const pieces = splitTopLevel(s);
314
+ if (!pieces)
315
+ return null;
316
+ const entries = [];
317
+ for (const piece of pieces) {
318
+ // key: identifier, "quoted", or 'quoted' — then `: value`
319
+ const m = /^\s*(?:([\w$-]+)|"([^"]*)"|'([^']*)')\s*:\s*([\s\S]*)$/.exec(piece);
320
+ if (!m)
321
+ return null;
322
+ const key = m[1] ?? m[2] ?? m[3];
323
+ entries.push([key, m[4].trim()]);
324
+ }
325
+ return entries;
326
+ }
327
+ /**
328
+ * Split a JS array literal into top-level element spans (quote/bracket
329
+ * aware with JSX-text tolerance — see js-scan.ts; no execution).
330
+ * Returns null when the text doesn't scan.
331
+ */
332
+ export function parseJsArrayElements(src) {
333
+ const s = src.trim();
334
+ if (!s.startsWith("[") || !s.endsWith("]"))
335
+ return null;
336
+ return splitTopLevel(s.slice(1, -1));
337
+ }
338
+ /** Unquote a raw JS value for display; non-literals pass through as source. */
339
+ function displayValue(raw) {
340
+ const t = raw.trim();
341
+ const m = t.match(/^(["'`])([\s\S]*)\1$/);
342
+ return m ? m[2] : t;
343
+ }
344
+ /**
345
+ * Resolve an attribute expression: a literal passes through; a bare
346
+ * identifier resolves against the file's `export const` capture
347
+ * (ctx.moduleData). Returns null when unresolvable.
348
+ */
349
+ function resolveAttrExpr(expr, moduleData) {
350
+ if (!expr)
351
+ return null;
352
+ const t = expr.trim();
353
+ if (/^[\w$]+$/.test(t))
354
+ return moduleData[t] ?? null;
355
+ return t;
356
+ }
357
+ const typeTableMapping = {
358
+ type: "table",
359
+ container: false,
360
+ toNode: (attrs, _inner, ctx) => {
361
+ const obj = resolveAttrExpr(attrs.type, ctx.moduleData);
362
+ if (!obj)
363
+ return "preserve";
364
+ const entries = parseJsObjectEntries(obj);
365
+ if (!entries || entries.length === 0)
366
+ return "preserve";
367
+ const rows = [];
368
+ for (const [prop, rawValue] of entries) {
369
+ const fields = parseJsObjectEntries(rawValue);
370
+ if (!fields)
371
+ return "preserve";
372
+ const byKey = Object.fromEntries(fields);
373
+ const typeText = byKey.type ? displayValue(byKey.type) : "";
374
+ const descText = byKey.description ? displayValue(byKey.description) : "";
375
+ const defaultText = byKey.default ? displayValue(byKey.default) : "";
376
+ rows.push({
377
+ type: "tr",
378
+ children: [
379
+ cell(`<code>${escapeHtml(prop)}</code>`),
380
+ cell(typeText ? `<code>${escapeHtml(typeText)}</code>` : ""),
381
+ cell(escapeHtml(descText)),
382
+ cell(defaultText ? `<code>${escapeHtml(defaultText)}</code>` : ""),
383
+ ],
384
+ });
385
+ }
386
+ return {
387
+ type: "table",
388
+ children: [
389
+ {
390
+ type: "thead",
391
+ children: [{
392
+ type: "tr",
393
+ children: [cell("Prop", true), cell("Type", true), cell("Description", true), cell("Default", true)],
394
+ }],
395
+ },
396
+ { type: "tbody", children: rows },
397
+ ],
398
+ };
399
+ },
400
+ };
401
+ // ── <Files>/<Folder>/<File> → nested list ────────────────
402
+ const filesMapping = {
403
+ type: "unordered-list",
404
+ container: true,
405
+ toNode: (_attrs, innerSource, ctx) => {
406
+ const items = ctx.parseBlocks(innerSource).filter((n) => n.type === "list-item");
407
+ if (items.length === 0)
408
+ return "preserve";
409
+ return { type: "unordered-list", children: items };
410
+ },
411
+ };
412
+ const folderMapping = {
413
+ type: "list-item",
414
+ container: true,
415
+ toNode: (attrs, innerSource, ctx) => {
416
+ const name = attrs.name || attrs.title || "";
417
+ const items = ctx.parseBlocks(innerSource).filter((n) => n.type === "list-item");
418
+ const children = [
419
+ { type: "prose", html: `<strong>${escapeHtml(name)}/</strong>` },
420
+ ];
421
+ if (items.length > 0)
422
+ children.push({ type: "unordered-list", children: items });
423
+ return { type: "list-item", children };
424
+ },
425
+ };
426
+ const fileMapping = {
427
+ type: "list-item",
428
+ container: false,
429
+ toNode: (attrs) => ({
430
+ type: "list-item",
431
+ children: [{ type: "prose", html: `<code>${escapeHtml(attrs.name || attrs.title || "")}</code>` }],
432
+ }),
433
+ };
434
+ // ── The adapter ──────────────────────────────────────────
435
+ export const fumadocsAdapter = {
436
+ name: "fumadocs",
437
+ components: {
438
+ // Fumadocs cards container is <Cards>, not <CardGroup>
439
+ Cards: { type: "cards", container: true },
440
+ // Fumadocs accordions — dogsbay has first-class accordion nodes
441
+ Accordions: { type: "accordion", container: true },
442
+ Accordion: {
443
+ type: "accordion-item",
444
+ container: true,
445
+ propsFromAttrs: (attrs) => ({ title: attrs.title || "", id: attrs.id }),
446
+ },
447
+ Banner: {
448
+ type: "callout",
449
+ container: true,
450
+ propsFromAttrs: (attrs) => ({
451
+ variant: attrs.variant === "rainbow" ? "info" : attrs.variant || "info",
452
+ }),
453
+ },
454
+ TypeTable: typeTableMapping,
455
+ Files: filesMapping,
456
+ Folder: folderMapping,
457
+ File: fileMapping,
458
+ ImageZoom: {
459
+ type: "image",
460
+ container: false,
461
+ propsFromAttrs: (attrs) => ({ src: attrs.src, alt: attrs.alt }),
462
+ },
463
+ // Auto-generated from source/OpenAPI at build time — nothing static to keep
464
+ AutoTypeTable: { type: "", container: false, strip: true },
465
+ },
466
+ selfClosingTags: ["TypeTable", "File", "ImageZoom", "AutoTypeTable"],
467
+ inlineTransforms: [
468
+ // <Link href="/x">text</Link> → [text](/x) — next/link passthrough
469
+ [/<Link\s+[^>]*?href=\{?["']([^"'}]+)["']\}?[^>]*?>([\s\S]*?)<\/Link>/g, "[$2]($1)"],
470
+ ],
471
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,12 @@
1
- export { mdxToTree, extractHeadings } from "./parse.js";
1
+ export { mdxToTree, parseMdx, parseAttrs, extractHeadings, type MdxParseOptions, type MdxParseResult, } from "./parse.js";
2
+ export { compileComponentConfig, type ComponentMapping, type ComponentHandlerContext, type ChildrenPolicy, type MdxAdapter, type MdxLedgerEntry, type MdxComponentConfig, type MdxPreprocessContext, } from "./adapter.js";
3
+ export { fumadocsAdapter, apiMethodMapping, databaseTableMapping, resourceCardsMapping, MDX_BUILTIN_HANDLERS, parseJsObjectEntries, parseJsArrayElements, } from "./fumadocs.js";
4
+ export { mintlifyAdapter } from "./mintlify.js";
5
+ export { makeExtractMapping, type ExtractSpec } from "./extract.js";
6
+ export { parseTsTypeBlock, type TsTypeBlock, type TsTypeProp, } from "./ts-type-block.js";
7
+ export { buildFumadocsNav, detectFumadocsDir, type FumadocsNavOptions, } from "./meta-nav.js";
8
+ export { buildMintlifyNav, detectMintlifyDir, type MintlifyNavOptions, mintlifyRedirects, } from "./mintlify-nav.js";
9
+ export { mintlifyOpenApiSpecRef, buildMintlifyEndpointMap, openApiPageKey, } from "./mintlify-openapi.js";
10
+ export { extractNavFromTsx, extractNavFromTsxFile, type NavExtractOptions, } from "./nav-extract.js";
11
+ export { scanBalancedLiteral, splitTopLevel, isQuoteStart, captureConstLiteral, displayJsValue, } from "./js-scan.js";
2
12
  export type { TreeNode, InlineNode, ExportPage, NavItem } from "@dogsbay/types";
3
- //# sourceMappingURL=index.d.ts.map