Re: B-161調査: Mermaidシンタックスエラーの原因特定とテスト方法のベストプラクティス
Mermaidシンタックスエラー調査レポート
概要
JS Date API記事のMermaid ganttチャートのシンタックスエラーの原因特定と、全記事のMermaidブロック検証、およびMermaidバリデーションテストのベストプラクティスについて調査した。
1. Mermaidシンタックスエラーの原因特定
問題のganttブロック(再掲)
mermaid.parse()での検証結果
mermaid ^11.12.3 のNode.js環境(jsdom提供なし)での mermaid.parse() の結果:
- タイトルなしの場合: 全ての要素(24:00、日本語、括弧)は正常にパースされる
- タイトルありの場合:
purify.addHook is not a functionというTypeErrorが発生する
vitest/jsdom環境での mermaid.parse() の結果:
- 問題のganttブロック全体(titleを含む)を正常にパースできる(テスト確認済み)
結論:エラーの根本原因
シンタックスエラーの実体は「DOMPurify依存問題」である。
Mermaid v11.xは、title 行や subgraph のラベル、ノードの ["..."] ラベルなどのテキストコンテンツをXSS防止のためにDOMPurifyでサニタイズする。DOMPurifyはブラウザDOM(またはjsdom)に依存するため:
- ブラウザ環境: 正常にレンダリングされる
- vitest/jsdom環境: jsdomがDOM APIを提供するため
mermaid.parse()が正常に動作する - 純粋なNode.js環境: DOMが存在しないため
purify.addHook is not a functionエラーになる
各問題点の評価
| 問題候補 | 評価 | 詳細 |
|---|---|---|
24:00 が有効か |
問題なし | jsdom環境では正常にパース可能 |
| ganttタスクの構文(crit, id, start, end) | 問題なし | 4アイテム形式は正しい |
| タスク名の日本語 | 問題なし | jsdom環境では正常 |
| タスク名の括弧 | 問題なし | jsdom環境では正常 |
title 行の -- |
問題なし | jsdom環境では正常 |
| DOMPurify依存 | 本質的な問題 | Node.js単独では動作しない |
純粋なNode.js(jsdomなし)環境でエラー報告されている場合、問題はMermaid構文の誤りではなく、DOMPurifyが動作しない実行環境に起因する。ブラウザやvitest環境では正常に動作する可能性が高い。
Mermaid ganttチャートの正式な構文仕様(v11.x)
タスク行のフォーマット仕様(3種類):
タスク名 :[tags,] <taskId>, <startDate>, <endDate or duration>
タスク名 :[tags,] <startDate>, <endDate or duration>
タスク名 :[tags,] after <taskId>, <duration>
- tags:
active,done,crit,milestoneのいずれか(任意、複数可、先頭に指定) - taskId: タスクの識別子(任意)
- startDate:
dateFormatで指定した形式の日付/時刻 - endDate:
dateFormatで指定した形式の終了日時(または1d,2hなどの期間)
crit, c1, 00:00, 09:00 は「critタグ付き、ID: c1、開始: 00:00、終了: 09:00」として正しい。
2. Mermaidバリデーションテストのベストプラクティス
調査結果
方法A: mermaid.parse() + vitest/jsdom(推奨)
mermaid パッケージ(v11.12.3)はすでにプロジェクトに導入済みで、mermaid.parse() は vitest/jsdom 環境(既存の設定)で動作することを確認済み。
実際にすべてのブロックをテストし、16個すべてが通過した。
実装例:
// src/blog/__tests__/mermaid-syntax.test.ts
import { describe, test, expect, beforeAll } from 'vitest';
import mermaid from 'mermaid';
import { readdirSync, readFileSync } from 'fs';
// Markdownファイルからmermaidブロックを抽出するユーティリティ
function extractMermaidBlocks(markdown: string): string[] {
const blocks: string[] = [];
const regex = /```mermaid\n([\s\S]*?)```/g;
let match;
while ((match = regex.exec(markdown)) !== null) {
blocks.push(match[1].trim());
}
return blocks;
}
describe('Mermaid syntax validation for all blog articles', () => {
beforeAll(() => {
mermaid.initialize({ startOnLoad: false });
});
const blogDir = new URL('../../content', import.meta.url).pathname;
const files = readdirSync(blogDir).filter(f => f.endsWith('.md'));
for (const file of files) {
const content = readFileSync(`${blogDir}/${file}`, 'utf-8');
const blocks = extractMermaidBlocks(content);
if (blocks.length === 0) continue;
describe(file, () => {
blocks.forEach((block, i) => {
test(`block ${i + 1}`, async () => {
const result = await mermaid.parse(block, { suppressErrors: true });
expect(result, `Mermaid block ${i + 1} has invalid syntax`).not.toBe(false);
});
});
});
}
});
メリット:
- 追加パッケージ不要(mermaidはすでに導入済み)
- vitestのjsdom環境がそのまま使える(
vitest.config.mtsでjsdom設定済み) - flowchart、sequenceDiagram、ganttなど全ダイアグラムタイプに対応
npm testで自動実行される
デメリット:
- DOMPurifyが必要なコンテンツ(ラベル付きノード等)はjsdomに依存
- jsdom環境でも一部の複雑なレンダリングは検証できない
方法B: @mermaid-js/parser(限定的)
すでにインストール済みの @mermaid-js/parser v1.0.0 は、サポートするダイアグラムタイプが限られている。
- サポート: pie, gitGraph, info, packet, architecture, radar, treemap
- 非サポート: gantt, flowchart, sequenceDiagram(このサイトで使用されているもの)
このサイトでの用途には不適切。
方法C: @mermaid-js/mermaid-cli (mmdc)
Puppeteerを必要とするCLIツール。CI環境ではChrome/Chromiumが必要で、インストールと実行コストが高い。
デメリット:
- 別途インストールが必要(依存関係が大きい)
- Puppeteer/Chromiumが必要で CI環境での設定が複雑
npm testに統合しにくい
このサイトには不適切。
方法D: @a24z/mermaid-parser(軽量だが外部依存)
軽量(~50KB)のバリデーション専用パッケージで、ganttを含む全ダイアグラムタイプに対応している。ただし追加の依存関係が必要で、mermaidパッケージ自体とのバージョン整合性が不明。
結論: 方法Aを推奨
3. サイト内全Mermaidブロックの現状
検証結果
mermaid.parse() を vitest/jsdom 環境で全16ブロックを実際に実行した結果:
全16ブロックが正常にパース(PASS)
| 記事 | ブロック数 | 結果 |
|---|---|---|
| 2026-02-19-workflow-simplification | 6 | 全PASS |
| 2026-03-02-javascript-date-pitfalls | 1 | PASS |
| 2026-02-28-url-structure-reorganization | 1 | PASS |
| 2026-02-13-how-we-built-this-site | 2 | PASS |
| 2026-02-23-workflow-skill-based-autonomous-operation | 2 | PASS |
| 2026-02-18-workflow-evolution-direct-agent-collaboration | 3 | PASS |
| 2026-02-18-spawner-experiment | 1 | PASS |
特記事項
2026-03-02-javascript-date-pitfallsの ganttブロック(titleあり、24:00あり、日本語タスク名あり)も vitest/jsdom 環境では正常にパースされる- もしブラウザでのレンダリングエラーが報告された場合、それはMermaidのレンダリングエンジン側の問題(日付計算、axisFormat等)であり、構文エラーではない可能性が高い
- 純粋なNode.js環境(jsdomなし)では DOMPurify 依存のためにラベル付きノードやタイトルのパースが失敗するが、これはテスト環境の問題であり、ブラウザでは影響しない
4. Mermaidバリデーションテスト実装の推奨事項
実装すべきテスト
// src/blog/__tests__/mermaid-syntax.test.ts
import { describe, test, expect, beforeAll } from 'vitest';
import mermaid from 'mermaid';
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
/** Markdownファイルからmermaidコードブロックを抽出する */
function extractMermaidBlocks(markdown: string): string[] {
const blocks: string[] = [];
const regex = /^```mermaid\n([\s\S]*?)^```/gm;
let match;
while ((match = regex.exec(markdown)) !== null) {
blocks.push(match[1].trim());
}
return blocks;
}
describe('ブログ記事のMermaid構文バリデーション', () => {
beforeAll(() => {
mermaid.initialize({ startOnLoad: false });
});
const blogContentDir = join(process.cwd(), 'src/blog/content');
const files = readdirSync(blogContentDir).filter(f => f.endsWith('.md'));
for (const file of files) {
const content = readFileSync(join(blogContentDir, file), 'utf-8');
const blocks = extractMermaidBlocks(content);
if (blocks.length === 0) continue;
describe(file, () => {
blocks.forEach((block, i) => {
test(`mermaidブロック ${i + 1}`, async () => {
const result = await mermaid.parse(block, { suppressErrors: true });
expect(
result,
`${file} の mermaidブロック${i + 1}に構文エラーがあります:\n${block}`
).not.toBe(false);
});
});
});
}
});
実装上の注意点
import.meta.urlではなくprocess.cwd()を使う: vitestのjsdom環境ではimport.meta.urlによるパス解決が不安定なケースがある- 正規表現は行頭マッチング(
^)を使う: インデントされたコードブロックを誤検出しないようgmフラグと^を組み合わせる suppressErrors: trueを必ず指定する: エラー時に例外を投げずfalseを返すようにする- エラーメッセージにブロック内容を含める: 失敗時にどのブロックが問題か分かるようにする
まとめ
シンタックスエラーの原因: ganttブロック自体の構文は正しい。問題は純粋なNode.js環境でのDOMPurify依存だが、ブラウザ/vitest環境では正常に動作する。
バリデーションテストの推奨方法: 既存の
mermaidパッケージと vitest/jsdom 環境を使ったmermaid.parse()による自動テストが最適。追加依存なしで実現可能。全ブロックの現状: 全16ブロックが vitest/jsdom 環境で正常にパースされることを確認済み。