Skip to content

Commit 54561ea

Browse files
authored
Merge pull request #2 from mertguvencli/feat/mermaid-support
feat: add mermaid diagram support
2 parents e199d00 + d1750fe commit 54561ea

4 files changed

Lines changed: 1000 additions & 2 deletions

File tree

lib/default-markdown.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ Inline HTML is supported, just like on GitHub.
118118
119119
---
120120
121+
## Diagrams
122+
123+
Mermaid diagrams are rendered automatically from fenced code blocks.
124+
125+
\`\`\`mermaid
126+
graph TD
127+
A[Write Markdown] --> B[Live Preview]
128+
B --> C{Satisfied?}
129+
C -->|Yes| D[Export PDF]
130+
C -->|No| A
131+
D --> E[Done!]
132+
\`\`\`
133+
134+
---
135+
121136
## Images
122137
123138
![Placeholder](https://placehold.co/800x300/f4f4f5/a1a1aa?text=MarkBloom+Image+Preview)

lib/markdown-components.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,63 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
3+
import { useEffect, useId, useState } from "react";
44
import type { Components } from "react-markdown";
55
import type { BundledLanguage } from "shiki";
66

7+
function MermaidBlock({ children }: { children: string }) {
8+
const id = useId().replace(/:/g, "m");
9+
const [svg, setSvg] = useState<string | null>(null);
10+
const [error, setError] = useState<string | null>(null);
11+
12+
useEffect(() => {
13+
let cancelled = false;
14+
15+
import("mermaid").then(({ default: mermaid }) => {
16+
if (cancelled) return;
17+
mermaid.initialize({
18+
startOnLoad: false,
19+
theme: "neutral",
20+
securityLevel: "loose",
21+
});
22+
mermaid
23+
.render(`mermaid-${id}`, children.replace(/\n$/, ""))
24+
.then(({ svg: renderedSvg }: { svg: string }) => {
25+
if (!cancelled) setSvg(renderedSvg);
26+
})
27+
.catch((err: unknown) => {
28+
if (!cancelled) setError(String(err));
29+
});
30+
});
31+
32+
return () => {
33+
cancelled = true;
34+
};
35+
}, [children, id]);
36+
37+
if (error) {
38+
return (
39+
<pre className="mb-5 rounded-lg border border-red-300 bg-red-50 text-red-700 p-4 text-sm">
40+
<code>{error}</code>
41+
</pre>
42+
);
43+
}
44+
45+
if (svg) {
46+
return (
47+
<div
48+
className="mb-5 flex justify-center rounded-lg border border-zinc-200 bg-white p-4 [&_svg]:max-w-full"
49+
dangerouslySetInnerHTML={{ __html: svg }}
50+
/>
51+
);
52+
}
53+
54+
return (
55+
<div className="mb-5 flex justify-center rounded-lg border border-zinc-200 bg-zinc-50 p-8 text-zinc-400 text-sm">
56+
Rendering diagram…
57+
</div>
58+
);
59+
}
60+
761
function CodeBlock({
862
language,
963
children,
@@ -77,6 +131,9 @@ export const markdownComponents: Components = {
77131
code: ({ children, className }) => {
78132
const match = className?.match(/language-(\w+)/);
79133
if (match) {
134+
if (match[1] === "mermaid") {
135+
return <MermaidBlock>{String(children)}</MermaidBlock>;
136+
}
80137
return <CodeBlock language={match[1]} children={String(children)} />;
81138
}
82139
// Block code without a language (inside <pre>)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"html2canvas-pro": "^2.0.2",
2222
"jspdf": "^4.2.1",
2323
"lucide-react": "^1.8.0",
24+
"mermaid": "^11.14.0",
2425
"next": "16.2.3",
2526
"radix-ui": "^1.4.3",
2627
"react": "19.2.4",

0 commit comments

Comments
 (0)