@commte/mdbrowse 0.1.0 → 0.1.1

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/README.ja.md CHANGED
@@ -25,6 +25,8 @@
25
25
  npm install -g @commte/mdbrowse
26
26
  ```
27
27
 
28
+ コマンドは `mdb` と `mdbrowse` の2つの名前で入る。以下の例は短いほうで書く
29
+
28
30
  インストールせずに試す場合
29
31
 
30
32
  ```sh
@@ -34,7 +36,7 @@ npx @commte/mdbrowse file.md
34
36
  スタイルシートはパッケージに同梱されている。自分で編集したい場合は `~/.config/mdbrowse/head.html` に取り出す。以後はそちらが同梱版より優先される
35
37
 
36
38
  ```sh
37
- mdbrowse --eject
39
+ mdb --eject
38
40
  ```
39
41
 
40
42
  <details>
@@ -46,24 +48,28 @@ cd mdbrowse
46
48
  ./install.sh
47
49
  ```
48
50
 
49
- `mdbrowse` `~/.local/bin` に、スタイルシートが `~/.config/mdbrowse/head.html` に入る。再インストールしても自分で編集したスタイルシートは上書きされない。配布時の状態に戻したいときは `--force` を付ける
51
+ `mdb`(と長いほうの `mdbrowse`)が `~/.local/bin` に、スタイルシートが `~/.config/mdbrowse/head.html` に入る。再インストールしても自分で編集したスタイルシートは上書きされない。配布時の状態に戻したいときは `--force` を付ける
50
52
 
51
53
  </details>
52
54
 
53
55
  プレビュー用のタブを1回だけ開いて、そのままにしておく
54
56
 
55
57
  ```sh
56
- mdbrowse --open
58
+ mdb --open
57
59
  ```
58
60
 
61
+ 必要なのは最初の1回だけ。以後は変換するたびに、そのタブの中身が入れ替わる。開き直すのは、タブを閉じたときとブラウザを再起動したときだけ
62
+
59
63
  ## 使い方
60
64
 
61
65
  ```sh
62
- mdbrowse file.md # 変換する(出力 HTML を上書きする)
63
- mdbrowse --open # プレビュー用のタブを開く
64
- mdbrowse --path # 出力先の HTML のパスを表示する
66
+ mdb file.md # 変換する(出力 HTML を上書きする)
67
+ mdb --open # プレビュー用のタブを開く
68
+ mdb --path # 出力先の HTML のパスを表示する
65
69
  ```
66
70
 
71
+ ファイルの監視はしない。保存しただけでは何も起きず、`mdb <file>` が走ったときにタブが切り替わる。エディタ側でキーに割り当てるか、保存時に走らせる
72
+
67
73
  | 環境変数 | 既定値 | |
68
74
  |---|---|---|
69
75
  | `MDBROWSE_OUT` | `/tmp/mdbrowse.html` | 出力先の HTML |
@@ -107,7 +113,7 @@ mdbrowse --path # 出力先の HTML のパスを表示する
107
113
  {
108
114
  "label": "Preview in browser",
109
115
  "type": "shell",
110
- "command": "mdbrowse",
116
+ "command": "mdb",
111
117
  "args": ["${file}"],
112
118
  "presentation": { "reveal": "never" },
113
119
  "problemMatcher": []
@@ -122,46 +128,48 @@ mdbrowse --path # 出力先の HTML のパスを表示する
122
128
 
123
129
  ```lua
124
130
  vim.keymap.set("n", "<leader>mp", function()
125
- vim.fn.jobstart({ "mdbrowse", vim.fn.expand("%:p") })
131
+ vim.fn.jobstart({ "mdb", vim.fn.expand("%:p") })
126
132
  end)
127
133
 
128
134
  -- 保存のたびに更新する場合
129
135
  vim.api.nvim_create_autocmd("BufWritePost", {
130
136
  pattern = "*.md",
131
- callback = function() vim.fn.jobstart({ "mdbrowse", vim.fn.expand("%:p") }) end,
137
+ callback = function() vim.fn.jobstart({ "mdb", vim.fn.expand("%:p") }) end,
132
138
  })
133
139
  ```
134
140
 
135
141
  ### JetBrains 系
136
142
 
137
- 設定 → ツール → 外部ツール で `mdbrowse` を追加し、引数に `$FilePath$` を指定する。Keymap でショートカットを割り当てる
143
+ 設定 → ツール → 外部ツール で `mdb` を追加し、引数に `$FilePath$` を指定する。Keymap でショートカットを割り当てる
138
144
 
139
145
  ### Emacs
140
146
 
141
147
  ```elisp
