mdv-live 0.5.17 → 0.5.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.18] - 2026-05-12
9
+
10
+ ### Fixed — Offline operation
11
+
12
+ ビューワがネットワーク接続なしでも完全に動くようになった。これまで
13
+ `index.html` が 5 つの CDN (highlight.js / Mermaid / html2pdf.js / Tailwind /
14
+ hljs テーマ CSS) を直接読み込んでいたため、Wi-Fi 切断時はシンタックスハイ
15
+ ライト・図表・PDF 出力・全 UI スタイルが死ぬ状態だった。
16
+
17
+ - `src/static/vendor/` に各ライブラリのオフライン版を同梱
18
+ - `scripts/sync-vendor.js` でメンテナが version bump 時に node_modules /
19
+ Tailwind Play CDN から再生成 (`node scripts/sync-vendor.js`)
20
+ - Tailwind は v3.4.17 で pin (v4 系は `tailwind.config` 構文が変わるため)
21
+ - `index.html` と `app.js` (`HLJS_THEMES`) の CDN URL を `/static/vendor/...`
22
+ に置換
23
+ - 各ライブラリのライセンス本文を `vendor/licenses/` に同梱、
24
+ html2pdf bundle が名指しする `html2pdf.bundle.min.js.LICENSE.txt` も sidecar 配置
25
+ - `@highlightjs/cdn-assets` / `mermaid` / `html2pdf.js` は **devDependencies**
26
+ (vendor 元、runtime では使わないので global install 時にダウンロードされない)
27
+ - `tests/test-offline-assets.js` で「served HTML/JS に外部 CDN URL がない」
28
+ 「必須 vendor ファイル / license 一式が揃う」「vendor-only パッケージが
29
+ dependencies に逆流していない」を 14 件の assert で常時保証
30
+
31
+ ### Verified
32
+
33
+ - 272 テスト 全 PASS (既存 258 + 新規 14)
34
+ - Playwright dogfood (`docs/dogfood-offline-2026-05-11/`): 非 localhost への
35
+ リクエスト 0 件、code highlight / mermaid / Tailwind / edit autosave /
36
+ theme 切替 / Marp split layout / inline notes すべて回帰なし
37
+ - Codex review 2 round で「No actionable regressions」収束
38
+
8
39
  ## [0.5.17] - 2026-05-10
9
40
 
10
41
  ### Added — Edit-mode Autosave
package/README.md CHANGED
@@ -11,11 +11,13 @@
11
11
  - 📄 Markdownをリアルタイムレンダリング
12
12
  - 🎬 **Marp完全対応**(公式テーマ・ディレクティブ・数式)
13
13
  - 🎤 **Presenter View**(スピーカーノート別ウィンドウ・自動保存・タイマー) — `P` キーで起動
14
+ - 🪟 **PowerPoint 風 Split Layout** — 上にスライド / 下にスピーカーノート、間にドラッグハンドルでサイズ変更(0.5.16+)
15
+ - 📝 **Inline Speaker Notes Editor** — メイン画面で直接ノート編集 → 800ms デバウンスで自動保存(0.5.16+)
14
16
  - 🔄 ファイル更新時に自動リロード(WebSocket)
15
17
  - 🎨 シンタックスハイライト(highlight.js)
16
18
  - 📊 Mermaid図のレンダリング
17
19
  - 🌙 ダーク/ライトテーマ切り替え
18
- - ✏️ インラインエディタ(Cmd+E
20
+ - ✏️ **インラインエディタ + 自動保存** — `Cmd+E` で編集モード、入力 → 1500ms で自動保存(0.5.17+)
19
21
  - ✅ タスクリスト(チェックボックス)対応
20
22
  - 📥 PDF出力(Cmd+P / CLI convert)
21
23
  - 🎛️ PDF用CSS・PDF options指定(CLI / Web UI)
@@ -216,6 +218,23 @@ paginate: true
216
218
  - **数式**: KaTeX対応(インライン `$...$`、ブロック `$$...$$`)
217
219
  - **スピーカーノート**: HTML コメント (`<!-- ... -->`) で記述
218
220
 
221
+ ## Inline Speaker Notes (PowerPoint-style Split Layout)
222
+
223
+ Marp ファイルを開くとメイン画面が **上下 2 ペイン** に分かれます。上がスライド、下がスピーカーノートエディタ、間に **ドラッグ可能な仕切り**。Presenter View を別ウィンドウで開かなくても、その場でノート編集できます。
224
+
225
+ ### 使い方
226
+
227
+ - **仕切りをドラッグ**: スライド / ノートの比率を変更
228
+ - **仕切りをダブルクリック**: 240px (デフォルト) にリセット
229
+ - **完全に閉じる**: 仕切りを画面下まで → ノート 0px (リロードしても復元)
230
+ - **ノートをクリックして入力**: 800ms デバウンスで自動保存。マークダウンソースの `<!-- ... -->` コメントが書き換わります
231
+ - **保存ステータス**: 編集中… / 保存中… / 保存済み / 失敗 (パネル右上に表示)
232
+ - **スライド切替で連動**: ナビ ←/→ または `Space` で active スライドが切り替わるとノートも対応スライドのものに
233
+
234
+ ### Presenter View との関係
235
+
236
+ 別ウィンドウの Presenter View (`P` キー) と **同じノートを共有**します。両方同時に開いて編集することも可能ですが、ETag 楽観ロックで **STALE 検出** されます (片方が STALE になったら後勝ちで上書きせず、ローカルストレージに退避)。
237
+
219
238
  ## Presenter View
220
239
 
221
240
  Marp ファイルを開いた状態で **`P` キー** を押すと、別ウィンドウで登壇者ビューが起動します。
@@ -259,13 +278,25 @@ Marp ファイルを開いた状態で **`P` キー** を押すと、別ウィ
259
278
  | `Space / PageDown` | 次のスライド |
260
279
  | `Home / End` | 最初 / 最後のスライド |
261
280
 
281
+ ## Editor (Edit モード)
282
+
283
+ `Cmd+E` で **編集モード** に入ると textarea が開きます。**入力 → 1500ms で自動保存**するので、`Cmd+S` を押し忘れて変更が消える心配はありません。
284
+
285
+ ### 自動保存の挙動
286
+
287
+ - **入力 → 1500ms debounce → POST `/api/file`** → ステータスバー: `Modified → Saving... → Saved! → Ready`
288
+ - **`Cmd+S`** は引き続き使えます。押すと **debounce を待たず即時 flush**
289
+ - **View 切替 / タブ切替 / 別ファイル open** 時に **flush + await** — 保存完了するまで遷移しません
290
+ - **保存失敗時** (ネットワーク断など) はエディタを閉じず Edit モードに留まります。`Error: ...` が表示されたらリトライしてください
291
+ - **Discard-on-close** (✕ で未保存タブを閉じる): 確認ダイアログが出ます。サーバーが既にリクエストを受信した直後の race window では、その時点までの内容がファイルに残る可能性があります(ダイアログに注記あり)
292
+
262
293
  ## Keyboard Shortcuts
263
294
 
264
295
  | ショートカット | 機能 |
265
296
  |---------------|------|
266
297
  | Cmd/Ctrl + B | サイドバー表示切替 |
267
- | Cmd/Ctrl + E | 編集モード切替 |
268
- | Cmd/Ctrl + S | 保存(編集モード時) |
298
+ | Cmd/Ctrl + E | 編集モード切替 (open: textarea / close: flush + 再 fetch + render) |
299
+ | Cmd/Ctrl + S | **保存を即時 flush** (編集モード時。autosave debounce 中なら待たずに POST) |
269
300
  | Cmd/Ctrl + P | PDF出力 |
270
301
  | Cmd/Ctrl + W | タブを閉じる |
271
302
  | ← / → | スライド移動(Marp時) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdv-live",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
4
4
  "description": "Markdown Viewer - File tree + Live preview + Marp support + Hot reload",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -59,5 +59,10 @@
59
59
  "optionalDependencies": {
60
60
  "@marp-team/marp-cli": "^4.0.3",
61
61
  "md-to-pdf": "^5.2.5"
62
+ },
63
+ "devDependencies": {
64
+ "@highlightjs/cdn-assets": "^11.11.1",
65
+ "html2pdf.js": "^0.14.0",
66
+ "mermaid": "^11.15.0"
62
67
  }
63
68
  }
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // Populates src/static/vendor/ with offline copies of the libraries that
3
+ // index.html used to load from CDN. Run manually when bumping versions.
4
+ //
5
+ // node scripts/sync-vendor.js
6
+ //
7
+ // Source map files are skipped to keep the npm tarball small.
8
+
9
+ import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
10
+ import { existsSync } from 'node:fs';
11
+ import { dirname, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import https from 'node:https';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const repoRoot = resolve(__dirname, '..');
17
+ const vendorDir = resolve(repoRoot, 'src/static/vendor');
18
+
19
+ const TAILWIND_VERSION = '3.4.17';
20
+ const TAILWIND_URL = `https://cdn.tailwindcss.com/${TAILWIND_VERSION}`;
21
+ const TAILWIND_LICENSE_URL = `https://raw.githubusercontent.com/tailwindlabs/tailwindcss/v${TAILWIND_VERSION}/LICENSE`;
22
+
23
+ async function copyFromNodeModules(relSource, relDest) {
24
+ const src = resolve(repoRoot, 'node_modules', relSource);
25
+ const dest = resolve(vendorDir, relDest);
26
+ if (!existsSync(src)) {
27
+ throw new Error(`Missing source file: ${src} (did you run npm install?)`);
28
+ }
29
+ await mkdir(dirname(dest), { recursive: true });
30
+ await cp(src, dest);
31
+ console.log(`copied ${relSource} -> vendor/${relDest}`);
32
+ }
33
+
34
+ function downloadToString(url) {
35
+ return new Promise((resolveDownload, rejectDownload) => {
36
+ https.get(url, { headers: { 'User-Agent': 'mdv-live sync-vendor' } }, (res) => {
37
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
38
+ res.resume();
39
+ resolveDownload(downloadToString(res.headers.location));
40
+ return;
41
+ }
42
+ if (res.statusCode !== 200) {
43
+ rejectDownload(new Error(`GET ${url} -> ${res.statusCode}`));
44
+ res.resume();
45
+ return;
46
+ }
47
+ const chunks = [];
48
+ res.on('data', (c) => chunks.push(c));
49
+ res.on('end', () => resolveDownload(Buffer.concat(chunks)));
50
+ res.on('error', rejectDownload);
51
+ }).on('error', rejectDownload);
52
+ });
53
+ }
54
+
55
+ async function downloadTailwind() {
56
+ const dest = resolve(vendorDir, 'tailwind.min.js');
57
+ const body = await downloadToString(TAILWIND_URL);
58
+ const header = `/*! Tailwind CSS Play CDN ${TAILWIND_VERSION} - downloaded from ${TAILWIND_URL} */\n`;
59
+ await mkdir(dirname(dest), { recursive: true });
60
+ await writeFile(dest, header + body.toString('utf8'));
61
+ console.log(`downloaded tailwind ${TAILWIND_VERSION} -> vendor/tailwind.min.js (${body.length} bytes)`);
62
+ }
63
+
64
+ async function downloadTailwindLicense() {
65
+ const dest = resolve(vendorDir, 'licenses/tailwindcss.LICENSE');
66
+ const body = await downloadToString(TAILWIND_LICENSE_URL);
67
+ await mkdir(dirname(dest), { recursive: true });
68
+ await writeFile(dest, body);
69
+ console.log(`downloaded tailwind LICENSE -> vendor/licenses/tailwindcss.LICENSE (${body.length} bytes)`);
70
+ }
71
+
72
+ async function main() {
73
+ if (existsSync(vendorDir)) {
74
+ await rm(vendorDir, { recursive: true, force: true });
75
+ }
76
+ await mkdir(vendorDir, { recursive: true });
77
+
78
+ await copyFromNodeModules('@highlightjs/cdn-assets/highlight.min.js', 'highlight.min.js');
79
+ await copyFromNodeModules('@highlightjs/cdn-assets/styles/github.min.css', 'highlight/github.min.css');
80
+ await copyFromNodeModules('@highlightjs/cdn-assets/styles/github-dark.min.css', 'highlight/github-dark.min.css');
81
+ await copyFromNodeModules('mermaid/dist/mermaid.min.js', 'mermaid.min.js');
82
+ await copyFromNodeModules('html2pdf.js/dist/html2pdf.bundle.min.js', 'html2pdf.bundle.min.js');
83
+
84
+ // Third-party license notices. html2pdf.bundle.min.js has an inline pointer
85
+ // ("For license information please see html2pdf.bundle.min.js.LICENSE.txt")
86
+ // that would otherwise dangle once the bundle ships in src/static/vendor/.
87
+ await copyFromNodeModules(
88
+ 'html2pdf.js/dist/html2pdf.bundle.min.js.LICENSE.txt',
89
+ 'html2pdf.bundle.min.js.LICENSE.txt',
90
+ );
91
+ await copyFromNodeModules('html2pdf.js/LICENSE', 'licenses/html2pdf.js.LICENSE');
92
+ await copyFromNodeModules('mermaid/LICENSE', 'licenses/mermaid.LICENSE');
93
+ await copyFromNodeModules('@highlightjs/cdn-assets/LICENSE', 'licenses/highlight.js.LICENSE');
94
+
95
+ await downloadTailwind();
96
+ await downloadTailwindLicense();
97
+
98
+ const readme = `# vendor/
99
+
100
+ This directory holds offline copies of third-party browser libraries that
101
+ index.html used to load from CDN. Regenerate it with:
102
+
103
+ node scripts/sync-vendor.js
104
+
105
+ Sources and licenses (full text in vendor/licenses/):
106
+ - highlight.min.js / highlight/*.css — @highlightjs/cdn-assets (BSD-3-Clause)
107
+ - mermaid.min.js — mermaid (MIT)
108
+ - html2pdf.bundle.min.js — html2pdf.js (MIT); see also
109
+ html2pdf.bundle.min.js.LICENSE.txt for embedded notices
110
+ - tailwind.min.js — Tailwind CSS Play CDN ${TAILWIND_VERSION} (MIT)
111
+ `;
112
+ await writeFile(resolve(vendorDir, 'README.md'), readme);
113
+ console.log('wrote vendor/README.md');
114
+ }
115
+
116
+ main().catch((err) => {
117
+ console.error(err);
118
+ process.exit(1);
119
+ });
package/src/static/app.js CHANGED
@@ -25,8 +25,8 @@
25
25
  const SLIDE_ROW_MIN_PX = 80;
