ターミナルから起動して、ブラウザで Markdown を閲覧する軽量ビューアです。
- 🌗 ライト / ダーク テーマ切替 (
prefers-color-schemeに追従、localStorageで記憶) - 🧜 Mermaid 図のレンダリング (
```mermaidブロック) - 🪟
```htmlブロックを プレビュー / ソース タブで切替表示 (sandbox iframe で隔離レンダリング) - 🧭 左サイドバーで TOC + 同階層 / 直下サブディレクトリの
.mdを一覧表示。TOC はデフォルト h2 のみ、閲覧中のセクション内のみ h3 / h4 を展開。リンククリックで軽量 SPA 遷移 (history.pushState) - 🔄 ファイル変更を検知してブラウザを自動リロード (Server-Sent Events、依存追加なし)
- 🎨 highlight.js によるコードブロックのシンタックスハイライト (テーマ連動)
- 🖼 Markdown と同ディレクトリ配下の画像・アセットを自動配信
- ☑️ タスクリストのチェックボックスをブラウザ上で操作してそのままソースファイルへ反映
- 💬 本文の範囲選択 → 右クリックでコメントを追加 (HTML コメント形式で元ファイルに保存、マウスオーバーで吹き出し表示)
- 📦 サーバー側依存は
markedのみ。シンプルでポータブル
GFM のタスクリスト (- [ ] todo / - [x] done) はブラウザ側でクリック可能。状態を変更すると POST /__mdview/edit/checkbox でサーバが直接元 Markdown を書き換えます (N 番目の項目を toggle)。
本文の任意の範囲を選択 → 右クリック → 「💬 コメントを追加」を選んでコメントを入力すると、ソースファイルが次のように書き換えられます。
本文の<span class="mdview-comment-mark" data-mdview-comment-id="1">対象範囲</span>にコメント。
<!--mdview-comment[1]: コメント本文-->- 該当範囲はスタイルに影響しない neutral な
<span>で wrap し、軽く下線とアイコン (💬) を表示 - コメント本体はHTML コメント
<!-- -->として元ファイルに保存 (画面には表示されない、git diff にも素直に出る) - マウスホバーでコメント内容が tooltip 表示される
- v1 制限: 範囲選択は同一ブロック内のプレーンテキストにのみ対応 (太字・リンク等を含む選択は前後コンテキストが一致しないと挿入できない)
- Node.js >= 18 (内蔵
node:testを利用するため。動作確認は 22.x) - Linux / macOS / WSL
git clone https://github.com/hawkymisc/mdview.git
cd mdview
npm install
npm link # `mdview` をグローバルコマンドとして使えるようにする
npm linkを使わない場合はnode /path/to/mdview/bin/mdview.js <file.md>で直接起動できます。
mdview path/to/document.mdローカルポートに HTTP サーバーが立ち上がり、デフォルトブラウザが自動で開きます。
Ctrl+C で停止します。
| Option | Default | 説明 |
|---|---|---|
-p, --port <num> |
0 (ランダムな空きポート) |
リッスンするポート番号 |
-H, --host <host> |
127.0.0.1 |
バインドするホスト |
--no-open |
(無指定で自動オープン) | ブラウザを自動で開かない |
-h, --help |
— | ヘルプを表示 |
# 固定ポートで起動
mdview README.md --port 4000
# ブラウザを開かない (curl/別端末確認用)
mdview docs/spec.md --no-open
# LAN 内の別マシンから見たい場合 (信頼できるネットワークでのみ)
mdview slides.md --host 0.0.0.0 --port 4000ターミナル
└─ mdview file.md
└─ Node.js HTTP server (127.0.0.1:PORT)
├─ GET / → Markdown を HTML にレンダリングして返す
├─ GET /raw → 起動 md の生 Markdown テキスト
├─ GET /__mdview/files → 同階層 + 直下サブ 1 階層の .md 一覧 (JSON)
├─ GET /__mdview/fragment?path → 指定 .md の <main> innerHTML (SPA 用)
├─ GET /__mdview/events?path → SSE (ファイル変更通知, path 指定可)
├─ GET /<path>.md → Accept: text/html ならレンダリング HTML
│ それ以外は raw md (?raw=1 で強制 raw)
└─ GET /<asset> → file.md と同じディレクトリから静的配信
(パストラバーサル防御あり)
ブラウザ
├─ HTML を表示 (CSS 変数でテーマ切替)
├─ Mermaid.js (CDN) が `<pre class="mermaid">` ブロックを SVG に変換
├─ サイドバーの .md リンクは fetch + main innerHTML 差替 + pushState で SPA 遷移
└─ EventSource("/__mdview/events?path=...") を購読し、`reload` 受信時に
スクロール位置を退避 → location.reload() を実行
ファイル監視は Node 標準の fs.watch をディレクトリ単位で行い (atomic save によるエディタの「rename 保存」にも追従できるように)、連続イベントは 75ms にデバウンスして 1 つの reload としてブロードキャストします。テーマ設定は localStorage、スクロール位置は sessionStorage に保持されるため、リロード後も状態が復元されます。
リポジトリに samples/demo.md を同梱しています。テーマ切替や Mermaid 図、表、タスクリストなどの動作確認に利用できます。
mdview samples/demo.mdnpm test # ユニット (node --test、ブラウザ不要)
npm run test:e2e # E2E (Playwright + Chromium、初回は npx playwright install chromium が必要)テストは 2 層構成です。
ユニット (test/) — node --test で実行、ブラウザ不要
test/render.test.js— Markdown → HTML 変換、Mermaid ブロック処理、XSS エスケープtest/server.test.js— HTTP エンドポイント、SSE 配信、close クリーンアップ、パストラバーサル防御
E2E (e2e/) — Playwright + Chromium で実ブラウザ検証
e2e/render.spec.js— 基本レンダリング (h1、テーマボタン、メタフッター)e2e/theme.spec.js— テーマ切替 (メニュー開閉、キーボード操作、prefers-color-scheme連動、localStorage 永続化)e2e/syntax-highlight.spec.js— hljs クラス付与、テーマ連動でスタイルシート切替、Mermaid 非干渉e2e/mermaid.spec.js—pre.mermaid内に<svg>が描画されることe2e/html-preview.spec.js—```htmlブロックのプレビュー / ソース切替、sandbox 属性、iframe 内描画e2e/sidebar.spec.js— サイドバー表示 / 折りたたみ永続化 / SPA 遷移 / 戻る進む / 直接 deep URL / TOC アンカー / 日本語名 / モバイル overlaye2e/live-reload.spec.js— ファイル変更で自動リロード、スクロール / テーマの維持
各 E2E テストは createMdviewServer を使って tmp ディレクトリに独立した mdview サーバを立ち上げるため、テスト間で状態が干渉しません。
main への push と全 PR で GitHub Actions により自動実行されます (.github/workflows/test.yml):
- unit ジョブ: Ubuntu / Node 22 /
npm test - e2e ジョブ: Ubuntu / Node 22 / Chromium /
npm run test:e2e(Playwright ブラウザはバージョン連動でキャッシュ、失敗時にplaywright-report/をアーティファクト化)
両ジョブは並列実行。同 ref で push が連続した場合は concurrency: cancel-in-progress で古い run を自動キャンセルします。
このほかにセキュリティ関連の自動化として CodeQL (.github/workflows/codeql.yml) と Dependabot (.github/dependabot.yml) を併用しています。運用詳細は Security notes を参照してください。
mdview/
├── bin/mdview.js CLI エントリ (parseArgs + ブラウザ起動)
├── src/
│ ├── render.js Markdown → HTML (marked + カスタムレンダラ、見出し ID 付与)
│ ├── server.js HTTP サーバー (静的配信 + パストラバーサル防御 + サイドバー API)
│ ├── paths.js パス正規化ユーティリティ (server/client 共通規約)
│ └── template.js HTML シェル / CSS (テーマ / サイドバー) / クライアント側スクリプト
├── samples/ 動作確認用サンプル (サブディレクトリ構造)
│ ├── demo.md
│ ├── guides/{basics,advanced,概要}.md
│ └── reference/api.md
├── test/ ユニット (node --test)
└── e2e/ E2E (Playwright)
└── fixtures/ 共通フィクスチャ + サンプル Markdown
- ローカル閲覧用ツールとして設計されています。デフォルトのバインドは
127.0.0.1で、外部公開は想定していません。 - Markdown 内の
<script>/<iframe>タグおよびon*=属性はサーバー側で除去されます (defense-in-depth)。 ```mermaidブロックの本文は HTML エスケープされた状態で出力されます (本文中の<script>等で XSS が発生しないように)。```htmlブロックは<iframe sandbox="allow-same-origin">で隔離レンダリングされます。allow-scriptsは付与しないため、ブロック内の<script>/on*=ハンドラはプレビュー上で実行されません (HTML / CSS のみが反映)。allow-same-origin単独はスクリプト実行能力を持たないため、allow-scriptsとの同時指定で起きる sandbox escape の問題は発生しません。- 外部 CDN (
cdn.jsdelivr.net) から読み込む Mermaid / highlight.js には SRI (Subresource Integrity) ハッシュ (sha384) を付与済み。CDN 改ざんを検知してブラウザがロードを拒否します。バージョンを上げる際はsrc/template.jsのSRI定数を再計算してください:curl -sL <url> | openssl dgst -sha384 -binary | openssl base64 -A - 依存パッケージと GitHub Actions の脆弱性は Dependabot が weekly (毎週月曜 09:00 JST) で監視し、自動 PR を作成します (
.github/dependabot.yml)。 - 自前コードの静的セキュリティ解析は CodeQL が push / PR / weekly (毎週月曜 13:37 JST) で実行します (
.github/workflows/codeql.yml)。検知結果は GitHub リポジトリの Security タブ で確認・トリアージしてください。CodeQL の結果は現状 Required check には含めていない (情報提供レーン) ので、main マージは block しません。 --host 0.0.0.0で外部にバインドする場合は 信頼できるネットワーク内 でのみ使用してください。
GET /<path>.mdの挙動がAcceptヘッダで分岐するようになりました。Accept: text/html(= ブラウザ直接アクセス) → 自動的にレンダリング済み HTML を返しますAccept: */*(= curl のデフォルト) → 従来通り raw markdown を返します?raw=1クエリで Accept に関わらず強制 raw (スクリプト向けエスケープハッチ)
- 既存スクリプトが
curl経由で.mdを取得していた場合は影響ありません。ブラウザ直アクセスで raw が欲しいときは?raw=1を付与してください。
- ファイル一覧の走査範囲は 同階層 + 直下サブディレクトリ (1 階層のみ)。深いツリーは v0.5 では非対応 (将来検討)
- サイドバーは常時 ON。
--no-sidebar等の opt-out フラグは v0.5 では未提供 - ファイル監視は Markdown 本体と同ディレクトリ のみ (画像差し替え時のリロードは対象外。次バージョンで検討)
- 日本語見出しの slug は
encodeURIComponentfallback (URL hash は動作するが見栄え劣)。完全な transliteration は依存追加になるため対象外 - highlight.js のテーマは GitHub light/dark 固定 (
@highlightjs/cdn-assetsのstyles/から差し替え可能だが、現状は CLI フラグなし)
MIT