Commit cc2d8e37 by Finley Ge Committed by GitHub

feat(docs): add mermaid diagram support (#7203)

- Add `MermaidDiagram` component to render charts
- Implement `remarkMermaid` plugin to transform mermaid code blocks
- Update `mdx-components` to handle mermaid language blocks
- Add `mermaid` dependency to documentation project
parent f5979ad9
'use client';
import { useEffect, useId, useMemo, useState } from 'react';
import type { MermaidConfig } from 'mermaid';
const mermaidConfig: MermaidConfig = {
startOnLoad: false,
securityLevel: 'strict',
theme: 'default'
};
type MermaidDiagramProps = {
chart: string;
};
export function MermaidDiagram({ chart }: MermaidDiagramProps) {
const reactId = useId();
const renderId = useMemo(
() => `mermaid-${reactId.replace(/[^a-zA-Z0-9_-]/g, '')}`,
[reactId]
);
const [svg, setSvg] = useState('');
const [error, setError] = useState('');
useEffect(() => {
let ignore = false;
async function renderMermaid() {
try {
const { default: mermaid } = await import('mermaid');
mermaid.initialize(mermaidConfig);
const { svg } = await mermaid.render(renderId, chart);
if (!ignore) {
setSvg(svg);
setError('');
}
} catch (err) {
if (!ignore) {
setSvg('');
setError(err instanceof Error ? err.message : 'Mermaid render failed');
}
}
}
void renderMermaid();
return () => {
ignore = true;
};
}, [chart, renderId]);
if (error) {
return (
<pre className="overflow-x-auto rounded-lg border border-fd-destructive/30 bg-fd-muted p-4 text-sm">
<code>{chart}</code>
</pre>
);
}
return (
<div className="not-prose my-4 overflow-x-auto rounded-lg border bg-fd-card p-4">
{svg ? (
<div
className="min-w-max [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-none"
dangerouslySetInnerHTML={{ __html: svg }}
/>
) : (
<pre className="text-sm text-fd-muted-foreground">
<code>{chart}</code>
</pre>
)}
</div>
);
}
......@@ -4,7 +4,8 @@ import { ImageZoom } from 'fumadocs-ui/components/image-zoom';
import * as TabsComponents from 'fumadocs-ui/components/tabs';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import { LocalizedLink } from '@/components/docs/LocalizedLink';
import type { ComponentProps } from 'react';
import { MermaidDiagram } from '@/components/docs/MermaidDiagram';
import type { ComponentProps, ComponentType } from 'react';
/**
* 兼容 Fumadocs/MDX 对本地图片的静态资源对象输出。
......@@ -35,14 +36,45 @@ function MdxImage(props: ComponentProps<'img'>) {
);
}
function getTextContent(node: unknown): string {
if (typeof node === 'string' || typeof node === 'number') return String(node);
if (Array.isArray(node)) return node.map(getTextContent).join('');
if (node && typeof node === 'object' && 'props' in node) {
const props = (node as { props?: { children?: unknown } }).props;
return getTextContent(props?.children);
}
return '';
}
function MdxPre(props: ComponentProps<'pre'>) {
const child = Array.isArray(props.children) ? props.children[0] : props.children;
const className =
child && typeof child === 'object' && 'props' in child
? (child as { props?: { className?: unknown } }).props?.className
: undefined;
if (typeof className === 'string' && className.split(/\s+/).includes('language-mermaid')) {
return <MermaidDiagram chart={getTextContent(child).trim()} />;
}
const DefaultPre = defaultMdxComponents.pre as ComponentType<ComponentProps<'pre'>> | undefined;
if (DefaultPre) return <DefaultPre {...props} />;
return <pre {...props} />;
}
// use this function to get MDX components, you will need it for rendering MDX
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
img: (props) => <MdxImage {...props} />,
pre: (props) => <MdxPre {...props} />,
a: (props) => <LocalizedLink {...(props as any)} />,
...TabsComponents,
...components,
MermaidDiagram,
TypeTable
};
}
......@@ -27,6 +27,7 @@
"fumadocs-ui": "15.6.3",
"gray-matter": "^4.0.3",
"lucide-react": "^0.525.0",
"mermaid": "^10.9.6",
"next": "^15.5.18",
"react": "^19.1.0",
"react-dom": "^19.1.0",
......
import { defineConfig, defineDocs, frontmatterSchema, metaSchema } from 'fumadocs-mdx/config';
import { z } from 'zod';
type MdxAstNode = {
type: string;
lang?: string;
value?: string;
children?: MdxAstNode[];
[key: string]: unknown;
};
type MdxRoot = {
type: 'root';
children: MdxAstNode[];
};
// You can customise Zod schemas for frontmatter and `meta.json` here
// see https://fumadocs.vercel.app/docs/mdx/collections#define-docs
export const docs = defineDocs({
......@@ -16,9 +29,40 @@ export const docs = defineDocs({
}
});
function remarkMermaid() {
return (tree: MdxRoot) => {
function visit(node: MdxRoot | MdxAstNode) {
if (node.children) {
node.children = node.children.map((child) => {
if (child.type === 'code' && child.lang === 'mermaid') {
return {
type: 'mdxJsxFlowElement',
name: 'MermaidDiagram',
attributes: [
{
type: 'mdxJsxAttribute',
name: 'chart',
value: child.value ?? ''
}
],
children: []
};
}
visit(child);
return child;
});
}
}
visit(tree);
};
}
export default defineConfig({
lastModifiedTime: 'git',
mdxOptions: {
remarkPlugins: (plugins) => [...plugins, remarkMermaid],
remarkImageOptions: {
external: false
}
......
......@@ -320,6 +320,9 @@ importers:
lucide-react:
specifier: ^0.525.0
version: 0.525.0(react@18.3.1)
mermaid:
specifier: ^10.9.6
version: 10.9.6
next:
specifier: ^15.5.18
version: 15.5.18(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment