@dogsbay/format-mdx 0.2.0-beta.15 → 0.2.0-beta.151
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/adapter.d.ts +158 -0
- package/dist/adapter.js +94 -0
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +269 -20
- package/dist/extract.d.ts +31 -0
- package/dist/extract.js +151 -0
- package/dist/fumadocs.d.ts +47 -0
- package/dist/fumadocs.js +471 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +11 -2
- package/dist/js-scan.d.ts +34 -0
- package/dist/js-scan.js +123 -0
- package/dist/meta-nav.d.ts +28 -0
- package/dist/meta-nav.js +193 -0
- package/dist/mintlify-nav.d.ts +30 -0
- package/dist/mintlify-nav.js +417 -0
- package/dist/mintlify-openapi.d.ts +108 -0
- package/dist/mintlify-openapi.js +362 -0
- package/dist/mintlify.d.ts +2 -0
- package/dist/mintlify.js +195 -0
- package/dist/nav-extract.d.ts +42 -0
- package/dist/nav-extract.js +134 -0
- package/dist/parse.d.ts +100 -5
- package/dist/parse.js +1231 -151
- package/dist/ts-type-block.d.ts +42 -0
- package/dist/ts-type-block.js +163 -0
- package/package.json +10 -8
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/parse.d.ts.map +0 -1
- package/dist/parse.js.map +0 -1
package/dist/extract.js
ADDED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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;
|
package/dist/fumadocs.js
ADDED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|