142
- (defun mdbrowse ()
148
+ (defun mdb ()
143
149
  (interactive)
144
- (start-process "mdbrowse" nil "mdbrowse" (buffer-file-name)))
150
+ (start-process "mdb" nil "mdb" (buffer-file-name)))
145
151
  ```
146
152
 
147
153
  ### それ以外
148
154
 
149
- `mdbrowse /path/to/current/file.md` を実行できるエディタなら動く
155
+ `mdb /path/to/current/file.md` を実行できるエディタなら動く
150
156
 
151
157
  ## 見た目を変える
152
158
 
153
- 見た目に関わるものは `~/.config/mdbrowse/head.html` に集まっている。先頭の設定ブロック、それを使う CSS、バーと更新検知のスクリプトという構成で、配色とタイポグラフィは GitHub(Primer)に合わせてある。このファイルを直せば全ファイルのプレビューに反映される。バーで変更した値は、その人のブラウザ側で既定値を上書きする
159
+ コードブロックの色付けは [Shiki](https://shiki.style/) を使う。既定のテーマは `github-dark` で、`MDBROWSE_SHIKI_THEME` `tokyo-night` などを入れれば変えられる。色付けは変換時に済ませるのでページは静的なまま。Node が無い環境では pandoc 内蔵の色付けに切り替わる
160
+
161
+ それ以外の見た目に関わるものは `~/.config/mdbrowse/head.html` に集まっている。先頭の設定ブロック、それを使う CSS、バーと更新検知のスクリプトという構成で、配色とタイポグラフィは GitHub(Primer)に合わせてある。このファイルを直せば全ファイルのプレビューに反映される。バーで変更した値は、その人のブラウザ側で既定値を上書きする
154
162
 
155
163
  表示確認用に `sample.md` が入っている
156
164
 
157
165
  ```sh
158
- mdbrowse sample.md
166
+ mdb sample.md
159
167
  ```
160
168
 
161
169
  ## 仕組み
162
170
 
163
171
  ```
164
- エディタのショートカット → mdbrowse <file> → pandoc → /tmp/mdbrowse.html
172
+ エディタのショートカット → mdb <file> → pandoc → /tmp/mdbrowse.html
165
173
  → /tmp/mdbrowse-stamp.js
166
174
 
167
175
  ブラウザがスタンプを見て、変化したときだけ再読み込み
@@ -169,7 +177,9 @@ mdbrowse sample.md
169
177
 
170
178
  変換のたびに1行のスタンプファイルも書き出す。ブラウザは無条件に再読み込みせず、このスタンプを見て、新しく変換されたときだけ読み直す。画像のあるページで定期的にちらつかないのはこのため。スクロール中と印刷中、バーを操作している間はスタンプの確認を止める。画像の寸法はセッションに記憶して、読み込み中にレイアウトがずれないようにしている
171
179
 
172
- 元ファイルからの相対パス(画像や隣のファイルへのリンク)は、変換時に絶対 `file://` へ書き換える。HTML `/tmp` にあっても画像が表示されるのはこのため。絶対パス、`http(s)`、`data:`、`mailto:`、ページ内アンカーはそのまま残す
180
+ YAML frontmatter は本文に出さない。ファイル側の `title` が本文の先頭に見出しとして増えることはない
181
+
182
+ 元ファイルからの相対パス(画像や隣のファイルへのリンク)は、変換時に絶対 `file://` へ書き換える。HTML が `/tmp` にあっても画像が表示されるのはこのため。書き換えるのは `img` や `a` などタグの属性だけなので、本文に `src="foo.png"` と書いても表示はそのまま。絶対パス、スキーム付きのもの(`http(s)`、`data:`、`mailto:` など)、`//` 始まり、ページ内アンカーには触らない。パスに `&` や `#`、空白が入っていても壊れない
173
183
 
174
184
  ページは静的なので、待ち受けているものは何も無く、終了させる必要もない
175
185
 
package/README.md CHANGED
@@ -25,6 +25,8 @@ Because the output path never changes, switching between files does not open new
25
25
  npm install -g @commte/mdbrowse
26
26
  ```
27
27
 
28
+ That gives you the command under two names, `mdb` and `mdbrowse`. The examples below use the short one.
29
+
28
30
  Or without installing anything:
29
31
 
30
32
  ```sh