26
26
 
27
27
  const HLJS_THEMES = {
28
- light: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css',
29
- dark: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css'
28
+ light: '/static/vendor/highlight/github.min.css',
29
+ dark: '/static/vendor/highlight/github-dark.min.css'
30
30
  };
31
31
 
32
32
  const MERMAID_THEMES = {
@@ -12,7 +12,7 @@
12
12
  <link rel="apple-touch-icon" sizes="128x128" href="/static/images/icon-128.png">
13
13
 
14
14
  <!-- Stylesheets -->
15
- <link id="hljs-theme" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
15
+ <link id="hljs-theme" rel="stylesheet" href="/static/vendor/highlight/github-dark.min.css">
16
16
  <link rel="stylesheet" href="/static/styles.css">
17
17
 
18
18
  <!-- Theme initialization (FOUC prevention) -->
@@ -24,13 +24,13 @@
24
24
  })();
25
25
  </script>
26
26
 
27
- <!-- External libraries -->
28
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
29
- <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
30
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
27
+ <!-- External libraries (offline, served from src/static/vendor/) -->
28
+ <script src="/static/vendor/highlight.min.js"></script>
29
+ <script src="/static/vendor/mermaid.min.js"></script>
30
+ <script src="/static/vendor/html2pdf.bundle.min.js"></script>
31
31
 
