【Emacs】
Markdownを発表スライドとして表示する
(logosパッケージ)

  • EmacsでMarkdownファイルをそのままスライドとして発表できるlogosパッケージの設定をしてみました。
  • logos-outlines-are-pagesを有効にすると、Markdownの見出しが自動的にページ区切りにできます。
  • C-c l lでfocus modeを起動し、キー操作でページを前後に移動できます。
  • 配布用スライドが必要な場合はMarpを使い、同じMarkdownファイルをそのまま変換できます。

関連記事

1. Emacsを発表に使いたい

手元のEmacsを画面に表示して、発表しているのをみることがあります。

Orgファイルの見出しを開閉して、スライドやマインドマップのように使っているのはわかりやすく、同じことをMarkdownでやりたくなりました。

1. Emacsを発表に使いたい

Org modeでもよいのですが、Markdownの方が生成AIとの相性がよく、編集できる原稿がすぐに返ってくるからです。
そこで、Emacsで「そのまま発表する」ために、 logos パッケージを試してみました。

1.1. logosパッケージとは

logos は、Emacsのバッファをページ単位で読み進めるための focus mode パッケージです1
MarpやBeamerのように「スライドファイルを出力する」ものではなく、Emacsのバッファをその場で発表者ビューのように使う道具です。

1.1. logosパッケージとは

ページ区切りには、^L という改ページ文字を使います。
ただ、logos-outlines-are-pagest にすれば、正規表現で判定した見出しが自動的にページになります。
そのため、見出し構造のあるMarkdownのバッファでは、そのまま動かせます2

2. 設定したキーバインドと使うイメージ

logosには、デフォルトのキーバインドは用意されていないので、使いやすいように自分で設定します。

私の場合は、Markdownファイルを開いた状態で C-c l l を押すと、logos-focus-mode がオンになり、現在の見出しのページだけに表示が絞るようにしました3

そして、→で次のページへ、←で前のページへ移動できます。
もう一度 C-c l l を押すと全文表示に戻ります。

たとえば、Markdownはこのように書けば十分です。

# Common Lispのキュー

今日話すこと。

## キューとは

- FIFO構造
- BFSでよく使う
- head と tail を持つ

## consセルによる実装

(defstruct queue
  head
  tail)

## まとめ

- キューは状態を持つ
- head と tail の更新が重要Code language: Markdown (markdown)

見出しがそのままページになり、C-c C-n を押すたびに次の表示に切り替わります。

2.1. init.elの設定コード

