@opendocsdev/cli 0.2.0
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/LICENSE +661 -0
- package/README.md +300 -0
- package/dist/bin/opendocs.js +712 -0
- package/dist/bin/opendocs.js.map +1 -0
- package/dist/templates/api-reference.mdx +308 -0
- package/dist/templates/components.mdx +286 -0
- package/dist/templates/configuration.mdx +190 -0
- package/dist/templates/docs.json +27 -0
- package/dist/templates/introduction.mdx +25 -0
- package/dist/templates/logo.svg +4 -0
- package/dist/templates/quickstart.mdx +59 -0
- package/dist/templates/writing-content.mdx +236 -0
- package/package.json +92 -0
- package/src/engine/astro.config.ts +75 -0
- package/src/engine/src/components/Analytics.astro +57 -0
- package/src/engine/src/components/ApiPlayground.astro +24 -0
- package/src/engine/src/components/Callout.astro +66 -0
- package/src/engine/src/components/Card.astro +75 -0
- package/src/engine/src/components/CardGroup.astro +29 -0
- package/src/engine/src/components/CodeGroup.astro +231 -0
- package/src/engine/src/components/CopyButton.astro +179 -0
- package/src/engine/src/components/Steps.astro +27 -0
- package/src/engine/src/components/Tab.astro +21 -0
- package/src/engine/src/components/TableOfContents.astro +119 -0
- package/src/engine/src/components/Tabs.astro +135 -0
- package/src/engine/src/components/index.ts +107 -0
- package/src/engine/src/components/react/ApiPlayground/AuthSection.tsx +91 -0
- package/src/engine/src/components/react/ApiPlayground/CodeBlock.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CodeSnippets.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CollapsibleSection.tsx +26 -0
- package/src/engine/src/components/react/ApiPlayground/KeyValueEditor.tsx +58 -0
- package/src/engine/src/components/react/ApiPlayground/ResponseDisplay.tsx +109 -0
- package/src/engine/src/components/react/ApiPlayground/Spinner.tsx +6 -0
- package/src/engine/src/components/react/ApiPlayground/constants.ts +16 -0
- package/src/engine/src/components/react/ApiPlayground/generators.test.ts +130 -0
- package/src/engine/src/components/react/ApiPlayground/generators.ts +75 -0
- package/src/engine/src/components/react/ApiPlayground/index.tsx +490 -0
- package/src/engine/src/components/react/ApiPlayground/types.ts +35 -0
- package/src/engine/src/components/react/Callout.tsx +54 -0
- package/src/engine/src/components/react/Card.tsx +48 -0
- package/src/engine/src/components/react/CardGroup.tsx +24 -0
- package/src/engine/src/components/react/FeedbackWidget.tsx +166 -0
- package/src/engine/src/components/react/GitHubLink.tsx +28 -0
- package/src/engine/src/components/react/NavigationCard.tsx +53 -0
- package/src/engine/src/components/react/PageActions.tsx +124 -0
- package/src/engine/src/components/react/PageFooter.tsx +91 -0
- package/src/engine/src/components/react/SearchModal.tsx +358 -0
- package/src/engine/src/components/react/SearchProvider.tsx +37 -0
- package/src/engine/src/components/react/Sidebar.tsx +369 -0
- package/src/engine/src/components/react/SidebarSearchTrigger.tsx +57 -0
- package/src/engine/src/components/react/Steps.tsx +25 -0
- package/src/engine/src/components/react/ThemeToggle.tsx +72 -0
- package/src/engine/src/components/react/index.ts +14 -0
- package/src/engine/src/env.d.ts +10 -0
- package/src/engine/src/layouts/DocsLayout.astro +357 -0
- package/src/engine/src/lib/__tests__/markdown.test.ts +124 -0
- package/src/engine/src/lib/__tests__/mdx-loader.test.ts +205 -0
- package/src/engine/src/lib/config.ts +79 -0
- package/src/engine/src/lib/markdown.ts +54 -0
- package/src/engine/src/lib/mdx-loader.ts +143 -0
- package/src/engine/src/lib/mdx-utils.ts +72 -0
- package/src/engine/src/lib/remark-opendocs.ts +195 -0
- package/src/engine/src/lib/utils.ts +221 -0
- package/src/engine/src/pages/[...slug].astro +115 -0
- package/src/engine/src/pages/index.astro +71 -0
- package/src/engine/src/scripts/mobile-sidebar.ts +56 -0
- package/src/engine/src/scripts/theme-init.ts +25 -0
- package/src/engine/src/styles/global.css +703 -0
- package/src/engine/tailwind.config.mjs +60 -0
- package/src/engine/tsconfig.json +15 -0
- package/src/templates/api-reference.mdx +308 -0
- package/src/templates/components.mdx +286 -0
- package/src/templates/configuration.mdx +190 -0
- package/src/templates/docs.json +27 -0
- package/src/templates/introduction.mdx +25 -0
- package/src/templates/logo.svg +4 -0
- package/src/templates/quickstart.mdx +59 -0
- package/src/templates/writing-content.mdx +236 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { DocsConfig } from "../../../config/schema.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the project directory from environment variable or fallback to cwd
|
|
7
|
+
*/
|
|
8
|
+
export function getProjectDir(): string {
|
|
9
|
+
return process.env.OPENDOCS_PROJECT_DIR || process.cwd();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Cache for loaded configuration to avoid repeated file reads
|
|
13
|
+
let cachedConfig: DocsConfig | null = null;
|
|
14
|
+
let cachedConfigPath: string | null = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load and parse the docs.json configuration file from the project directory.
|
|
18
|
+
* Results are cached to prevent re-reading on every component render.
|
|
19
|
+
* Caching is disabled in development mode to allow hot-reloading of config changes.
|
|
20
|
+
*/
|
|
21
|
+
export async function loadConfig(): Promise<DocsConfig> {
|
|
22
|
+
const projectDir = getProjectDir();
|
|
23
|
+
const configPath = path.join(projectDir, "docs.json");
|
|
24
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
25
|
+
|
|
26
|
+
// Return cached config if available, path hasn't changed, and not in dev mode
|
|
27
|
+
if (cachedConfig && cachedConfigPath === configPath && !isDev) {
|
|
28
|
+
return cachedConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const configContent = await fs.readFile(configPath, "utf-8");
|
|
33
|
+
cachedConfig = JSON.parse(configContent) as DocsConfig;
|
|
34
|
+
cachedConfigPath = configPath;
|
|
35
|
+
return cachedConfig;
|
|
36
|
+
} catch {
|
|
37
|
+
// Return minimal default config if docs.json is not found
|
|
38
|
+
const defaultConfig: DocsConfig = {
|
|
39
|
+
name: "Documentation",
|
|
40
|
+
navigation: [],
|
|
41
|
+
snippets: ["snippets"],
|
|
42
|
+
};
|
|
43
|
+
cachedConfig = defaultConfig;
|
|
44
|
+
cachedConfigPath = configPath;
|
|
45
|
+
return defaultConfig;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clear the config cache. Useful for development/hot-reload scenarios.
|
|
51
|
+
*/
|
|
52
|
+
export function clearConfigCache(): void {
|
|
53
|
+
cachedConfig = null;
|
|
54
|
+
cachedConfigPath = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the path to the user's content directory
|
|
59
|
+
*/
|
|
60
|
+
export function getContentDir(): string {
|
|
61
|
+
return getProjectDir();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the path to the user's public assets directory
|
|
66
|
+
*/
|
|
67
|
+
export function getPublicDir(): string {
|
|
68
|
+
return path.join(getProjectDir(), "public");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the path to an MDX file in the user's project
|
|
73
|
+
*/
|
|
74
|
+
export function getMdxPath(pagePath: string): string {
|
|
75
|
+
const projectDir = getProjectDir();
|
|
76
|
+
// Add .mdx extension if not present
|
|
77
|
+
const fileName = pagePath.endsWith(".mdx") ? pagePath : `${pagePath}.mdx`;
|
|
78
|
+
return path.join(projectDir, fileName);
|
|
79
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface Frontmatter {
|
|
2
|
+
title?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
[key: string]: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Heading {
|
|
8
|
+
depth: number;
|
|
9
|
+
slug: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse frontmatter from markdown/MDX content
|
|
15
|
+
*/
|
|
16
|
+
export function parseFrontmatter(content: string): {
|
|
17
|
+
frontmatter: Frontmatter;
|
|
18
|
+
body: string;
|
|
19
|
+
} {
|
|
20
|
+
// Match frontmatter: starts with ---, optional content, ends with ---
|
|
21
|
+
const match = content.match(/^---[\r\n]+([\s\S]*?)[\r\n]*---[\r\n]*/);
|
|
22
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
23
|
+
|
|
24
|
+
const fm: Frontmatter = {};
|
|
25
|
+
const frontmatterContent = match[1].trim();
|
|
26
|
+
if (frontmatterContent) {
|
|
27
|
+
for (const line of frontmatterContent.split("\n")) {
|
|
28
|
+
const m = line.match(/^(\w+):\s*["']?(.+?)["']?\s*$/);
|
|
29
|
+
if (m) fm[m[1]] = m[2];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { frontmatter: fm, body: content.slice(match[0].length).trim() };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract headings from markdown content for table of contents
|
|
37
|
+
*/
|
|
38
|
+
export function extractHeadings(content: string): Heading[] {
|
|
39
|
+
const headings: Heading[] = [];
|
|
40
|
+
const regex = /^(#{2,6})\s+(.+)$/gm;
|
|
41
|
+
let m;
|
|
42
|
+
while ((m = regex.exec(content)) !== null) {
|
|
43
|
+
const text = m[2].trim();
|
|
44
|
+
headings.push({
|
|
45
|
+
depth: m[1].length,
|
|
46
|
+
text,
|
|
47
|
+
slug: text
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
50
|
+
.replace(/(^-|-$)/g, ""),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return headings;
|
|
54
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MDX Loader - Dynamically imports MDX files from user's content directory
|
|
3
|
+
* Uses the @content Vite alias which points to OPENDOCS_PROJECT_DIR
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AstroComponentFactory } from "astro/runtime/server/index.js";
|
|
7
|
+
import {
|
|
8
|
+
isSnippetPath as isSnippetPathUtil,
|
|
9
|
+
extractSlugFromPath as extractSlugFromPathUtil,
|
|
10
|
+
filterPathsToSlugs as filterPathsToSlugsUtil,
|
|
11
|
+
} from "./mdx-utils.js";
|
|
12
|
+
|
|
13
|
+
export interface Frontmatter {
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for the MDX Content component
|
|
21
|
+
* The components prop allows injecting custom components for MDX rendering
|
|
22
|
+
*/
|
|
23
|
+
export interface ContentProps {
|
|
24
|
+
components?: Record<string, AstroComponentFactory>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MDXModule {
|
|
28
|
+
default: AstroComponentFactory;
|
|
29
|
+
frontmatter?: Frontmatter;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LoadedMDX {
|
|
33
|
+
Content: AstroComponentFactory;
|
|
34
|
+
frontmatter: Frontmatter;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get configured snippet folders from environment variable (set by dev.ts/build.ts)
|
|
38
|
+
// Default to ["snippets"] if not configured
|
|
39
|
+
function getSnippetFolders(): string[] {
|
|
40
|
+
const snippetsEnv = import.meta.env.OPENDOCS_SNIPPETS;
|
|
41
|
+
if (snippetsEnv) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(snippetsEnv);
|
|
44
|
+
} catch {
|
|
45
|
+
// Invalid JSON in env var, fall back to default
|
|
46
|
+
return ["snippets"];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return ["snippets"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const snippetFolders = getSnippetFolders();
|
|
53
|
+
|
|
54
|
+
// Get all MDX files from user's content directory
|
|
55
|
+
// This glob pattern is resolved at build time via the @content Vite alias
|
|
56
|
+
const mdxModules = import.meta.glob<MDXModule>("@content/**/*.mdx");
|
|
57
|
+
const mdModules = import.meta.glob<MDXModule>("@content/**/*.md");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a path is in a configured snippet folder
|
|
61
|
+
* Uses default snippet folders from environment
|
|
62
|
+
*/
|
|
63
|
+
function isSnippetPath(path: string): boolean {
|
|
64
|
+
return isSnippetPathUtil(path, snippetFolders);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract slug from a glob path
|
|
69
|
+
* Handles both @content/... and relative paths like ../../../../test-docs/...
|
|
70
|
+
* Returns null for paths in configured snippet folders (they should not become pages)
|
|
71
|
+
* Uses default snippet folders from environment
|
|
72
|
+
*/
|
|
73
|
+
function extractSlugFromPath(globPath: string): string | null {
|
|
74
|
+
return extractSlugFromPathUtil(globPath, snippetFolders);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a map of slug -> loader function
|
|
79
|
+
* Excludes files in configured snippet folders
|
|
80
|
+
*/
|
|
81
|
+
function buildSlugMap(): Map<string, () => Promise<MDXModule>> {
|
|
82
|
+
const slugMap = new Map<string, () => Promise<MDXModule>>();
|
|
83
|
+
|
|
84
|
+
for (const [path, loader] of Object.entries(mdxModules)) {
|
|
85
|
+
const slug = extractSlugFromPath(path);
|
|
86
|
+
// Skip snippet folder paths (extractSlugFromPath returns null for these)
|
|
87
|
+
if (slug !== null) {
|
|
88
|
+
slugMap.set(slug, loader);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const [path, loader] of Object.entries(mdModules)) {
|
|
93
|
+
const slug = extractSlugFromPath(path);
|
|
94
|
+
// Skip snippet folder paths and don't override .mdx with .md
|
|
95
|
+
if (slug !== null && !slugMap.has(slug)) {
|
|
96
|
+
slugMap.set(slug, loader);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return slugMap;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build the slug map once at module load time
|
|
104
|
+
const slugMap = buildSlugMap();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load an MDX file by slug (e.g., "introduction" or "api/reference")
|
|
108
|
+
* Returns the Content component and frontmatter
|
|
109
|
+
*/
|
|
110
|
+
export async function loadMDX(slug: string): Promise<LoadedMDX | null> {
|
|
111
|
+
const loader = slugMap.get(slug);
|
|
112
|
+
|
|
113
|
+
if (!loader) {
|
|
114
|
+
console.error(`MDX file not found for slug: ${slug}`);
|
|
115
|
+
console.error("Available slugs:", Array.from(slugMap.keys()));
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const module = await loader();
|
|
121
|
+
return {
|
|
122
|
+
Content: module.default,
|
|
123
|
+
frontmatter: module.frontmatter || {},
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`Error loading MDX for slug ${slug}:`, error);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get all available content slugs (for static path generation)
|
|
133
|
+
*/
|
|
134
|
+
export function getAvailableSlugs(): string[] {
|
|
135
|
+
return Array.from(slugMap.keys());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Re-export pure utility functions for testing purposes
|
|
139
|
+
export {
|
|
140
|
+
isSnippetPath as isSnippetPathUtil,
|
|
141
|
+
extractSlugFromPath as extractSlugFromPathUtil,
|
|
142
|
+
filterPathsToSlugs as filterPathsToSlugsUtil,
|
|
143
|
+
} from "./mdx-utils.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MDX Utility Functions - Pure functions for path and slug manipulation
|
|
3
|
+
* Extracted from mdx-loader.ts for testability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if a path is in a configured snippet folder
|
|
8
|
+
*/
|
|
9
|
+
export function isSnippetPath(path: string, folders: string[]): boolean {
|
|
10
|
+
return folders.some(
|
|
11
|
+
(folder) => path.includes(`/${folder}/`) || path.startsWith(`${folder}/`)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract slug from a glob path
|
|
17
|
+
* Handles both @content/... and relative paths like ../../../../test-docs/...
|
|
18
|
+
* Returns null for paths in configured snippet folders (they should not become pages)
|
|
19
|
+
*/
|
|
20
|
+
export function extractSlugFromPath(
|
|
21
|
+
globPath: string,
|
|
22
|
+
folders: string[]
|
|
23
|
+
): string | null {
|
|
24
|
+
// Remove @content/ prefix if present
|
|
25
|
+
if (globPath.startsWith("@content/")) {
|
|
26
|
+
const relativePath = globPath.replace(/^@content\//, "");
|
|
27
|
+
// Check if this is a snippet folder path
|
|
28
|
+
if (isSnippetPath(relativePath, folders)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return relativePath.replace(/\.mdx?$/, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle relative paths - check if in snippet folder
|
|
35
|
+
if (isSnippetPath(globPath, folders)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle relative paths from Vite glob
|
|
40
|
+
// e.g., "../../../../test-docs/guides/using-snippets.mdx" -> "guides/using-snippets"
|
|
41
|
+
// e.g., "../../../../test-docs/components.mdx" -> "components"
|
|
42
|
+
// Find the project directory boundary (last ../ followed by project-dir/)
|
|
43
|
+
// The pattern matches all the "../" parts, then project-dir/, then captures the slug
|
|
44
|
+
const relativeMatch = globPath.match(/(?:\.\.\/)+[^/]+\/(.+)\.mdx?$/);
|
|
45
|
+
if (relativeMatch) {
|
|
46
|
+
return relativeMatch[1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback: just extract filename
|
|
50
|
+
const filenameMatch = globPath.match(/\/([^/]+)\.mdx?$/);
|
|
51
|
+
if (filenameMatch) {
|
|
52
|
+
return filenameMatch[1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback: just remove extension
|
|
56
|
+
return globPath.replace(/\.mdx?$/, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Filter paths and return valid slugs (excludes snippet folders)
|
|
61
|
+
* Used for testing purposes
|
|
62
|
+
*/
|
|
63
|
+
export function filterPathsToSlugs(paths: string[], folders: string[]): string[] {
|
|
64
|
+
const slugs: string[] = [];
|
|
65
|
+
for (const path of paths) {
|
|
66
|
+
const slug = extractSlugFromPath(path, folders);
|
|
67
|
+
if (slug !== null && !slugs.includes(slug)) {
|
|
68
|
+
slugs.push(slug);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return slugs;
|
|
72
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified remark plugin for opendocs MDX transformations.
|
|
3
|
+
* Handles all custom AST modifications in a single pass.
|
|
4
|
+
*/
|
|
5
|
+
import { visit } from "unist-util-visit";
|
|
6
|
+
import type { Root } from "mdast";
|
|
7
|
+
|
|
8
|
+
interface MdxJsxAttribute {
|
|
9
|
+
type: "mdxJsxAttribute";
|
|
10
|
+
name: string;
|
|
11
|
+
value: string | { type: string; value: string } | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface MdxJsxFlowElement {
|
|
15
|
+
type: "mdxJsxFlowElement";
|
|
16
|
+
name: string | null;
|
|
17
|
+
attributes: MdxJsxAttribute[];
|
|
18
|
+
children: unknown[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CodeNode {
|
|
22
|
+
type: "code";
|
|
23
|
+
lang?: string | null;
|
|
24
|
+
meta?: string | null;
|
|
25
|
+
value: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// Type Guards
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
function isMdxJsxFlowElement(node: unknown): node is MdxJsxFlowElement {
|
|
33
|
+
return (
|
|
34
|
+
typeof node === "object" &&
|
|
35
|
+
node !== null &&
|
|
36
|
+
(node as { type?: string }).type === "mdxJsxFlowElement"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isCodeNode(node: unknown): node is CodeNode {
|
|
41
|
+
return (
|
|
42
|
+
typeof node === "object" &&
|
|
43
|
+
node !== null &&
|
|
44
|
+
(node as { type?: string }).type === "code"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================
|
|
49
|
+
// CodeGroup: Extract code block labels
|
|
50
|
+
// ============================================
|
|
51
|
+
|
|
52
|
+
function extractCodeLabels(children: unknown[]): string[] {
|
|
53
|
+
const labels: string[] = [];
|
|
54
|
+
|
|
55
|
+
function traverse(nodes: unknown[]) {
|
|
56
|
+
for (const node of nodes) {
|
|
57
|
+
if (isCodeNode(node)) {
|
|
58
|
+
const label = node.meta || node.lang || "code";
|
|
59
|
+
labels.push(label);
|
|
60
|
+
} else if (
|
|
61
|
+
typeof node === "object" &&
|
|
62
|
+
node !== null &&
|
|
63
|
+
Array.isArray((node as { children?: unknown[] }).children)
|
|
64
|
+
) {
|
|
65
|
+
traverse((node as { children: unknown[] }).children);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
traverse(children);
|
|
71
|
+
return labels;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function processCodeGroup(node: MdxJsxFlowElement) {
|
|
75
|
+
const labels = extractCodeLabels(node.children);
|
|
76
|
+
if (labels.length === 0) return;
|
|
77
|
+
|
|
78
|
+
node.attributes = node.attributes || [];
|
|
79
|
+
node.attributes.push({
|
|
80
|
+
type: "mdxJsxAttribute",
|
|
81
|
+
name: "data-labels",
|
|
82
|
+
value: JSON.stringify(labels),
|
|
83
|
+
});
|
|
84
|
+
node.attributes.push({
|
|
85
|
+
type: "mdxJsxAttribute",
|
|
86
|
+
name: "data-sync-key",
|
|
87
|
+
value: labels.slice().sort().join(":"),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================
|
|
92
|
+
// Tabs: Extract tab labels and icons
|
|
93
|
+
// ============================================
|
|
94
|
+
|
|
95
|
+
interface TabInfo {
|
|
96
|
+
label: string;
|
|
97
|
+
icon?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getAttributeValue(
|
|
101
|
+
attributes: MdxJsxAttribute[],
|
|
102
|
+
name: string
|
|
103
|
+
): string | undefined {
|
|
104
|
+
const attr = attributes.find((a) => a.name === name);
|
|
105
|
+
if (!attr) return undefined;
|
|
106
|
+
if (typeof attr.value === "string") return attr.value;
|
|
107
|
+
if (attr.value && typeof attr.value === "object" && "value" in attr.value) {
|
|
108
|
+
return attr.value.value;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extractTabsInfo(children: unknown[]): TabInfo[] {
|
|
114
|
+
const tabs: TabInfo[] = [];
|
|
115
|
+
for (const child of children) {
|
|
116
|
+
if (isMdxJsxFlowElement(child) && child.name === "Tab") {
|
|
117
|
+
const label = getAttributeValue(child.attributes, "label");
|
|
118
|
+
const icon = getAttributeValue(child.attributes, "icon");
|
|
119
|
+
if (label) tabs.push({ label, icon });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return tabs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function processTabs(node: MdxJsxFlowElement) {
|
|
126
|
+
const tabsInfo = extractTabsInfo(node.children);
|
|
127
|
+
if (tabsInfo.length === 0) return;
|
|
128
|
+
|
|
129
|
+
node.attributes = node.attributes || [];
|
|
130
|
+
node.attributes.push({
|
|
131
|
+
type: "mdxJsxAttribute",
|
|
132
|
+
name: "data-tabs",
|
|
133
|
+
value: JSON.stringify(tabsInfo),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================
|
|
138
|
+
// React Hooks: Auto-inject imports
|
|
139
|
+
// ============================================
|
|
140
|
+
|
|
141
|
+
const REACT_HOOKS_IMPORT = {
|
|
142
|
+
type: "mdxjsEsm",
|
|
143
|
+
value: `import { useState, useEffect, useMemo, useCallback, useRef, useContext, useReducer, Fragment } from 'react';`,
|
|
144
|
+
data: {
|
|
145
|
+
estree: {
|
|
146
|
+
type: "Program",
|
|
147
|
+
sourceType: "module",
|
|
148
|
+
body: [
|
|
149
|
+
{
|
|
150
|
+
type: "ImportDeclaration",
|
|
151
|
+
specifiers: [
|
|
152
|
+
"useState",
|
|
153
|
+
"useEffect",
|
|
154
|
+
"useMemo",
|
|
155
|
+
"useCallback",
|
|
156
|
+
"useRef",
|
|
157
|
+
"useContext",
|
|
158
|
+
"useReducer",
|
|
159
|
+
"Fragment",
|
|
160
|
+
].map((name) => ({
|
|
161
|
+
type: "ImportSpecifier",
|
|
162
|
+
imported: { type: "Identifier", name },
|
|
163
|
+
local: { type: "Identifier", name },
|
|
164
|
+
})),
|
|
165
|
+
source: { type: "Literal", value: "react" },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// ============================================
|
|
173
|
+
// Main Plugin
|
|
174
|
+
// ============================================
|
|
175
|
+
|
|
176
|
+
export function remarkOpendocs() {
|
|
177
|
+
return (tree: Root) => {
|
|
178
|
+
// Inject React hooks import at the beginning
|
|
179
|
+
tree.children.unshift(REACT_HOOKS_IMPORT as any);
|
|
180
|
+
|
|
181
|
+
// Process component-specific transformations
|
|
182
|
+
visit(tree, (node) => {
|
|
183
|
+
if (!isMdxJsxFlowElement(node)) return;
|
|
184
|
+
|
|
185
|
+
switch (node.name) {
|
|
186
|
+
case "CodeGroup":
|
|
187
|
+
processCodeGroup(node);
|
|
188
|
+
break;
|
|
189
|
+
case "Tabs":
|
|
190
|
+
processTabs(node);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
}
|