0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Emacs で Mermaid の図を含む Markdown ファイルをプレビューする

0
Last updated at Posted at 2026-04-04

Emacs で Markdown を書いていると、見出しやコードブロックだけでなく、Mermaid のような図もあわせて確認したくなります。そこで Emacs から Markdown を HTML へ変換し、ブラウザーでプレビューする仕組みを追加しました。

方針は単純です。Markdown バッファーの内容を一時的な HTML へ変換し、その HTML をブラウザーで開きます。Emacs の中で完結させるのではなく、描画はブラウザーへ任せます。これにより、Mermaid の描画も同じプレビューの流れへ載せられます。

使い方

Markdown バッファーで C-c C-p を実行すると、HTML を生成してブラウザーで開きます。いったんプレビューを開いた後は、保存のたびに同じ HTML を更新します。文書を修正して保存し、ブラウザー側を見れば結果を確認できます。

実装方針

HTML 側では marked.js で Markdown を描画し、mermaid コードブロックだけを Mermaid.js で図へ変換します。通常の Markdown はそのまま HTML になり、図が必要な部分だけ追加の処理を入れる構成です。

Markdown の本文をそのまま JavaScript に埋め込むと、バッククォート、-->、日本語を含む場合に壊れます。そこで Emacs 側では本文を UTF-8 へ変換してから base64 化し、ブラウザー側で復元して marked.parse へ渡します。この形なら、Markdown や Mermaid の記法をそのまま扱えます。

制約

この実装は CDN から marked.js と mermaid.js を読み込みます。そのため、ネットワークのない環境ではプレビューが動きません。また、ASCII アートのような等幅レイアウトは Markdown の段落やコードブロックの扱いに影響されるため、今回の実装で特別な補正は入れていません。図にできるものは Mermaid で書くほうが安定します。

実装

~/.emacs.d/init/40-markdown-mode.el へ追加した主要部分は次のとおりです。

(defvar-local my-markdown-mermaid-preview-file nil)

(defun my-markdown-mermaid-preview-output-file ()
  "Return the HTML output path for the current Markdown buffer."
  (let* ((source (or (buffer-file-name) (buffer-name)))
         (base (file-name-base source))
         (dir (expand-file-name "markdown-preview" temporary-file-directory)))
    (make-directory dir t)
    (expand-file-name (concat base ".preview.html") dir)))

(defun my-markdown-mermaid-preview-render ()
  "Render the current Markdown buffer to an HTML file with Mermaid support."
  (interactive)
  (unless (derived-mode-p 'markdown-mode)
    (user-error "Not in markdown-mode"))
  (let* ((output-file (my-markdown-mermaid-preview-output-file))
         (markdown-text (buffer-substring-no-properties (point-min) (point-max)))
         (markdown-base64
          (base64-encode-string
           (encode-coding-string markdown-text 'utf-8)
           t))
         (html (format "<!doctype html>
<html lang=\"ja\">
<head>
  <meta charset=\"utf-8\">
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
  <title>%s</title>
  <style>
    body {
      margin: 0;
      background: #f6f7fb;
      color: #1f2937;
      font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;
    }
    main {
      max-width: 960px;
      margin: 0 auto;
      padding: 40px 24px 64px;
      background: #ffffff;
      min-height: 100vh;
      box-sizing: border-box;
    }
    pre {
      overflow-x: auto;
      padding: 12px 16px;
      border-radius: 10px;
      background: #111827;
      color: #f9fafb;
      white-space: pre;
      overflow-wrap: normal;
      word-break: normal;
      tab-size: 2;
    }
    code {
      font-family: \"SFMono-Regular\", Consolas, monospace;
    }
    img {
      max-width: 100%%;
    }
    .mermaid {
      overflow-x: auto;
      padding: 8px 0;
    }
  </style>
</head>
<body>
  <main id=\"preview\"></main>
  <script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\"></script>
  <script type=\"module\">
    import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';

    const sourceBytes = Uint8Array.from(atob('%s'), c => c.charCodeAt(0));
    const source = new TextDecoder().decode(sourceBytes);
    const preview = document.getElementById('preview');
    preview.innerHTML = marked.parse(source, { breaks: true, gfm: true });

    mermaid.initialize({ startOnLoad: false });

    const blocks = Array.from(preview.querySelectorAll('pre code.language-mermaid'));
    for (const block of blocks) {
      const pre = block.parentElement;
      const container = document.createElement('div');
      container.className = 'mermaid';
      container.textContent = block.textContent;
      pre.replaceWith(container);
    }

    await mermaid.run({ querySelector: '.mermaid' });
  </script>
</body>
</html>"
                       (file-name-nondirectory (or (buffer-file-name) (buffer-name)))
                       markdown-base64)))
    (with-temp-file output-file
      (insert html))
    (setq my-markdown-mermaid-preview-file output-file)
    output-file))

(defun my-markdown-mermaid-preview-open ()
  "Render the current Markdown buffer and open the preview in a browser."
  (interactive)
  (browse-url-of-file (my-markdown-mermaid-preview-render)))

(defun my-markdown-mermaid-preview-refresh ()
  "Refresh the Markdown preview HTML after saving."
  (when (and my-markdown-mermaid-preview-file
             (derived-mode-p 'markdown-mode))
    (my-markdown-mermaid-preview-render)))

(add-hook 'markdown-mode-hook
          (lambda ()
            (define-key markdown-mode-map (kbd "C-c C-p") #'my-markdown-mermaid-preview-open)
            (add-hook 'after-save-hook #'my-markdown-mermaid-preview-refresh nil t)))

まとめ

大がかりな実装ではありませんでした。ただし、Markdown の本文を安全にブラウザーへ渡す方法と、Mermaid ブロックだけを取り出す方法に工夫が要りました。Emacs だけで完結させるより、HTML を生成してブラウザーで描画する方式が扱いやすくなります。

Mermaid を含む Markdown を軽く確認する用途なら、この構成で実用になります。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?