# 前言

Mermaid 作為 Markdown 的流程圖繪製解決方案,大大增加了 Markdown 的實用性。在搭建 Hexo 部落格時,我選擇了 Shoka 主題,它宣稱支援豐富的 Markdown 語法,包括 Mermaid。然而,在實際使用時卻發現 Mermaid 代碼塊無法正確渲染,只能顯示純文字。

本文記錄了完整的問題排查與修復過程。

# 初始環境狀態

# Package 依賴

專案已安裝以下主要依賴:

{
  "hexo": "^8.1.1",
  "hexo-renderer-multi-markdown-it": "^0.1.4",
  "puppeteer": "^23.11.1"
}

hexo-renderer-multi-markdown-it 是一個強大的 Markdown 渲染器,內建支援多種插件,包括 markdown-it-mermaid 。

# Hexo 主配置(_config.yml)

# 語法高亮設置
syntax_highlighter: highlight.js
highlight:
  line_number: true
  auto_detect: false
  tab_replace: ''
  wrap: true
  hljs: false
  exclude_languages:
    - mermaid
# Markdown 設置
markdown:
  render:
    html: false
    xhtmlOut: true
    breaks: true
    linkify: true
  plugins:
    - plugin:
        name: markdown-it-toc-and-anchor
        enable: true
    # ... 其他插件
    # 注意:沒有 markdown-it-mermaid 配置

# Shoka 主題配置(themes/shoka/_config.yml)

vendors:
  css:
    katex: npm/katex@0.12.0/dist/katex.min.css
    mermaid: css/mermaid.css  # 只有 CSS
  js:
    pace: npm/pace-js@1.0.2/pace.min.js
    # ... 其他 JS
    # 注意:沒有 mermaid.js

# 問題現象

在 Markdown 文件中使用 Mermaid 語法:

---
title: 測試文章
mermaid: true
---
```mermaid
graph LR
    A[Square Rect] --> B((Circle))
    A --> C(Round Rect)
    B --> D{Rhombus}
    C --> D
```

渲染結果:代碼塊被當作普通代碼顯示,沒有轉換成流程圖 SVG。

# 排查過程

# 第一步:檢查生成的 HTML

查看生成的 HTML 文件:

<p>
  <pre><code class="highlight mermaid">graph LR
    A[Square Rect] -- Link text --&gt; B((Circle))
    ...
  </code></pre>
</p>

發現問題 #1:Mermaid 代碼塊被渲染成了 <code class="highlight mermaid"> 而不是被 markdown-it-mermaid 插件處理。

# 第二步:添加調試代碼

在 node_modules/hexo-renderer-multi-markdown-it/lib/renderer/markdown-it-mermaid/index.js 中添加調試:

md.renderer.rules.fence = (tokens, idx, options, env, self) => {
    const token = tokens[idx]
    const code = token.content.trim()
    console.log('Token info:', token.info, '<------ equals mermaid?', (token.info === 'mermaid'))
    if (token.info === 'mermaid') {
        // ...
    }
    return defaultRenderer(tokens, idx, options, env, self)
}

執行 hexo generate ,卻沒有看到任何 console.log 輸出。

發現問題 #2: markdown-it-mermaid 插件根本沒有被執行。

# 第三步:檢查插件載入

在 node_modules/hexo-renderer-multi-markdown-it/lib/renderer/index.js 添加調試:

parser = plugins.reduce((parser, plugins) => {
    if (plugins.enable) {
        let plugin = require(plugins.name);
        if (plugins.name.includes('mermaid')) {
            console.log('Loading mermaid plugin:', plugins.name, 'enabled:', plugins.enable);
        }
        // ...
    }
}, parser);

重新生成後看到:

Mermaid plugin is DISABLED: ./markdown-it-mermaid enabled: false

發現問題 #3:雖然 hexo-renderer-multi-markdown-it 內建 mermaid 插件,但在 _config.yml 中沒有明確啟用。

# 第四步:啟用 Mermaid 插件

在 _config.yml 中添加:

markdown:
  plugins:
    # ... 其他插件
    - plugin:
        name: ./markdown-it-mermaid
        enable: true
        options:
          theme: default

再次生成,看到:

Loading mermaid plugin: ./markdown-it-mermaid enabled: true

但生成的 HTML 仍然是 <code class="highlight mermaid"> 。

發現問題 #4:代碼塊在到達 markdown-it-mermaid 之前就被 Hexo 的內建高亮器處理了。

# 第五步:禁用 Hexo 內建語法高亮

修改 _config.yml :

syntax_highlighter:  # 設為空值

重新生成後,終於看到 Token info 輸出了!但是:

Prism encountered mermaid! lang: mermaid info: mermaid
Token info:  <------ equals mermaid? false

查看生成的 HTML:

<figure class="highlight mermaid">
  <figcaption data-lang="mermaid"></figcaption>
  <table>
    <tr><td data-num="1"></td>
    <td><pre><span class="token keyword">graph</span> LR</pre></td></tr>
    <!-- Prism 語法高亮的結果 -->
  </table>
</figure>

發現問題 #5:代碼塊被 markdown-it-prism 插件進行了語法高亮,而不是交給 markdown-it-mermaid 處理。

原因是在 hexo-renderer-multi-markdown-it/lib/renderer/index.js 中,插件加載順序為:

const default_plugins = [
    // ...
    './markdown-it-mermaid',   // 第 19 個
    // ...
    './markdown-it-prism',     // 第 21 個(最後處理)
    // ...
];

Prism 插件在 Mermaid 之後加載,會覆蓋 md.renderer.rules.fence 。

# 第六步:修改 Prism 跳過 Mermaid

在 node_modules/hexo-renderer-multi-markdown-it/lib/renderer/markdown-it-prism/index.js 中,在處理代碼塊的開頭添加:

md.renderer.rules.fence = (tokens, idx, options, env, self) => {
    const token = tokens[idx]
    const info = token.info
    const text = token.content.trim()
    const lang = info.trim().split(" ")[0]
    // Skip mermaid - let it be handled by markdown-it-mermaid
    if (lang.toLowerCase().includes('mermaid')) {
        return defaultRenderer(tokens, idx, options, env, self);
    }
    //... Prism 處理其他語言
}

# 第七步:驗證修復

重新生成後,查看 HTML:

<pre class="mermaid graph">
  <svg id="mermaid-..." width="100%" xmlns="http://www.w3.org/2000/svg">
    <!-- 完整的 SVG 流程圖 -->
  </svg>
</pre>

成功!Mermaid 代碼塊被正確渲染成 SVG 了。

# 完整解決方案

# 1. 禁用 Hexo 內建語法高亮

檔案: _config.yml

# 將 syntax_highlighter 設為空值
syntax_highlighter:

# 2. 啟用 markdown-it-mermaid 插件

檔案: _config.yml

markdown:
  plugins:
    # ... 其他插件
    - plugin:
        name: ./markdown-it-mermaid
        enable: true
        options:
          theme: default

# 3. 修改 Prism 跳過 Mermaid 處理

檔案: node_modules/hexo-renderer-multi-markdown-it/lib/renderer/markdown-it-prism/index.js

在 md.renderer.rules.fence 函數開頭添加:

// Skip mermaid - let it be handled by markdown-it-mermaid
if (lang.toLowerCase().includes('mermaid')) {
    return defaultRenderer(tokens, idx, options, env, self);
}

# 4. (可選)配置前端 Mermaid.js 渲染

如果要使用前端渲染而非構建時渲染,還需:

檔案: themes/shoka/_config.yml

vendors:
  js:
    mermaid: npm/mermaid@8.13.5/dist/mermaid.min.js

檔案: themes/shoka/source/js/_app/pjax.js

vendorCss('mermaid');
vendorJs('mermaid', function() {
  if (window.mermaid) {
    mermaid.initialize({
      theme: 'default',
      startOnLoad: false,
      flowchart: {
        useMaxWidth: true,
        htmlLabels: false
      }
    });
    mermaid.init(undefined, '.mermaid');
  }
}, window.mermaid);

# 使用方法

在文章的 Front Matter 中添加 mermaid: true :

---
title: 我的文章
mermaid: true
---
```mermaid
graph TD
    A[開始] --> B{判斷}
    B -->|是| C[執行]
    B -->|否| D[結束]
```

# 驗證效果

以下是三個測試範例:

# 範例 1:流程圖

Link textSquare RectCircleRound RectRhombus

# 範例 2:序列圖

AliceBobHello Bob, how are you?Great!AliceBob

# 範例 3:類別圖

Animal+int ageDuck+String beakColor+swim()Fish

# 範例 4: 複雜流程圖

現場付款信用卡付款失敗成功失敗成功On-siteOnline付款成功付款失敗前端:客戶填寫訂單內容 + 選擇付款方式前端判斷付款方式準備現場付款資料呼叫統一API POST /createOrder前端:TapPay SDK 獲取 Prime TokenPrime Token 結果顯示信用卡錯誤準備線上付款資料呼叫統一API POST /createOrder後端:驗證訂單資料資料驗證返回驗證錯誤檢查 paymentType驗證現場付款資料創建 unpaid 訂單返回 unpaid 訂單驗證線上付款資料生成唯一 transactionIdStep 1: 創建 TransactionStep 2: 標記處理中Step 3: 呼叫 TapPay APIStep 4: TapPay 付款結果Step 5: 標記付款完成Step 6: 創建正式訂單Step 7: 關聯訂單與交易處理付款後續邏輯返回 paid 訂單標記付款失敗清理失敗交易返回付款失敗錯誤前端:顯示等待付款確認前端:顯示付款成功前端:顯示付款失敗前端:顯示驗證錯誤前端:信用卡錯誤