@@ -34,7 +36,7 @@ npx @commte/mdbrowse file.md
34
36
  The stylesheet ships with the package. To customize it, copy it to `~/.config/mdbrowse/head.html` — that copy wins over the bundled one from then on:
35
37
 
36
38
  ```sh
37
- mdbrowse --eject
39
+ mdb --eject
38
40
  ```
39
41
 
40
42
  <details>
@@ -46,24 +48,28 @@ cd mdbrowse
46
48
  ./install.sh
47
49
  ```
48
50
 
49
- This installs `mdbrowse` into `~/.local/bin` and the stylesheet into `~/.config/mdbrowse/head.html`. Your stylesheet is never overwritten on reinstall; pass `--force` when you do want the shipped one back.
51
+ This installs `mdb` (and the longer `mdbrowse`) into `~/.local/bin`, and the stylesheet into `~/.config/mdbrowse/head.html`. Your stylesheet is never overwritten on reinstall; pass `--force` when you do want the shipped one back.
50
52
 
51
53
  </details>
52
54
 
53
55
  Open the preview tab once and leave it open:
54
56
 
55
57
  ```sh
56
- mdbrowse --open
58
+ mdb --open
57
59
  ```
58
60
 
61
+ Once is enough: from then on every render swaps the contents of that tab. You only open it again after closing the tab or restarting the browser.
62
+
59
63
  ## Usage
60
64
 
61
65
  ```sh
62
- mdbrowse file.md # render (overwrites the output HTML)
63
- mdbrowse --open # open the preview tab
64
- mdbrowse --path # print the output HTML path
66
+ mdb file.md # render (overwrites the output HTML)
67
+ mdb --open # open the preview tab
68
+ mdb --path # print the output HTML path
65
69
  ```
66
70
 
71
+ Nothing watches the filesystem. Saving a file does not update the preview by itself — the tab changes when `mdb <file>` runs, so bind it to a key or to save in your editor.
72
+
67
73
  | Variable | Default | |
68
74
  |---|---|---|
69
75
  | `MDBROWSE_OUT` | `/tmp/mdbrowse.html` | output HTML path |
@@ -107,7 +113,7 @@ Two things worth knowing:
107
113
  {
108
114
  "label": "Preview in browser",
109
115
  "type": "shell",
110
- "command": "mdbrowse",
116
+ "command": "mdb",
111
117
  "args": ["${file}"],
112
118
  "presentation": { "reveal": "never" },
113
119
  "problemMatcher": []
@@ -122,52 +128,54 @@ Bind it in `keybindings.json` with `workbench.action.tasks.runTask`.
122
128
 
123
129
  ```lua
124
130
  vim.keymap.set("n", "<leader>mp", function()
125
- vim.fn.jobstart({ "mdbrowse", vim.fn.expand("%:p") })
131
+ vim.fn.jobstart({ "mdb", vim.fn.expand("%:p") })
126
132
  end)
127
133
 
128
134
  -- or update the preview on every save
129
135
  vim.api.nvim_create_autocmd("BufWritePost", {
130
136
  pattern = "*.md",
131
- callback = function() vim.fn.jobstart({ "mdbrowse", vim.fn.expand("%:p") }) end,
137
+ callback = function() vim.fn.jobstart({ "mdb", vim.fn.expand("%:p") }) end,
132
138
  })
133
139
  ```
134
140
 
135
141
  ### JetBrains IDEs
136
142
 
137
- Settings → Tools → External Tools → add `mdbrowse` with `$FilePath$` as the argument, then assign a shortcut under Keymap.
143
+ Settings → Tools → External Tools → add `mdb` with `$FilePath$` as the argument, then assign a shortcut under Keymap.
138
144
 
139
145
  ### Emacs
140
146
 
141
147
  ```elisp
142
- (defun mdbrowse ()
148
+ (defun mdb ()
143
149
  (interactive)
144
- (start-process "mdbrowse" nil "mdbrowse" (buffer-file-name)))
150
+ (start-process "mdb" nil "mdb" (buffer-file-name)))
145
151
  ```
146
152
 
147
153
  ### Anything else
148
154
 
149
- If your editor can run `mdbrowse /path/to/the/current/file.md`, it works.
155
+ If your editor can run `mdb /path/to/the/current/file.md`, it works.
150
156
 
151
157
  ## Styling
152
158
 
153
- Everything visual lives in `~/.config/mdbrowse/head.html`: a settings block at the top, the rules that use it, and the script that draws the bar and runs the reload loop. Colors and typography follow GitHub (Primer) by default. Edit that one file and every preview follows; values changed in the bar override them per browser.
159
+ Code blocks are highlighted with [Shiki](https://shiki.style/) (`github-dark` by default; set `MDBROWSE_SHIKI_THEME` to any bundled theme, e.g. `tokyo-night`). Highlighting happens at conversion time, so the page stays static. Without Node available it falls back to pandoc's built-in highlighting.
160
+
161
+ Everything else visual lives in `~/.config/mdbrowse/head.html`: a settings block at the top, the rules that use it, and the script that draws the bar and runs the reload loop. Colors and typography follow GitHub (Primer) by default. Edit that one file and every preview follows; values changed in the bar override them per browser.
154
162
 
155
163
  `sample.md` in this repository exercises headings, lists, task lists, quotes, code blocks, tables and links — render it to check your styling:
156
164
 
157
165
  ```sh
