mdv-live 0.5.16 → 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 +73 -0
- package/README.md +34 -3
- package/package.json +6 -1
- package/scripts/sync-vendor.js +119 -0
- package/src/static/app.js +409 -42
- package/src/static/index.html +6 -6
- package/src/static/lib/apiClient.js +3 -2
- package/src/static/vendor/README.md +13 -0
- package/src/static/vendor/highlight/github-dark.min.css +10 -0
- package/src/static/vendor/highlight/github.min.css +10 -0
- package/src/static/vendor/highlight.min.js +1244 -0
- package/src/static/vendor/html2pdf.bundle.min.js +3 -0
- package/src/static/vendor/html2pdf.bundle.min.js.LICENSE.txt +2115 -0
- package/src/static/vendor/licenses/highlight.js.LICENSE +29 -0
- package/src/static/vendor/licenses/html2pdf.js.LICENSE +21 -0
- package/src/static/vendor/licenses/mermaid.LICENSE +21 -0
- package/src/static/vendor/licenses/tailwindcss.LICENSE +21 -0
- package/src/static/vendor/mermaid.min.js +3405 -0
- package/src/static/vendor/tailwind.min.js +84 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,79 @@ 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
|
+
|
|
39
|
+
## [0.5.17] - 2026-05-10
|
|
40
|
+
|
|
41
|
+
### Added — Edit-mode Autosave
|
|
42
|
+
|
|
43
|
+
Markdown エディタを **入力 → 1500ms debounce で自動保存** に。これまで Cmd+S を
|
|
44
|
+
押し忘れると未保存で View に戻すと内容が消える挙動だった。
|
|
45
|
+
|
|
46
|
+
- `input` で 1500ms debounce → `EditorManager.save()` が `/api/file` に POST
|
|
47
|
+
- toolbar status の遷移: `Modified → Saving... → Saved! → (2s 後) Ready`
|
|
48
|
+
- **Cmd+S** は引き続き使えて、押すと pending な debounce を即 flush
|
|
49
|
+
- View 切替 / タブ切替 / 別ファイル open 時に **flush + await** で未保存破棄事故を防止
|
|
50
|
+
- save 中の連続 input は serialize(chain)。古い save が後着して新しい save を
|
|
51
|
+
上書きしないよう、各 save 自身の Promise を chain tail にして flush は末尾まで drain
|
|
52
|
+
- save 失敗時は `hide()` / `switch()` / `open()` がすべて navigation を中止して
|
|
53
|
+
Edit mode を維持。toolbar に `Error: ...` を表示してリトライ余地を残す
|
|
54
|
+
- discard-on-close ダイアログ: AbortController で in-flight POST も abort。
|
|
55
|
+
ただしサーバーが既に request 受信済みの race window は残るため、ダイアログ
|
|
56
|
+
メッセージで「自動保存処理中の場合、その時点までの内容がファイルに残る可能性が
|
|
57
|
+
あります」と明示
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- `MDVApi.saveFile(path, content, signal?)` に AbortSignal 引数追加(既存
|
|
62
|
+
caller は signal 省略で動作継続)
|
|
63
|
+
- `TabManager.switch()` を `async` 化(path で target を pin → flush await →
|
|
64
|
+
index 再 lookup で navigation race 回避)
|
|
65
|
+
- save 成功時に `MDVApi.fetchFile(path)` を chain 内で await して
|
|
66
|
+
`tab.{content,css,notes,notesMultiplicity,etag,isMarp}` を更新(古い fetch
|
|
67
|
+
が新しい fetch の後に到着して content を上書きする race を排除)
|
|
68
|
+
|
|
69
|
+
### Fixed (codex review round 1〜14 で潰した issues)
|
|
70
|
+
|
|
71
|
+
- 保存中に typing 続いた場合の dirty フラグ誤クリア(live editor とのテキスト一致
|
|
72
|
+
を確認してから "Saved!" 表示)
|
|
73
|
+
- 連続 autosave で古い ETag の POST が新しい save の後に到着して overwrite
|
|
74
|
+
- BroadcastChannel 経由じゃない、HTTP 経路独自の serialize chain
|
|
75
|
+
- 編集中のタブを close したときに edit mode flag が残る regression
|
|
76
|
+
- discard-on-close で saveTimer / inFlight 両方 abort + lastAutosaveError も clear
|
|
77
|
+
- open() で fetch 中の typing をブロック(textarea.readOnly)+ error 時に restore
|
|
78
|
+
- debounce-fired save が silent fail したまま flushAutosave が成功扱いする問題
|
|
79
|
+
(`lastAutosaveError` を保持し、navigation 時に再 throw)
|
|
80
|
+
|
|
8
81
|
## [0.5.16] - 2026-05-09
|
|
9
82
|
|
|
10
83
|
### Added — Inline Speaker Notes (PowerPoint-style)
|
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
|
-
- ✏️
|
|
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.
|
|
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
|
+
});
|