Skip to content

Commit b8dbf4d

Browse files
committed
feat(zbugs): support mermaid diagrams
1 parent 42cf792 commit b8dbf4d

File tree

6 files changed

+3165
-15291
lines changed

6 files changed

+3165
-15291
lines changed

apps/zbugs/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@
3131
"postgres": "^3.4.4",
3232
"react": ">=16.0 <19.0",
3333
"react-dom": ">=16.0 <19.0",
34-
"react-markdown": "^9.0.1",
3534
"react-textarea-autosize": "^8.5.3",
3635
"react-window": "^1.8.10",
3736
"rehype-raw": "^7.0.0",
3837
"rehype-sanitize": "^6.0.0",
38+
"rehype-stringify": "^10.0.1",
3939
"remark-gfm": "^4.0.0",
40+
"remark-mermaidjs": "^7.0.0",
41+
"remark-parse": "^11.0.0",
42+
"remark-rehype": "^11.1.1",
43+
"unified": "^11.0.5",
4044
"use-debounce": "^10.0.4",
4145
"wouter": "^3.3.5"
4246
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import remarkMermaid from 'remark-mermaidjs';
2+
import {useMarkdownAsync} from '../hooks/use-markdown.js';
3+
import {useEffect, useState} from 'react';
4+
5+
const plugins = [remarkMermaid];
6+
export default function MarkdownExtended({
7+
children,
8+
fallback,
9+
}: {
10+
children: string;
11+
fallback: string;
12+
}) {
13+
const mdPromise = useMarkdownAsync(children, plugins);
14+
15+
const [md, setMd] = useState<string>(fallback);
16+
useEffect(() => {
17+
let mounted = true;
18+
mdPromise.then(result => {
19+
if (!mounted) {
20+
return;
21+
}
22+
setMd(result);
23+
});
24+
return () => {
25+
mounted = false;
26+
};
27+
}, [mdPromise]);
28+
29+
return <div dangerouslySetInnerHTML={{__html: md}}></div>;
30+
}

apps/zbugs/src/components/markdown-internal.tsx

-9
This file was deleted.
+16-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
import MarkdownBase from 'react-markdown';
2-
import remarkGfm from 'remark-gfm';
1+
import {lazy, Suspense} from 'react';
2+
import {useMarkdown} from '../hooks/use-markdown.js';
3+
4+
// Mermaid is a pretty huge library so we're going to lazy load it
5+
const MarkdownExtended = lazy(() => import('./markdown-extended.js'));
36

4-
/**
5-
* Do not import this component directly. Use `Markdown` instead.
6-
*/
77
export default function Markdown({children}: {children: string}) {
8-
return <MarkdownBase rehypePlugins={[remarkGfm]}>{children}</MarkdownBase>;
8+
const text = useMarkdown(children);
9+
10+
if (children.includes('```mermaid')) {
11+
return (
12+
<Suspense fallback={<div dangerouslySetInnerHTML={{__html: text}}></div>}>
13+
<MarkdownExtended fallback={text}>{children}</MarkdownExtended>
14+
</Suspense>
15+
);
16+
}
17+
18+
return <div dangerouslySetInnerHTML={{__html: text}}></div>;
919
}

apps/zbugs/src/hooks/use-markdown.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {useMemo} from 'react';
2+
import rehypeStringify from 'rehype-stringify';
3+
import remarkGfm from 'remark-gfm';
4+
import remarkParse from 'remark-parse';
5+
import remarkRehype from 'remark-rehype';
6+
import {unified, type Plugin} from 'unified';
7+
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
type P = Plugin<any, any, any>;
10+
11+
const emptyArray: P[] = [];
12+
export function useMarkdown(text: string, plugins: P[] = emptyArray) {
13+
return useMemo(
14+
() => configureUnified(plugins).processSync(text).value.toString(),
15+
[text, plugins],
16+
);
17+
}
18+
19+
export function useMarkdownAsync(text: string, plugins: P[] = emptyArray) {
20+
return useMemo(
21+
() =>
22+
configureUnified(plugins)
23+
.process(text)
24+
.then(result => result.value.toString()),
25+
[text, plugins],
26+
);
27+
}
28+
29+
function configureUnified(plugins: P[]) {
30+
let u = unified().use(remarkParse);
31+
for (const plugin of plugins) {
32+
u = u.use(plugin);
33+
}
34+
return u.use(remarkGfm).use(remarkRehype).use(rehypeStringify);
35+
}

0 commit comments

Comments
 (0)