@dogsbay/format-docusaurus 0.2.0-beta.78

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,70 @@
1
+ /**
2
+ * Docusaurus site adapter — extension points for components and conventions
3
+ * that a specific site layered on top of *standard* Docusaurus.
4
+ *
5
+ * The generic parser handles only built-in Docusaurus syntax (admonitions,
6
+ * Tabs/TabItem, details, CodeBlock, DocCardList, frontmatter, sidebars).
7
+ * Anything a site invented — custom MDX components, a variable-substitution
8
+ * plugin, partial includes — lives in a separate adapter package and is
9
+ * injected via `--adapter`.
10
+ *
11
+ * Usage:
12
+ * docusaurusToTree(source) // generic only
13
+ * docusaurusToTree(source, { adapter: tigera }) // generic + Tigera
14
+ */
15
+ export interface ComponentMapping {
16
+ /** TreeNode type to produce (e.g. "card", "callout", "badge"). */
17
+ type: string;
18
+ /** Extract props from JSX attributes. */
19
+ propsFromAttrs?: (attrs: Record<string, string>) => Record<string, unknown>;
20
+ /** Whether this component wraps children. */
21
+ container: boolean;
22
+ /** Strip the component entirely. If container=true, inner content is kept. */
23
+ strip?: boolean;
24
+ }
25
+ export interface PreprocessContext {
26
+ /** Absolute path of the source file being parsed (for partial resolution). */
27
+ filePath?: string;
28
+ /** Absolute path of the docs instance directory (e.g. .../calico). */
29
+ instanceDir?: string;
30
+ /**
31
+ * Absolute path of the Docusaurus project root (where docusaurus.config.js
32
+ * lives). Lets adapters resolve `@site/...` import aliases when inlining
33
+ * partials.
34
+ */
35
+ projectDir?: string;
36
+ /**
37
+ * How partials are handled (mirrors AsciiDoc's `inlineIncludes`):
38
+ * - "inline" (default) — partial bodies spliced into each page verbatim.
39
+ * - "include" — emit `{% include "…" %}` directives + collect partials as
40
+ * fragments (resolved at build time by the Minja preprocessor).
41
+ */
42
+ partialMode?: "inline" | "include";
43
+ /**
44
+ * Fragment collector for `partialMode: "include"`. Maps a resolved partial
45
+ * source path → its include path (relative to the content root). The importer
46
+ * drains this after parsing to emit fragment `.md` files. Adapters populate it
47
+ * when they emit `{% include %}`.
48
+ */
49
+ fragments?: Map<string, string>;
50
+ }
51
+ export interface DocusaurusAdapter {
52
+ /** Adapter name (e.g. "tigera"). */
53
+ name: string;
54
+ /**
55
+ * Raw-text rewrites applied to the MDX source BEFORE any parsing. This is
56
+ * where non-standard preprocessing lives — e.g. Tigera's `$[variable]`
57
+ * substitution (a custom remark plugin, not standard Docusaurus) and
58
+ * `_includes/` partial inlining. Inlined content gets full parser treatment.
59
+ */
60
+ preprocess?: (source: string, ctx: PreprocessContext) => string;
61
+ /** Block-level component mappings (components on their own line). */
62
+ components?: Record<string, ComponentMapping>;
63
+ /** Self-closing tags (no closing tag expected). */
64
+ selfClosingTags?: string[];
65
+ /**
66
+ * Inline component transformations applied to source text before parsing.
67
+ * Each entry: [regex, replacement string or function].
68
+ */
69
+ inlineTransforms?: [RegExp, string | ((substring: string, ...args: string[]) => string)][];
70
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Docusaurus site adapter — extension points for components and conventions
3
+ * that a specific site layered on top of *standard* Docusaurus.
4
+ *
5
+ * The generic parser handles only built-in Docusaurus syntax (admonitions,
6
+ * Tabs/TabItem, details, CodeBlock, DocCardList, frontmatter, sidebars).
7
+ * Anything a site invented — custom MDX components, a variable-substitution
8
+ * plugin, partial includes — lives in a separate adapter package and is
9
+ * injected via `--adapter`.
10
+ *
11
+ * Usage:
12
+ * docusaurusToTree(source) // generic only
13
+ * docusaurusToTree(source, { adapter: tigera }) // generic + Tigera
14
+ */
15
+ export {};
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { FormatPlugin } from "@dogsbay/types";
2
+ export declare const plugin: FormatPlugin;
package/dist/cli.js ADDED
@@ -0,0 +1,246 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join, relative, isAbsolute } from "node:path";
3
+ import { parseMeta } from "@dogsbay/types";
4
+ import { extractFrontmatter } from "./frontmatter.js";
5
+ import { docusaurusToTree } from "./parse-mdx.js";
6
+ import { findConfig, parseDocusaurusConfig, selectInstance } from "./config.js";
7
+ import { buildNav, makeDocHref, makeDocLabel } from "./nav.js";
8
+ /**
9
+ * Detect a Docusaurus project: a docusaurus.config.* at the root, or a
10
+ * sidebars*.js alongside docs content.
11
+ */
12
+ function detectDocusaurusSource(path) {
13
+ if (findConfig(path))
14
+ return true;
15
+ try {
16
+ const entries = readdirSync(path);
17
+ if (entries.some((e) => /^sidebars.*\.(js|ts|cjs|mjs)$/.test(e)))
18
+ return true;
19
+ }
20
+ catch {
21
+ // not a directory
22
+ }
23
+ return false;
24
+ }
25
+ /** Collect doc files in an instance dir, skipping `_`-prefixed files/dirs. */
26
+ function walkDocFiles(dir, rootDir) {
27
+ const results = [];
28
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
29
+ if (entry.name.startsWith("_"))
30
+ continue; // _includes, _category_.json, partials
31
+ const fullPath = join(dir, entry.name);
32
+ if (entry.isDirectory()) {
33
+ results.push(...walkDocFiles(fullPath, rootDir));
34
+ }
35
+ else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
36
+ const slug = relative(rootDir, fullPath)
37
+ .replace(/\\/g, "/")
38
+ .replace(/\.mdx?$/, "")
39
+ .replace(/\/index$/, "") || "index";
40
+ results.push({ filePath: fullPath, slug });
41
+ }
42
+ }
43
+ return results;
44
+ }
45
+ async function importDocusaurus(source, opts) {
46
+ const projectDir = source;
47
+ // Default to root ("") — the platform's default basePath is now host-root
48
+ // (plans/default-basepath-root.md), so pages emit at /<slug>, not /docs/<slug>.
49
+ // `site build` threads the real basePath via opts.hrefPrefix; only the
50
+ // prefix-less `convert` path falls back to this default.
51
+ const hrefPrefix = typeof opts.hrefPrefix === "string" ? opts.hrefPrefix : "";
52
+ // The CLI resolves `--adapter <name>` to an adapter object and threads it here,
53
+ // keeping this generic package free of any site-specific imports.
54
+ const adapter = opts.adapterInstance;
55
+ // Partial handling: "inline" (default) splices bodies in; "include" emits
56
+ // {% include %} directives + collects partials as fragment pages (mirrors the
57
+ // AsciiDoc importer). Structural mode targets dogsbay-md, where the directives
58
+ // stay live for `site build`'s Minja preprocessor to resolve.
59
+ const partialMode = opts.partialMode === "include" ? "include" : "inline";
60
+ if (partialMode === "include" && opts.to && opts.to !== "dogsbay-md") {
61
+ throw new Error(`--partial-mode include requires --to dogsbay-md (the directives must stay ` +
62
+ `live for \`site build\` to resolve). Got --to ${opts.to}.`);
63
+ }
64
+ // resolvedPartialPath → include path; drained into fragment pages after parsing.
65
+ const fragments = new Map();
66
+ // Resolve which docs instance to import.
67
+ const configPath = findConfig(projectDir);
68
+ let instanceDir = projectDir;
69
+ let routeBasePath = "docs";
70
+ let sidebarPath = null;
71
+ if (configPath) {
72
+ const instances = parseDocusaurusConfig(configPath);
73
+ const instance = selectInstance(instances, opts.instance);
74
+ instanceDir = isAbsolute(instance.path) ? instance.path : join(projectDir, instance.path);
75
+ routeBasePath = instance.routeBasePath;
76
+ if (instance.sidebarPath) {
77
+ sidebarPath = isAbsolute(instance.sidebarPath)
78
+ ? instance.sidebarPath
79
+ : join(projectDir, instance.sidebarPath);
80
+ }
81
+ console.log(`Docs instance: ${instance.id} (path: ${instance.path}, route: /${routeBasePath})`);
82
+ }
83
+ else {
84
+ // No config — treat the source itself as a docs dir; look for a sibling sidebar.
85
+ for (const name of readdirSync(projectDir)) {
86
+ if (/^sidebars.*\.(js|ts|cjs|mjs)$/.test(name)) {
87
+ sidebarPath = join(projectDir, name);
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ if (!existsSync(instanceDir)) {
93
+ throw new Error(`Docs instance directory not found: ${instanceDir}`);
94
+ }
95
+ // Docusaurus serves the project's `static/` directory at the site root
96
+ // (`static/img/foo.svg` → `/img/foo.svg`). Dogsbay's standard is the
97
+ // `content/_assets/` convention (docs/images.md) — content imagery rooted at
98
+ // `/_assets/...`, with `public/` reserved for site-level files. So we map
99
+ // Docusaurus static → `_assets`: signal the dir for the exporter to copy into
100
+ // `_assets/`, and the parser rewrites image srcs `/img/…` → `/_assets/img/…`.
101
+ // Threaded through opts so convert + site-build flows pick it up.
102
+ const staticDir = join(projectDir, "static");
103
+ if (existsSync(staticDir)) {
104
+ const existing = Array.isArray(opts.assetMounts)
105
+ ? opts.assetMounts
106
+ : [];
107
+ opts.assetMounts = [...existing, { dir: staticDir }];
108
+ }
109
+ // When a static dir is mounted into _assets, root-absolute image srcs are
110
+ // rewritten `/img/x` → `/_assets/img/x` (the Dogsbay convention).
111
+ const assetPrefix = opts.assetMounts ? "/_assets" : undefined;
112
+ // Resolve a doc id (e.g. from <DocCardLink docId=…>) to its href + title.
113
+ const docHref = makeDocHref(hrefPrefix);
114
+ const docLabel = makeDocLabel(instanceDir);
115
+ const resolveDoc = (docId) => ({ href: docHref(docId), title: docLabel(docId) });
116
+ // Build nav up front so DocCardList (which renders the current page's sidebar
117
+ // category as a card grid) can resolve a page's category children. Map each
118
+ // clickable category's href → its child nav items.
119
+ const nav = await buildNav({ instanceDir, hrefPrefix, sidebarPath });
120
+ const docCardListMap = new Map();
121
+ const collectCategories = (items) => {
122
+ for (const it of items) {
123
+ if (it.children && it.children.length > 0) {
124
+ if (it.href)
125
+ docCardListMap.set(it.href, it.children);
126
+ collectCategories(it.children);
127
+ }
128
+ }
129
+ };
130
+ collectCategories(nav);
131
+ const resolveDocCardList = (slug) => docCardListMap.get(docHref(slug));
132
+ // Parse pages.
133
+ const docFiles = walkDocFiles(instanceDir, instanceDir);
134
+ const taxonomyNames = Array.isArray(opts.taxonomyNames)
135
+ ? opts.taxonomyNames
136
+ : undefined;
137
+ const warnings = new Map();
138
+ const onWarn = (msg) => warnings.set(msg, (warnings.get(msg) ?? 0) + 1);
139
+ const pages = [];
140
+ for (const { filePath, slug } of docFiles) {
141
+ const content = readFileSync(filePath, "utf-8");
142
+ const { frontmatter, raw, body } = extractFrontmatter(content);
143
+ if (frontmatter.draft)
144
+ continue;
145
+ const tree = docusaurusToTree(content, {
146
+ adapter,
147
+ routeBasePath,
148
+ hrefPrefix,
149
+ assetPrefix,
150
+ currentSlug: slug,
151
+ resolveDoc,
152
+ resolveDocCardList,
153
+ preprocessContext: { filePath, instanceDir, projectDir, partialMode, fragments },
154
+ onWarn,
155
+ });
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
+ const title = frontmatter.title ||
164
+ headingTitle(body) ||
165
+ slug.split("/").pop() ||
166
+ slug;
167
+ pages.push({
168
+ slug,
169
+ title,
170
+ tree,
171
+ headings,
172
+ frontmatter: raw,
173
+ meta: parseMeta(raw, { slug, taxonomyNames }),
174
+ });
175
+ }
176
+ // Drain collected fragments (partialMode "include" only). Each referenced
177
+ // partial is converted the same way — recursively, since a fragment may
178
+ // reference more partials — and emitted as a frontmatter-less fragment page
179
+ // at its include path. The exporter writes these as plain `.md` include
180
+ // targets; they never enter nav. Mirrors migrate-asciidoc's fragments.
181
+ const processed = new Set();
182
+ while (fragments.size > processed.size) {
183
+ for (const [resolvedPath, includePath] of [...fragments]) {
184
+ if (processed.has(resolvedPath))
185
+ continue;
186
+ processed.add(resolvedPath);
187
+ if (!existsSync(resolvedPath))
188
+ continue;
189
+ const fragContent = readFileSync(resolvedPath, "utf-8");
190
+ const fragTree = docusaurusToTree(fragContent, {
191
+ adapter,
192
+ routeBasePath,
193
+ hrefPrefix,
194
+ assetPrefix,
195
+ currentSlug: includePath.replace(/\.md$/, ""),
196
+ resolveDoc,
197
+ resolveDocCardList,
198
+ preprocessContext: {
199
+ filePath: resolvedPath,
200
+ instanceDir,
201
+ projectDir,
202
+ partialMode,
203
+ fragments,
204
+ },
205
+ onWarn,
206
+ });
207
+ pages.push({
208
+ slug: includePath.replace(/\.md$/, ""),
209
+ title: "",
210
+ tree: fragTree,
211
+ headings: [],
212
+ frontmatter: {},
213
+ fragment: true,
214
+ });
215
+ }
216
+ }
217
+ // Infer site name from the index page if not provided.
218
+ if (!opts.siteName && pages.length > 0) {
219
+ const indexPage = pages.find((p) => p.slug === "index" || p.slug === "");
220
+ if (indexPage)
221
+ opts.siteName = indexPage.title;
222
+ }
223
+ // Summarize warnings once.
224
+ for (const [msg, count] of warnings) {
225
+ console.warn(`Warning (${count}×): ${msg}`);
226
+ }
227
+ console.log(`Parsed ${pages.length} pages`);
228
+ return { pages, nav };
229
+ }
230
+ function headingTitle(body) {
231
+ const m = body.match(/^#\s+(.+?)\s*$/m);
232
+ return m ? m[1].trim() : undefined;
233
+ }
234
+ export const plugin = {
235
+ name: "docusaurus",
236
+ canImport: true,
237
+ canExport: false,
238
+ detectSource: detectDocusaurusSource,
239
+ importOptions: [
240
+ { flags: "--instance <id>", description: "plugin-content-docs instance id to import (e.g. calico)" },
241
+ { flags: "--adapter <name>", description: "Site adapter for custom components (e.g. tigera)" },
242
+ ],
243
+ async import(source, opts) {
244
+ return importDocusaurus(source, opts);
245
+ },
246
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Parse a Docusaurus fenced-code metastring.
3
+ *
4
+ * Docusaurus extends the info string after the language with:
5
+ * - title="..." or title='...' → code block title
6
+ * - {1,4-6,11} → highlighted line ranges
7
+ * - showLineNumbers → render line numbers
8
+ *
9
+ * Example: ```jsx title="src/foo.js" {1,4-6} showLineNumbers
10
+ *
11
+ * Docs: https://docusaurus.io/docs/markdown-features/code-blocks
12
+ */
13
+ export interface CodeMeta {
14
+ lang: string;
15
+ title?: string;
16
+ /** Expanded list of highlighted line numbers (1-based). */
17
+ highlights?: number[];
18
+ showLineNumbers?: boolean;
19
+ }
20
+ export declare function parseCodeMeta(info: string): CodeMeta;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Parse a Docusaurus fenced-code metastring.
3
+ *
4
+ * Docusaurus extends the info string after the language with:
5
+ * - title="..." or title='...' → code block title
6
+ * - {1,4-6,11} → highlighted line ranges
7
+ * - showLineNumbers → render line numbers
8
+ *
9
+ * Example: ```jsx title="src/foo.js" {1,4-6} showLineNumbers
10
+ *
11
+ * Docs: https://docusaurus.io/docs/markdown-features/code-blocks
12
+ */
13
+ /** Expand a range spec like "1,4-6,11" into [1,4,5,6,11]. */
14
+ function expandRanges(spec) {
15
+ const out = [];
16
+ for (const part of spec.split(",")) {
17
+ const trimmed = part.trim();
18
+ if (!trimmed)
19
+ continue;
20
+ const range = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
21
+ if (range) {
22
+ const start = parseInt(range[1], 10);
23
+ const end = parseInt(range[2], 10);
24
+ const [lo, hi] = start <= end ? [start, end] : [end, start];
25
+ for (let n = lo; n <= hi; n++)
26
+ out.push(n);
27
+ }
28
+ else if (/^\d+$/.test(trimmed)) {
29
+ out.push(parseInt(trimmed, 10));
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+ export function parseCodeMeta(info) {
35
+ const raw = (info ?? "").trim();
36
+ if (!raw)
37
+ return { lang: "plaintext" };
38
+ // Language is the first whitespace-delimited token.
39
+ const lang = raw.split(/\s+/)[0] || "plaintext";
40
+ const rest = raw.slice(lang.length);
41
+ const titleMatch = rest.match(/title=(?:"([^"]*)"|'([^']*)')/);
42
+ const title = titleMatch ? (titleMatch[1] ?? titleMatch[2]) : undefined;
43
+ const highlightMatch = rest.match(/\{([\d\s,-]+)\}/);
44
+ const highlights = highlightMatch ? expandRanges(highlightMatch[1]) : undefined;
45
+ const showLineNumbers = /\bshowLineNumbers\b/.test(rest) || undefined;
46
+ const meta = { lang };
47
+ if (title)
48
+ meta.title = title;
49
+ if (highlights && highlights.length)
50
+ meta.highlights = highlights;
51
+ if (showLineNumbers)
52
+ meta.showLineNumbers = true;
53
+ return meta;
54
+ }
@@ -0,0 +1,18 @@
1
+ export interface DocsInstance {
2
+ /** Plugin instance id (e.g. "calico"); "default" for the preset docs. */
3
+ id: string;
4
+ /** Source directory relative to the project root (e.g. "calico"). */
5
+ path: string;
6
+ /** URL route base (e.g. "calico"). Defaults to "docs" when unset. */
7
+ routeBasePath: string;
8
+ /** Sidebar module path relative to the project root, or null if disabled. */
9
+ sidebarPath: string | null;
10
+ }
11
+ /** Find the docusaurus config file in a project directory. */
12
+ export declare function findConfig(projectDir: string): string | null;
13
+ export declare function parseDocusaurusConfig(configPath: string): DocsInstance[];
14
+ /**
15
+ * Resolve a single instance by id, or the sole instance when there is exactly
16
+ * one usable one. Throws a helpful error otherwise.
17
+ */
18
+ export declare function selectInstance(instances: DocsInstance[], requestedId?: string): DocsInstance;
package/dist/config.js ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Extract plugin-content-docs instances from a `docusaurus.config.js`/`.ts`.
3
+ *
4
+ * We STATICALLY PARSE the config rather than executing it. A real Docusaurus
5
+ * config commonly imports theme packages (prism-react-renderer), remark
6
+ * plugins, and may be an async function — executing it would require the source
7
+ * repo's node_modules and run arbitrary code. The instance metadata we need
8
+ * (id / path / routeBasePath / sidebarPath) are simple string literals, so a
9
+ * lightweight brace-aware scan is both more robust and safer.
10
+ *
11
+ * Recognised instance sources:
12
+ * - preset `docs: { path, routeBasePath, sidebarPath }` (id defaults to "default")
13
+ * - each `['@docusaurus/plugin-content-docs', { id, path, routeBasePath, sidebarPath }]`
14
+ */
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ const CONFIG_NAMES = [
18
+ "docusaurus.config.js",
19
+ "docusaurus.config.ts",
20
+ "docusaurus.config.mjs",
21
+ "docusaurus.config.cjs",
22
+ ];
23
+ /** Find the docusaurus config file in a project directory. */
24
+ export function findConfig(projectDir) {
25
+ for (const name of CONFIG_NAMES) {
26
+ const p = join(projectDir, name);
27
+ if (existsSync(p))
28
+ return p;
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Strip `//` and block comments while preserving string/template literals.
34
+ * Necessary because Docusaurus configs annotate instances with JSDoc comments
35
+ * like `/** @type {import('...').Options} *​/` whose braces would otherwise be
36
+ * mistaken for the object literal.
37
+ */
38
+ function stripJsComments(src) {
39
+ let out = "";
40
+ let i = 0;
41
+ let inStr = null;
42
+ while (i < src.length) {
43
+ const ch = src[i];
44
+ const next = src[i + 1];
45
+ if (inStr) {
46
+ out += ch;
47
+ if (ch === "\\") {
48
+ out += next ?? "";
49
+ i += 2;
50
+ continue;
51
+ }
52
+ if (ch === inStr)
53
+ inStr = null;
54
+ i++;
55
+ continue;
56
+ }
57
+ if (ch === '"' || ch === "'" || ch === "`") {
58
+ inStr = ch;
59
+ out += ch;
60
+ i++;
61
+ continue;
62
+ }
63
+ if (ch === "/" && next === "/") {
64
+ while (i < src.length && src[i] !== "\n")
65
+ i++;
66
+ continue;
67
+ }
68
+ if (ch === "/" && next === "*") {
69
+ i += 2;
70
+ while (i < src.length && !(src[i] === "*" && src[i + 1] === "/"))
71
+ i++;
72
+ i += 2;
73
+ continue;
74
+ }
75
+ out += ch;
76
+ i++;
77
+ }
78
+ return out;
79
+ }
80
+ /** Extract the `{...}` object literal that follows `fromIndex` (balanced braces). */
81
+ function extractObjectAfter(src, fromIndex) {
82
+ const open = src.indexOf("{", fromIndex);
83
+ if (open === -1)
84
+ return null;
85
+ let depth = 0;
86
+ let inStr = null;
87
+ for (let i = open; i < src.length; i++) {
88
+ const ch = src[i];
89
+ const prev = src[i - 1];
90
+ if (inStr) {
91
+ if (ch === inStr && prev !== "\\")
92
+ inStr = null;
93
+ continue;
94
+ }
95
+ if (ch === '"' || ch === "'" || ch === "`") {
96
+ inStr = ch;
97
+ continue;
98
+ }
99
+ if (ch === "{")
100
+ depth++;
101
+ else if (ch === "}") {
102
+ depth--;
103
+ if (depth === 0)
104
+ return src.slice(open, i + 1);
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ /** Read a top-level `key: 'value'` string literal from an object-literal source. */
110
+ function readStringKey(objSrc, key) {
111
+ const re = new RegExp(`\\b${key}\\s*:\\s*(?:"([^"]*)"|'([^']*)'|\`([^\`]*)\`)`);
112
+ const m = objSrc.match(re);
113
+ if (!m)
114
+ return undefined;
115
+ return m[1] ?? m[2] ?? m[3];
116
+ }
117
+ /** Detect `key: false` (e.g. `sidebarPath: false`). */
118
+ function isFalseKey(objSrc, key) {
119
+ return new RegExp(`\\b${key}\\s*:\\s*false\\b`).test(objSrc);
120
+ }
121
+ function toInstance(objSrc, idFallback) {
122
+ const path = readStringKey(objSrc, "path");
123
+ // The preset `docs` block has no plugin id; plugins carry their own id.
124
+ const id = readStringKey(objSrc, "id") ?? idFallback;
125
+ const routeBasePath = readStringKey(objSrc, "routeBasePath") ?? path ?? "docs";
126
+ const sidebarPath = isFalseKey(objSrc, "sidebarPath")
127
+ ? null
128
+ : readStringKey(objSrc, "sidebarPath") ?? null;
129
+ if (!path)
130
+ return null;
131
+ return { id, path, routeBasePath, sidebarPath };
132
+ }
133
+ export function parseDocusaurusConfig(configPath) {
134
+ const src = stripJsComments(readFileSync(configPath, "utf-8"));
135
+ const instances = [];
136
+ // Each plugin-content-docs entry: ['@docusaurus/plugin-content-docs', { ... }]
137
+ const pluginRe = /['"]@docusaurus\/plugin-content-docs['"]\s*,/g;
138
+ let m;
139
+ while ((m = pluginRe.exec(src)) !== null) {
140
+ const objSrc = extractObjectAfter(src, m.index + m[0].length);
141
+ if (!objSrc)
142
+ continue;
143
+ const inst = toInstance(objSrc, "default");
144
+ if (inst)
145
+ instances.push(inst);
146
+ }
147
+ // Preset docs instance: `docs: { ... }` (id defaults to "default").
148
+ const docsKey = src.match(/\bdocs\s*:\s*\{/);
149
+ if (docsKey) {
150
+ const objSrc = extractObjectAfter(src, docsKey.index);
151
+ if (objSrc) {
152
+ const inst = toInstance(objSrc, "default");
153
+ // Only include if it has a real path (skip `docs: false` etc.).
154
+ if (inst && inst.path && inst.path !== "default") {
155
+ instances.push(inst);
156
+ }
157
+ else if (inst && inst.path === "default") {
158
+ // Tigera's placeholder default instance — keep it so callers can see it.
159
+ instances.push(inst);
160
+ }
161
+ }
162
+ }
163
+ return instances;
164
+ }
165
+ /**
166
+ * Resolve a single instance by id, or the sole instance when there is exactly
167
+ * one usable one. Throws a helpful error otherwise.
168
+ */
169
+ export function selectInstance(instances, requestedId) {
170
+ const usable = instances.filter((i) => i.path && i.path !== "default");
171
+ if (requestedId) {
172
+ const found = instances.find((i) => i.id === requestedId);
173
+ if (!found) {
174
+ const ids = instances.map((i) => i.id).join(", ");
175
+ throw new Error(`No plugin-content-docs instance with id "${requestedId}". Available: ${ids || "(none)"}`);
176
+ }
177
+ return found;
178
+ }
179
+ if (usable.length === 1)
180
+ return usable[0];
181
+ const ids = usable.map((i) => i.id).join(", ");
182
+ throw new Error(`Multiple docs instances found (${ids}). Pass --instance <id> to choose one.`);
183
+ }
@@ -0,0 +1,35 @@
1
+ export interface DocusaurusFrontmatter {
2
+ /** Explicit doc id (overrides the file-path-derived id). */
3
+ id?: string;
4
+ /** Page title. Falls back to the first H1 when absent. */
5
+ title?: string;
6
+ /** Custom URL segment(s) for this page. */
7
+ slug?: string;
8
+ /** Sort weight within its sidebar category (lower = earlier). */
9
+ sidebar_position?: number;
10
+ /** Sidebar display label (overrides title in the sidebar). */
11
+ sidebar_label?: string;
12
+ /** Extra class on the sidebar item. */
13
+ sidebar_class_name?: string;
14
+ /** Hide this page from the autogenerated sidebar. */
15
+ sidebar_custom_props?: Record<string, unknown>;
16
+ /** Meta description. */
17
+ description?: string;
18
+ /** Suppress the auto-rendered H1 title. */
19
+ hide_title?: boolean;
20
+ /** Exclude from production builds. */
21
+ draft?: boolean;
22
+ /** Tags. */
23
+ tags?: unknown;
24
+ /** Pagination label override. */
25
+ pagination_label?: string;
26
+ }
27
+ export interface ExtractedFrontmatter {
28
+ /** Typed subset of well-known fields. */
29
+ frontmatter: DocusaurusFrontmatter;
30
+ /** Full YAML as a flat record — includes all custom fields. */
31
+ raw: Record<string, unknown>;
32
+ /** Source with the frontmatter block removed. */
33
+ body: string;
34
+ }
35
+ export declare function extractFrontmatter(source: string): ExtractedFrontmatter;