「描く」から「生成する」へ:AIとMermaidの強力な親和性
これまで、フロー図やシーケンス図を作成するには、Draw.ioや各種ドローツールを開き、
マウスを使って時間をかけてボックスや矢印を配置するのが一般的だったように感じます。
しかし、生成AIが日常的なツールとなった今、この「手作業で描く」スタイルは大きな転換点を迎えています。
Mermaidは、テキストで図の構造を定義できます。この形式を使えば、AIに図のたたき台となるコードを生成させ、エンジニアが構文と内容を確認してからMarkdownへ組み込む、という使い方ができます。生成された図が実際の処理や関係を正しく表しているかは、元のコードや仕様と照らして確認する必要があります。
本記事では、RailsアプリケーションのブログにMermaid.jsを組み込み、
AIで作った図の定義を確認し、Markdownから図を生成するアーキテクチャ
について解説します。
アーキテクチャ全体像
単にライブラリを読み込むだけでなく、「サーバー側でのマークアップ」と「クライアント側での描画」の役割を明確に分離しました。
1. サーバー側:Markdownレンダラーの拡張
Markdownを解析する際、Mermaidのコードブロックを「ただのコード」としてではなく、「図の種」として出力します。
-
Gem選定:
redcarpetを採用。高速かつ、レンダリング処理を自由にオーバーライドできるためです。 -
カスタムレンダラー:
block_codeメソッドを上書きし、言語指定がmermaidの場合のみ<div class="mermaid">を出力するように変更。これにより、既存のシンタックスハイライト(Prism.js等)との干渉を防ぎます。 -
セキュリティ:
html_escapeは、図の定義をHTMLに埋め込む際の特殊文字をエスケープします。当サイトの実装では、同じ目的でCGI.escapeHTMLを使用しています。これだけで、Mermaidによる描画後まで含めたXSSリスクを排除できるわけではありません。当サイトはHTMLラベルやクリックを許可するsecurityLevel: 'loose'を使っているため、図の入力を信頼できる内容に限定する必要があります。任意のユーザー入力を扱う場合は、既定のstrictを含め、入力元に応じた設定を確認してください(Mermaid公式のsecurityLevel )。
2. クライアント側:ライフサイクルに合わせた描画制御
Rails特有の「画面遷移(Turbo/Turbolinks)」と「描画パフォーマンス」の両立がキモです。
-
遅延ロード: 常にMermaidを読み込むのではなく、ページ内に
.mermaidクラスが存在する時だけ CDN から動的にロード。 -
二重描画の防止:
data-rendered属性を活用。Turboによるページ復元時や、動的なコンテンツ追加時に、「まだ図解されていないノードだけ」を狙ってmermaid.run()を実行します。
3. プレゼンテーション層:レスポンシブ対応
MermaidのSVGは、放っておくと画面幅を突き抜けることがあります。
CSSで「枠」と「余白」を制御し、スマホ閲覧時は横スクロールを許容する設計にしました。
実装のコア・スニペット
サーバーサイド(Ruby)
パーサーを拡張し、特定のフェンス(```mermaid)を検知するロジックです。
# app/helpers/custom_markdown_renderer.rb
class CustomMarkdownRenderer < Redcarpet::Render::HTML
def block_code(code, language)
return '' if code.nil?
lang = language.to_s.strip.downcase
if lang == 'mermaid'
# Mermaid.jsが解釈できる形式に変換
%(<div class="mermaid">#{ERB::Util.html_escape(code)}</div>)
else
# 通常のコードハイライト用
%(<pre><code class="language-#{ERB::Util.html_escape(lang)}">#{ERB::Util.html_escape(code)}</code></pre>)
end
end
end
クライアントサイド(JavaScript)
以下はMermaid 10系のES Modules版を使う、Turbo向けの簡略例です。初回の turbo:load より前に、一度だけ読み込むJavaScriptファイルへ置きます。Turbolinks 5の環境では、末尾を document.addEventListener("turbolinks:load", initMermaid); に置き換えてください。動的に図を追加した場合は、追加後に initMermaid() を呼ぶ必要があります。当サイトの実装では、script 要素による遅延ロードと turbolinks:load / DOMContentLoaded を組み合わせています(Mermaid公式のrun 、Turboのイベント 、Turbolinksのイベント )。
// Mermaidの初期化と実行
const initMermaid = async () => {
const targets = document.querySelectorAll('.mermaid:not([data-rendered])');
if (targets.length === 0) return;
// 動的インポート(必要な時だけロード)
const { default: mermaid } = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs');
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
await mermaid.run({ nodes: targets });
// 描画済みフラグを立てる
targets.forEach(n => n.dataset.rendered = 'true');
};
// Turboの遷移イベントに合わせて発火
document.addEventListener("turbo:load", initMermaid);
お披露目:シーケンス図サンプル
ブラウザ上では、中央寄せのSVGとして表示されます。コードブロックのコピー機能や他のハイライト表示は従来どおり維持できました。
まとめ
ちょうどブログにも図を使った説明を使っていきたいと思っていたところだったので、意外に簡単に導入できてよかったです!
ぜひ皆さんも表現の幅を広げるために導入してみてはいかがでしょうか?