Skip to content

feat: support mermaid rendering#84

Merged
kagol merged 1 commit intodevfrom
feat/supportMermaidRender
Feb 13, 2026
Merged

feat: support mermaid rendering#84
kagol merged 1 commit intodevfrom
feat/supportMermaidRender

Conversation

@chilingling
Copy link
Member

@chilingling chilingling commented Feb 13, 2026

【特性】新增 mermaid 渲染支持

【社区插件】

  1. vitepress-plugin-mermaid
    优点:start 171
    缺点:该插件最近更新时间是2年前,更新不活跃(或者已经被弃坑了?)

  2. vitepress-mermaid-renderer
    优点:支持放大、全屏等高级特性
    缺点:该插件 license 为 gpl 3.0,有开源传染风险。

【实现方案】自定义 markdown-it 插件 + Vue 组件
理由:当前实现简单,后续可根据自己的业务需求进行特性添加或者维护。(使用大模型进行编码维护也降低了自维护的成本。)

【实现截图】

iShot_2026-02-13_14 31 49 iShot_2026-02-13_14 31 41

Summary by CodeRabbit

  • New Features

    • Mermaid diagram support in Markdown with responsive rendering, loading states, and error fallback.
    • Automatic light/dark theming for Mermaid diagrams.
  • Style

    • Added UI styles for Mermaid diagrams, loading, and error presentations.
  • Chores

    • Added mermaid library as a dev dependency.

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Walkthrough

Adds Mermaid diagram support to VitePress: markdown fence detection and renderer, a ClientOnly MermaidBlock Vue component, rendering utilities with lazy Mermaid loading and serialized queue, CSS for Mermaid UI, and a devDependency on mermaid v11.12.2.

Changes

Cohort / File(s) Summary
Configuration & Dependency
\.vitepress/config.mts, package.json
Routes mermaid fences through a custom renderer and adds mermaid v11.12.2 to devDependencies.
Markdown Processing
\.vitepress/theme/markdown/mermaid.ts
Adds fence detection (isMermaidFence), language helpers, ID sanitization, and renderMermaidFence producing ClientOnly + MermaidBlock HTML.
Vue Component & Theme Registration
\.vitepress/theme/components/MermaidBlock.vue, \.vitepress/theme/index.ts
New MermaidBlock.vue component (loading/error/success states, dark-mode detection, MutationObserver, render versioning). Registered globally in theme enhanceApp.
Rendering Engine
\.vitepress/theme/utils/mermaid.ts
New rendering utilities: palette handling, theme variable mapping, lazy dynamic mermaid import, sanitization, serialized render queue, and renderMermaidSvg API.
Styling
\.vitepress/theme/style.css
Adds .vp-mermaid-* styles for wrapper/loading/diagram/error states; includes cosmetic selector/format normalizations across theme CSS.

Sequence Diagram

sequenceDiagram
    participant MD as Markdown Processor
    participant CFG as VitePress Config
    participant MB as MermaidBlock Component
    participant RE as Render Engine
    participant DOM as DOM / MutationObserver

    MD->>CFG: pass fenced code token
    CFG->>CFG: isMermaidFence()? -> true
    CFG->>MD: renderMermaidFence() -> ClientOnly + MermaidBlock
    MD->>DOM: render page with MermaidBlock

    MB->>MB: mount, decode graph, compute id and isDark
    MB->>RE: request renderMermaidSvg(id, code, isDark)
    RE->>RE: enqueue render, load mermaid (lazy), init with theme vars
    RE-->>MB: return SVG or error
    MB->>DOM: display SVG or show error state

    DOM->>DOM: theme/dark-mode class change
    DOM->>MB: MutationObserver triggers
    MB->>RE: re-render with updated isDark
    RE-->>MB: updated SVG
    MB->>DOM: update displayed SVG
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I nibble code and weave a chart,

Lines of flow from root to heart,
Light and dark, a queued advance,
SVG blooms from a tiny glance,
Hooray — diagrams hop into dance!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support mermaid rendering' directly and clearly describes the main change—adding Mermaid diagram rendering support throughout the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into dev

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/supportMermaidRender

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.vitepress/theme/style.css (1)

405-415: ⚠️ Potential issue | 🟡 Minor

info is not a valid HTML type selector — should this be .info?

Both Stylelint and Biome flag info as an unknown type selector. If this is meant to style elements with class info, it should be .info. As-is, this rule will never match anything.

Same issue at line 631 (.dark info).

Proposed fix
-info {
+.info {
   display: inline-block;
-.dark info {
+.dark .info {
   background: `#1a1a1a`;
🧹 Nitpick comments (5)
.vitepress/theme/style.css (1)

756-823: New Mermaid styles look good.

The loading, diagram, and error states are well-structured with appropriate use of CSS variables for theme integration. Minor stylelint nits:

  • Line 807: margin: 0 0 8px 0margin: 0 0 8px (redundant value).
  • Line 790: geometricPrecisiongeometricprecision per value-keyword-case rule.
.vitepress/theme/utils/mermaid.ts (2)

174-192: Re-initializing mermaid on every render call could be simplified, but is functionally correct given the serialized queue.

Since mermaid.initialize() is called inside the serialized renderQueue, there's no race between init and render. However, calling initialize on every single render is slightly wasteful when multiple diagrams share the same theme. Consider caching the last config and skipping re-init when unchanged — but this is optional and can be deferred.


148-157: Lazy-loading pattern is clean. The singleton promise for dynamic import avoids redundant loads.

One minor note: if the dynamic import('mermaid') fails (e.g., network issue in dev), mermaidPromise will cache the rejected promise permanently. Consider resetting it on failure so retries are possible.

Proposed fix
 const loadMermaid = async () => {
   if (!mermaidPromise) {
-    mermaidPromise = import('mermaid')
+    mermaidPromise = import('mermaid').catch((err) => {
+      mermaidPromise = null
+      throw err
+    })
   }
   const module = await mermaidPromise
   return module.default
 }
.vitepress/theme/components/MermaidBlock.vue (2)

101-109: Consider debouncing the MutationObserver callback.

The observer fires on any class attribute change on <html>, which could happen frequently (not just dark mode toggles). Each fire triggers a full mermaid re-render. A simple debounce would prevent unnecessary render churn.

Proposed fix
+let debounceTimer: ReturnType<typeof setTimeout> | null = null
+
 onMounted(() => {
   mutationObserver = new MutationObserver(() => {
-    renderDiagram()
+    if (debounceTimer) clearTimeout(debounceTimer)
+    debounceTimer = setTimeout(renderDiagram, 50)
   })
   mutationObserver.observe(document.documentElement, {
     attributes: true,
     attributeFilter: ['class'],
   })
   renderDiagram()
 })

46-68: getIsDarkMode() is called 6 times in readThemePalette.

Each call re-evaluates document.documentElement.classList.contains('dark'). Cache the result in a local variable.

Proposed fix
 const readThemePalette = (): MermaidPalette => {
   const styles = getComputedStyle(document.documentElement)
+  const isDark = getIsDarkMode()
   const read = (name: string, fallback: string) => {
     const value = styles.getPropertyValue(name).trim()
     return value || fallback
   }
 
   return {
-    bg: read('--vp-c-bg', getIsDarkMode() ? '#0b1220' : '#ffffff'),
-    bgSoft: read('--vp-c-bg-soft', getIsDarkMode() ? '#1f2937' : '#f6f8fa'),
-    text1: read('--vp-c-text-1', getIsDarkMode() ? '#f8fafc' : '#1f2937'),
-    text2: read('--vp-c-text-2', getIsDarkMode() ? '#cbd5e1' : '#475569'),
-    border: read('--vp-c-border', getIsDarkMode() ? '#64748b' : '#94a3b8'),
-    brand: read('--vp-c-brand-1', getIsDarkMode() ? '#60a5fa' : '#2563eb'),
+    bg: read('--vp-c-bg', isDark ? '#0b1220' : '#ffffff'),
+    bgSoft: read('--vp-c-bg-soft', isDark ? '#1f2937' : '#f6f8fa'),
+    text1: read('--vp-c-text-1', isDark ? '#f8fafc' : '#1f2937'),
+    text2: read('--vp-c-text-2', isDark ? '#cbd5e1' : '#475569'),
+    border: read('--vp-c-border', isDark ? '#64748b' : '#94a3b8'),
+    brand: read('--vp-c-brand-1', isDark ? '#60a5fa' : '#2563eb'),
   }
 }

@chilingling chilingling force-pushed the feat/supportMermaidRender branch from b45ba52 to 93786ca Compare February 13, 2026 07:41
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.vitepress/theme/style.css (1)

405-415: ⚠️ Potential issue | 🟡 Minor

Unknown type selector info — likely should be a class.

Stylelint flags info as an unknown HTML element (line 405 and similarly at line 631 for .dark info). This should probably be .info or a custom element needs to be declared.

Proposed fix
-info {
+.info {

And similarly at line 631:

-.dark info {
+.dark .info {
🤖 Fix all issues with AI agents
In @.vitepress/theme/utils/mermaid.ts:
- Around line 148-157: The dynamic import of Mermaid is being cached in
mermaidPromise even when it rejects, causing permanent failures; update
loadMermaid to wrap the import call in a try/catch so that if import('mermaid')
rejects you set mermaidPromise back to null and rethrow the error; specifically
modify the loadMermaid function (and the mermaidPromise setter logic) so a
failed import clears mermaidPromise to allow retries on subsequent calls while
still returning module.default on success.
🧹 Nitpick comments (1)
.vitepress/theme/style.css (1)

756-823: Mermaid styles look solid overall — a couple of minor stylelint fixes.

The new .vp-mermaid-* classes are well-structured, use VitePress CSS variables appropriately, and handle loading/error/diagram states cleanly.

Two stylelint issues in the new code:

  • Line 790: geometricPrecisiongeometricprecision (value-keyword-case)
  • Line 807: margin: 0 0 8px 0margin: 0 0 8px (shorthand-property-no-redundant-values)
Proposed fixes
 .vp-mermaid-diagram svg text {
-  text-rendering: geometricPrecision;
+  text-rendering: geometricprecision;
   font-family: var(--vp-font-family-base) !important;
 }
 .vp-mermaid-error-title {
-  margin: 0 0 8px 0;
+  margin: 0 0 8px;
   color: var(--vp-c-danger-1);

Comment on lines +148 to +157
let mermaidPromise: Promise<typeof import('mermaid')> | null = null
let renderQueue = Promise.resolve()

const loadMermaid = async () => {
if (!mermaidPromise) {
mermaidPromise = import('mermaid')
}
const module = await mermaidPromise
return module.default
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mermaid import failure is permanently cached.

If the dynamic import('mermaid') fails (e.g., network issue in a lazy-loaded chunk scenario), mermaidPromise retains the rejected promise forever, and all subsequent calls to loadMermaid will fail without retrying.

Proposed fix: reset on failure
 const loadMermaid = async () => {
   if (!mermaidPromise) {
-    mermaidPromise = import('mermaid')
+    mermaidPromise = import('mermaid').catch((err) => {
+      mermaidPromise = null
+      throw err
+    })
   }
   const module = await mermaidPromise
   return module.default
 }
🤖 Prompt for AI Agents
In @.vitepress/theme/utils/mermaid.ts around lines 148 - 157, The dynamic import
of Mermaid is being cached in mermaidPromise even when it rejects, causing
permanent failures; update loadMermaid to wrap the import call in a try/catch so
that if import('mermaid') rejects you set mermaidPromise back to null and
rethrow the error; specifically modify the loadMermaid function (and the
mermaidPromise setter logic) so a failed import clears mermaidPromise to allow
retries on subsequent calls while still returning module.default on success.

@kagol kagol merged commit ca0fa43 into dev Feb 13, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants