Skip to content

Commit

Permalink
feat: add mermaid diagram support ootb facebook#1258
Browse files Browse the repository at this point in the history
  • Loading branch information
sjwall committed May 25, 2022
1 parent cd21a31 commit 54ee20f
Show file tree
Hide file tree
Showing 18 changed files with 1,246 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/docusaurus-mdx-loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@docusaurus/types": "2.0.0-beta.20",
"@types/escape-html": "^1.0.2",
"@types/mdast": "^3.0.10",
"@types/mermaid": "^8.2.9",
"@types/stringify-object": "^3.3.1",
"@types/unist": "^2.0.6",
"remark": "^12.0.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/docusaurus-mdx-loader/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import toc from './remark/toc';
import unwrapMdxCodeBlocks from './remark/unwrapMdxCodeBlocks';
import transformImage from './remark/transformImage';
import transformLinks from './remark/transformLinks';
import mermaid from './remark/mermaid';

import type {LoaderContext} from 'webpack';
import type {Processor, Plugin} from 'unified';
Expand All @@ -38,7 +39,7 @@ const pragma = `

const DEFAULT_OPTIONS: MDXOptions = {
rehypePlugins: [],
remarkPlugins: [unwrapMdxCodeBlocks, emoji, headings, toc],
remarkPlugins: [unwrapMdxCodeBlocks, emoji, headings, toc, mermaid],
beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [],
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {createCompiler} from '@mdx-js/mdx';
import mermaid from '..';

describe('mermaid remark plugin', () => {
function createTestCompiler() {
return createCompiler({
remarkPlugins: [mermaid],
});
}

it('no mermaid', async () => {
const mdxCompiler = createTestCompiler();
const result = await mdxCompiler.process(
'# Heading 1\n\nNo Mermaid diagram :(',
);
expect(result.contents).toBe(
'\n\n\nconst layoutProps = {\n \n};\nconst MDXLayout = "wrapper"\nexport default function MDXContent({\n components,\n ...props\n}) {\n return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout">\n <h1>{`Heading 1`}</h1>\n <p>{`No Mermaid diagram :(`}</p>\n </MDXLayout>;\n}\n\n;\nMDXContent.isMDXComponent = true;',
);
});

it('basic', async () => {
const mdxCompiler = createTestCompiler();
const result = await mdxCompiler.process(`# Heading 1\n
\`\`\`mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
\`\`\``);
expect(result.contents).toBe(`
const layoutProps = {
${' '}
};
const MDXLayout = "wrapper"
export default function MDXContent({
components,
...props
}) {
return <MDXLayout {...layoutProps} {...props} components={components} mdxType="MDXLayout">
<h1>{\`Heading 1\`}</h1>
<mermaid value={\`graph TD;
A-->B;
A-->C;
B-->D;
C-->D;\`} />
</MDXLayout>;
}
;
MDXContent.isMDXComponent = true;`);
});
});
48 changes: 48 additions & 0 deletions packages/docusaurus-mdx-loader/src/remark/mermaid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import visit from 'unist-util-visit';
import type {Transformer} from 'unified';
import type {Data, Literal, Node, Parent} from 'unist';

type CodeMermaid = Literal<string> & {
type: 'code';
lang: 'mermaid';
};

function processMermaidNode(
node: CodeMermaid,
index: number,
parent: Parent<Node<Data> | Literal, Data>,
) {
parent.children.splice(index, 1, {
type: 'jsx',
value: `<mermaid value={\`${node.value}\`}/>`,
position: node.position,
});
}

export default function plugin(): Transformer {
return async (root) => {
// Find all the mermaid diagram code blocks. i.e. ```mermaid
const instances: [CodeMermaid, number, Parent<Node<Data>, Data>][] = [];
visit(
root,
{type: 'code', lang: 'mermaid'},
(node: CodeMermaid, index, parent) => {
if (parent) {
instances.push([node, index, parent]);
}
},
);

// Replace each Mermaid code block with the Mermaid component
instances.forEach(([node, index, parent]) => {
processMermaidNode(node, index, parent);
});
};
}
2 changes: 2 additions & 0 deletions packages/docusaurus-theme-classic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"copy-text-to-clipboard": "^3.0.1",
"infima": "0.2.0-alpha.39",
"lodash": "^4.17.21",
"mermaid": "^9.1.1",
"nprogress": "^0.2.0",
"postcss": "^8.4.14",
"prism-react-renderer": "^1.3.3",
Expand All @@ -46,6 +47,7 @@
"@docusaurus/module-type-aliases": "2.0.0-beta.20",
"@docusaurus/types": "2.0.0-beta.20",
"@types/mdx-js__react": "^1.5.5",
"@types/mermaid": "^8.2.9",
"@types/nprogress": "^0.2.0",
"@types/prismjs": "^1.26.0",
"@types/rtlcss": "^3.5.0",
Expand Down
22 changes: 22 additions & 0 deletions packages/docusaurus-theme-classic/src/theme-classic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,14 @@ declare module '@theme/Heading' {
export default function Heading(props: Props): JSX.Element;
}

