| name | fix-mermaid |
| description | Use this skill to fix Mermaid diagram syntax errors inside HTML, Markdown, and TSX files. Trigger when the user mentions: "mermaid error", "Syntax error in text", "mermaid not rendering", "diagram is broken", "all diagrams crashed", or references a Mermaid version error (e.g. "mermaid version 10.9.5"). Fixes formatter-induced indentation pollution and statement concatenation that break Mermaid v10 parsing.
|
| allowed-tools | ["Read","Edit","Grep","Bash"] |
Mermaid v10 構文修正スキル
🚀 まず再利用スクリプトを使う(トークン節約・最優先)
静的 HTML の Mermaid 描画崩れを直すときは、ボイラープレート(render ループ・SVG 後処理・中央寄せ CSS)を手書きで再生成しないこと。以下の再利用スクリプトで機械的処理を一括適用できる。
-
図ソースを JS テンプレートリテラルで定義(LLM の判断が必要なのはここだけ):
各図を 1 ステートメント 1 行・カラム 0・改行は <br/> で const DIAGRAMS = { 'diag-1': 'flowchart TD ...' } として HTML の <script> 内に書く。
-
描画パイプラインを冪等適用:
bun run .claude/skills/fix-mermaid/scripts/apply_render_pipeline.ts <file.html>
これが <div class="mermaid">…</div> → 連番 id 付き空 div への置換、startOnLoad:false+securityLevel:'loose' 付与、applySvgFixups+render ループ注入、中央寄せ CSS 注入をまとめて行う(再実行しても二重適用しない)。
-
正本 Markdown から図を復元する場合(HTML 側ソースが破壊された等):
bun run .claude/skills/fix-mermaid/scripts/restore_diagrams.ts <file.html> <source.md>
-
インデント汚染・行分断のみの修正(.html/.md/.tsx)は fix_mermaid.ts:
bun run .claude/skills/fix-mermaid/scripts/fix_mermaid.ts <file>
SVG 幅の鉄則: apply_render_pipeline.ts は SVG 幅に viewBox 由来の自然 px 幅 + maxWidth:100% を使う。width:'100%' も width:'auto'(viewBox のみで intrinsic サイズを持たない SVG ではコンテナ全幅へ伸びる)も、小さい flowchart LR 図を異常拡大させるため使わない。
対象
.html ファイル内の <div class="mermaid"> ブロック
.md ファイル内の ```mermaid ブロック
.tsx / .ts / .jsx / .js ファイル内の chart={...} などのテンプレートリテラル内の Mermaid 構文
Mermaid v10 の必須ルール
- コンテンツはカラム0配置(先頭空白なし)
- 各ステートメントは改行で分離(1行に複数連結しない)
- ノードラベル
A["text"] の内容は1行に収める
mindmap のみ例外 — 内部インデントは階層構造を表すため保持する
block-beta は使用禁止 — v10.9.5 で全体クラッシュの原因になる。graph TD で代替する
よくある原因
HTMLやコードのフォーマッタ(Prettier等)による破壊パターン:
- 14スペース等のHTML/コードインデントがMermaidコンテンツに混入する
- 長いノードラベルが行分断される(
A["テキスト と 続き"] に分かれる)
- 複数ステートメントが1行に連結される(
graph TD A["x"] B["y"] A --> B)
修正手順
Grep で修正対象ファイルを検索し、Mermaid ブロックを把握する
Read で各ブロックを確認し、上記ルール違反を特定する
Edit または自動修正スクリプトで各ブロックの内容を修正する
自動修正を行う場合は TypeScript 版スクリプト fix_mermaid.ts を bun で実行します:
bun run .claude/skills/fix-mermaid/scripts/fix_mermaid.ts path/to/file.tsx
変換例
Before(壊れた状態):
<div class="mermaid">
graph LR A["ノードA"] B["ノードB"] A --> B
style A fill:#fff
</div>
After(修正後):
<div class="mermaid">
graph LR
A["ノードA"]
B["ノードB"]
A --> B
style A fill:#fff
</div>
ダイアグラム別の注意点
詳細は references/mermaid-v10-guide.md を参照。要点のみ:
| 種別 | 注意点 |
|---|
graph / flowchart | 最頻出。カラム0ルールを厳守 |
sequenceDiagram | Note over A,B: は1行に収める |
mindmap | 内部インデント保持(唯一の例外) |
block-beta | 使用禁止(全体クラッシュ) |
htmlLabels: true 環境 | < → <、> → > に変換 |
実地検証済み:ブラウザレンダラー固有の問題(2026年3月)
静的パーサー @mermaid-js/parser ではエラーにならないが、ブラウザの Mermaid v10.9.5 レンダラーで Syntax error in text が発生するパターン。
IDEフォーマッター(Prettier)による破壊が根本原因
<div class="mermaid"> に Mermaid ソースを直接書くと、VSCode/Prettier が保存のたびにインデントを付加して構文を壊す。恒久対策は JS テンプレートリテラルへの移管。
<div class="mermaid">
graph LR
A --> B
</div>
<div id="diag-0"></div>
<script>
const DIAGRAMS = {
'diag-0': `graph LR
A --> B`,
};
mermaid.initialize({ startOnLoad: false });
(async () => {
for (const [id, src] of Object.entries(DIAGRAMS)) {
const { svg } = await mermaid.render('svg-' + id, src);
document.getElementById(id).innerHTML = svg;
}
})();
</script>
この方式では --> を --> にエスケープする必要もなくなる。
ブラウザレンダラーで Syntax Error を起こす文字・構文
| 箇所 | 問題のある記述 | 対処 |
|---|
subgraph ラベル | 丸括弧 () を含む | 削除または別表現に置換 |
subgraph ラベル | 絵文字(🌐 🖥️ 等) | 削除 |
participant ... as | 絵文字(👤 ⚡ 等) | 削除 |
エッジラベル |...| | 先頭スラッシュ |/command| | スラッシュを除去 |
ノードラベル ["..."] | 全角波ダッシュ 〜 | から 等の日本語に置換 |
ノードラベル ["..."] | スラッシュ path/to | - またはスペースに置換 |
菱形ノード {} | クォートなし日本語 {新しいファイル} | {"新しいファイル"} とクォートする |
quadrantChart の座標 / テキスト | ダブルクォーテーションなしの文字列 | "" で囲む (例: "CEO/CTO": [0.8, 0.9]) |
| 全ての図解 (全般) | 全角丸括弧 () | 半角丸括弧 ( ) に置換する |
| 全ての図解 (全般) | 全角ダッシュ ― | 半角ハイフン - に置換する |
| 全ての図解 (全般) | 全角コロン : | 半角コロン : に置換する |
SVG サイズ制御
Mermaid v10 は SVG 要素に絶対ピクセル値の width/height 属性を付与する。mermaid.render() 後に必ず除去する。
svgEl.removeAttribute('width');
svgEl.removeAttribute('height');
svgEl.style.width = `${w}px`;
svgEl.style.maxWidth = '100%';
svgEl.style.height = 'auto';
CSS にもフォールバックを追加する:
.mermaid-wrap svg {
max-width: 100% !important;
height: auto !important;
}
シーケンス図・状態遷移図等の下部見切れ(クリッピング)対策(2026年6月追記)
Mermaid v10 のシーケンス図(sequenceDiagram)や状態遷移図(stateDiagram)のレンダラーには、描画される最下部要素(ライフライン下端、下部アクターボックス、ループブロック、警告メモ等)の境界座標を正しく計算できず、生成される SVG の viewBox 属性の高さ(height)が不足するバグがあります。
親要素(.diagram-wrap 等)に overflow-x: auto などが指定されている場合、CSSの仕様により縦方向もクリッピング(hidden 同等)されるため、はみ出た下部要素が切り落とされて見えなくなります。
【対策】
mermaid.render() 後に、JS で動的に viewBox の高さを拡張し、十分なスペースを確保した上で再適用します。
const viewBoxStr = svgEl.getAttribute('viewBox');
if (viewBoxStr) {
const parts = viewBoxStr.split(' ').map(Number);
if (parts.length === 4) {
const isSequenceOrState = src.trim().startsWith('sequenceDiagram') || src.trim().startsWith('stateDiagram');
const extraHeight = isSequenceOrState ? 110 : 15;
svgEl.setAttribute('viewBox', `${parts[0]} ${parts[1]} ${parts[2]} ${parts[3] + extraHeight}`);
}
}
quadrantChart の文字被り対策(2026年6月追記)
quadrantChart でプロットされる各要素のテキストラベルが重なって表示される場合は、mermaid.initialize の設定にて内部描画解像度を大きく指定します。
mermaid.initialize({
quadrantChart: {
chartWidth: 800,
chartHeight: 600,
pointRadius: 8,
pointLabelFontSize: 14
}
});
このうえで、HTML のラッパー(.mermaid-wrap 等)のインラインスタイル(例: style="max-width: 750px; margin: 0 auto;")で表示幅を制限することで、描画文字同士の被りを完全に回避しつつ、画面に収まる綺麗さでレスポンシブ表示できます。
HTML での Mermaid 図解の中央寄せ Flexbox スタイル(2026年6月追記)
静的 HTML で Mermaid を表示する際、図解が左寄せになるのを防ぎ中央寄せにするための CSS 実装例です。
.mermaid-wrap {
display: flex;
justify-content: center;
}
.mermaid {
display: flex;
justify-content: center;
width: 100%;
}
.mermaid svg {
display: block;
margin: 0 auto;
max-width: 100% !important;
height: auto !important;
}
React/Next.js (CSS Modules) 移植時の表示と中央寄せ(2026年5月追記)
React (Next.js App Router) 移行に際して共通の MermaidDiagram コンポーネントを使用する場合、CSS Modules との競合やテスト環境(Vitest)での描画エラーに注意する必要があります。
1. CSS Modules 環境下での中央寄せとサイズ制限
共通の MermaidDiagram コンポーネントは出力時にグローバルクラス "mermaid" を付与します。しかし、CSS Modules(*.module.css)で指定した .mermaid はクラス名がハッシュ化されるため、スタイルが当たらなくなり左寄せになってしまいます。
【対策】
- TSX 側:
MermaidDiagram をラッパー div で囲み、ハッシュ化されるクラス名 (styles.mermaid) と、個別幅制限用のグローバル ID (id="diag-X") を付与します。
<div id="diag-0" className={styles.mermaid}>
<MermaidDiagram chart={DIAGRAM_0} />
</div>
- CSS 側: ハッシュ化クラスから下位のグローバルな
svg をターゲットするため、:global セレクタを使用します。
.mermaid {
display: flex;
justify-content: center;
}
.mermaid :global(svg) {
display: block;
margin: 0 auto;
width: 100%;
max-width: 100%;
height: auto;
}
個別 ID セレクタ(#diag-0 svg 等)は CSS Modules でも変換されないため、グローバル ID セレクタ経由で最大幅(max-width)を制御できます。
2. テスト環境(Vitest)での MermaidDiagram のモック化
MermaidDiagram はクライアントサイドで動的に mermaid ライブラリを読み込んで動作するため、テスト環境での DOM レンダリング時にエラーを起こす原因となります。
テストファイル(page.test.tsx)では、必ず vi.mock を使ってダミー要素にモック化してください。
vi.mock("@/components/MermaidDiagram", () => ({
default: function DummyMermaidDiagram({ chart }: { chart: string }) {
return <pre data-testid="mermaid">{chart}</pre>;
},
}));
【移行後/将来用】Mermaid v11 + React 共通コンポーネントの可読性・文字切れ・文字色対策(2026年6月追記)
将来的な Next.js (App Router) 移行後は、mermaid@^11.15.0 を使用し、図は共通コンポーネント components/Mermaid.tsx('use client')で描画します。mermaid.render() が返す SVG 文字列を dangerouslySetInnerHTML で注入し、ページ固有スタイルは通常の CSS (.css) または CSS Modules に置きます。
症状と根本原因の対応表
| 症状 | 根本原因 | 対策 |
|---|
| 文字が低コントラストで読みづらい(特にエッジラベル・subgraph 見出し・シーケンス図 Note) | theme:'base'(非 darkMode)が edgeLabelBackground=lighten(...)・noteBkgColor="#fff5ad" 等の明色背景を算出。そこへ CSS で明色文字を当てると明×明で読めない | theme:'dark' + ソリッド濃色の themeVariables を明示(下記) |
| ノード内の文字が下端で切れる | 採寸と実描画の数 px 差で SVG viewBox 下端が見切れる | 描画後に viewBox の高さを拡張(flowchart +15 / sequence・state +110)+ overflow:visible |
| ノード文字が右端で切れる(emoji を含む図のみ。emoji 無しの図は無傷=切り分けの目印) | <foreignObject> は SVG 仕様上 overflow:hidden がデフォルト。emoji はラベル採寸時に「豆腐(tofu)」幅で測られ実描画で広がるため foreignObject 幅 < 実テキスト幅 となりクリップ | CSS で .mermaidTarget foreignObject { overflow: visible }(ノード矩形は十分広く、はみ出した文字も枠内に収まる) |
| 文字色を変えても全く反映されない | mermaid.initialize() はモジュール最上位で一度だけ実行されるため HMR では再実行されず古いテーマのまま。加えて .next キャッシュ汚染 | .next 削除 + dev サーバー完全再起動 + ブラウザのハードリロード(後述) |
| 日本語ラベルの幅不足による軽微な切れ | Web フォント(Noto Sans JP)読込前に採寸 | mermaid.render() 直前に await document.fonts.ready(jsdom 等は型ガードで skip) |
正準の mermaid.initialize 設定(v11)
mermaid.initialize({
startOnLoad: false,
theme: 'dark',
securityLevel: 'loose',
themeVariables: {
primaryColor: '#1a73e8', primaryTextColor: '#e8f0fe', primaryBorderColor: '#1a73e8',
lineColor: '#5f7fb8', secondaryColor: '#0f9d58', tertiaryColor: '#0d1a2e',
background: '#060b14', mainBkg: '#0f2040', nodeBorder: '#1a73e8',
clusterBkg: '#0d1a2e', titleColor: '#e8f0fe', edgeLabelBackground: '#0d1a2e',
fontFamily: "'Noto Sans JP', sans-serif", fontSize: '13px',
},
flowchart: { curve: 'basis', padding: 20 },
sequence: { actorMargin: 60, mirrorActors: true },
});
mainBkg を透明や半透明にしないこと。ノード背景がソリッド濃色だからこそ白文字が読め、CSS の !important 強制上書きが不要になる。
⚠️ SVG 後処理は「文字列加工」ではなく「ライブ DOM 操作」で行う
mermaid.render() の戻り値(SVG 文字列)を DOMParser('image/svg+xml') + XMLSerializer で往復させてはならない。foreignObject 内の htmlLabels(XHTML 名前空間の HTML)が壊れ、ラベルが width=0・テキスト空になって表示が潰れる。
innerHTML 注入後の実 DOM 要素を直接操作する。React では ref + svgStr 依存の useEffect で、注入済み <svg> に対して後処理を適用する。
const applySvgFixups = (svgEl: SVGSVGElement, chart: string): void => {
svgEl.removeAttribute('width');
svgEl.removeAttribute('height');
svgEl.style.height = 'auto';
svgEl.style.overflow = 'visible';
svgEl.style.marginBottom = '10px';
const viewBox = svgEl.getAttribute('viewBox');
if (!viewBox) return;
const parts = viewBox.split(/\s+/).map(Number);
if (parts.length !== 4 || !parts.every((n) => Number.isFinite(n))) return;
const trimmed = chart.trim();
const isSequenceOrState =
trimmed.startsWith('sequenceDiagram') || trimmed.startsWith('stateDiagram');
const extraHeight = isSequenceOrState ? 110 : 15;
const [x, y, w, h] = parts as [number, number, number, number];
svgEl.style.width = `${w}px`;
svgEl.style.maxWidth = '100%';
svgEl.setAttribute('viewBox', `${x} ${y} ${w} ${h + extraHeight}`);
};
React での使用例(components/Mermaid.tsx):
export default function Mermaid({ chart }: { chart: string }) {
const [svgStr, setSvgStr] = useState('');
const wrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let isMounted = true;
if (!chart) return;
const render = async () => {
try {
const id = `mermaid-svg-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
const { svg } = await mermaid.render(id, chart);
if (isMounted) setSvgStr(svg);
} catch (e) {
console.error('Mermaid rendering failed:', e);
if (isMounted) setSvgStr(`<p class="text-red-500">図表の描画に失敗しました</p>`);
}
};
render();
return () => { isMounted = false; };
}, [chart]);
useEffect(() => {
if (!svgStr || !wrapperRef.current) return;
const svgEl = wrapperRef.current.querySelector('svg');
if (svgEl) applySvgFixups(svgEl as SVGSVGElement, chart);
}, [svgStr, chart]);
return (
<div ref={wrapperRef} className="mermaid-wrapper"
dangerouslySetInnerHTML={{ __html: svgStr }} />
);
}
文字色は「ノードラベル限定」で当てる(明背景×明文字の再発防止)
過去、全 SVG テキストへ color/fill:#e6e9ee !important を当てた結果、エッジラベル・subgraph 見出し・シーケンス図 Note(明色背景)まで明色文字になり読めなくなる「もぐら叩き」を繰り返した。theme:'dark' で背景色は適正化されるため、CSS で色を当てるのはノードラベル(.node .nodeLabel)に限定する。エッジラベル / Note はテーマ任せ(暗背景+明文字)にする。
ノード文字色の方針(ユーザー選択:暗ノード=白 / 黄ノード=黒 が最も読みやすい):
.mermaidTarget :global(foreignObject) { overflow: visible; }
.mermaidTarget :global(foreignObject > div),
.mermaidTarget :global(.nodeLabel),
.mermaidTarget :global(.edgeLabel) { overflow: visible; }
.mermaidTarget :global(.node .nodeLabel),
.mermaidTarget :global(.node .nodeLabel *) { color: #ffffff !important; }
.mermaidTarget :global(.node[style*="fbbc04" i] .nodeLabel),
.mermaidTarget :global(.node[style*="fbbc04" i] .nodeLabel *),
.mermaidTarget :global(.node:has([style*="fbbc04" i]) .nodeLabel),
.mermaidTarget :global(.node:has([style*="fbbc04" i]) .nodeLabel *),
.mermaidTarget :global(.node:has([fill="#fbbc04" i]) .nodeLabel),
.mermaidTarget :global(.node:has([fill="#fbbc04" i]) .nodeLabel *) { color: #000000 !important; }
.edgeLabel * に fill:#fff を当てない。エッジラベルの背景 rect が白く塗り潰される。色を当てるのはラベルテキストのみ・color のみに留める。
外側 DOMPurify による過剰サニタイズで図が壊れる(QA_Studies 実地 2026年6月)
症状: ダークモードでノード内テキストが消える(背景と同化)/矢印・枠線のスタイルが当たらない。構文は正しくブラウザ Console にも構文エラーは出ない。
根本原因: mermaid.render() の出力 SVG に対し、コンポーネント側で外側から DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true } }) をかけていた。SVG プロファイルは HTML 要素を許可しないため、以下が同時に除去される。
htmlLabels: true のノードラベルは <foreignObject> 内の <div class="nodeLabel">(=HTML)として描画される → div/span が除去され テキスト消失。
- ノード/エッジ/テキストの色を与える
<style> ブロックが <style> 要素ごと除去 → 色・矢印・枠線が既定(黒)に戻る。
ADD_TAGS: ['style'] を足しても USE_PROFILES 制約下では除去が続く。
恒久対策:
- 図表が開発者直書きの静的定数(外部入力なし)なら、外側 DOMPurify は不要・有害。撤去して
mermaid.render() の出力をそのまま描画する。XSS 攻撃面が無く、Mermaid v11 は内部に DOMPurify を同梱しているため自前で無害化される。
- どうしても外側サニタイズを残すなら
USE_PROFILES: { svg: true }(HTML を排除するプロファイル)は使わず、style タグ+CSS 内容・foreignObject・div/span・style/class 属性を明示許可する。ただし CSS 無害化が脆く mermaid 出力変化に弱いため非推奨。
テストの落とし穴(再発防止):
- モックが
<svg><g>ok</g></svg> のような自明 SVG だと、<style> や <foreignObject> を含まないため壊れるサニタイズ経路を一度も通らず、テストが緑のまま本番が壊れる。モックには必ず <style> ブロックと <foreignObject> 内 HTML ラベルを含めること。
- ただし happy-dom / jsdom は SVG 内
<style> 内容や foreignObject の HTML 子要素の DOMParser/XMLSerializer ラウンドトリップを完全再現できない。ユニットテストでは「<style> 要素が除去されないこと」など機械的に確実な差分のみを検証し、色・ラベル文字列の可視性は実ブラウザの目視で担保する。
詳細な解決記録は docs/archive/MERMAID_TROUBLESHOOTING.md を参照。
確認手順(重要・順序厳守)
-
*.css 変更時は dev サーバーを完全再起動する。mermaid.initialize がモジュール最上位= HMR で再実行されないため。
kill $(lsof -ti:3000) 2>/dev/null; rm -rf .next; bun run dev
-
ブラウザはハードリロード(⌘+Shift+R)。通常リロードでは古い SVG/CSS が残る。
-
目視確認はユーザー側で実施する(このリポジトリでは Playwright/ブラウザ自動操作は使わない方針)。
Mermaid を諦めて HTML/CSS に置き換えるべきケース
以下は CSS では対処不能なため、純粋な HTML/CSS ウィジェットに置き換える:
flowchart TD で 5〜6 ノードを直列チェーン → 縦長 900px 超
- 接続されていない複数のサブグラフ(ノード数が非対称なためアスペクト比が崩れる)
判断基準:「ノード増減に関わらず、他の図と同じ高さに収まる保証がない場合」