@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.
package/dist/index.js CHANGED
@@ -1,2 +1,11 @@
1
- export { mdxToTree, extractHeadings } from "./parse.js";
2
- //# sourceMappingURL=index.js.map
1
+ export { mdxToTree, parseMdx, parseAttrs, extractHeadings, } from "./parse.js";
2
+ export { compileComponentConfig, } 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 } from "./extract.js";
6
+ export { parseTsTypeBlock, } from "./ts-type-block.js";
7
+ export { buildFumadocsNav, detectFumadocsDir, } from "./meta-nav.js";
8
+ export { buildMintlifyNav, detectMintlifyDir, mintlifyRedirects, } from "./mintlify-nav.js";
9
+ export { mintlifyOpenApiSpecRef, buildMintlifyEndpointMap, openApiPageKey, } from "./mintlify-openapi.js";
10
+ export { extractNavFromTsx, extractNavFromTsxFile, } from "./nav-extract.js";
11
+ export { scanBalancedLiteral, splitTopLevel, isQuoteStart, captureConstLiteral, displayJsValue, } from "./js-scan.js";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared quote/bracket-aware scanning helpers for statically reading JS/TSX
3
+ * data literals — no execution, ever.
4
+ *
5
+ * The one subtlety: apostrophes and quotes inside JSX TEXT content are not
6
+ * string delimiters (`<p>';--</p>` — better-auth's actual SQL-joke icon —
7
+ * breaks a naive tracker). A quote only OPENS a string in a value position:
8
+ * when the previous non-whitespace character is one that can precede a JS
9
+ * expression (`= ( [ { , : ? & | ! ;` or a `=>` arrow) or at the start of
10
+ * the scanned span. A bare `>` (JSX tag close) is NOT a value position —
11
+ * that is exactly where JSX text apostrophes live.
12
+ */
13
+ /** Is src[i] a quote that starts a string (value position)? */
14
+ export declare function isQuoteStart(src: string, i: number, spanStart: number): boolean;
15
+ /**
16
+ * Scan a balanced literal starting at src[start] (which must be at or
17
+ * before the opening `{`/`[`/`(`). Returns the end index (exclusive, past
18
+ * the closing bracket) or -1 when the span never balances.
19
+ */
20
+ export declare function scanBalancedLiteral(src: string, start: number): number;
21
+ /**
22
+ * Capture the right-hand-side literal of a `const NAME = …` /
23
+ * `export const NAME = …` declaration (type annotations tolerated).
24
+ * Returns the raw literal text or null.
25
+ */
26
+ export declare function captureConstLiteral(source: string, name: string): string | null;
27
+ /** Unquote a raw JS value for display; non-literals pass through as source. */
28
+ export declare function displayJsValue(raw: string): string;
29
+ /**
30
+ * Split the top level of a balanced span into comma-separated pieces.
31
+ * `src` must already be the INNER text (without the outer brackets).
32
+ * Returns null when the span doesn't scan.
33
+ */
34
+ export declare function splitTopLevel(src: string): string[] | null;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Shared quote/bracket-aware scanning helpers for statically reading JS/TSX
3
+ * data literals — no execution, ever.
4
+ *
5
+ * The one subtlety: apostrophes and quotes inside JSX TEXT content are not
6
+ * string delimiters (`<p>';--</p>` — better-auth's actual SQL-joke icon —
7
+ * breaks a naive tracker). A quote only OPENS a string in a value position:
8
+ * when the previous non-whitespace character is one that can precede a JS
9
+ * expression (`= ( [ { , : ? & | ! ;` or a `=>` arrow) or at the start of
10
+ * the scanned span. A bare `>` (JSX tag close) is NOT a value position —
11
+ * that is exactly where JSX text apostrophes live.
12
+ */
13
+ const VALUE_POSITION_CHARS = new Set([
14
+ "=", "(", "[", "{", ",", ":", "?", "&", "|", "!", ";", "+", "-", "*", "%",
15
+ ]);
16
+ /** Is src[i] a quote that starts a string (value position)? */
17
+ export function isQuoteStart(src, i, spanStart) {
18
+ const ch = src[i];
19
+ if (ch !== '"' && ch !== "'" && ch !== "`")
20
+ return false;
21
+ for (let k = i - 1; k >= spanStart; k--) {
22
+ const prev = src[k];
23
+ if (prev === " " || prev === "\t" || prev === "\n" || prev === "\r")
24
+ continue;
25
+ if (prev === ">")
26
+ return src[k - 1] === "="; // `=>` arrow body, not JSX close
27
+ return VALUE_POSITION_CHARS.has(prev);
28
+ }
29
+ return true; // start of span
30
+ }
31
+ /**
32
+ * Scan a balanced literal starting at src[start] (which must be at or
33
+ * before the opening `{`/`[`/`(`). Returns the end index (exclusive, past
34
+ * the closing bracket) or -1 when the span never balances.
35
+ */
36
+ export function scanBalancedLiteral(src, start) {
37
+ let depth = 0;
38
+ let started = false;
39
+ let quote = null;
40
+ for (let i = start; i < src.length; i++) {
41
+ const ch = src[i];
42
+ if (quote) {
43
+ if (ch === quote && src[i - 1] !== "\\")
44
+ quote = null;
45
+ continue;
46
+ }
47
+ if (isQuoteStart(src, i, start)) {
48
+ quote = ch;
49
+ continue;
50
+ }
51
+ if (ch === "{" || ch === "[" || ch === "(") {
52
+ depth++;
53
+ started = true;
54
+ }
55
+ else if (ch === "}" || ch === "]" || ch === ")") {
56
+ depth--;
57
+ if (started && depth === 0)
58
+ return i + 1;
59
+ }
60
+ }
61
+ return -1;
62
+ }
63
+ /**
64
+ * Capture the right-hand-side literal of a `const NAME = …` /
65
+ * `export const NAME = …` declaration (type annotations tolerated).
66
+ * Returns the raw literal text or null.
67
+ */
68
+ export function captureConstLiteral(source, name) {
69
+ const re = new RegExp(`(?:^|\\n)\\s*(?:export\\s+)?const\\s+${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*(?::[^=]*?)?=\\s*`);
70
+ const m = re.exec(source);
71
+ if (!m)
72
+ return null;
73
+ const start = m.index + m[0].length;
74
+ const end = scanBalancedLiteral(source, start);
75
+ if (end === -1)
76
+ return null;
77
+ return source.slice(start, end).trim();
78
+ }
79
+ /** Unquote a raw JS value for display; non-literals pass through as source. */
80
+ export function displayJsValue(raw) {
81
+ const t = raw.trim();
82
+ const m = t.match(/^(["'`])([\s\S]*)\1$/);
83
+ return m ? m[2] : t;
84
+ }
85
+ /**
86
+ * Split the top level of a balanced span into comma-separated pieces.
87
+ * `src` must already be the INNER text (without the outer brackets).
88
+ * Returns null when the span doesn't scan.
89
+ */
90
+ export function splitTopLevel(src) {
91
+ const pieces = [];
92
+ let depth = 0;
93
+ let quote = null;
94
+ let start = 0;
95
+ for (let i = 0; i < src.length; i++) {
96
+ const ch = src[i];
97
+ if (quote) {
98
+ if (ch === quote && src[i - 1] !== "\\")
99
+ quote = null;
100
+ continue;
101
+ }
102
+ if (isQuoteStart(src, i, start)) {
103
+ quote = ch;
104
+ continue;
105
+ }
106
+ if (ch === "{" || ch === "[" || ch === "(")
107
+ depth++;
108
+ else if (ch === "}" || ch === "]" || ch === ")")
109
+ depth--;
110
+ else if (ch === "," && depth === 0) {
111
+ const piece = src.slice(start, i).trim();
112
+ if (piece)
113
+ pieces.push(piece);
114
+ start = i + 1;
115
+ }
116
+ }
117
+ if (quote || depth !== 0)
118
+ return null;
119
+ const last = src.slice(start).trim();
120
+ if (last)
121
+ pieces.push(last);
122
+ return pieces;
123
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Fumadocs `meta.json` → NavItem[] builder.
3
+ *
4
+ * Fumadocs organizes navigation with per-folder `meta.json` files:
5
+ *
6
+ * { "title": "Guide", "root": true, "pages": ["introduction", "...",
7
+ * "email-password/sign-in-and-sign-up", "[GitHub](https://…)"] }
8
+ *
9
+ * Supported `pages` entry forms:
10
+ * - "name" — file (name.mdx / name.md) or folder
11
+ * - "nested/path" — page inside a subfolder
12
+ * - "..." — rest expansion (unlisted items, alphabetical)
13
+ * - "z...a" — rest expansion, reverse alphabetical
14
+ * - "!name" — exclude from rest expansion
15
+ * - "[Label](https://…)" — external link
16
+ * - "---Label---" — separator (skipped: NavItem has no separator)
17
+ *
18
+ * Folders with `root: true` are Fumadocs "root folders" (tab-like separate
19
+ * trees); they are rendered as ordinary top-level groups here.
20
+ */
21
+ import type { NavItem } from "@dogsbay/types";
22
+ export interface FumadocsNavOptions {
23
+ /** Prefix for hrefs (e.g. "/docs"). */
24
+ hrefPrefix?: string;
25
+ }
26
+ /** Detect a Fumadocs content directory: meta.json + .md/.mdx pages. */
27
+ export declare function detectFumadocsDir(path: string): boolean;
28
+ export declare function buildFumadocsNav(rootDir: string, opts?: FumadocsNavOptions): NavItem[];
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Fumadocs `meta.json` → NavItem[] builder.
3
+ *
4
+ * Fumadocs organizes navigation with per-folder `meta.json` files:
5
+ *
6
+ * { "title": "Guide", "root": true, "pages": ["introduction", "...",
7
+ * "email-password/sign-in-and-sign-up", "[GitHub](https://…)"] }
8
+ *
9
+ * Supported `pages` entry forms:
10
+ * - "name" — file (name.mdx / name.md) or folder
11
+ * - "nested/path" — page inside a subfolder
12
+ * - "..." — rest expansion (unlisted items, alphabetical)
13
+ * - "z...a" — rest expansion, reverse alphabetical
14
+ * - "!name" — exclude from rest expansion
15
+ * - "[Label](https://…)" — external link
16
+ * - "---Label---" — separator (skipped: NavItem has no separator)
17
+ *
18
+ * Folders with `root: true` are Fumadocs "root folders" (tab-like separate
19
+ * trees); they are rendered as ordinary top-level groups here.
20
+ */
21
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import YAML from "yaml";
24
+ /** Detect a Fumadocs content directory: meta.json + .md/.mdx pages. */
25
+ export function detectFumadocsDir(path) {
26
+ try {
27
+ if (!existsSync(join(path, "meta.json")))
28
+ return false;
29
+ return readdirSync(path).some((e) => e.endsWith(".mdx") || e.endsWith(".md"));
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ export function buildFumadocsNav(rootDir, opts = {}) {
36
+ const prefix = (opts.hrefPrefix ?? "").replace(/\/$/, "");
37
+ return walkDir(rootDir, rootDir, prefix);
38
+ }
39
+ function readMeta(dir) {
40
+ const metaPath = join(dir, "meta.json");
41
+ if (!existsSync(metaPath))
42
+ return undefined;
43
+ try {
44
+ const parsed = JSON.parse(readFileSync(metaPath, "utf-8"));
45
+ return parsed && typeof parsed === "object" ? parsed : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ function pageTitle(filePath, fallback) {
52
+ try {
53
+ const source = readFileSync(filePath, "utf-8");
54
+ const match = /^---\n([\s\S]*?)\n---/.exec(source);
55
+ if (match) {
56
+ const fm = YAML.parse(match[1]);
57
+ if (fm && typeof fm.title === "string")
58
+ return fm.title;
59
+ }
60
+ }
61
+ catch {
62
+ // fall through to the filename
63
+ }
64
+ return fallback;
65
+ }
66
+ function humanize(name) {
67
+ return name
68
+ .replace(/[-_]/g, " ")
69
+ .replace(/\b\w/g, (c) => c.toUpperCase());
70
+ }
71
+ function resolvePageFile(dir, name) {
72
+ for (const ext of [".mdx", ".md"]) {
73
+ const p = join(dir, name + ext);
74
+ if (existsSync(p))
75
+ return p;
76
+ }
77
+ return null;
78
+ }
79
+ function slugFor(root, dir, name) {
80
+ const rel = join(dir, name).slice(root.length).replace(/\\/g, "/").replace(/^\//, "");
81
+ return rel;
82
+ }
83
+ /** One nav item for a file entry, or null when the file doesn't exist. */
84
+ function fileItem(root, dir, name, prefix) {
85
+ const filePath = resolvePageFile(dir, name);
86
+ if (!filePath)
87
+ return null;
88
+ const slug = slugFor(root, dir, name);
89
+ // Root index → "/"; a subfolder's index → the folder path itself.
90
+ const href = slug === "index" ? `${prefix}/` : `${prefix}/${slug.replace(/\/index$/, "")}`;
91
+ return { label: pageTitle(filePath, humanize(name)), href };
92
+ }
93
+ const EXTERNAL_LINK_RE = /^\[(.*)\]\((.*)\)$/;
94
+ const SEPARATOR_RE = /^---.*---$/;
95
+ function walkDir(root, dir, prefix) {
96
+ const meta = readMeta(dir);
97
+ // Inventory of this folder's files and subfolders.
98
+ const files = new Set();
99
+ const folders = new Set();
100
+ for (const entry of readdirSync(dir)) {
101
+ const full = join(dir, entry);
102
+ if (statSync(full).isDirectory()) {
103
+ folders.add(entry);
104
+ }
105
+ else if ((entry.endsWith(".mdx") || entry.endsWith(".md")) &&
106
+ entry !== "llms.txt") {
107
+ files.add(entry.replace(/\.(mdx|md)$/, ""));
108
+ }
109
+ }
110
+ const restItems = (excluded, reverse) => {
111
+ const names = [...new Set([...files, ...folders])]
112
+ .filter((n) => !excluded.has(n))
113
+ .sort();
114
+ if (reverse)
115
+ names.reverse();
116
+ return names.flatMap((n) => itemFor(n));
117
+ };
118
+ const itemFor = (name) => {
119
+ if (folders.has(name)) {
120
+ const childDir = join(dir, name);
121
+ const childMeta = readMeta(childDir);
122
+ const children = walkDir(root, childDir, prefix);
123
+ if (children.length === 0)
124
+ return [];
125
+ return [{ label: childMeta?.title ?? humanize(name), children }];
126
+ }
127
+ const item = fileItem(root, dir, name, prefix);
128
+ return item ? [item] : [];
129
+ };
130
+ if (!meta?.pages) {
131
+ // No meta.json ordering — alphabetical, index first.
132
+ const excluded = new Set();
133
+ const items = [];
134
+ if (files.has("index")) {
135
+ const idx = fileItem(root, dir, "index", prefix);
136
+ if (idx)
137
+ items.push(idx);
138
+ excluded.add("index");
139
+ }
140
+ items.push(...restItems(excluded, false));
141
+ return items;
142
+ }
143
+ const consumed = new Set();
144
+ const excluded = new Set();
145
+ // Pre-scan exclusions so "..." earlier in the list still honors them.
146
+ for (const entry of meta.pages) {
147
+ if (entry.startsWith("!"))
148
+ excluded.add(entry.slice(1));
149
+ }
150
+ const hasRest = meta.pages.some((e) => e === "..." || e === "z...a");
151
+ const items = [];
152
+ for (const entry of meta.pages) {
153
+ if (entry.startsWith("!"))
154
+ continue;
155
+ if (SEPARATOR_RE.test(entry))
156
+ continue;
157
+ const linkMatch = EXTERNAL_LINK_RE.exec(entry);
158
+ if (linkMatch) {
159
+ items.push({ label: linkMatch[1], href: linkMatch[2] });
160
+ continue;
161
+ }
162
+ if (entry === "..." || entry === "z...a") {
163
+ items.push(...restItems(new Set([...consumed, ...excluded]), entry === "z...a"));
164
+ // Everything is consumed after a rest expansion.
165
+ for (const n of [...files, ...folders])
166
+ consumed.add(n);
167
+ continue;
168
+ }
169
+ if (entry.includes("/")) {
170
+ // Page inside a subfolder (e.g. "email-password/sign-in-and-sign-up")
171
+ const parts = entry.split("/");
172
+ const name = parts.pop();
173
+ const subDir = join(dir, ...parts);
174
+ const item = fileItem(root, subDir, name, prefix);
175
+ if (item)
176
+ items.push(item);
177
+ continue;
178
+ }
179
+ consumed.add(entry);
180
+ items.push(...itemFor(entry));
181
+ }
182
+ // Implicit rest expansion: when the meta has no explicit "...",
183
+ // unlisted files and folders are appended alphabetically instead of
184
+ // being hidden. Strict Fumadocs hides them — but sites with heavily
185
+ // customized sidebars (a hand-written page tree overriding meta.json)
186
+ // often leave meta.json stale, and an IMPORT that silently drops most
187
+ // pages from the nav loses real structure. Never-drop applies to
188
+ // navigation too; `!name` exclusions are still honored.
189
+ if (!hasRest) {
190
+ items.push(...restItems(new Set([...consumed, ...excluded]), false));
191
+ }
192
+ return items;
193
+ }
@@ -0,0 +1,30 @@
1
+ import type { NavItem } from "@dogsbay/types";
2
+ export interface MintlifyNavOptions {
3
+ /** Prefixed onto every internal href (e.g. a base path). */
4
+ hrefPrefix?: string;
5
+ }
6
+ /**
7
+ * True when `dir` looks like a Mintlify docs source: a `docs.json`/`mint.json`
8
+ * that actually reads as a Mintlify config, plus md/mdx pages. The config sniff
9
+ * (a `navigation` key or a `mintlify` `$schema`) keeps an unrelated `docs.json`
10
+ * — e.g. TypeDoc's output — from being claimed by the mdx importer.
11
+ */
12
+ export declare function detectMintlifyDir(dir: string): boolean;
13
+ /**
14
+ * Build NavItem[] from a Mintlify config, or null when no config is present or
15
+ * it declares no navigation.
16
+ */
17
+ export declare function buildMintlifyNav(source: string, opts?: MintlifyNavOptions): NavItem[] | null;
18
+ /**
19
+ * Mintlify `redirects` as a source → destination map, `$ref` resolved.
20
+ *
21
+ * A docs site accumulates these as pages move, and its OWN pages then link to
22
+ * the old paths — Mintlify serves the redirect, so the link works and nobody
23
+ * notices it is stale. On import the redirect does not exist, so those links
24
+ * 404: measured on Mintlify's own docs, 16 internal links point at a redirect
25
+ * source, including `/installation` on the front page (→ `/cli/install`).
26
+ *
27
+ * `redirects` is itself commonly a `$ref` to a sibling file (121 entries in
28
+ * `redirects.json` here), so it is resolved the same way as navigation.
29
+ */
30
+ export declare function mintlifyRedirects(source: string): Record<string, string>;