declare module '@theme/Mermaid' {
export interface Props {
value: string;
}

export default function Mermaid(props: Props): JSX.Element;
}

declare module '@theme/Layout' {
import type {ReactNode} from 'react';

Expand Down Expand Up @@ -667,6 +675,14 @@ declare module '@theme/MDXComponents/Pre' {
export default function MDXPre(props: Props): JSX.Element;
}

declare module '@theme/MDXComponents/Mermaid' {
export interface Props {
value: string;
}

export default function MDXMermaid(props: Props): JSX.Element;
}

declare module '@theme/MDXComponents' {
import type {ComponentType, ComponentProps} from 'react';

Expand All @@ -677,6 +693,7 @@ declare module '@theme/MDXComponents' {
import type MDXDetails from '@theme/MDXComponents/Details';
import type MDXUl from '@theme/MDXComponents/Ul';
import type MDXImg from '@theme/MDXComponents/Img';
import type MDXMermaid from '@theme/MDXComponents/Mermaid';

export type MDXComponentsObject = {
readonly head: typeof MDXHead;
Expand All @@ -692,6 +709,7 @@ declare module '@theme/MDXComponents' {
readonly h4: (props: ComponentProps<'h4'>) => JSX.Element;
readonly h5: (props: ComponentProps<'h5'>) => JSX.Element;
readonly h6: (props: ComponentProps<'h6'>) => JSX.Element;
readonly mermaid: typeof MDXMermaid;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[tagName: string]: ComponentType<any>;
};
Expand Down Expand Up @@ -1278,3 +1296,7 @@ declare module '@theme/prism-include-languages' {
PrismObject: typeof PrismNamespace,
): void;
}

declare module '@theme/useMermaid' {
export default function useMermaid(): void;
}
2 changes: 2 additions & 0 deletions packages/docusaurus-theme-classic/src/theme/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import LayoutProviders from '@theme/LayoutProviders';
import ErrorPageContent from '@theme/ErrorPageContent';
import './styles.css';
import type {Props} from '@theme/Layout';
import useMermaid from '@theme/useMermaid';

export default function Layout(props: Props): JSX.Element {
const {
Expand All @@ -33,6 +34,7 @@ export default function Layout(props: Props): JSX.Element {
} = props;

useKeyboardNavigation();
useMermaid();

return (
<LayoutProviders>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';
import Mermaid from '@theme/Mermaid';
import type {Props} from '@theme/MDXComponents/Mermaid';

export default function MDXMermaid(props: Props): JSX.Element {
return <Mermaid {...props} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import MDXDetails from '@theme/MDXComponents/Details';
import MDXHeading from '@theme/MDXComponents/Heading';
import MDXUl from '@theme/MDXComponents/Ul';
import MDXImg from '@theme/MDXComponents/Img';
import MDXMermaid from '@theme/MDXComponents/Mermaid';

import type {MDXComponentsObject} from '@theme/MDXComponents';

Expand All @@ -31,6 +32,7 @@ const MDXComponents: MDXComponentsObject = {
h4: (props) => <MDXHeading as="h4" {...props} />,
h5: (props) => <MDXHeading as="h5" {...props} />,
h6: (props) => <MDXHeading as="h6" {...props} />,
mermaid: MDXMermaid,
};

export default MDXComponents;
64 changes: 64 additions & 0 deletions packages/docusaurus-theme-classic/src/theme/Mermaid/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import React, {useEffect, useState} from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';
import mermaid from 'mermaid';
import type {Props} from '@theme/Mermaid';

/**
* Assign a unique ID to each mermaid svg as per requirements
* of `mermaid.render`.
*/
let id = 0;

export default function Mermaid({value}: Props): JSX.Element {
// When theme updates, rerender the SVG.
const [svg, setSvg] = useState<string>('');
const isBrowser = useIsBrowser();

useEffect(() => {
const render = () => {
mermaid.render(`mermaid-svg-${id.toString()}`, value, (renderedSvg) =>
setSvg(renderedSvg),
);
id += 1;
};

render();

if (isBrowser) {
const html: HTMLHtmlElement = document.querySelector('html')!;

const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (
mutation.type !== 'attributes' ||
mutation.attributeName !== 'data-mermaid'
) {
continue;
}
render();
break;
}
});

observer.observe(html, {attributes: true});
return () => {
try {
observer.disconnect();
} catch {
// Do nothing
}
};
}
return undefined;
}, [isBrowser, value]);

// eslint-disable-next-line react/no-danger
return <div dangerouslySetInnerHTML={{__html: svg}} />;
}
Loading

0 comments on commit 54ee20f

Please sign in to comment.