# 範例 5: 時序圖

用戶前端後端API支付服務提供商資料庫選擇支付方式並提交訂單POST /api/order/:orderId/payment檢查訂單狀態(是否為待支付)返回訂單資訊發起支付請求(建立交易)返回支付URL/參數更新訂單狀態為"處理中"返回支付URL/參數顯示支付頁面或重定向至支付網址進行支付操作POST /api/order/:orderId/payment/callback驗證回調請求的真實性更新訂單狀態(已支付/失敗)確認收到(200 OK)推送支付成功通知(可選)顯示支付成功頁面確認收到(200 OK)推送支付失敗通知(可選)顯示支付失敗,提供重試選項alt[支付成功][支付失敗]用戶前端後端API支付服務提供商資料庫

# 保存修改(重要)

由於修改了 node_modules 中的檔案,每次執行 npm install 或 yarn install 都會被覆蓋。建議使用 patch-package 保存修改:

# 安裝 patch-package
npm install patch-package --save-dev
# 創建補丁
npx patch-package hexo-renderer-multi-markdown-it
# 在 package.json 添加
"scripts": {
  "postinstall": "patch-package"
}

執行後會在專案根目錄生成 patches/hexo-renderer-multi-markdown-it+版本號.patch 文件,未來重新安裝依賴時會自動應用補丁。

# 問題根源總結

這個問題的根本原因是多層代碼塊處理器的執行順序衝突:

  1. Hexo 層:內建的 highlight.js 在 Markdown 渲染之前就處理了代碼塊
  2. Markdown-it 層: markdown-it-prism 覆蓋了 markdown-it-mermaid 的 fence 規則
  3. 配置層: markdown-it-mermaid 插件未在配置中明確啟用

解決方案是:

  • 禁用 Hexo 層的語法高亮,交給 markdown-it 處理
  • 讓 Prism 跳過 mermaid,交給專門的 mermaid 插件處理
  • 明確啟用 mermaid 插件

# 參考資料

  • Hexo 官方文檔
  • hexo-renderer-multi-markdown-it
  • Mermaid 官方文檔
  • 參考文章:Hexo 博客 + Shoka 主題 Mermaid 流程图问题

# 進階修復:三個額外問題

在基本修復後,我又遇到並解決了三個額外問題。

# 問題 A:複雜圖表的 removeChild 錯誤

現象:

Error: Evaluation failed: DOMException: Failed to execute 'removeChild' on 'Node':
The node to be removed is not a child of this node.

原因: markdown-it-mermaid/index.js 中移除 style 標籤的代碼假設了不正確的 DOM 結構。

解決方案:保留 style 標籤(同時也能保留 classDef 樣式)

檔案: node_modules/hexo-renderer-multi-markdown-it/lib/renderer/markdown-it-mermaid/index.js

// 修改前(第 29-32 行)
const svg = await page.$eval('#container', container => {
    container.lastChild.removeChild(container.getElementsByTagName('style')[0])
    return container.innerHTML
})
// 修改後
const svg = await page.$eval('#container', container => {
    // 保留 style 標籤,讓 classDef 自定義樣式可以正常工作
    // 不再移除 style,避免丟失用戶自定義的 classDef 樣式
    return container.innerHTML
})

# 問題 B:classDef 自定義顏色不顯示

現象:使用 classDef 定義的自定義顏色無效,圖表只顯示黑白。

原因:移除 style 標籤會導致用戶自定義的 classDef 樣式丟失。

解決方案:保留 style 標籤(與問題 A 的解決方案相同)。

效果:

  • ✅ 支援 classDef 自定義顏色
  • ✅ 支援 Mermaid 內建主題(default, dark, forest, neutral)

# 問題 C:大圖顯示被壓縮

現象:複雜的大圖(如長流程圖)被壓縮到很小的空間,內容看不清楚。

原因:Shoka 主題的 CSS 限制了圖表最大高度為 300px。

解決方案:移除高度限制

檔案: themes/shoka/source/css/_common/components/third-party/mermaid/mermaid.styl

// 修改前
.mermaid {
  &.graph svg{
    max-height: 18.75rem;  // 約 300px
  }
}
// 修改後
.mermaid {
  &.graph svg{
    //max-height: 18.75rem;  // 移除高度限制
    max-width: 100%;  // 確保寬度不超出容器
  }
}

