mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-02 08:19:07 +02:00
feat(theme-mermaid): upgrade Mermaid to v10.4 - handle async rendering (#9305)
This commit is contained in:
parent
dc7ae426ac
commit
58be496da2
11 changed files with 327 additions and 86 deletions
|
@ -5,9 +5,10 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {useState, useEffect, useMemo, useRef} from 'react';
|
||||
import {useColorMode, useThemeConfig} from '@docusaurus/theme-common';
|
||||
import mermaid, {type MermaidConfig} from 'mermaid';
|
||||
import mermaid from 'mermaid';
|
||||
import type {RenderResult, MermaidConfig} from 'mermaid';
|
||||
import type {ThemeConfig} from '@docusaurus/theme-mermaid';
|
||||
|
||||
// Stable className to allow users to easily target with CSS
|
||||
|
@ -30,48 +31,84 @@ export function useMermaidConfig(): MermaidConfig {
|
|||
);
|
||||
}
|
||||
|
||||
export function useMermaidSvg(
|
||||
txt: string,
|
||||
mermaidConfigParam?: MermaidConfig,
|
||||
): string {
|
||||
function useMermaidId(): string {
|
||||
/*
|
||||
Random client-only id, we don't care much but mermaid want an id so...
|
||||
Note: Mermaid doesn't like values provided by Rect.useId() and throws
|
||||
*/
|
||||
// return useId(); // tried that, doesn't work ('#d:re:' is not a valid selector.)
|
||||
return useRef(`mermaid-svg-${Math.round(Math.random() * 10000000)}`).current!;
|
||||
}
|
||||
|
||||
async function renderMermaid({
|
||||
id,
|
||||
text,
|
||||
config,
|
||||
}: {
|
||||
id: string;
|
||||
text: string;
|
||||
config: MermaidConfig;
|
||||
}): Promise<RenderResult> {
|
||||
/*
|
||||
Mermaid API is really weird :s
|
||||
It is a big mutable singleton with multiple config levels
|
||||
Note: most recent API type definitions are missing
|
||||
|
||||
There are 2 kind of configs:
|
||||
|
||||
- siteConfig: some kind of global/protected shared config
|
||||
you can only set with "initialize"
|
||||
|
||||
- config/currentConfig
|
||||
the config the renderer will use
|
||||
it is reset to siteConfig before each render
|
||||
but it can be altered by the mermaid txt content itself through directives
|
||||
|
||||
To use a new mermaid config (on colorMode change for example) we should
|
||||
update siteConfig, and it can only be done with initialize()
|
||||
*/
|
||||
mermaid.mermaidAPI.initialize(config);
|
||||
|
||||
try {
|
||||
return await mermaid.render(id, text);
|
||||
} catch (e) {
|
||||
// Because Mermaid add a weird SVG/Message to the DOM on error
|
||||
// https://github.com/mermaid-js/mermaid/issues/3205#issuecomment-1719620183
|
||||
document.querySelector(`#d${id}`)?.remove();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function useMermaidRenderResult({
|
||||
text,
|
||||
config: providedConfig,
|
||||
}: {
|
||||
text: string;
|
||||
config?: MermaidConfig;
|
||||
}): RenderResult | null {
|
||||
const [result, setResult] = useState<RenderResult | null>(null);
|
||||
const id = useMermaidId();
|
||||
|
||||
/*
|
||||
For flexibility, we allow the hook to receive a custom Mermaid config
|
||||
The user could inject a modified version of the default config for example
|
||||
*/
|
||||
const defaultMermaidConfig = useMermaidConfig();
|
||||
const mermaidConfig = mermaidConfigParam ?? defaultMermaidConfig;
|
||||
const config = providedConfig ?? defaultMermaidConfig;
|
||||
|
||||
return useMemo(() => {
|
||||
/*
|
||||
Mermaid API is really weird :s
|
||||
It is a big mutable singleton with multiple config levels
|
||||
Note: most recent API type definitions are missing
|
||||
useEffect(() => {
|
||||
renderMermaid({id, text, config})
|
||||
// TODO maybe try to use Suspense here and throw the promise?
|
||||
// See also https://github.com/pmndrs/suspend-react
|
||||
.then(setResult)
|
||||
.catch((e) => {
|
||||
// Funky way to trigger parent React error boundary
|
||||
// See https://twitter.com/sebastienlorber/status/1628340871899893768
|
||||
setResult(() => {
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
}, [id, text, config]);
|
||||
|
||||
There are 2 kind of configs:
|
||||
|
||||
- siteConfig: some kind of global/protected shared config
|
||||
you can only set with "initialize"
|
||||
|
||||
- config/currentConfig
|
||||
the config the renderer will use
|
||||
it is reset to siteConfig before each render
|
||||
but it can be altered by the mermaid txt content itself through directives
|
||||
|
||||
To use a new mermaid config (on colorMode change for example) we should
|
||||
update siteConfig, and it can only be done with initialize()
|
||||
*/
|
||||
mermaid.mermaidAPI.initialize(mermaidConfig);
|
||||
|
||||
/*
|
||||
Random client-only id, we don't care much about it
|
||||
But mermaid want an id so...
|
||||
*/
|
||||
const mermaidId = `mermaid-svg-${Math.round(Math.random() * 10000000)}`;
|
||||
|
||||
/*
|
||||
Not even documented: mermaid.render returns the svg string
|
||||
Using the documented form is un-necessary
|
||||
*/
|
||||
return mermaid.render(mermaidId, txt);
|
||||
}, [txt, mermaidConfig]);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -5,28 +5,54 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||
import {ErrorBoundaryErrorMessageFallback} from '@docusaurus/theme-common';
|
||||
import {
|
||||
MermaidContainerClassName,
|
||||
useMermaidSvg,
|
||||
useMermaidRenderResult,
|
||||
} from '@docusaurus/theme-mermaid/client';
|
||||
|
||||
import type {Props} from '@theme/Mermaid';
|
||||
import type {RenderResult} from 'mermaid';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function MermaidDiagram({value}: Props): JSX.Element {
|
||||
const svg = useMermaidSvg(value);
|
||||
function MermaidRenderResult({
|
||||
renderResult,
|
||||
}: {
|
||||
renderResult: RenderResult;
|
||||
}): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const div = ref.current!;
|
||||
renderResult.bindFunctions?.(div);
|
||||
}, [renderResult]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${MermaidContainerClassName} ${styles.container}`}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{__html: svg}}
|
||||
dangerouslySetInnerHTML={{__html: renderResult.svg}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Mermaid(props: Props): JSX.Element {
|
||||
return <BrowserOnly>{() => <MermaidDiagram {...props} />}</BrowserOnly>;
|
||||
function MermaidRenderer({value}: Props): ReactNode {
|
||||
const renderResult = useMermaidRenderResult({text: value});
|
||||
if (renderResult === null) {
|
||||
return null;
|
||||
}
|
||||
return <MermaidRenderResult renderResult={renderResult} />;
|
||||
}
|
||||
|
||||
export default function Mermaid(props: Props): JSX.Element {
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={(params) => <ErrorBoundaryErrorMessageFallback {...params} />}>
|
||||
<MermaidRenderer {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue