最終更新日時:

mermaid.jsによるSSRを試す

はじめに

 先日から本ブログで使っているライブラリ(KaTeXやPrism.jsなど)についてSSRを行う方法を調べていたが、最後にmermaid.jsを試す。本当はGW中はブログをいじるつもりはなかったが、消化しておかないと落ち着かなかったため、さっさと調べてまとめておくことにした。
 当初はこれまでの記事のようにJSDOMでやろうとしたが、SVGの出力が完全にブラウザ依存(具体的にはSVGTextElement.getBBoxができない)であることが判明したため、今回はmermaid-cliを利用する。これは内部でPuppeteerを利用しているため原理上は動作する。

npm install @mermaid-js/mermaid-cli
Bash

 今回利用したパッケージのバージョンは以下のとおりである。なお、このバージョンではPuppeteerが古いと警告が出るが無視する。

{
  "dependencies": {
    "@mermaid-js/mermaid-cli": "^10.8.0"
  }
}
Plain text

ブラウザでのmermaid.jsの方法

 書き方を割と忘れてしまっていたため、貼っておく。ついでに、SSRを行う場合の比較としても機能するだろう。

<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<title>mermaid.jsを使ってみる</title>
		<script type="module">
			import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
			mermaid.initialize({ startOnLoad: false });
			// ダイアグラムの初期化
			await mermaid.run({
				querySelector: '.mermaid',
			});
		</script>
	</head>
	<body>
		<div class="mermaid">
			%%{init:{&#039;theme&#039;:&#039;dark&#039;}}%%
			sequenceDiagram
				オブジェクト1 クラス1 -&gt;&gt; オブジェクト2 クラス2: メッセージ1
				オブジェクト2 クラス2 -&gt;&gt; オブジェクト3 クラス3: メッセージ2
				オブジェクト3 クラス3 --) オブジェクト2 クラス2: リプライ1
				オブジェクト2 クラス2 -&gt;&gt; オブジェクト2 クラス2: 再帰
				オブジェクト2 クラス2 --) オブジェクト1 クラス1: リプライ2
		</div>
		<div class="mermaid">
			%%{init:{&#039;theme&#039;:&#039;dark&#039;}}%%
			stateDiagram
				direction LR
				[*] --&gt; 状態1
				状態1 --&gt; コンポジット状態: トリガー1
				state コンポジット状態 {
					[*] --&gt; 状態2
					状態2 --&gt; 状態2: トリガー2
					状態2 --&gt; 状態3: トリガー3
					状態3 --&gt; 状態2: トリガー4
					状態3 --&gt; 状態4: トリガー5
					状態4 --&gt; [*]: トリガー6
				}
				コンポジット状態 --&gt; 状態5
				状態5 --&gt; [*]: トリガー7
		</div>
		<div class="mermaid">
			%%{init:{&#039;theme&#039;:&#039;dark&#039;}}%%
			classDiagram
				direction LR
				class Interface {
					&lt;&lt;interface&gt;&gt;
				}
				class Abstract {
					&lt;&lt;abstract&gt;&gt;
					+abstractMethod()* type
				}
				class Class {
					+type publicStaticField$
					+type publicField
					-type privateField
					#type protectedField
					~type packagePrivateField
					+publicStaticMethod(arg: type)$ type
					+publicMethod(arg: type) type
					-privateMethod(arg: type) type
					#protectedMethod(arg: type) type
					~packagePrivateMethod(arg: type) type
				}
				Interface &lt;|.. Abstract
				Abstract &lt;|-- Class
		</div>
	</body>
</html>
Markup

SSRの方法

 今回は初手でHTMLファイル中のmermaid記法をダイアグラムに変換する処理を示す。それを行うにあたって以下のパッケージを導入する。

npm install jsdom
npm install webpack
npm install webpack-cli
Bash

 今回利用したパッケージのバージョンは以下のとおりである。

{
  "dependencies": {
    "@mermaid-js/mermaid-cli": "^10.8.0",
    "jsdom": "^24.0.0",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4"
  }
}
Plain text

 また、変換対象のHTMLは以下のとおりとする。これは、上で示したブラウザ上でmermaid.jsを利用するサンプルからJavaScriptを除去したものである。

<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<title>mermaid.jsを使ってみる</title>
	</head>
	<body>
		<div class="mermaid">
			%%{init:{&#039;theme&#039;:&#039;dark&#039;}}%%
			sequenceDiagram
				オブジェクト1 クラス1 -&gt;&gt; オブジェクト2 クラス2: メッセージ1
				オブジェクト2 クラス2 -&gt;&gt; オブジェクト3 クラス3: メッセージ2
				オブジェクト3 クラス3 --) オブジェクト2 クラス2: リプライ1
				オブジェクト2 クラス2 -&gt;&gt; オブジェクト2 クラス2: 再帰
				オブジェクト2 クラス2 --) オブジェクト1 クラス1: リプライ2
		</div>
		<div class="mermaid">
			%%{init:{&#039;theme&#039;:&#039;dark&#039;}}%%
			stateDiagram
				direction LR
				[*] --&gt; 状態1
				状態1 --&gt; コンポジット状態: トリガー1
				state コンポジット状態 {
					[*] --&gt; 状態2
					状態2 --&gt; 状態2: トリガー2
					状態2 --&gt; 状態3: トリガー3
					状態3 --&gt; 状態2: トリガー4
					状態3 --&gt; 状態4: トリガー5
					状態4 --&gt; [*]: トリガー6
				}
				コンポジット状態 --&gt; 状態5
				状態5 --&gt; [*]: トリガー7
		</div>
		<div class="mermaid">
			%%{init:{&#039;theme&#039;:&#039;dark&#039;}}%%
			classDiagram
				direction LR
				class Interface {
					&lt;&lt;interface&gt;&gt;
				}
				class Abstract {
					&lt;&lt;abstract&gt;&gt;
					+abstractMethod()* type
				}
				class Class {
					+type publicStaticField$
					+type publicField
					-type privateField
					#type protectedField
					~type packagePrivateField
					+publicStaticMethod(arg: type)$ type
					+publicMethod(arg: type) type
					-privateMethod(arg: type) type
					#protectedMethod(arg: type) type
					~packagePrivateMethod(arg: type) type
				}
				Interface &lt;|.. Abstract
				Abstract &lt;|-- Class
		</div>
	</body>
</html>
Markup

 以上で準備が整ったため、以下にSSRを行うコードを示す。mermaid-cliについてはESModule形式のインポートでなければ正しく動作しないためこのようにしている。やはり、ヘッドレスブラウザ(Pouppeteer)を利用する都合上、速度が遅いのが気になるとことである(おそらくオプションでいくらか高速化は可能と思われる)。なので、利用する場面はSSGなどにした方が無難ではあろう。

const { JSDOM } = require('jsdom');
import { renderMermaid } from '@mermaid-js/mermaid-cli';
const puppeteer = require('puppeteer');
const fs = require('fs');

// 入力するファイル
const inFile = 'in.html';
// 出力するファイル
const outFile = 'out.html';

// 入力されたファイルについてのDOMツリーの構築
const dom = new JSDOM(fs.readFileSync(inFile).toString(), { contentType: 'text/html'});

(async () => {
    const browser = await puppeteer.launch({});
    try {
        for (const e of dom.window.document.querySelectorAll('.mermaid')) {
            // ダイアグラムを構築
            const { data } = await renderMermaid(browser, e.textContent, 'svg', {});
            e.innerHTML = data.toString();
        }
    
        // ファイルの出力
        fs.writeFileSync(outFile, dom.serialize());
    }
    finally {
        await browser.close();
    }
})();
JavaScript

おわりに

 Pouppeteerを使ってしまうのならば、SSRも全部Pouppeteerのレンダリング結果を使えばいいのでは的なことも考えられるが、どのタイミングがレンダリングの完了とみなせるかといった問題があったり、その問題によりいわゆるハイドレーションが正しく実施できないなどの問題が連鎖的に生じることも考えられる。そのため、Pouppeteerの利用は限定的にしていきたいところである。まぁ前回までに示したMathJaxやprism.jsならば、非同期処理を適切に同期するようにするのならば、Pouppeteerを利用してSSRを行うことも可能だろう(利点が部分的にCSRで行う場合と同じコードが利用できることとJSDOMからの開放くらいしかないためそこまでの意味はないが)。