From 8c56a080134bbd3500a0d509ef561c57b7c6595b Mon Sep 17 00:00:00 2001 From: limjihoon Date: Mon, 11 May 2026 13:55:22 +0900 Subject: [PATCH 1/4] feat(remark): add mermaid code block transform plugin Convert mermaid code blocks into renderable placeholder HTML (
) so that
mermaid.js can pick them up on the client side.
---
 src/lib/remark/mermaidPlugin.ts | 13 +++++++++++++
 1 file changed, 13 insertions(+)
 create mode 100644 src/lib/remark/mermaidPlugin.ts

diff --git a/src/lib/remark/mermaidPlugin.ts b/src/lib/remark/mermaidPlugin.ts
new file mode 100644
index 00000000..54b5fb00
--- /dev/null
+++ b/src/lib/remark/mermaidPlugin.ts
@@ -0,0 +1,13 @@
+import visit from 'unist-util-visit';
+
+export default function mermaidPlugin() {
+  function visitor(node: any) {
+    const { lang, value } = node;
+    if (lang !== 'mermaid') return;
+
+    node.type = 'html';
+    node.value = `
${value}
`; + } + + return (ast: any) => visit(ast, 'code', visitor); +} From 9dba669860c4f6afb9b2aa0b50b43a47bf7f0077 Mon Sep 17 00:00:00 2001 From: limjihoon Date: Mon, 11 May 2026 13:55:27 +0900 Subject: [PATCH 2/4] fix(remark): skip mermaid blocks in prismPlugin Prevent Prism.js from syntax-highlighting mermaid code blocks, which would turn diagram definitions into colored code text instead of renderable diagrams. --- src/lib/remark/prismPlugin.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/remark/prismPlugin.js b/src/lib/remark/prismPlugin.js index 2007f8ec..ca2b623f 100644 --- a/src/lib/remark/prismPlugin.js +++ b/src/lib/remark/prismPlugin.js @@ -36,6 +36,8 @@ export default function attacher({ include, exclude } = {}) { function visitor(node) { let { lang, data } = node; + if (lang === 'mermaid') return; + // if ( // !lang || // (include && include.indexOf(lang) === -1) || From bed4803f0641ba32fb337426cdb09a3b1badf3e9 Mon Sep 17 00:00:00 2001 From: limjihoon Date: Mon, 11 May 2026 13:55:33 +0900 Subject: [PATCH 3/4] feat(markdown): integrate mermaid rendering in MarkdownRender - Add mermaidPlugin to remark pipeline (SSR + client) - Dynamically load mermaid.js from CDN when mermaid blocks detected - Auto-switch mermaid theme based on code theme setting - Add .mermaid-block styles for centered, responsive diagrams --- src/components/common/MarkdownRender.tsx | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/components/common/MarkdownRender.tsx b/src/components/common/MarkdownRender.tsx index 976bbd69..56095a78 100644 --- a/src/components/common/MarkdownRender.tsx +++ b/src/components/common/MarkdownRender.tsx @@ -8,6 +8,7 @@ import prismThemes from '../../lib/styles/prismThemes'; import breaks from 'remark-breaks'; import Typography from './Typography'; import embedPlugin from '../../lib/remark/embedPlugin'; +import mermaidPlugin from '../../lib/remark/mermaidPlugin'; import { loadScript, ssrEnabled } from '../../lib/utils'; import media from '../../lib/styles/media'; import parse from 'html-react-parser'; @@ -125,6 +126,23 @@ const MarkdownRenderBlock = styled.div` .katex-mathml { display: none; } + + .mermaid-block { + display: flex; + justify-content: center; + margin: 1.5rem 0; + + pre.mermaid { + background: transparent; + padding: 0; + text-align: center; + } + + svg { + max-width: 100%; + height: auto; + } + } `; function filter(html: string) { @@ -218,6 +236,7 @@ const MarkdownRender: React.FC = ({ .use(breaks) .use(remarkParse) .use(slug) + .use(mermaidPlugin) .use(prismPlugin) .use(embedPlugin) .use(remark2rehype, { allowDangerousHTML: true }) @@ -242,6 +261,7 @@ const MarkdownRender: React.FC = ({ .use(breaks) .use(remarkParse) .use(slug) + .use(mermaidPlugin) .use(prismPlugin) .use(embedPlugin) .use(remark2rehype, { allowDangerousHTML: true }) @@ -291,6 +311,33 @@ const MarkdownRender: React.FC = ({ throttledUpdate(markdown); }, [markdown, throttledUpdate]); + useEffect(() => { + if (!html || html.indexOf('class="mermaid"') === -1) return; + + const renderMermaid = () => { + const mermaid = (window as any).mermaid; + if (!mermaid) return; + mermaid.initialize({ + startOnLoad: false, + theme: + codeTheme === 'atom-one' || + codeTheme === 'monokai' || + codeTheme === 'dracula' + ? 'dark' + : 'default', + }); + mermaid.run({ querySelector: '.mermaid' }); + }; + + if ((window as any).mermaid) { + renderMermaid(); + } else { + loadScript( + 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js', + ).then(renderMermaid); + } + }, [html, codeTheme]); + useEffect(() => { if (!shouldShowAds || !html) { setHtmlWithAds(''); From 1398ff823611dfd0af1de2b7531384b7a4f9805f Mon Sep 17 00:00:00 2001 From: limjihoon Date: Mon, 11 May 2026 13:55:40 +0900 Subject: [PATCH 4/4] test(mermaid): add unit tests and visual test page - mermaidPlugin: 5 unit tests (transform, skip non-mermaid, diagram types) - prismPlugin: 3 unit tests (mermaid skip, JS highlight, plain code) - test-mermaid.html: standalone visual test (10 cases, no backend needed) --- .../remark/__tests__/mermaidPlugin.test.ts | 76 +++++ src/lib/remark/__tests__/prismPlugin.test.ts | 40 +++ test-mermaid.html | 299 ++++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 src/lib/remark/__tests__/mermaidPlugin.test.ts create mode 100644 src/lib/remark/__tests__/prismPlugin.test.ts create mode 100644 test-mermaid.html diff --git a/src/lib/remark/__tests__/mermaidPlugin.test.ts b/src/lib/remark/__tests__/mermaidPlugin.test.ts new file mode 100644 index 00000000..496ebc51 --- /dev/null +++ b/src/lib/remark/__tests__/mermaidPlugin.test.ts @@ -0,0 +1,76 @@ +import remark from 'remark'; +import remarkParse from 'remark-parse'; +import remark2rehype from 'remark-rehype'; +import raw from 'rehype-raw'; +import stringify from 'rehype-stringify'; +import mermaidPlugin from '../mermaidPlugin'; + +function processMarkdown(md: string): string { + return remark() + .use(remarkParse) + .use(mermaidPlugin) + .use(remark2rehype, { allowDangerousHTML: true }) + .use(raw) + .use(stringify) + .processSync(md) + .toString(); +} + +describe('mermaidPlugin', () => { + it('mermaid 코드 블록을 mermaid-block div로 변환한다', () => { + const md = '```mermaid\ngraph TD;\n A-->B;\n```'; + const html = processMarkdown(md); + expect(html).toContain('class="mermaid-block"'); + expect(html).toContain('class="mermaid"'); + expect(html).toContain('graph TD;'); + expect(html).toContain('A-->B;'); + }); + + it('mermaid가 아닌 코드 블록은 변환하지 않는다', () => { + const md = '```javascript\nconsole.log("hello");\n```'; + const html = processMarkdown(md); + expect(html).not.toContain('mermaid-block'); + expect(html).toContain('console.log'); + }); + + it('언어 지정 없는 코드 블록은 변환하지 않는다', () => { + const md = '```\nplain code\n```'; + const html = processMarkdown(md); + expect(html).not.toContain('mermaid-block'); + expect(html).toContain('plain code'); + }); + + it('다양한 mermaid 다이어그램 타입을 지원한다', () => { + const diagrams = [ + 'sequenceDiagram\n Alice->>Bob: Hello', + 'classDiagram\n Animal <|-- Duck', + 'pie title Pets\n "Dogs" : 386\n "Cats" : 85', + 'flowchart LR\n A --> B', + 'erDiagram\n CUSTOMER ||--o{ ORDER : places', + 'gantt\n title A Gantt\n section Section\n A task :a1, 2024-01-01, 30d', + 'stateDiagram-v2\n [*] --> Active', + 'gitGraph\n commit\n branch develop', + ]; + diagrams.forEach(diagram => { + const md = `\`\`\`mermaid\n${diagram}\n\`\`\``; + const html = processMarkdown(md); + expect(html).toContain('class="mermaid"'); + }); + }); + + it('mermaid 블록과 일반 코드 블록이 함께 있을 때 각각 올바르게 처리된다', () => { + const md = [ + '```mermaid', + 'graph TD;', + ' A-->B;', + '```', + '', + '```javascript', + 'const x = 1;', + '```', + ].join('\n'); + const html = processMarkdown(md); + expect(html).toContain('class="mermaid"'); + expect(html).toContain('const x = 1;'); + }); +}); diff --git a/src/lib/remark/__tests__/prismPlugin.test.ts b/src/lib/remark/__tests__/prismPlugin.test.ts new file mode 100644 index 00000000..fcdaeca6 --- /dev/null +++ b/src/lib/remark/__tests__/prismPlugin.test.ts @@ -0,0 +1,40 @@ +import remark from 'remark'; +import remarkParse from 'remark-parse'; +import remark2rehype from 'remark-rehype'; +import raw from 'rehype-raw'; +import stringify from 'rehype-stringify'; +import prismPlugin from '../prismPlugin'; + +function processMarkdown(md: string): string { + return remark() + .use(remarkParse) + .use(prismPlugin) + .use(remark2rehype, { allowDangerousHTML: true }) + .use(raw) + .use(stringify) + .processSync(md) + .toString(); +} + +describe('prismPlugin', () => { + it('mermaid 코드 블록을 Prism으로 처리하지 않는다', () => { + const md = '```mermaid\ngraph TD;\n A-->B;\n```'; + const html = processMarkdown(md); + // Prism이 처리했다면 토큰이 생성됨 + expect(html).not.toContain('class="token'); + // 원본 텍스트가 그대로 유지되어야 함 + expect(html).toContain('graph TD;'); + }); + + it('javascript 코드 블록은 정상적으로 하이라이팅한다', () => { + const md = '```javascript\nconst x = 1;\n```'; + const html = processMarkdown(md); + expect(html).toContain('language-javascript'); + }); + + it('언어 지정 없는 코드 블록도 처리한다', () => { + const md = '```\nplain text\n```'; + const html = processMarkdown(md); + expect(html).toContain('
');
+  });
+});
diff --git a/test-mermaid.html b/test-mermaid.html
new file mode 100644
index 00000000..99db3fef
--- /dev/null
+++ b/test-mermaid.html
@@ -0,0 +1,299 @@
+
+
+
+  
+  Mermaid Rendering Integration Test
+  
+  
+
+
+  
+

