@dogsbay/format-mdx 0.2.0-beta.13 → 0.2.0-beta.130
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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static sidebar extraction from a hardcoded Fumadocs page tree.
|
|
3
|
+
*
|
|
4
|
+
* Passing a hand-built `PageTree.Root` to `DocsLayout tree={…}` is a
|
|
5
|
+
* documented first-class Fumadocs pattern; sites that use it (better-auth's
|
|
6
|
+
* sidebar-content.tsx) keep the REAL sidebar in a TSX data literal and let
|
|
7
|
+
* meta.json go stale. Page trees must be serializable by design, so the
|
|
8
|
+
* literal is statically readable: this module captures the exported array
|
|
9
|
+
* and maps it to NavItem[] — no execution, ever (icons and other JSX
|
|
10
|
+
* values ride through the balanced scanner and are dropped).
|
|
11
|
+
*
|
|
12
|
+
* The mapping is shape-tolerant rather than better-auth-specific: an entry
|
|
13
|
+
* contributes a nav item when it has a string `title`/`name`/`label`;
|
|
14
|
+
* `href`/`url` become the link; the first array-valued key among
|
|
15
|
+
* `list`/`subpages`/`children`/`items`/`pages` becomes the children.
|
|
16
|
+
*
|
|
17
|
+
* Href prefix: sites hardcode their mount point (`/docs/introduction`).
|
|
18
|
+
* When every internal href shares the same first path segment it is
|
|
19
|
+
* stripped and replaced with the importer's own `hrefPrefix` (auto-detected
|
|
20
|
+
* — pass `stripPrefix` to override).
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { scanBalancedLiteral } from "./js-scan.js";
|
|
24
|
+
import { parseJsArrayElements, parseJsObjectEntries } from "./fumadocs.js";
|
|
25
|
+
const CHILD_KEYS = ["list", "subpages", "children", "items", "pages"];
|
|
26
|
+
function unquote(raw) {
|
|
27
|
+
if (!raw)
|
|
28
|
+
return undefined;
|
|
29
|
+
const m = raw.trim().match(/^(["'`])([\s\S]*)\1$/);
|
|
30
|
+
return m ? m[2] : undefined;
|
|
31
|
+
}
|
|
32
|
+
function entryToItem(objSrc) {
|
|
33
|
+
const entries = parseJsObjectEntries(objSrc);
|
|
34
|
+
if (!entries)
|
|
35
|
+
return null;
|
|
36
|
+
const o = Object.fromEntries(entries);
|
|
37
|
+
const label = unquote(o.title) ?? unquote(o.name) ?? unquote(o.label);
|
|
38
|
+
if (!label)
|
|
39
|
+
return null;
|
|
40
|
+
const item = { label };
|
|
41
|
+
const href = unquote(o.href) ?? unquote(o.url);
|
|
42
|
+
if (href)
|
|
43
|
+
item.href = href;
|
|
44
|
+
if (o.external === "true" || href?.match(/^[a-z]+:\/\//))
|
|
45
|
+
item.external = true;
|
|
46
|
+
for (const key of CHILD_KEYS) {
|
|
47
|
+
const value = o[key]?.trim();
|
|
48
|
+
if (value?.startsWith("[")) {
|
|
49
|
+
const elements = parseJsArrayElements(value);
|
|
50
|
+
if (elements) {
|
|
51
|
+
const children = elements.map(entryToItem).filter((c) => c !== null);
|
|
52
|
+
if (children.length > 0)
|
|
53
|
+
item.children = children;
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return item;
|
|
59
|
+
}
|
|
60
|
+
/** First path segment of an internal href ("/docs/x" → "/docs"). */
|
|
61
|
+
function firstSegment(href) {
|
|
62
|
+
const m = href.match(/^(\/[^/]+)(\/|$)/);
|
|
63
|
+
return m ? m[1] : null;
|
|
64
|
+
}
|
|
65
|
+
function collectInternalHrefs(items, out) {
|
|
66
|
+
for (const item of items) {
|
|
67
|
+
if (item.href && !item.external)
|
|
68
|
+
out.push(item.href);
|
|
69
|
+
if (item.children)
|
|
70
|
+
collectInternalHrefs(item.children, out);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function toNavItems(items, stripPrefix, hrefPrefix) {
|
|
74
|
+
return items.map((item) => {
|
|
75
|
+
const nav = { label: item.label };
|
|
76
|
+
if (item.href) {
|
|
77
|
+
if (item.external) {
|
|
78
|
+
nav.href = item.href;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
let path = item.href;
|
|
82
|
+
if (stripPrefix && path.startsWith(stripPrefix)) {
|
|
83
|
+
path = path.slice(stripPrefix.length) || "/";
|
|
84
|
+
}
|
|
85
|
+
nav.href = `${hrefPrefix}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (item.children) {
|
|
89
|
+
nav.children = toNavItems(item.children, stripPrefix, hrefPrefix);
|
|
90
|
+
}
|
|
91
|
+
return nav;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extract NavItem[] from TSX source containing an exported data-literal
|
|
96
|
+
* page tree. Returns null when the export is missing or doesn't scan —
|
|
97
|
+
* callers fall back to the meta.json walk (never guess).
|
|
98
|
+
*/
|
|
99
|
+
export function extractNavFromTsx(source, opts = {}) {
|
|
100
|
+
const exportName = opts.exportName ?? "contents";
|
|
101
|
+
const re = new RegExp(`export\\s+const\\s+${exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*(?::[^=]*?)?=\\s*`);
|
|
102
|
+
const m = re.exec(source);
|
|
103
|
+
if (!m)
|
|
104
|
+
return null;
|
|
105
|
+
const start = m.index + m[0].length;
|
|
106
|
+
const end = scanBalancedLiteral(source, start);
|
|
107
|
+
if (end === -1)
|
|
108
|
+
return null;
|
|
109
|
+
const elements = parseJsArrayElements(source.slice(start, end).trim());
|
|
110
|
+
if (!elements || elements.length === 0)
|
|
111
|
+
return null;
|
|
112
|
+
const items = elements.map(entryToItem).filter((i) => i !== null);
|
|
113
|
+
if (items.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
// Auto-detect the hardcoded mount prefix ("/docs") when not given.
|
|
116
|
+
let stripPrefix = opts.stripPrefix;
|
|
117
|
+
if (stripPrefix === undefined) {
|
|
118
|
+
const hrefs = [];
|
|
119
|
+
collectInternalHrefs(items, hrefs);
|
|
120
|
+
const segments = new Set(hrefs.map(firstSegment));
|
|
121
|
+
stripPrefix =
|
|
122
|
+
segments.size === 1 && !segments.has(null) ? [...segments][0] : "";
|
|
123
|
+
}
|
|
124
|
+
return toNavItems(items, stripPrefix ?? "", (opts.hrefPrefix ?? "").replace(/\/$/, ""));
|
|
125
|
+
}
|
|
126
|
+
/** File-reading convenience for the CLI layer. */
|
|
127
|
+
export function extractNavFromTsxFile(filePath, opts = {}) {
|
|
128
|
+
try {
|
|
129
|
+
return extractNavFromTsx(readFileSync(filePath, "utf-8"), opts);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/parse.d.ts
CHANGED
|
@@ -8,12 +8,108 @@
|
|
|
8
8
|
* 4. Parse markdown sections with markdown-it for inline formatting.
|
|
9
9
|
* 5. Map components to TreeNode types.
|
|
10
10
|
*
|
|
11
|
-
* The MDX served by Mintlify/Next.js uses well-formed, XML-like JSX
|
|
12
|
-
* string attributes — no complex JSX expressions. This means a
|
|
13
|
-
* tag parser works without a full JSX/AST parser.
|
|
11
|
+
* The MDX served by Mintlify/Next.js/Fumadocs uses well-formed, XML-like JSX
|
|
12
|
+
* tags with string attributes — no complex JSX expressions. This means a
|
|
13
|
+
* simple recursive tag parser works without a full JSX/AST parser.
|
|
14
|
+
*
|
|
15
|
+
* Extension points (plans/format-fumadocs.md): the component registry is
|
|
16
|
+
* open — `parseMdx(source, { adapter, components })` merges
|
|
17
|
+
* builtins < adapter < per-source config. Unknown block components are
|
|
18
|
+
* NEVER dropped: they become `mdx-raw` nodes carrying the verbatim source
|
|
19
|
+
* span plus a fidelity-ledger entry.
|
|
14
20
|
*/
|
|
15
21
|
import type { TreeNode } from "@dogsbay/types";
|
|
16
|
-
|
|
22
|
+
import type { ComponentMapping, MdxAdapter, MdxLedgerEntry } from "./adapter.js";
|
|
23
|
+
export interface MdxParseOptions {
|
|
24
|
+
/** Framework adapter (e.g. fumadocs) merged over the builtins. */
|
|
25
|
+
adapter?: MdxAdapter;
|
|
26
|
+
/** Per-source component mappings merged over builtins + adapter. */
|
|
27
|
+
components?: Record<string, ComponentMapping>;
|
|
28
|
+
/** Additional self-closing tag names. */
|
|
29
|
+
selfClosingTags?: string[];
|
|
30
|
+
/** Source file path — stamped onto ledger entries. */
|
|
31
|
+
file?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Docs root. Needed to resolve a ROOT-relative snippet import
|
|
34
|
+
* (`import X from "/snippets/foo.mdx"`) — Mintlify resolves those against
|
|
35
|
+
* the docs root, not the filesystem root.
|
|
36
|
+
*/
|
|
37
|
+
rootDir?: string;
|
|
38
|
+
/** Internal: absolute paths already being inlined, to break import cycles. */
|
|
39
|
+
inlining?: Set<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Source-declared redirects (`source` → `destination`), applied to internal
|
|
42
|
+
* links so a page that links to a path the site redirects still resolves
|
|
43
|
+
* after import. See `rewriteInternalHref`.
|
|
44
|
+
*/
|
|
45
|
+
linkRedirects?: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
export interface MdxParseResult {
|
|
48
|
+
tree: TreeNode[];
|
|
49
|
+
/** Fidelity ledger: what was preserved-as-raw or deliberately stripped. */
|
|
50
|
+
ledger: MdxLedgerEntry[];
|
|
51
|
+
/** Same-file `export const` literals captured during parsing. */
|
|
52
|
+
moduleData: Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse MDX into TreeNode[] + fidelity ledger.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseMdx(source: string, opts?: MdxParseOptions): MdxParseResult;
|
|
58
|
+
/**
|
|
59
|
+
* Back-compat convenience: parse and return only the tree.
|
|
60
|
+
* Prefer `parseMdx` — it also returns the fidelity ledger.
|
|
61
|
+
*/
|
|
62
|
+
export declare function mdxToTree(source: string, opts?: MdxParseOptions): TreeNode[];
|
|
63
|
+
/**
|
|
64
|
+
* Remove `{/* … */}` MDX comments, fence-aware.
|
|
65
|
+
*
|
|
66
|
+
* Only an expression whose ENTIRE body is a block comment is removed: `{foo /*
|
|
67
|
+
* note */}` is a real expression and must survive to the mdx-raw path. When
|
|
68
|
+
* the comment occupied a whole line, the line goes with it, so no blank
|
|
69
|
+
* paragraph is left behind.
|
|
70
|
+
*/
|
|
71
|
+
export declare function stripMdxComments(source: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Rewrite an internal link so it resolves in the IMPORTED site.
|
|
74
|
+
*
|
|
75
|
+
* Two corrections, both measured on Mintlify's own docs, where they broke two
|
|
76
|
+
* of the four cards on the front page:
|
|
77
|
+
*
|
|
78
|
+
* 1. `/dir/index` → `/dir`. A `dir/index.mdx` page is routed at `/dir` (every
|
|
79
|
+
* importer collapses it, and so does the nav), but links written to the
|
|
80
|
+
* file path kept the `/index` and 404'd. 107 links in that corpus.
|
|
81
|
+
* 2. A path that the source's own `redirects` map moves — a docs site
|
|
82
|
+
* accumulates these as pages are renamed, and its own pages go on linking
|
|
83
|
+
* to the old path because the live site serves the redirect. After import
|
|
84
|
+
* nothing serves it. 16 links, including `/installation` → `/cli/install`.
|
|
85
|
+
*
|
|
86
|
+
* Anchors and query strings are preserved, and redirect chains are followed
|
|
87
|
+
* with a seen-set so a cycle in someone's config cannot hang the import.
|
|
88
|
+
*/
|
|
89
|
+
export declare function rewriteInternalHref(href: string, redirects?: Record<string, string>): string;
|
|
90
|
+
/** Collapse the opening tag (up to and incl. the first unquoted `>`, tracking
|
|
91
|
+
* quotes across newlines) onto one line; leave content after the tag intact. */
|
|
92
|
+
/**
|
|
93
|
+
* Rewrite JSX-only attributes on a LOWERCASE HTML tag into HTML markdown-it
|
|
94
|
+
* will accept.
|
|
95
|
+
*
|
|
96
|
+
* markdown-it's HTML rules require quoted or bare attribute values, so a JSX
|
|
97
|
+
* expression attribute — `style={{ borderRadius: "12px" }}` — makes the whole
|
|
98
|
+
* tag fail to parse as HTML and it is emitted as ESCAPED TEXT. On ollama that
|
|
99
|
+
* showed a reader a literal `<img src="…" />` and lost the image: 25
|
|
100
|
+
* occurrences across 17 pages, 5 of them the page's only images.
|
|
101
|
+
*
|
|
102
|
+
* `style={{ … }}` converts (camelCase → kebab-case, the real CSS); any other
|
|
103
|
+
* expression attribute is dropped, because its value is JavaScript and there
|
|
104
|
+
* is nothing faithful to put in its place.
|
|
105
|
+
*/
|
|
106
|
+
export declare function normalizeJsxAttrs(tag: string, onDrop?: (attr: string) => void): string;
|
|
107
|
+
/**
|
|
108
|
+
* Parse a JSX attribute string. Supports `key="v"`, `key='v'`,
|
|
109
|
+
* `key={expr}` (balanced braces — `{{obj}}` keeps the inner `{obj}`),
|
|
110
|
+
* and bare boolean attributes (`requireSession` → "true").
|
|
111
|
+
*/
|
|
112
|
+
export declare function parseAttrs(attrStr: string): Record<string, string>;
|
|
17
113
|
interface Heading {
|
|
18
114
|
depth: number;
|
|
19
115
|
slug: string;
|
|
@@ -25,4 +121,3 @@ interface Heading {
|
|
|
25
121
|
*/
|
|
26
122
|
export declare function extractHeadings(nodes: TreeNode[]): Heading[];
|
|
27
123
|
export {};
|
|
28
|
-
//# sourceMappingURL=parse.d.ts.map
|