mp
Markdown を全画面プレゼンテーションにする Ruby 製ツールです。スライドに本物のターミナルと Web ページを埋め込めます。
必要なもの
- Ruby 3.3 以上
- Chrome 系ブラウザ(Chrome / Chromium / Edge / Brave。別の場所にある場合は
MP_BROWSERでパスを指定)
使い方
bundle install
bundle exec exe/mp examples/demo.md| オプション | 説明 |
|---|---|
-w, --window
|
全画面にせずウィンドウで開く |
-P, --presenter
|
発表者ビューも開く |
-p, --port PORT
|
ローカルサーバのポート(既定は空きポート) |
--profile DIR |
Chrome のプロファイルを保存する(埋め込みページのログインを保持) |
--no-watch |
ファイル変更時に再読み込みしない |
Markdown や CSS を保存すると自動で再読み込みします。このときターミナルのセッションはリセットされます。
スライドの書き方
| 記法 | 結果 |
|---|---|
# 見出し |
表紙スライド。次の H2 までの本文も表紙に入る |
## 見出し |
通常のスライド 1 枚。見出しがページタイトルになる |
--- |
タイトルなしの続きスライド |
見出しだけで本文がないスライドは、タイトルを画面の中央に置きます。左に寄せるには <!-- _class: left --> を書きます。
ターミナルの埋め込み
```terminal cwd=./demo height=60% font=20
npm test
git status
```本文に書いたコマンドは 1 行ずつ入力欄に打ち込まれますが、Enter を押すまで実行されません。実行すると次の行が打ち込まれます。本文は空でも構いません。
| 属性 | 説明 |
|---|---|
cwd |
作業ディレクトリ(デッキからの相対パス) |
height / width
|
大きさ(60%、400px)。省略すると残りの領域を埋める |
font |
文字サイズ(既定 18) |
Web ページの埋め込み
```web zoom=0.8 height=70%
https://example.com
```本文には http(s) の URL か、デッキからの相対パス(./page.html)を書きます。X-Frame-Options などで埋め込みを拒否するサイトも表示できます。JavaScript、WebAssembly、Web Worker を使うページも動きます。ディレクトリを指すときは ./app/ のように末尾に / を付けると index.html を開きます。
| 属性 | 説明 |
|---|---|
zoom |
表示倍率 |
height / width
|
ターミナルと同じ |
クリックするとページを操作でき、マウスが外に出るとスライドのキー操作に戻ります。
画面を左右に分けるには <!-- _class: columns --> を使います。本文の最後の要素が右の列に、それ以外が左の列に入ります。
## タイトル
<!-- _class: columns -->
説明の文章
- 箇条書き
```terminal
```ターミナルと Web を 1 つずつ書けば左右に並びます。上下に分けるには height=50% を指定します。
Marp 互換の記法
フロントマター / ディレクティブ
---
theme: default # default / dark / ./my-theme.css
size: 16:9 # 16:9 / 4:3
title: タイトル
paginate: true
duration: 20 # トークの想定時間(分)。"20:00" "90s" も可
header: ヘッダー
footer: フッター
style: |
section { font-size: 28px; }
---
<!-- class: lead --> 以降のスライドに適用
<!-- _backgroundColor: black --> このスライドだけに適用(先頭に _)duration を指定すると、表紙の次のページへ進んだ時点からタイマーが動き(最初のスライドが表紙でなければ表示した時点から)、右下に「ページ番号 / 総枚数」「このページの残り」「全体の残り」を表示します(paginate のページ番号はタイマーの中に移ります)。
- 1 ページの持ち時間は、全体の時間を表紙を除いた枚数で均等に割った長さです(20 分で表紙と 10 枚なら 2 分)
- 表紙にいる間は全体の残りが止まったままで、ページの残りは表示しません
- ページごとに使った時間を積算するので、戻って再び開くと続きから数えます
- 全体の残りは残り 2 分(短いトークでは全体の 2 割)、ページの残りは持ち時間の 2 割を切ると色が変わり、超過すると赤で超過時間を表示します
デッキを保存して再読み込みしてもタイマーは続きます。発表者ビューにも残り時間が出て、
rで両方をリセットできます。
使えるディレクティブは paginate header footer class color backgroundColor backgroundImage backgroundPosition backgroundRepeat backgroundSize です。クラスには lead(中央寄せ)、left(左寄せ)、right(本文だけ右寄せ)、invert(配色反転)、columns(2 列)があります。
画像
 サイズ指定
 本文の残りの領域に画像全体を収める
 フィルタ
 背景(複数並べると分割)
 cover / contain / fit / auto / 50%
 画面を分割して片側に配置その他