Mermaid Rendering Integration Test

+

Loading mermaid...

+
+ + +
+
PENDING
+

1. Flowchart (graph TD)

+
+
graph TD;
+    A[Start] --> B{Condition?};
+    B -->|Yes| C[Process];
+    B -->|No| D[End];
+    C --> D;
+
+
+ + +
+
PENDING
+

2. Sequence Diagram

+
+
sequenceDiagram
+    participant U as User
+    participant S as Server
+    participant DB as Database
+    U->>S: API Request
+    S->>DB: Query
+    DB-->>S: Result
+    S-->>U: JSON Response
+
+
+ + +
+
PENDING
+

3. Class Diagram

+
+
classDiagram
+    class Animal {
+        +String name
+        +int age
+        +makeSound()
+    }
+    class Dog {
+        +fetch()
+        +bark()
+    }
+    class Cat {
+        +purr()
+        +scratch()
+    }
+    Animal <|-- Dog
+    Animal <|-- Cat
+
+
+ + +
+
PENDING
+

4. State Diagram (v2)

+
+
stateDiagram-v2
+    [*] --> Idle
+    Idle --> Processing : submit
+    Processing --> Success : done
+    Processing --> Error : fail
+    Error --> Idle : retry
+    Success --> [*]
+
+
+ + +
+
PENDING
+

5. ER Diagram

+
+
erDiagram
+    USER ||--o{ POST : writes
+    USER ||--o{ COMMENT : writes
+    POST ||--o{ COMMENT : has
+    POST ||--o{ TAG : has
+
+
+ + +
+
PENDING
+

6. Gantt Chart

+
+
gantt
+    title Sprint Plan
+    dateFormat  YYYY-MM-DD
+    section Backend
+    API Design     :a1, 2024-01-01, 7d
+    Implementation :a2, after a1, 14d
+    section Frontend
+    UI Design      :b1, 2024-01-01, 5d
+    Development    :b2, after b1, 14d
+
+
+ + +
+
PENDING
+

7. Pie Chart

+
+
pie title Tech Stack Usage
+    "React" : 45
+    "Vue" : 25
+    "Angular" : 15
+    "Svelte" : 10
+    "Other" : 5
+
+
+ + +
+
PENDING
+

8. Git Graph

+
+
gitGraph
+    commit id: "init"
+    branch develop
+    commit id: "feat-1"
+    commit id: "feat-2"
+    checkout main
+    merge develop id: "merge" tag: "v1.0"
+    commit id: "hotfix"
+
+
+ + +
+
PENDING
+

9. Prism Code Block (should remain as code, not diagram)

+
const greeting = "Hello, Mermaid!";
+console.log(greeting);
+
+function renderDiagram(md) {
+  return mermaid.render(md);
+}
+
+ + +
+
PENDING
+

10. Invalid Mermaid Syntax (should not crash)

+
+
this is not valid mermaid syntax!!!
+
+
+ + + + +