重新生成 CSS:

hexo clean && hexo generate

# 使用 classDef 自定義樣式

保留 style 標籤後,我們可以使用 Mermaid 的 classDef 功能自定義節點顏色。

# 方法 1:使用 classDef 自定義顏色(推薦)

```mermaid
graph TD
    A[前端節點] --> B[後端節點]
    C[成功節點] --> D[錯誤節點]
    classDef frontendClass fill:#e3f2fd,stroke:#1976d2
    classDef backendClass fill:#f3e5f5,stroke:#7b1fa2
    classDef successClass fill:#c8e6c9,stroke:#388e3c
    classDef errorClass fill:#ffcdd2,stroke:#d32f2f
    class A frontendClass
    class B backendClass
    class C successClass
    class D errorClass
```

優點:

  • 完全自定義,可以精確控制每個節點的顏色
  • 支援 Material Design 等設計系統的顏色
  • 可以定義 stroke-width 等進階樣式

# 方法 2:使用 Mermaid 內建主題

在 _config.yml 中設定主題:

markdown:
  plugins:
    - plugin:
        name: ./markdown-it-mermaid
        enable: true
        options:
          theme: default  # 可選:default, dark, forest, neutral

主題預覽:

  • default - 淺色主題,適合白色背景
  • dark - 深色主題,適合深色背景
  • forest - 綠色主題
  • neutral - 中性色主題

# 方法 3:混合使用

全域使用內建主題,個別圖表使用 classDef 覆寫:

# _config.yml
options:
  theme: default
```mermaid
graph TD
    A[使用預設主題] --> B[使用自定義顏色]
    classDef customClass fill:#ff6b6b,stroke:#c92a2a
    class B customClass
```

# 補丁管理

所有 node_modules 的修改已通過 patch-package 持久化保存:

patches/hexo-renderer-multi-markdown-it+0.1.5.patch

已在 package.json 中配置 postinstall 腳本:

{
  "scripts": {
    "postinstall": "patch-package"
  },
  "devDependencies": {
    "patch-package": "^8.0.1"
  }
}

每次執行 npm install 或 yarn install 後,補丁會自動應用。

# 使用 yarn

如果使用 yarn:

# 1. 安裝 patch-package 和 postinstall-postinstall
yarn add patch-package postinstall-postinstall --dev
# 2. 創建補丁
yarn patch-package hexo-renderer-multi-markdown-it
# 3. 配置 postinstall(package.json 中已配置)
# "scripts": { "postinstall": "patch-package" }
# 4. 驗證
yarn install  # 補丁會自動應用
hexo clean && hexo generate

# 在其他專案中應用

# 方法 1:複製補丁文件

# 1. 複製 patches 目錄到新專案
cp -r patches /path/to/new-project/
# 2. 在新專案中安裝 patch-package
cd /path/to/new-project
npm install patch-package --save-dev
# 3. 添加 postinstall 腳本
npm pkg set scripts.postinstall="patch-package"
# 4. 安裝依賴(會自動應用補丁)
npm install

# 方法 2:手動應用修改

參考本文的修復步驟,手動修改對應文件。

# Mermaid 語法兼容性

注意: hexo-renderer-multi-markdown-it@0.1.5 使用 Mermaid v8.x。

支援的語法:

  • ✅ graph TD / graph LR - 流程圖
  • ✅ sequenceDiagram - 序列圖
  • ✅ classDiagram - 類別圖
  • ✅ stateDiagram - 狀態圖
  • ✅ gantt - 甘特圖
  • ✅ pie - 餅圖

不支援的語法(Mermaid v9+ 新增):

  • ❌ flowchart TD - 使用 graph TD 代替
  • ❌ flowchart LR - 使用 graph LR 代替

# 已知限制

  1. Mermaid 版本:目前使用 mermaid v8.x,不支援 v9+ 的新語法(如 flowchart )
  2. Puppeteer 依賴:構建時需要啟動 Chromium,構建速度較慢
  3. 記憶體需求:複雜圖表可能需要更多記憶體

# 後記

這次除錯過程花了不少時間,但也讓我更深入理解了 Hexo 的渲染流程和插件系統。從最初的插件衝突,到 removeChild 錯誤,再到樣式和高度限制問題,每一步都需要仔細分析和測試。

希望這篇文章能幫助到遇到類似問題的人。

關鍵經驗:

  1. 善用 console.log:在關鍵節點添加調試輸出
  2. 理解執行順序:找出各個處理器的加載和執行順序
  3. 查看生成結果:實際查看生成的 HTML 來驗證問題
  4. 保存修改:使用 patch-package 避免修改丟失
  5. 完整測試:不僅測試基本功能,也要測試複雜場景和樣式
更新於 閱讀次數 次