- 上記以外の
<!-- コメント -->は発表者ノートになる(nで表示) -
*のリストは 1 項目ずつ表示し、-のリストは一度に表示する -
# <!-- fit --> 見出しは見出しを横幅いっぱいに拡大する -
<style scoped>はそのスライドだけに効く - 数式は
$...$と$$...$$(KaTeX) - コードはシンタックスハイライトされる(Rouge)
発表者ビュー
bundle exec exe/mp --presenter examples/demo.md発表中に p を押しても開けます。現在のスライド、次のスライド、発表者ノート、経過時間、現在時刻を表示します。
- 操作: 発表者ビューのキー操作は聴衆画面に反映されます。聴衆画面を操作した場合も発表者ビューが追従します
-
次の枠:
*リストの未表示項目が残っていれば、次に表示される項目を先に見せます - ターミナルと Web: 現在の枠には聴衆画面をそのまま映します(1 秒に数回更新)。次の枠にはコマンドや URL を表示します
-
台本の実行: ターミナルのスライドでは次のコマンドを表示し、発表者ビューの
Enterで聴衆画面のターミナルを実行します - ディスプレイ: 外部ディスプレイがあれば、聴衆画面をそちらの全画面に移し、発表者ビューを手元の画面に置きます
-
閉じる: 発表者ビューだけを閉じても発表は続きます。
qを押すと全体が終了します
| キー | 動作 |
|---|---|
→ ↓ Space PageDown
|
次へ |
← ↑ PageUp
|
前へ |
Home / End
|
最初 / 最後 |
数字 + Enter
|
そのページへ |
Enter |
台本のコマンドを実行 |
r |
経過時間をリセット(duration のタイマーも戻す。表紙にいれば開始前に戻る) |
+ / -
|
ノートの文字サイズ |
q |
終了 |
1 画面で使うと、発表者ビューが聴衆画面の全画面の裏に隠れます。macOS ではスペースを切り替えて表示してください。
キー操作
| キー | 動作 |
|---|---|
→ ↓ Space PageDown
|
次へ |
← ↑ PageUp
|
前へ |
Home / End
|
最初 / 最後 |
数字 + Enter
|
そのページへ |
t またはクリック |
ターミナルを操作 |
⌘. |
ターミナルから抜ける |
⌘← / ⌘→
|
ターミナル操作中でもページ移動 |
f |
全画面の切り替え |
n |
発表者ノート |
p |
発表者ビュー |
? |
ヘルプ |
q |
終了 |
しくみと安全性
Ruby のローカルサーバ(puma)がスライドを配信し、Chrome をアプリモードで開きます。ターミナルは Ruby の PTY と WebSocket で xterm.js につなぎます。Web 埋め込みでは、Chrome DevTools Protocol でフレームのレスポンスから X-Frame-Options と CSP の frame-ancestors を取り除きます。
ターミナルはコマンドを実行できるので、次のように保護しています。
- サーバは
127.0.0.1のみで待ち受け、全パスにランダムトークンを要求する - WebSocket は Origin を検証する
- CSP により、デッキ内の HTML に書いたスクリプトは実行されない
- 埋め込むローカルページは別 origin(
localhost)で配信し、ターミナルへ接続できない - コマンドは Enter を押すまで実行されない
それでも、Enter を押せば書かれたコマンドは実行されます。他人が作ったデッキは内容を確認してから発表してください。
埋め込みを拒否するヘッダを外すのは、mp が起動する Chrome の中だけです。この Chrome は既定で一時プロファイルを使い、普段のプロファイルには影響しません。
制限
-
SharedArrayBufferが必要なページ(Cross-Origin Isolation が前提のもの)は動きません - 埋め込みページは iframe なので、サードパーティ Cookie の制限によりログイン状態を維持できないサイトがあります
-
:smile:形式の絵文字ショートコードは未対応です(Unicode の絵文字は使えます) - 発表者ビューから入力できるのは
Enterだけです。台本にないコマンドを打つときは、聴衆画面のターミナルをクリックしてください
開発
bundle exec rake test
bundle exec ruby test/e2e.rb
bundle exec ruby test/presenter_e2e.rb
bundle exec ruby test/embed_e2e.rbtest/*_e2e.rb と test/e2e.rb はヘッドレス Chrome で実際に動かして確認します。Web 埋め込みの確認にはネットワーク接続が必要です。
vendor/assets には xterm.js と KaTeX を同梱しています。実行に Node.js は不要です。
リリース
| コマンド | 説明 |
|---|---|
bundle exec rake build |
テストを実行し、pkg/ に gem を作る |
bundle exec rake install |
gem を作ってローカルにインストールする |
bundle exec rake release |
テスト、ビルド、タグ付け、push、RubyGems への公開を行う |
手順は次のとおりです。
-
lib/mp/version.rbのバージョンを上げる -
bundle installでGemfile.lockを更新する - 変更をコミットする
-
bundle exec rake releaseを実行する
rake release にはコミット済みの git リポジトリと origin リモート、RubyGems の認証情報が必要です。未コミットの変更があると公開前に中断します。