(use-package logos
  :ensure t
  :custom
  (logos-outlines-are-pages t)

  :bind
  (("C-c l l" . my/logos-focus-mode))

  :config
  ;; logos-focus-mode がオンのときだけ効くキー
  (define-key logos-focus-mode-map (kbd "<right>")
              #'logos-forward-page-dwim)
  (define-key logos-focus-mode-map (kbd "<left>")
              #'logos-backward-page-dwim)

  ;; face-remap を戻すための記録
  (defvar-local my/logos-face-remap-cookies nil
    "Face remap cookies used while `logos-focus-mode' is active.")

  (defun my/logos-apply-presentation-faces ()
    "Apply presentation-oriented faces in current buffer."
    (setq my/logos-face-remap-cookies
          (list
           ;; 全体の文字サイズ
           (face-remap-add-relative 'default
                                    :height 1.5)

           ;; Markdown 見出し2
           (face-remap-add-relative 'markdown-header-face-2
                                    :overline nil
                                    :foreground "#3c3c3c"
                                    :background "#F1F0DF"
                                    :height 1.2
                                    :slant 'normal)

           ;; Markdown 見出し3
           (face-remap-add-relative 'markdown-header-face-3
                                    :overline nil
                                    :foreground "#3c3c3c"
                                    :background "#F1F0DF"
                                    :height 1.1
                                    :slant 'normal)

           ;; Markdown 記号部分
           (face-remap-add-relative 'markdown-markup-face
                                    :foreground "#555400"
                                    :background "#F1F0DF"
                                    :inherit 'normal))))

  (defun my/logos-clear-presentation-faces ()
    "Clear presentation-oriented faces in current buffer."
    (when my/logos-face-remap-cookies
      (mapc #'face-remap-remove-relative
            my/logos-face-remap-cookies)
      (setq my/logos-face-remap-cookies nil)))

  (defun my/logos-focus-setup ()
    "Setup buffer-local presentation view for `logos-focus-mode'."
    (if logos-focus-mode
        (progn
          ;; 二重適用を避ける
          (my/logos-clear-presentation-faces)
          (my/logos-apply-presentation-faces))
      (my/logos-clear-presentation-faces)))

  ;; logos-focus-mode の ON/OFF に表示変更を連動させる
  (add-hook 'logos-focus-mode-hook
            #'my/logos-focus-setup)

  (defun my/logos-focus-mode ()
    "Toggle `logos-focus-mode' and narrow/widen with it."
    (interactive)
    (if logos-focus-mode
        (progn
          (when (buffer-narrowed-p)
            (widen))
          (logos-focus-mode -1))
      (logos-focus-mode 1)
      (unless (buffer-narrowed-p)
        (logos-narrow-dwim)))))Code language: Lisp (lisp)

設定には、3つの工夫があります。

logos-outlines-are-pages t によって、Markdownの見出しをページとして扱います。
^L を手で入れる必要がないので、普通にMarkdownを書けば区切りができます。

→ と ← は logos-focus-mode-map に定義しています。
focus mode がオンのときだけページ送りのキーとして機能し、オフのときは markdown-mode 側の割り当てがそのまま使われます。

logos-focus-mode を直接キーに割り当てず、my/logos-focus-mode というラッパー関数を挟んでいます。
オンにするときは focus mode を有効化してから現在ページへ narrow し、オフにするときは先に widen してから無効化します。
あと、text-scale-modeで、フォントを拡大したり、戻したりします。
逆順にすると logos-focus-mode-hook 経由では無効化後の後処理が書きにくくなるためです。

なお、logos-narrow-dwim はトグル動作で、すでに narrow されている状態で呼ぶと widen します4
unless (buffer-narrowed-p) で囲んでいるのは、二重に呼ばれても widen にならないようにするためです。

3. logosとMarpとの使い分け

Emacs内でそのまま話すなら logos が向いています。

PDFやHTMLへの変換が不要で、編集しながら発表できます。
生成AIと往復しながらスライドを育てる用途にも合います。

一方、配布用のPDFやHTMLスライドが必要なら Marp です。
Marpは --- をスライド区切りとして、Markdownからスライドデッキを出力できます5
frontmatterに marp: true を書くだけで使えて、VS Code拡張やCLIが充実しています。

Emacsで logos を使って発表し、同じMarkdownをMarpで変換して配布する、という流れはそのまま成立します。

4. 最終的なコード

Code language: Lisp (lisp)

  1. 作者はProtesilaos Stavrou氏で、GNU ELPAから入れられます。現在の安定版は1.2.0(2024年9月3日リリース)で、開発版として1.3.0-devが進行中です。GNU ELPAからは M-x package-install RET logos で入れられます。 – logos GNU ELPA
  2. logos は設計上デフォルトでキーを一切バインドしません。公式マニュアルにも「To get the do-what-I-mean page motions add your own key bindings.」と明記されています。 – Logos Manual
  3. narrowingはEmacsの機能で、バッファの特定範囲だけをアクセス可能にし、それ以外を一時的に非表示にします。ファイルの保存時は全体が書き込まれます。解除には C-x n w(widen)を使います。 – Narrowing (GNU Emacs Manual)
  4. この挙動は logos-narrow-dwim のソースコード末尾に (t (widen)) として実装されており、公式マニュアルにも「narrowing が有効なら widen する」と説明されています。 – Logos Manual
  5. MarpはHTML・PDF・PowerPointへの変換に対応しています。変換にはGoogle ChromeまたはChromiumを使用します。VS Code拡張とMarp CLIが主な利用ツールです。 – Marp: Markdown Presentation Ecosystem