158
- mdbrowse sample.md
166
+ mdb sample.md
159
167
  ```
160
168
 
161
169
  ## How it works
162
170
 
163
171
  ```
164
- editor shortcut → mdbrowse <file> → pandoc → /tmp/mdbrowse.html
172
+ editor shortcut → mdb <file> → pandoc → /tmp/mdbrowse.html
165
173
  → /tmp/mdbrowse-stamp.js
166
174
 
167
175
  browser polls the stamp, reloads only on change
168
176
  ```
169
177
 
170
- Relative paths in the source file (images, links to neighbouring files) are rewritten to absolute `file://` URLs during conversion, so images show up even though the HTML lives in `/tmp`. Absolute paths, `http(s)`, `data:`, `mailto:` and in-page anchors are left alone.
178
+ YAML front matter is consumed rather than printed: the file's own `title` does not become a second heading above your document. Relative paths in the source file (images, links to neighbouring files) are rewritten to absolute `file://` URLs during conversion, so images show up even though the HTML lives in `/tmp`. Only attributes of tags such as `img` and `a` are touched, so `src="foo.png"` written in your prose stays as you typed it. Absolute paths, anything with a scheme (`http(s)`, `data:`, `mailto:`), protocol-relative URLs and in-page anchors are left alone, and `&`, `#` or spaces in the path do not break the result.
171
179
 
172
180
  Each render also writes a one-line stamp file. The page polls that stamp instead of reloading blindly, so it refreshes only when you actually preview something new — no periodic flicker on pages with images. Polling pauses while you scroll, while you print, and while the pointer is on the bar. Image dimensions are remembered per session, so a refresh does not shift the layout while images load.
173
181
 
package/assets/head.html CHANGED
@@ -25,6 +25,8 @@
25
25
  --muted: #59636e;
26
26
  --link: #0969da;
27
27
  --inline-code-bg: rgba(129, 139, 152, .12);
28
+ --code-bg: #0d1117; /* コードブロックはテーマに関係なく暗い面 */
29
+ --code-fg: #e6edf3;
28
30
 
29
31
  /* 右上のバー */
30
32
  --bar-bg: #ffffff;
@@ -41,6 +43,8 @@
41
43
  --muted: #9198a1; /* fg-muted */
42
44
  --link: #4493f8; /* accent-fg */
43
45
  --inline-code-bg: rgba(101, 108, 118, .2);
46
+ --code-bg: #0d1117;
47
+ --code-fg: #e6edf3;
44
48
 
45
49
  /* 右上のバー */
46
50
  --bar-bg: #212830; /* canvas-inset より一段明るい面 */
@@ -61,8 +65,15 @@
61
65
  word-wrap: break-word;
62
66
  }
63
67
  h1, h2, h3, h4, h5, h6 { line-height: 1.25; font-weight: 600; margin: 24px 0 16px; }
64
- h1 { font-size: 2em; padding-bottom: .3em; border-bottom: 1px solid var(--border); }
65
- h2 { font-size: 1.5em; padding-bottom: .3em; border-bottom: 1px solid var(--border); }
68
+ h1 { font-size: 2em; }
69
+ h2 { font-size: 1.5em; }
70
+ /* 線を引くのは、その文書でいちばん浅い2階層(JS が付ける)。
71
+ 節の切れ目が分かるように、上に大きめの余白を取る */
72
+ .mdrule {
73
+ margin-top: 80px; padding-bottom: .3em;
74
+ border-bottom: 1px solid var(--border);
75
+ }
76
+ body > :first-child, .mdrule:first-child { margin-top: 0; }
66
77
  h3 { font-size: 1.25em; }
67
78
  h4 { font-size: 1em; }
68
79
  h1:first-child { margin-top: 0; }
@@ -73,11 +84,15 @@
73
84
  font-family: var(--code-font-family); font-size: 85%;
74
85
  background: var(--inline-code-bg); padding: .2em .4em; border-radius: 6px;
75
86
  }
76
- pre {
77
- background: var(--surface); padding: 16px; border-radius: 6px;
87
+ /* pandoc 側の .sourceCode より優先度を上げて、コードブロックを暗い面に統一する */
88
+ pre, pre.sourceCode, div.sourceCode {
89
+ background: var(--code-bg); color: var(--code-fg);
90
+ padding: 16px; border-radius: 6px;
78
91
  overflow-x: auto; font-size: 85%; line-height: 1.45;
79
92
  }
80
- pre code { background: none; padding: 0; font-size: 100%; }
93
+ div.sourceCode { padding: 0; }
94
+ div.sourceCode > pre { margin: 0; font-size: 100%; }
95
+ pre code, pre > code.sourceCode { background: transparent; padding: 0; color: inherit; font-size: 100%; }
81
96
  blockquote {
82
97
  padding: 0 1em; color: var(--muted);
83
98
  border-left: .25em solid var(--border);
@@ -122,7 +137,7 @@
122
137
  }
123
138
  #mdtoc a:hover { background: var(--surface); color: var(--fg); text-decoration: none; }
124
139
  #mdtoc a.is-current { background: var(--surface); color: var(--fg); font-weight: 600; }
125
- #mdtoc a[data-level="3"] { padding-left: 22px; font-size: 12.5px; }
140
+ #mdtoc a[data-level="2"] { padding-left: 22px; font-size: 12.5px; }
126
141
  /* 目次は幅に関係なく出す。その分の余白をページ右に確保する */
127
142
  html.has-toc { padding-right: 280px; }
128
143
 
@@ -130,6 +145,8 @@
130
145
  @page { margin: 18mm; }
131
146
  @media print {
132
147
  #mdbar, #mdtoc { display: none; }
148
+ /* 目次のために空けている右の余白は、紙では要らない */
149
+ html.has-toc { padding-right: 0; }
133
150
  /* テーマに関係なく紙はライトで刷る */
134
151
  :root, :root[data-theme="dark"] {
135
152
  --bg: #ffffff; --surface: #f6f8fa; --border: #d1d9e0;
@@ -141,6 +158,12 @@
141
158
  pre, blockquote, table, img, li { break-inside: avoid; }
142
159
  h1, h2, h3, h4 { break-after: avoid; }
143
160
  pre { white-space: pre-wrap; word-break: break-word; }
161
+ /* Shiki は暗い背景前提の色を要素に直接持つので、紙では黒字に戻す */
162
+ pre.shiki, pre.shiki span, pre code, pre code span {
163
+ background: transparent !important;
164
+ color: #1f2328 !important;
165
+ }
166
+ pre { border: 1px solid #d1d9e0; }
144
167
  }
145
168
  </style>
146
169
  <script>
@@ -160,7 +183,6 @@
160
183
  var s = window.__mdSettings || {};
161
184
  var THEMES = ['light', 'dark'];
162
185
  var WIDTHS = [800, 680, 960, 1200];
163
- var paused = false;
164
186
  var printing = false;
165
187
  addEventListener('beforeprint', function () { printing = true; });
166
188
  addEventListener('afterprint', function () { printing = false; });
@@ -193,38 +215,58 @@
193
215
  s.theme = (r.dataset.theme === 'dark') ? 'light' : 'dark';
194
216
  r.dataset.theme = s.theme;
195
217
  } else if (act === 'inc' || act === 'dec') {
196
- s.fontSize = Math.min(24, Math.max(12, fontSize() + (act === 'inc' ? 1 : -1)));
218
+ s.fontSize = Math.min(24, Math.max(16, fontSize() + (act === 'inc' ? 1 : -1)));
197
219
  r.style.setProperty('--font-size', s.fontSize + 'px');
198
220
  } else if (act === 'toc') {
199
221
  s.toc = (s.toc === false);
200
222
  applyToc();
201
223
  } else if (act === 'width') {
202
- var i = WIDTHS.indexOf(s.width || 780);
224
+ var i = WIDTHS.indexOf(s.width || WIDTHS[0]);
203
225
  s.width = WIDTHS[(i + 1) % WIDTHS.length];
204
226
  r.style.setProperty('--content-width', s.width + 'px');
205
227
  }
206
228
  save(); render();
207
229
  });
208
230
 
