BloggerにMermaidを導入して、フローチャートを描く
<!-- markdown-mode-on -->
<img alt="JT logo" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiun9GB33GNzIYOiICg1ujk8_gbZpE-ipnQ8BNlL5BjdfhCSzV86LsBLOXBmf3HuiktPcWqHQEE0zCoTFnDNHEQkjleNvR_5eFu1yJP790q4-HsguNiSIsmo4dR3mnsy6M4dMNhdZFEYo9npCerqMCWQO_CB7QCdBWmjY4Gp5922YQsaazfmuxCb0K8lrdY/s320/jettheme_2.png" style="display:none;"/>
## 概要
Mermaidとは
<figure class="blogcard b-link"><a aria-label="記事詳細へ(別窓で開く)" href="https://mermaid.js.org/" rel="noopener noreferrer" target="_blank"><div class="blogcard-content"><div class="blogcard-image bi-link"><div class="blogcard-image-wrapper biw-link"><img alt="Mermaid" height="100" loading="lazy" src="https://mermaid.js.org/mermaid-logo-horizontal.svg" width="100"/></div></div><div class="blogcard-text"><p class="blogcard-title bt-link">Mermaid</p><p class="blogcard-description bd-link">Create diagrams and visualizations using text and code.</p></div></div><div class="blogcard-footer bf-link"> <img alt="ファビコン" height="16" loading="lazy" src="https://www.google.com/s2/favicons?domain=https://mermaid.js.org/" width="16"/>mermaid.js.org</div></a></figure>
Mermaidは、Markdown風のテキスト記法を使って、フローチャートやシーケンス図、ER図などのダイアグラムを簡単に作成できるオープンソースで無料利用可能なツールです。JavaScriptベースで動作し、GitHubやQiitaなどのMarkdown対応プラットフォームで利用できます。
<a name="more"></a>
これをはてなブログでは簡単に導入できたので、味をしめてこの Blogger/ JetTheme に導入してみます。まずは、サンプル図を載せます。
**フローチャート**
<div class="mermaid">
graph TD;
A-->B;
</div>
```mermaid
flowchart LR
Start --> Stop
```
```mermaid
graph
A["$$\rho \left( \frac{\partial v}{\partial t} + (v \cdot \nabla) v \right) = -\nabla p + \mu \nabla^2 v + \rho f$$"]
B["$$ \frac{d}{dr} \left( r \frac{d u_z}{dr} \right) = -\frac{r}{\mu} \frac{dp}{dz}$$"]
C["$$ u_z(r) = \frac{1}{4\mu} \left( \frac{dp}{dz} \right) (R^2-r^2) $$"]
S((S)) --> A
A --> B
B --> C
```
**ガントチャート**
```mermaid
gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1, 20d
section Another
Task in Another :2014-01-12, 12d
another task :24d
```
**アーキテクチャチャート**
```mermaid
architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
```
## 導入手順
### はてなブログの方法:失敗
はてなブログと同様に下記のスクリプトを `` の手前に書き入れたのですが、
```javascript
<!-- flowChart tool Marmaid -->
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
</script>
```
フォローチャートを描く書き方をしたところ、
````markdown
```mermaid
flowchart LR
Start --> Stop
```
````
**mermaid のコードブロックが Markdown のコードハイライト対象として処理されてしまう**ため、結果はコードブロックの表示にしかなりません。
### 解決策
**Mermaid の描画対象**として認識させるには、**Markdown のコードハイライトとの明確な切り分けが必要**です。
その方策は、Markdown で <code>```mermaid</code> と書いたときに、**Prism.jsではなく Mermaid が呼び出されるルール**を作ります。
既報の[「Markdown エディタを実装し、さらに Prism.js を適用する」](https://newprivatelibrary.blogspot.com/2025/05/markdown-prismjs.html) で作った `maked.js` のスクリプトを改良して、<code>```mermaid</code> だけを特別に `<pre class="mermaid">...</pre>` に変換させる `marked.js` の**カスタムレンダラー**を追加します。
変更箇所は、`code({ text, lang }) {...},` からの50行~59行です。
<details>
<summary>Mermaid対応のMarkdown エディタスクリプト</summary>
<pre class="line-numbers"><code class="language-javascript">
<b:comment>Markdown エディタ</b:comment>
<b:with value='"<!-- markdown-mode-on -->"' var='md_comment'>
<b:with value='data:post.body contains data:md_comment' var='md_enabled'>
<b:comment>メッセージ</b:comment>
<b:if cond='data:view.isPreview'>
<div class='md-message'>
<b:if cond='data:md_enabled'>
<p>Markdown モードがオンになっています。オフにしたい場合は投稿から <input expr:value='data:md_comment.escaped' type='text'/> を削除してください。</p>
<details class='md-convert'>
<summary>変換後の HTML</summary>
<textarea aria-label='変換後の HTML' class='md-output'/>
<button aria-label='コードを全選択する' class='md-select'>コード全選択</button>
</details>
<b:else/>
<p>Markdown モードをオンにしたい場合は投稿に <input expr:value='data:md_comment.escaped' type='text'/> を追加してください。</p>
</b:if>
</div>
</b:if>
<b:comment>投稿, ページ本文</b:comment>
<div class='entry-text text-break mb-5' id='post-body'>
<b:if cond='data:md_enabled'>
<b:class cond='data:md_enabled' name='md-input'/>
<data:post.body.escaped/>
<b:else/>
<data:post.body/>
</b:if>
</div>
<b:comment>Markdown 変換</b:comment>
<b:if cond='data:md_enabled'>
<script src='https://cdn.jsdelivr.net/npm/marked/marked.min.js'/>
<script>
(() => {
const comment = '<data:md_comment/>';
//<![CDATA[
const input = document.querySelector('div.md-input');
const output = document.querySelector('textarea.md-output');
const select = document.querySelector('button.md-select');
if(!input) return;
const escapeHTML = (text) => {
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
}
const renderer = {
code({ text, lang }) {
const validLanguage = lang ? `language-${lang}` : 'language-plaintext';
const code = escapeHTML(text);
if (lang === 'mermaid') {
return `<pre class="mermaid">${text}</pre>\n\n`;
}
return `\n<pre><code class="${validLanguage}">${code}</code></pre>\n\n`;
},
blockquote({tokens}){
const body = this.parser.parse(tokens).replace(/\n$/, '');
return `\n<blockquote>${body}</blockquote>\n\n`;
},
heading({tokens, depth}){
return `\n<h${depth}>${this.parser.parseInline(tokens)}</h${depth}>\n\n`;
},
hr(token){
return '\n<hr/>\n\n';
},
list(token){
const ordered = token.ordered;
const start = token.start;
let body = '';
token.items.forEach(item => {
body += ' ' + this.listitem(item);
})
const type = ordered ? 'ol' : 'ul';
const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : '';
return '\n<' + type + startAttr + '>\n' + body + '</' + type + '>\n\n';
},
paragraph({tokens}){
const text = this.parser.parseInline(tokens);
const regex = /^\s*(<a name=["']more['"]>).*?(<\/a>)\s*$/;
if(regex.test(text)){
return `\n${text.replace(regex, '$1$2')}\n`;
}else{
return `<p>${text}</p>\n`;
}
}
}
marked.use({ renderer });
const converted = marked.parse(input.textContent.replace(comment, ''));
input.innerHTML = converted;
input.classList.remove('md-input');
if(output){
output.value = converted.replace(/<a name=["']more["']>.*?<\/a>/, '<!--more-->').replace(/\n{3,}/g, '\n\n').trim();
}
if(!select) return;
select.addEventListener('click', () => {
output.focus();
output.setSelectionRange(0, output.value.length);
});
})();
//]]></script>
</b:if>
</b:with>
</b:with>
</code></pre></details>
続いて、チャートを描く背景を装飾がないようにCSSで定義します。ついでにダークモードで線が見えなくなることにも対応しました。
<details>
<summary>Mermaid対応のCSS</summary>
<pee><code class="languege-css">
pre.mermaid {
background: none;
border: none;
padding: 0;
overflow: auto;
}
pre.code.mermaid[data-lang="mermaid"]:before {
content: none;
}
/* ダークモード時の線の色 */
.dark-mode svg path,
.dark-mode svg line,
.dark-mode svg polyline {
stroke: #cccccc !important;
stroke-width: 1.5 !important;
opacity: 1 !important;
}
</code>
</pee></details>
## 番外編
前項でこのように説明しました。
**mermaid のコードブロックが Markdown のコードハイライト対象として処理されてしまう**ため、結果はコードブロックの表示にしかなりません。
言い換えれば、Markdown記法を使わず、HTML で書くならば `marked.js` の**カスタムレンダラー**は必要ありません。`<div>`タグと、`<pre>`タグが使えます。
```markdown
<div class="mermaid">
graph TD;
A-->B;
</div>
```
<div class="mermaid">
graph TD;
A-->B;
</div>
```markdown
<pre class="mermaid">
graph TD;
A-->B;
</pre>
```
<pre class="mermaid">
graph TD;
A-->B;
</pre>
## レイアウト
Mermaidで作成したチャートを**画面の中央に配置**するには、**CSSを使う方法**が一般的です。
HTML書式を使うと、テキストや図形と同じにレイアウトを設定できます。しかし、<code>\`\`\`mermaid</code> 書式ではそれができません。`marked.js` での <code>```mermaid</code> から `<pre class="mermaid">` へのDOM変換が失敗しているようです。解決策は今のところ見つかっていません。
### 方法1: CSSで中央配置
Mermaidのチャートを囲む`div`要素に**CSSの`text-align: center;`を適用**すると、中央に配置できます。
```html
<div style="text-align: center;">
<pre class="mermaid">
graph TD;
A-->B;
B-->C;
</pre>
</div>
```
<div style="text-align: center;">
<pre class="mermaid">
graph TD;
A-->B;
B-->C;
</pre>
</div>
````html
<div style="text-align: center;">
```mermaid
graph TD;
A-->B;
B-->C;
```
</div>
````
<b>失敗</b>
<div style="text-align: center;">
```mermaid
graph TD;
A-->B;
B-->C;
```
</div>
### 方法2: `display: flex;` を使う
`display: flex;` を使うことで、より柔軟に中央配置できます。
```html
<div style="display: flex; justify-content: center;">
<pre class="mermaid">
graph TD;
A-->B;
B-->C;
</pre>
</div>
```
<div style="display: flex; justify-content: center;">
<pre class="mermaid">
graph TD;
A-->B;
B-->C;
</pre>
</div>
### 方法3: Mermaidの設定を変更
Mermaidの設定で**`themeVariables`を使う**ことで、チャートの配置を調整できます。
これはテーマXMLでの変更になるので、使えません。
```html
<script>
mermaid.initialize({
themeVariables: {
nodeTextAlign: "center"
}
});
</script>
```
## 関連リンク
<figure class="blogcard b-link"><a aria-label="記事詳細へ(別窓で開く)" href="https://qiita.com/opengl-8080/items/a275119c5ff3012ff23a#%E5%8F%82%E8%80%83" rel="noopener noreferrer" target="_blank"><div class="blogcard-content"><div class="blogcard-image bi-link"><div class="blogcard-image-wrapper biw-link"><img alt="Mermaid使い方メモ - Qiita" height="100" loading="lazy" src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F28302%2Fdbddd92b-c7b2-c7f3-a421-5701b2d48272.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=04ca7e4b20659581dc9c6e8c46e304a0" width="100"/></div></div><div class="blogcard-text"><p class="blogcard-title bt-link">Mermaid使い方メモ - Qiita</p><p class="blogcard-description bd-link">Mermaid とは UML やフローチャートなどの図をテキストで記述できるツール JavaScript 製 似たものとして PlantUML がある こっちはJava製 PlantUML は名前の通り UML に特化している Mermaid は UML だけでなくガ...</p></div></div><div class="blogcard-footer bf-link"> <img alt="ファビコン" height="16" loading="lazy" src="https://www.google.com/s2/favicons?domain=https://qiita.com/opengl-8080/items/a275119c5ff3012ff23a#%E5%8F%82%E8%80%83" width="16"/>qiita.com</div></a></figure>
</pre></pre></div>