32
32
  <!-- Tailwind CSS (for Marp slides) -->
33
- <script src="https://cdn.tailwindcss.com"></script>
33
+ <script src="/static/vendor/tailwind.min.js"></script>
34
34
  <script>
35
35
  tailwind.config = {
36
36
  corePlugins: { preflight: false, container: false },
@@ -0,0 +1,13 @@
1
+ # vendor/
2
+
3
+ This directory holds offline copies of third-party browser libraries that
4
+ index.html used to load from CDN. Regenerate it with:
5
+
6
+ node scripts/sync-vendor.js
7
+
8
+ Sources and licenses (full text in vendor/licenses/):
9
+ - highlight.min.js / highlight/*.css — @highlightjs/cdn-assets (BSD-3-Clause)
10
+ - mermaid.min.js — mermaid (MIT)
11
+ - html2pdf.bundle.min.js — html2pdf.js (MIT); see also
12
+ html2pdf.bundle.min.js.LICENSE.txt for embedded notices
13
+ - tailwind.min.js — Tailwind CSS Play CDN 3.4.17 (MIT)
@@ -0,0 +1,10 @@
1
+ pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
2
+ Theme: GitHub Dark
3
+ Description: Dark theme as seen on github.com
4
+ Author: github.com
5
+ Maintainer: @Hirse
6
+ Updated: 2021-05-15
7
+
8
+ Outdated base version: https://github.com/primer/github-syntax-dark
9
+ Current colors taken from GitHub's CSS
10
+ */.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
@@ -0,0 +1,10 @@
1
+ pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
2
+ Theme: GitHub
3
+ Description: Light theme as seen on github.com
4
+ Author: github.com
5
+ Maintainer: @Hirse
6
+ Updated: 2021-05-15
7
+
8
+ Outdated base version: https://github.com/primer/github-syntax-light
9
+ Current colors taken from GitHub's CSS
10
+ */.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}