209
- // バーを操作している間は再読み込みを止める
210
- bar.addEventListener('pointerenter', function () { paused = true; });
211
- bar.addEventListener('pointerleave', function () { paused = false; });
212
-
213
231
  document.body.appendChild(bar);
214
232
  render();
215
233
 
216
234
  // 目次(右サイドバー)。見出しが少ないファイルでは出さない
217
- var heads = [].slice.call(document.querySelectorAll('h2[id], h3[id]'));
235
+ // その文書でいちばん浅い2階層の見出しに線を引く
236
+ var allHeads = [].slice.call(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
237
+ if (allHeads.length) {
238
+ var lvOf = function (h) { return +h.tagName.slice(1); };
239
+ var shallow = Math.min.apply(null, allHeads.map(lvOf));
240
+ allHeads.forEach(function (h) {
241
+ if (lvOf(h) <= shallow + 1) h.classList.add('mdrule');
242
+ });
243
+ }
244
+
245
+ // 見出しのレベルは文書によって違う(h2 起点とは限らない)。
246
+ // 拾えた中でいちばん浅いレベルを基準にして、その1つ下までを目次に出す
247
+ var heads = [].slice.call(document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]'));
248
+ if (heads.length) {
249
+ var lv = function (h) { return +h.tagName.slice(1); };
250
+ var top = Math.min.apply(null, heads.map(lv));
251
+ // 最上位が1つだけなら文書のタイトル。目次には出さず、その下を基準にする
252
+ if (heads.filter(function (h) { return lv(h) === top; }).length === 1) {
253
+ heads = heads.filter(function (h) { return lv(h) > top; });
254
+ if (heads.length) top = Math.min.apply(null, heads.map(lv));
255
+ }
256
+ heads = heads.filter(function (h) { return lv(h) <= top + 1; });
257
+ }
218
258
  if (heads.length >= 3) {
259
+ var topLevel = Math.min.apply(null, heads.map(function (h) { return +h.tagName.slice(1); }));
219
260
  var toc = document.createElement('nav');
220
261
  toc.id = 'mdtoc';
221
262
  var links = {};
222
263
  heads.forEach(function (h) {
223
264
  var a = document.createElement('a');
224
265
  a.href = '#' + h.id;
225
- a.textContent = h.textContent;
226
- a.title = h.textContent;
227
- a.dataset.level = (h.tagName === 'H3') ? '3' : '2';
266
+ var label = h.textContent.replace(/\s+/g, ' ').trim();
267
+ a.textContent = label;
268
+ a.title = label;
269
+ a.dataset.level = (+h.tagName.slice(1) === topLevel) ? '1' : '2';
228
270
  a.addEventListener('click', function (e) {
229
271
  e.preventDefault();
230
272
  h.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -286,8 +328,13 @@
286
328
  document.head.appendChild(el);
287
329
  }
288
330
  checkStamp();
331
+ // 背面タブではタイマーが大幅に間引かれるので、表示に戻った時点で必ず確認する
332
+ addEventListener('visibilitychange', function () { if (!document.hidden) checkStamp(); });
333
+ addEventListener('focus', function () { checkStamp(); });
334
+ // バーを操作している間は再読み込みを止める。状態を持たずにその場で見る
335
+ // (持つと、バーの上にポインタを置いたままウィンドウを離れたときに止まったままになる)
289
336
  setInterval(function () {
290
- if (paused || printing) return;
337
+ if (printing || bar.matches(':hover')) return;
291
338
  if (Date.now() - lastScroll < 900) return;
292
339
  checkStamp();
293
340
  }, 1000);
package/bin/mdbrowse CHANGED
@@ -3,7 +3,8 @@
3
3
  # browser tab can stay open and just swap its contents.
4
4
  set -euo pipefail
5
5
 
6
- VERSION="0.1.0"
6
+ VERSION="0.1.1"
7
+ PROG="$(basename "$0")"
7
8
  OUT="${MDBROWSE_OUT:-/tmp/mdbrowse.html}"
8
9
  CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/mdbrowse"
9
10
 
@@ -32,14 +33,16 @@ fi
32
33
 
33
34
  usage() {
34
35
  cat <<'USAGE'
35
- mdbrowse — render Markdown to a fixed HTML file for browser preview
36
+ mdb — render Markdown to a fixed HTML file for browser preview
36
37
 
37
38
  Usage:
38
- mdbrowse <file.md> render the file (overwrites the output HTML)
39
- mdbrowse --open open the preview tab in the default browser
40
- mdbrowse --eject copy the bundled stylesheet into your config directory
41
- mdbrowse --path print the output HTML path
42
- mdbrowse --version print the version
39
+ mdb <file.md> render the file (overwrites the output HTML)
40
+ mdb --open open the preview tab in the default browser
41
+ mdb --eject copy the bundled stylesheet into your config directory
42
+ mdb --path print the output HTML path
43
+ mdb --version print the version
44
+
45
+ The same command is also installed under its full name, `mdbrowse`.
43
46
 
44
47
  Environment:
45
48
  MDBROWSE_OUT output HTML path (default: /tmp/mdbrowse.html)
@@ -65,7 +68,7 @@ case "${1:-}" in
65
68
  --eject)
66
69
  mkdir -p "$CONFIG_DIR"
67
70
  if [ -f "$CONFIG_DIR/head.html" ] && [ "${2:-}" != "--force" ]; then
68
- echo "mdbrowse: $CONFIG_DIR/head.html already exists (pass --force to overwrite)" >&2
71
+ echo "$PROG: $CONFIG_DIR/head.html already exists (pass --force to overwrite)" >&2
69
72
  exit 1
70
73
  fi
71
74
  cp "$BUNDLED_HEAD" "$CONFIG_DIR/head.html"
@@ -81,28 +84,65 @@ esac
81
84
  src="$1"
82
85
 
83
86
  if ! command -v pandoc >/dev/null 2>&1; then
84
- echo "mdbrowse: pandoc not found. Install it first (macOS: brew install pandoc)" >&2
87
+ echo "$PROG: pandoc not found. Install it first (macOS: brew install pandoc)" >&2
85
88
  exit 127
86
89
  fi
87
90
  if [ ! -f "$src" ]; then
88
- echo "mdbrowse: no such file: $src" >&2
91
+ echo "$PROG: no such file: $src" >&2
89
92
  exit 66
90
93
  fi
91
94
 
92
- args=(--standalone --from gfm --variable "pagetitle=$(basename "$src")" --output "$OUT")
95
+ # コードの色付けは Shiki(node がある場合)。無ければ pandoc 内蔵の skylighting
96
+ SELF_DIR="$(resolve_dir "$0")"
97
+ HIGHLIGHTER="$SELF_DIR/../lib/highlight.mjs"
98
+ use_shiki=0
99
+ if command -v node >/dev/null 2>&1 && [ -f "$HIGHLIGHTER" ]; then
100
+ use_shiki=1
101
+ fi
102
+
103
+ # シンタックスハイライトの指定は pandoc 3.9 で名前が変わった
104
+ pv="$(pandoc --version | head -1 | awk '{print $2}')"
105
+ if [ "$(printf '%s\n3.9\n' "$pv" | sort -V | head -1)" = "3.9" ]; then
106
+ if [ "$use_shiki" -eq 1 ]; then hl=(--syntax-highlighting none); else hl=(--syntax-highlighting breezedark); fi
107
+ else
108
+ if [ "$use_shiki" -eq 1 ]; then hl=(--no-highlight); else hl=(--highlight-style breezedark); fi
109
+ fi
110
+
111
+ args=(--standalone --from gfm "${hl[@]}" --variable "pagetitle=$(basename "$src")" --output "$OUT")
93
112
  [ -f "$HEAD" ] && args+=(--include-in-header "$HEAD")
94
113
 
95
114
  pandoc "$src" "${args[@]}"
96
115
 
116
+ # コードブロックを Shiki で色付けし直す。失敗しても変換自体は成立させる
117
+ if [ "$use_shiki" -eq 1 ]; then
118
+ node "$HIGHLIGHTER" "$OUT" || true
119
+ fi
120
+
121
+ # frontmatter の title を pandoc が拾うと本文の先頭に見出しが増えるので落とす。
122
+ # ブラウザのタブ名には pagetitle を使っている
123
+ perl -0pi -e 's#<header id="title-block-header">.*?</header>\n?##s' "$OUT"
124
+
97
125
  # 出力先が /tmp なので、元ファイルからの相対パス(画像・ローカルリンク)が
98
126
  # そのままでは解決できない。ソースのあるディレクトリ基準の file:// に書き換える。
99
- # http(s) / data: / mailto: / 絶対パス / ページ内アンカーはそのまま残す
127
+ # 書き換えるのはタグの属性だけ。本文中の src="..." のようなインラインコードは触らない。
128
+ # スキーム付き(http(s) data: mailto: など)/絶対パス/ページ内アンカーはそのまま残す
100
129
  src_dir="$(cd "$(dirname "$src")" && pwd)"
101
- sed -E \
102
- -e 's#(src|href)="(https?://|data:|mailto:|file://|/|\#)#\1="@@KEEP@@\2#g' \
103
- -e "s#(src|href)=\"#\1=\"file://$src_dir/#g" \
104
- -e 's#(src|href)="file://[^"]*@@KEEP@@#\1="#g' \
105
- "$OUT" > "$OUT.tmp" && mv "$OUT.tmp" "$OUT"
130
+ MDBROWSE_SRC_DIR="$src_dir" perl -0pi -e '
131
+ BEGIN {
132
+ $base = $ENV{MDBROWSE_SRC_DIR};
133
+ # file:// の中で意味を持つ文字は先に逃がす(% は必ず最初)
134
+ $base =~ s/%/%25/g;
135
+ $base =~ s/#/%23/g;
136
+ $base =~ s/\?/%3F/g;
137
+ $base =~ s/ /%20/g;
138
+ }
139
+ s{(<(?:img|a|source|video|audio|embed|object|track)\b)([^>]*?)(/?>)}{
140
+ my ($tag, $attrs, $close) = ($1, $2, $3);
141
+ $attrs =~ s{((?:src|href|poster|data)=")(?![a-zA-Z][a-zA-Z0-9+.\-]*:|//|/|\#|")([^"]*)"}
142
+ {$1 . "file://" . $base . "/" . $2 . "\""}ge;
143
+ $tag . $attrs . $close;
144
+ }gse;
145
+ ' "$OUT"
106
146
 
107
147
  # 更新の目印。ブラウザはこれを見て、中身が変わったときだけ読み直す
108
148
  printf 'window.__mdbrowseStamp="%s";\n' "$(date +%s)-$RANDOM" > "${OUT%.html}-stamp.js"
@@ -0,0 +1,37 @@
1
+ // pandoc が出したプレーンなコードブロックを Shiki で色付けし直す。
2
+ // 変換時に静的な HTML にするので、ブラウザ側で JS は動かない
3
+ import { readFile, writeFile } from 'node:fs/promises'
4
+
5
+ const THEME = process.env.MDBROWSE_SHIKI_THEME || 'github-dark'
6
+ const file = process.argv[2]
7
+ if (!file) process.exit(0)
8
+
9
+ const unescape = (s) =>
10
+ s.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
11
+ .replace(/&quot;/g, '"').replace(/&#39;/g, "'")
12
+ .replace(/&amp;/g, '&')
13
+
14
+ const html = await readFile(file, 'utf8')
15
+ const blocks = [...html.matchAll(/<pre(?: class="([^"]*)")?><code(?: class="[^"]*")?>([\s\S]*?)<\/code><\/pre>/g)]
16
+ if (!blocks.length) process.exit(0)
17
+
18
+ const { createHighlighter, bundledLanguages } = await import('shiki')
19
+
20
+ const langOf = (cls) => {
21
+ const first = (cls || '').split(/\s+/).filter(Boolean)[0]
22
+ if (!first) return 'text'
23
+ const name = first.replace(/^language-/, '').toLowerCase()
24
+ return name in bundledLanguages ? name : 'text'
25
+ }
26
+
27
+ const langs = [...new Set(blocks.map((m) => langOf(m[1])))].filter((l) => l !== 'text')
28
+ const highlighter = await createHighlighter({ themes: [THEME], langs })
29
+
30
+ let out = html
31
+ for (const m of blocks) {
32
+ const lang = langOf(m[1])
33
+ const code = unescape(m[2])
34
+ const rendered = highlighter.codeToHtml(code, { lang, theme: THEME })
35
+ out = out.replace(m[0], rendered)
36
+ }
37
+ await writeFile(file, out)
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@commte/mdbrowse",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Preview the Markdown file you are editing in a real browser, with your own CSS, from any editor that can run a shell command. No server, no port.",
5
5
  "bin": {
6
- "mdbrowse": "bin/mdbrowse"
6
+ "mdbrowse": "bin/mdbrowse",
7
+ "mdb": "bin/mdbrowse"
7
8
  },
8
9
  "files": [
9
10
  "bin/mdbrowse",
11
+ "lib/highlight.mjs",
10
12
  "assets/head.html",
11
13
  "sample.md",
12
14
  "README.md",
@@ -38,5 +40,8 @@
38
40
  ],
39
41
  "publishConfig": {
40
42
  "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "shiki": "^4.4.3"
41
46
  }
42
47
  }