🕸️

MarpでMermaidを安定表示する

に公開

はじめに

MarpでMermaidを使ってスライドを作成していると、プレビューとPDFで表示が異なることがあります。特に日本語や絵文字を含む場合、フォントやレイアウトが崩れやすいという問題があります。この記事では、Zennスクラップ「Marpにいれたmermaid記法をVSCodeでPreviewとExport」の内容をベースに、安定して表示するための対処法をまとめます。

問題の概要

  • MermaidをMarpスライドに埋め込むと、プレビューとPDF出力で見た目が違う
  • フォントや文字の配置がずれる
  • mermaid.init() は後方互換として動作可能(非推奨)。v10以降の推奨APIは initialize()run() / render()。将来的な変更に備え、新APIへの移行を推奨
  • 日本語や絵文字を含むと描画時にエラーが発生することがある。

解決の方向性

Marp内でMermaidを扱う場合、Mermaidを直接描画させるのではなく、SVGに変換して画像として埋め込む方法を表示の安定に採用しています。

コード例

以下は、MermaidをSVG画像に変換してMarp内に表示するスクリプト例です。

---
marp: true
size: 16:9
paginate: true
---

<style>
  img.mermaid-100h { max-height: 100%; }
</style>

<script type="module">
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
  mermaid.initialize({ startOnLoad: false, theme: 'default' });

  const svgToDataURL = (svg) =>
    'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);

  window.addEventListener('DOMContentLoaded', async () => {
    const targets = document.querySelectorAll('pre.mermaid');
    let n = 0;

    for (const el of targets) {
      const code = (el.textContent ?? '').replace(/\u00A0/g, ' ').replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
      if (!code) continue;

      const id = (crypto?.randomUUID ? `m-${crypto.randomUUID()}` : `m-${Date.now()}-${n++}`);

      try {
        const { svg } = await mermaid.render(id, code);
        const img = document.createElement('img');
        img.src = svgToDataURL(svg);
        img.className = el.className;
        const style = el.getAttribute('style');
        if (style) img.setAttribute('style', style);
        el.replaceWith(img);
      } catch (e) {
        const warn = document.createElement('div');
        warn.style.cssText = 'padding:.75rem 1rem;border-left:6px solid #e53935;background:#fdecea;color:#b71c1c;border-radius:.5rem;';
        warn.textContent = `Mermaid描画エラー: ${e?.message || e}`;
        el.replaceWith(warn);
      }
    }
  });
</script>

### Flowchart test
<pre class="mermaid mermaid-100h">
flowchart LR
  A[開始] --> B[処理中(日本語OK)]
  B --> C[完了]
  B --> D[要確認 ❓👨‍👩‍👧‍👦(家族絵文字)]
  style A fill:#e0f7fa,stroke:#006064,stroke-width:2px
  style C fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
  style D fill:#fff3e0,stroke:#e65100,stroke-width:2px
</pre>

HTMLでテキストを取得したい場合はscriptを以下に書き換える

<script type="module">
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
  mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'strict' });

  const sanitize = (s) =>
    s.replace(/\u00A0/g, ' ')           // NBSPを通常スペースへ
     //.replace(/[\u200B-\u200D\uFEFF]/g, '') // ZWSP等の不可視文字を除去
     .replace(/[\u200B\uFEFF]/g, '') //絵文字などの影響がある場合
     .trim();

  window.addEventListener('DOMContentLoaded', async () => {
    const blocks = document.querySelectorAll('pre.mermaid, div.mermaid');
    let n = 0;

    for (const el of blocks) {
      const code = sanitize(el.textContent || '');
      if (!code) continue;

      const id = (crypto?.randomUUID ? `m-${crypto.randomUUID()}` : `m-${Date.now()}-${n++}`);

      try {
        const { svg } = await mermaid.render(id, code);
        // imgにせずインラインSVGで置換
        // 既存クラス/スタイルは必要ならラッパーdivに引き継ぎ
        const wrapper = document.createElement('div');
        wrapper.className = (el.className || '') + ' mermaid-inline';
        const inline = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement;
        wrapper.appendChild(inline);
        el.replaceWith(wrapper);
      } catch (e) {
        const warn = document.createElement('div');
        warn.style.cssText = 'padding:.75rem 1rem;border-left:6px solid #e53935;background:#fdecea;color:#b71c1c;border-radius:.5rem;';
        warn.textContent = `Mermaid描画エラー: ${e?.message || e}`;
        el.replaceWith(warn);
      }
    }
  });
</script>

ポイントまとめ

  • VSCode拡張では「Enable HTML」をONにする。
  • CLI利用時は --html --allow-local-files オプションを付ける。
  • btoa() は非ASCII文字に対応していないため、encodeURIComponent() でSVGをエンコードする。
  • NBSP(\u00A0)やゼロ幅スペースが混入していると構文エラーの原因になるため除去する。
    • 絵文字の結合(👨‍👩‍👧‍👦 など)やアラビア語・インド系文字では、ZWJ/ZWNJを削除すると表記が崩れることがあり対応する場合は次の置換に変える。
      • s.replace(/[\u200B\uFEFF]/g, '')
        
  • ID生成には crypto.randomUUID() を利用すると安全。

Mermaidテーマの調整

Mermaidでは、先頭に設定を記述することでテーマやスタイルを指定できます。
Marpではこの設定をHTMLスクリプト内でレンダリングする形で利用します。

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '24px'}, 'flowchart': {'nodeSpacing': 80}} }%%
flowchart LR
  A --> B

よくある問題と対策

症状 原因 対策
プレビューとPDFで色が違う スタイルの指定漏れ 色やフォントを明示的に指定
ラベルがはみ出す フォントやoverflow設定 overflow: visible を指定
mermaid.init()の扱いが混乱する 実際は動作するが非推奨 initialize() + render()の利用を推奨

まとめ

  • Mermaidは画像(SVG)として出力すると扱いやすい。ただし、SVGとして出力することで、Mermaidのインタラクティブな機能(例:クリックイベント)は利用できなくなる点には注意が必要
  • 日本語や絵文字を含む場合は、encodeURIComponent()でエンコードする。
  • mermaid.initialize()render() を使う(init()は動作するが非推奨)。

参考

Discussion