@commte/mdbrowse 0.1.0 → 0.2.0
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 +43 -26
- package/README.md +41 -26
- package/assets/head.html +65 -18
- package/bin/mdbrowse +266 -31
- package/lib/highlight.mjs +37 -0
- package/package.json +7 -2
package/README.ja.md
CHANGED
|
@@ -2,22 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
# mdbrowse
|
|
4
4
|
|
|
5
|
-
編集中の Markdown を、自分の CSS
|
|
5
|
+
編集中の Markdown を、自分の CSS でブラウザに表示する。エディタは何でもいい
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`mdb` を1回実行するとタブが開き、以後はどのプロジェクトのどの Markdown を保存しても、そのタブに出る。設定は要らない。サーバーもポートも無い
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
## 何のために作ったか
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
エディタ内蔵のプレビューは、エディタの中で描画されるので見た目に手を入れられない。サーバーを立てるツールなら自由になるが、常駐とポートを引き受けることになる。これは pandoc の上に乗せたシェルスクリプトと、スタイルを収めた HTML が1枚あるだけで、固定パスに書き出してタブが自分で読み直す
|
|
14
14
|
|
|
15
15
|
出力先が常に同じなので、ファイルを切り替えてもタブは増えない。すでに開いているタブの中身が入れ替わる
|
|
16
16
|
|
|
17
17
|
## 必要なもの
|
|
18
18
|
|
|
19
19
|
- [pandoc](https://pandoc.org/) macOS は `brew install pandoc`、Debian/Ubuntu は `apt install pandoc`
|
|
20
|
-
- 現在開いているファイルのパスを渡してシェルコマンドを実行できるエディタ
|
|
21
20
|
|
|
22
21
|
## インストール
|
|
23
22
|
|
|
@@ -25,6 +24,8 @@
|
|
|
25
24
|
npm install -g @commte/mdbrowse
|
|
26
25
|
```
|
|
27
26
|
|
|
27
|
+
コマンドは `mdb` と `mdbrowse` の2つの名前で入る。以下の例は短いほうで書く
|
|
28
|
+
|
|
28
29
|
インストールせずに試す場合
|
|
29
30
|
|
|
30
31
|
```sh
|
|
@@ -34,7 +35,7 @@ npx @commte/mdbrowse file.md
|
|
|
34
35
|
スタイルシートはパッケージに同梱されている。自分で編集したい場合は `~/.config/mdbrowse/head.html` に取り出す。以後はそちらが同梱版より優先される
|
|
35
36
|
|
|
36
37
|
```sh
|
|
37
|
-
|
|
38
|
+
mdb --eject
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
<details>
|
|
@@ -46,28 +47,36 @@ cd mdbrowse
|
|
|
46
47
|
./install.sh
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
`mdbrowse
|
|
50
|
+
`mdb`(と長いほうの `mdbrowse`)が `~/.local/bin` に、スタイルシートが `~/.config/mdbrowse/head.html` に入る。再インストールしても自分で編集したスタイルシートは上書きされない。配布時の状態に戻したいときは `--force` を付ける
|
|
50
51
|
|
|
51
52
|
</details>
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
あとは1回実行する
|
|
54
55
|
|
|
55
56
|
```sh
|
|
56
|
-
|
|
57
|
+
mdb
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
プレビュー用のタブが開き、小さな背景プロセスが動きだす。以後どの Markdown を保存しても、どのプロジェクトのものでも、そのタブに出る。止めるときは `mdb --stop`、再開はまた `mdb`
|
|
61
|
+
|
|
59
62
|
## 使い方
|
|
60
63
|
|
|
61
64
|
```sh
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
mdb # タブを開いて、以後ずっと追従させる
|
|
66
|
+
mdb --stop # 背景の追従を止める
|
|
67
|
+
mdb --status # いま何が出ているかを表示する
|
|
68
|
+
mdb file.md # そのファイルを1回だけ変換する
|
|
69
|
+
mdb -w file.md # 1つのファイルだけを前面で監視する(Ctrl-C で終了)
|
|
70
|
+
mdb --path # 出力先の HTML のパスを表示する
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
### どうやって保存したファイルを見つけているか
|
|
74
|
+
|
|
75
|
+
Spotlight(`mdfind`)に「直近に保存された Markdown」を尋ねて、それを変換している。ディスクを歩き回らないので軽い。隠しディレクトリ、`node_modules`、`~/Library` の下は無視する。Spotlight が使えない環境では、`mdb` を実行したディレクトリを見る動きに落ちる
|
|
76
|
+
|
|
77
|
+
いま開いているファイルは毎秒そのまま見ているので、続けて保存したぶんはすぐ出る。別のファイルに移ったときだけ、Spotlight が気づくまで数秒かかる
|
|
78
|
+
|
|
79
|
+
最後に保存されたものを追うので、別のプログラムが Markdown を書くとプレビューが持っていかれる。1つのファイルに固定したいときは `mdb -w そのファイル.md` を使う。指定したものだけを見て、`Ctrl-C` で止まる
|
|
71
80
|
|
|
72
81
|
## ページ内の操作
|
|
73
82
|
|
|
@@ -79,6 +88,8 @@ mdbrowse --path # 出力先の HTML のパスを表示する
|
|
|
79
88
|
|
|
80
89
|
## エディタの設定
|
|
81
90
|
|
|
91
|
+
`mdb` が保存を追いかけるので、以下は要らない。背景で何も動かさず、キーを押して出したい場合はこちらを設定する。どれも1回だけ変換する `mdb <file>` を呼んでいる
|
|
92
|
+
|
|
82
93
|
### Zed
|
|
83
94
|
|
|
84
95
|
`install.sh` が `~/.config/zed/tasks.json` を作る。既存のファイルがある場合は上書きせず、貼り付ける内容を表示する。あとは `~/.config/zed/keymap.json` にキーを追加する
|
|
@@ -107,7 +118,7 @@ mdbrowse --path # 出力先の HTML のパスを表示する
|
|
|
107
118
|
{
|
|
108
119
|
"label": "Preview in browser",
|
|
109
120
|
"type": "shell",
|
|
110
|
-
"command": "
|
|
121
|
+
"command": "mdb",
|
|
111
122
|
"args": ["${file}"],
|
|
112
123
|
"presentation": { "reveal": "never" },
|
|
113
124
|
"problemMatcher": []
|
|
@@ -122,46 +133,48 @@ mdbrowse --path # 出力先の HTML のパスを表示する
|
|
|
122
133
|
|
|
123
134
|
```lua
|
|
124
135
|
vim.keymap.set("n", "<leader>mp", function()
|
|
125
|
-
vim.fn.jobstart({ "
|
|
136
|
+
vim.fn.jobstart({ "mdb", vim.fn.expand("%:p") })
|
|
126
137
|
end)
|
|
127
138
|
|
|
128
139
|
-- 保存のたびに更新する場合
|
|
129
140
|
vim.api.nvim_create_autocmd("BufWritePost", {
|
|
130
141
|
pattern = "*.md",
|
|
131
|
-
callback = function() vim.fn.jobstart({ "
|
|
142
|
+
callback = function() vim.fn.jobstart({ "mdb", vim.fn.expand("%:p") }) end,
|
|
132
143
|
})
|
|
133
144
|
```
|
|
134
145
|
|
|
135
146
|
### JetBrains 系
|
|
136
147
|
|
|
137
|
-
設定 → ツール → 外部ツール で `
|
|
148
|
+
設定 → ツール → 外部ツール で `mdb` を追加し、引数に `$FilePath$` を指定する。Keymap でショートカットを割り当てる
|
|
138
149
|
|
|
139
150
|
### Emacs
|
|
140
151
|
|
|
141
152
|
```elisp
|
|
142
|
-
(defun
|
|
153
|
+
(defun mdb ()
|
|
143
154
|
(interactive)
|
|
144
|
-
(start-process "
|
|
155
|
+
(start-process "mdb" nil "mdb" (buffer-file-name)))
|
|
145
156
|
```
|
|
146
157
|
|
|
147
158
|
### それ以外
|
|
148
159
|
|
|
149
|
-
`
|
|
160
|
+
`mdb /path/to/current/file.md` を実行できるエディタなら動く
|
|
150
161
|
|
|
151
162
|
## 見た目を変える
|
|
152
163
|
|
|
153
|
-
|
|
164
|
+
コードブロックの色付けは [Shiki](https://shiki.style/) を使う。既定のテーマは `github-dark` で、`MDBROWSE_SHIKI_THEME` に `tokyo-night` などを入れれば変えられる。色付けは変換時に済ませるのでページは静的なまま。Node が無い環境では pandoc 内蔵の色付けに切り替わる
|
|
165
|
+
|
|
166
|
+
それ以外の見た目に関わるものは `~/.config/mdbrowse/head.html` に集まっている。先頭の設定ブロック、それを使う CSS、バーと更新検知のスクリプトという構成で、配色とタイポグラフィは GitHub(Primer)に合わせてある。このファイルを直せば全ファイルのプレビューに反映される。バーで変更した値は、その人のブラウザ側で既定値を上書きする
|
|
154
167
|
|
|
155
168
|
表示確認用に `sample.md` が入っている
|
|
156
169
|
|
|
157
170
|
```sh
|
|
158
|
-
|
|
171
|
+
mdb sample.md
|
|
159
172
|
```
|
|
160
173
|
|
|
161
174
|
## 仕組み
|
|
162
175
|
|
|
163
176
|
```
|
|
164
|
-
|
|
177
|
+
ファイルを保存 → mdb(またはエディタのショートカット) → pandoc → /tmp/mdbrowse.html
|
|
165
178
|
→ /tmp/mdbrowse-stamp.js
|
|
166
179
|
↑
|
|
167
180
|
ブラウザがスタンプを見て、変化したときだけ再読み込み
|
|
@@ -169,7 +182,11 @@ mdbrowse sample.md
|
|
|
169
182
|
|
|
170
183
|
変換のたびに1行のスタンプファイルも書き出す。ブラウザは無条件に再読み込みせず、このスタンプを見て、新しく変換されたときだけ読み直す。画像のあるページで定期的にちらつかないのはこのため。スクロール中と印刷中、バーを操作している間はスタンプの確認を止める。画像の寸法はセッションに記憶して、読み込み中にレイアウトがずれないようにしている
|
|
171
184
|
|
|
172
|
-
|
|
185
|
+
YAML の frontmatter は本文に出さない。ファイル側の `title` が本文の先頭に見出しとして増えることはない
|
|
186
|
+
|
|
187
|
+
元ファイルからの相対パス(画像や隣のファイルへのリンク)は、変換時に絶対 `file://` へ書き換える。HTML が `/tmp` にあっても画像が表示されるのはこのため。書き換えるのは `img` や `a` などタグの属性だけなので、本文に `src="foo.png"` と書いても表示はそのまま。絶対パス、スキーム付きのもの(`http(s)`、`data:`、`mailto:` など)、`//` 始まり、ページ内アンカーには触らない。パスに `&` や `#`、空白が入っていても壊れない
|
|
188
|
+
|
|
189
|
+
監視は同じ変換を繰り返しているだけ。背景プロセスは、開いているファイルの更新時刻とサイズを1秒ごとに、直近に保存された Markdown を2秒ごとに見て、変わったら変換し直す。待ち受けるものは無くポートも持たない。`mdb --stop` で終わり、`ps` に見えるシェルのプロセスが1つあるだけ
|
|
173
190
|
|
|
174
191
|
ページは静的なので、待ち受けているものは何も無く、終了させる必要もない
|
|
175
192
|
|
package/README.md
CHANGED
|
@@ -2,22 +2,21 @@ English | [日本語](README.ja.md)
|
|
|
2
2
|
|
|
3
3
|
# mdbrowse
|
|
4
4
|
|
|
5
|
-
Preview the Markdown file you are editing in a real browser, with your own CSS, from any editor
|
|
5
|
+
Preview the Markdown file you are editing in a real browser, with your own CSS, from any editor.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Run `mdb` once. It opens a tab, and from then on whatever Markdown file you save — in any project, from any editor — appears in it. No configuration, no server, no port.
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
11
|
## Why
|
|
12
12
|
|
|
13
|
-
Editors render Markdown previews inside their own window and give you little control over the result. Server-based previewers give you control but ask you to keep a process and a port around. This
|
|
13
|
+
Editors render Markdown previews inside their own window and give you little control over the result. Server-based previewers give you control but ask you to keep a process and a port around. This is a shell script on top of `pandoc` plus one HTML file holding the styles: it renders to a fixed path and the tab reloads itself.
|
|
14
14
|
|
|
15
15
|
Because the output path never changes, switching between files does not open new tabs. The tab you already have simply shows the new file.
|
|
16
16
|
|
|
17
17
|
## Requirements
|
|
18
18
|
|
|
19
19
|
- [pandoc](https://pandoc.org/) — macOS: `brew install pandoc`, Debian/Ubuntu: `apt install pandoc`
|
|
20
|
-
- An editor that can run a shell command with the path of the active file
|
|
21
20
|
|
|
22
21
|
## Install
|
|
23
22
|
|
|
@@ -25,6 +24,8 @@ Because the output path never changes, switching between files does not open new
|
|
|
25
24
|
npm install -g @commte/mdbrowse
|
|
26
25
|
```
|
|
27
26
|
|
|
27
|
+
That gives you the command under two names, `mdb` and `mdbrowse`. The examples below use the short one.
|
|
28
|
+
|
|
28
29
|
Or without installing anything:
|
|
29
30
|
|
|
30
31
|
```sh
|
|
@@ -34,7 +35,7 @@ npx @commte/mdbrowse file.md
|
|
|
34
35
|
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
36
|
|
|
36
37
|
```sh
|
|
37
|
-
|
|
38
|
+
mdb --eject
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
<details>
|
|
@@ -46,28 +47,36 @@ cd mdbrowse
|
|
|
46
47
|
./install.sh
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
This installs `mdbrowse` into `~/.local/bin
|
|
50
|
+
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
51
|
|
|
51
52
|
</details>
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
Then run it once:
|
|
54
55
|
|
|
55
56
|
```sh
|
|
56
|
-
|
|
57
|
+
mdb
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
That opens the preview tab and starts a small background process. Save any Markdown file from here on and it shows up in that tab, whichever project it lives in. `mdb --stop` ends it; `mdb` starts it again.
|
|
61
|
+
|
|
59
62
|
## Usage
|
|
60
63
|
|
|
61
64
|
```sh
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
mdb # open the tab and keep it in sync
|
|
66
|
+
mdb --stop # stop the background sync
|
|
67
|
+
mdb --status # show what is being previewed
|
|
68
|
+
mdb file.md # render one file, once
|
|
69
|
+
mdb -w file.md # follow one file in the foreground (Ctrl-C to stop)
|
|
70
|
+
mdb --path # print the output HTML path
|
|
65
71
|
```
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
### How the sync finds your file
|
|
74
|
+
|
|
75
|
+
It asks Spotlight (`mdfind`) which Markdown file was saved most recently, twice a second-and-a-bit, and renders that one. Walking your disk is never involved, so it stays cheap. Files under hidden directories, `node_modules` and `~/Library` are ignored. Without Spotlight it falls back to watching the directory you started `mdb` in.
|
|
76
|
+
|
|
77
|
+
The file you are editing right now is checked every second directly, so repeated saves show up immediately; moving to a different file takes a couple of seconds longer, while Spotlight notices it.
|
|
78
|
+
|
|
79
|
+
Because it follows whatever was saved last, another program writing a Markdown file can pull the preview away. If you want the tab pinned to one file, run `mdb -w that-file.md` instead — that watches only what you name, and stops when you press `Ctrl-C`.
|
|
71
80
|
|
|
72
81
|
## In-page controls
|
|
73
82
|
|
|
@@ -79,6 +88,8 @@ The table of contents is built from the headings of the current file and follows
|
|
|
79
88
|
|
|
80
89
|
## Editor setup
|
|
81
90
|
|
|
91
|
+
None of this is needed — `mdb` already follows what you save. Set one of these up if you would rather press a key and have nothing running in the background. They all call `mdb <file>`, the render-once mode.
|
|
92
|
+
|
|
82
93
|
### Zed
|
|
83
94
|
|
|
84
95
|
`install.sh` writes `~/.config/zed/tasks.json` for you — it will not overwrite an existing one; it prints the entries to paste instead. Then bind a key in `~/.config/zed/keymap.json`:
|
|
@@ -107,7 +118,7 @@ Two things worth knowing:
|
|
|
107
118
|
{
|
|
108
119
|
"label": "Preview in browser",
|
|
109
120
|
"type": "shell",
|
|
110
|
-
"command": "
|
|
121
|
+
"command": "mdb",
|
|
111
122
|
"args": ["${file}"],
|
|
112
123
|
"presentation": { "reveal": "never" },
|
|
113
124
|
"problemMatcher": []
|
|
@@ -122,55 +133,59 @@ Bind it in `keybindings.json` with `workbench.action.tasks.runTask`.
|
|
|
122
133
|
|
|
123
134
|
```lua
|
|
124
135
|
vim.keymap.set("n", "<leader>mp", function()
|
|
125
|
-
vim.fn.jobstart({ "
|
|
136
|
+
vim.fn.jobstart({ "mdb", vim.fn.expand("%:p") })
|
|
126
137
|
end)
|
|
127
138
|
|
|
128
139
|
-- or update the preview on every save
|
|
129
140
|
vim.api.nvim_create_autocmd("BufWritePost", {
|
|
130
141
|
pattern = "*.md",
|
|
131
|
-
callback = function() vim.fn.jobstart({ "
|
|
142
|
+
callback = function() vim.fn.jobstart({ "mdb", vim.fn.expand("%:p") }) end,
|
|
132
143
|
})
|
|
133
144
|
```
|
|
134
145
|
|
|
135
146
|
### JetBrains IDEs
|
|
136
147
|
|
|
137
|
-
Settings → Tools → External Tools → add `
|
|
148
|
+
Settings → Tools → External Tools → add `mdb` with `$FilePath$` as the argument, then assign a shortcut under Keymap.
|
|
138
149
|
|
|
139
150
|
### Emacs
|
|
140
151
|
|
|
141
152
|
```elisp
|
|
142
|
-
(defun
|
|
153
|
+
(defun mdb ()
|
|
143
154
|
(interactive)
|
|
144
|
-
(start-process "
|
|
155
|
+
(start-process "mdb" nil "mdb" (buffer-file-name)))
|
|
145
156
|
```
|
|
146
157
|
|
|
147
158
|
### Anything else
|
|
148
159
|
|
|
149
|
-
If your editor can run `
|
|
160
|
+
If your editor can run `mdb /path/to/the/current/file.md`, it works.
|
|
150
161
|
|
|
151
162
|
## Styling
|
|
152
163
|
|
|
153
|
-
|
|
164
|
+
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.
|
|
165
|
+
|
|
166
|
+
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
167
|
|
|
155
168
|
`sample.md` in this repository exercises headings, lists, task lists, quotes, code blocks, tables and links — render it to check your styling:
|
|
156
169
|
|
|
157
170
|
```sh
|
|
158
|
-
|
|
171
|
+
mdb sample.md
|
|
159
172
|
```
|
|
160
173
|
|
|
161
174
|
## How it works
|
|
162
175
|
|
|
163
176
|
```
|
|
164
|
-
|
|
177
|
+
save a file → mdb (or an editor shortcut) → pandoc → /tmp/mdbrowse.html
|
|
165
178
|
→ /tmp/mdbrowse-stamp.js
|
|
166
179
|
↑
|
|
167
180
|
browser polls the stamp, reloads only on change
|
|
168
181
|
```
|
|
169
182
|
|
|
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.
|
|
183
|
+
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
184
|
|
|
172
185
|
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
186
|
|
|
187
|
+
Watching is the same render in a loop. The background process compares the file's timestamp and size once a second, asks Spotlight for the newest Markdown file every other second, and renders again when either changes. It listens on nothing and has no port; `mdb --stop` ends it, and it is a single shell process you can see in `ps`.
|
|
188
|
+
|
|
174
189
|
The page is static, so nothing is listening and nothing needs to be shut down.
|
|
175
190
|
|
|
176
191
|
## License
|
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;
|
|
65
|
-
h2 { font-size: 1.5em;
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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="
|
|
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(
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
a.
|
|
227
|
-
a.
|
|
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 (
|
|
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.
|
|
6
|
+
VERSION="0.2.0"
|
|
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,27 @@ fi
|
|
|
32
33
|
|
|
33
34
|
usage() {
|
|
34
35
|
cat <<'USAGE'
|
|
35
|
-
|
|
36
|
+
mdb — render Markdown to a fixed HTML file for browser preview
|
|
36
37
|
|
|
37
38
|
Usage:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
mdb open the preview tab and keep it in sync from then on
|
|
40
|
+
mdb <file.md> render that file once
|
|
41
|
+
mdb --stop stop the background sync
|
|
42
|
+
mdb --status show what is being previewed
|
|
43
|
+
mdb -w <file|dir> watch in the foreground instead (Ctrl-C to stop)
|
|
44
|
+
mdb --eject copy the bundled stylesheet into your config directory
|
|
45
|
+
mdb --path print the output HTML path
|
|
46
|
+
mdb --version print the version
|
|
47
|
+
|
|
48
|
+
With no arguments, mdb opens the preview tab and starts a small background
|
|
49
|
+
process. From then on, whatever Markdown file you save — in any project, from
|
|
50
|
+
any editor — shows up in that tab. Nothing else to set up. `mdb --stop` ends it.
|
|
51
|
+
|
|
52
|
+
Finding the file you just saved uses Spotlight (mdfind). Files under hidden
|
|
53
|
+
directories, node_modules and ~/Library are ignored. Without Spotlight, mdb
|
|
54
|
+
falls back to watching the directory you started it in.
|
|
55
|
+
|
|
56
|
+
The same command is also installed under its full name, `mdbrowse`.
|
|
43
57
|
|
|
44
58
|
Environment:
|
|
45
59
|
MDBROWSE_OUT output HTML path (default: /tmp/mdbrowse.html)
|
|
@@ -58,51 +72,272 @@ open_url() {
|
|
|
58
72
|
fi
|
|
59
73
|
}
|
|
60
74
|
|
|
75
|
+
open_tab() {
|
|
76
|
+
[ -f "$OUT" ] || printf '<!doctype html><meta charset="utf-8"><p>No preview yet.' > "$OUT"
|
|
77
|
+
[ -f "${OUT%.html}-stamp.js" ] || printf 'window.__mdbrowseStamp="0";\n' > "${OUT%.html}-stamp.js"
|
|
78
|
+
open_url
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
PIDFILE="${OUT%.html}-sync.pid"
|
|
82
|
+
|
|
83
|
+
running_pid() {
|
|
84
|
+
[ -f "$PIDFILE" ] || return 1
|
|
85
|
+
local pid; pid="$(cat "$PIDFILE" 2>/dev/null)"
|
|
86
|
+
[ -n "$pid" ] || return 1
|
|
87
|
+
kill -0 "$pid" 2>/dev/null || return 1
|
|
88
|
+
echo "$pid"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
stop_sync() {
|
|
92
|
+
local pid
|
|
93
|
+
if pid="$(running_pid)"; then
|
|
94
|
+
kill "$pid" 2>/dev/null || true
|
|
95
|
+
rm -f "$PIDFILE"
|
|
96
|
+
echo "$PROG: stopped"
|
|
97
|
+
else
|
|
98
|
+
rm -f "$PIDFILE"
|
|
99
|
+
echo "$PROG: not running"
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
start_sync() {
|
|
104
|
+
if running_pid >/dev/null; then
|
|
105
|
+
echo "$PROG: already in sync (mdb --stop to end it)"
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
nohup "$0" --sync-daemon "$PWD" >/dev/null 2>&1 &
|
|
109
|
+
echo $! > "$PIDFILE"
|
|
110
|
+
echo "$PROG: in sync — save any .md and it shows up here (mdb --stop to end it)"
|
|
111
|
+
}
|
|
112
|
+
|
|
61
113
|
case "${1:-}" in
|
|
62
114
|
-h|--help) usage; exit 0 ;;
|
|
63
115
|
-v|--version) echo "mdbrowse $VERSION"; exit 0 ;;
|
|
64
116
|
--path) echo "$OUT"; exit 0 ;;
|
|
117
|
+
--stop) stop_sync; exit 0 ;;
|
|
118
|
+
--status)
|
|
119
|
+
if running_pid >/dev/null; then
|
|
120
|
+
echo "$PROG: in sync (pid $(running_pid))"
|
|
121
|
+
else
|
|
122
|
+
echo "$PROG: not running"
|
|
123
|
+
fi
|
|
124
|
+
[ -f "$OUT" ] && echo "showing: $(grep -o '<title>[^<]*' "$OUT" 2>/dev/null | sed 's/<title>//')"
|
|
125
|
+
exit 0 ;;
|
|
65
126
|
--eject)
|
|
66
127
|
mkdir -p "$CONFIG_DIR"
|
|
67
128
|
if [ -f "$CONFIG_DIR/head.html" ] && [ "${2:-}" != "--force" ]; then
|
|
68
|
-
echo "
|
|
129
|
+
echo "$PROG: $CONFIG_DIR/head.html already exists (pass --force to overwrite)" >&2
|
|
69
130
|
exit 1
|
|
70
131
|
fi
|
|
71
132
|
cp "$BUNDLED_HEAD" "$CONFIG_DIR/head.html"
|
|
72
133
|
echo "$CONFIG_DIR/head.html"
|
|
73
134
|
exit 0 ;;
|
|
74
|
-
--open)
|
|
75
|
-
[ -f "$OUT" ] || printf '<!doctype html><meta charset="utf-8"><p>No preview yet.' > "$OUT"
|
|
76
|
-
[ -f "${OUT%.html}-stamp.js" ] || printf 'window.__mdbrowseStamp="0";\n' > "${OUT%.html}-stamp.js"
|
|
77
|
-
open_url; exit 0 ;;
|
|
78
|
-
"") usage; exit 2 ;;
|
|
79
135
|
esac
|
|
80
136
|
|
|
81
|
-
|
|
137
|
+
# 残りは「オプション+ファイル」
|
|
138
|
+
watch=0
|
|
139
|
+
do_open=0
|
|
140
|
+
daemon=0
|
|
141
|
+
sync_cwd=""
|
|
142
|
+
src=""
|
|
143
|
+
while [ $# -gt 0 ]; do
|
|
144
|
+
case "$1" in
|
|
145
|
+
-w|--watch) watch=1 ;;
|
|
146
|
+
-o|--open) do_open=1 ;;
|
|
147
|
+
--sync-daemon) daemon=1; shift; sync_cwd="${1:-$PWD}" ;;
|
|
148
|
+
--) shift; [ $# -gt 0 ] && src="$1"; break ;;
|
|
149
|
+
-*) echo "$PROG: unknown option: $1" >&2; exit 2 ;;
|
|
150
|
+
*) src="$1" ;;
|
|
151
|
+
esac
|
|
152
|
+
shift
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
# 引数なし(または --open だけ)が既定の使い方。タブを開いて、以後は勝手に追従する
|
|
156
|
+
if [ -z "$src" ] && [ "$watch" -eq 0 ] && [ "$daemon" -eq 0 ]; then
|
|
157
|
+
if ! command -v pandoc >/dev/null 2>&1; then
|
|
158
|
+
echo "$PROG: pandoc not found. Install it first (macOS: brew install pandoc)" >&2
|
|
159
|
+
exit 127
|
|
160
|
+
fi
|
|
161
|
+
open_tab
|
|
162
|
+
start_sync
|
|
163
|
+
exit 0
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# -w だけならカレントディレクトリを見る
|
|
167
|
+
[ -z "$src" ] && [ "$watch" -eq 1 ] && src="."
|
|
82
168
|
|
|
83
169
|
if ! command -v pandoc >/dev/null 2>&1; then
|
|
84
|
-
echo "
|
|
170
|
+
echo "$PROG: pandoc not found. Install it first (macOS: brew install pandoc)" >&2
|
|
85
171
|
exit 127
|
|
86
172
|
fi
|
|
87
|
-
|
|
88
|
-
|
|
173
|
+
root=""
|
|
174
|
+
if [ "$daemon" -eq 1 ]; then
|
|
175
|
+
:
|
|
176
|
+
elif [ -d "$src" ]; then
|
|
177
|
+
if [ "$watch" -eq 0 ]; then
|
|
178
|
+
echo "$PROG: $src is a directory (pass -w to watch it)" >&2
|
|
179
|
+
exit 2
|
|
180
|
+
fi
|
|
181
|
+
root="$src"
|
|
182
|
+
elif [ ! -f "$src" ]; then
|
|
183
|
+
echo "$PROG: no such file: $src" >&2
|
|
89
184
|
exit 66
|
|
90
185
|
fi
|
|
91
186
|
|
|
92
|
-
|
|
93
|
-
|
|
187
|
+
# コードの色付けは Shiki(node がある場合)。無ければ pandoc 内蔵の skylighting
|
|
188
|
+
SELF_DIR="$(resolve_dir "$0")"
|
|
189
|
+
HIGHLIGHTER="$SELF_DIR/../lib/highlight.mjs"
|
|
190
|
+
use_shiki=0
|
|
191
|
+
if command -v node >/dev/null 2>&1 && [ -f "$HIGHLIGHTER" ]; then
|
|
192
|
+
use_shiki=1
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# シンタックスハイライトの指定は pandoc 3.9 で名前が変わった
|
|
196
|
+
pv="$(pandoc --version | head -1 | awk '{print $2}')"
|
|
197
|
+
if [ "$(printf '%s\n3.9\n' "$pv" | sort -V | head -1)" = "3.9" ]; then
|
|
198
|
+
if [ "$use_shiki" -eq 1 ]; then hl=(--syntax-highlighting none); else hl=(--syntax-highlighting breezedark); fi
|
|
199
|
+
else
|
|
200
|
+
if [ "$use_shiki" -eq 1 ]; then hl=(--no-highlight); else hl=(--highlight-style breezedark); fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
render() {
|
|
204
|
+
local args=(--standalone --from gfm "${hl[@]}" --variable "pagetitle=$(basename "$src")" --output "$OUT")
|
|
205
|
+
[ -f "$HEAD" ] && args+=(--include-in-header "$HEAD")
|
|
206
|
+
|
|
207
|
+
pandoc "$src" "${args[@]}"
|
|
208
|
+
|
|
209
|
+
# コードブロックを Shiki で色付けし直す。失敗しても変換自体は成立させる
|
|
210
|
+
if [ "$use_shiki" -eq 1 ]; then
|
|
211
|
+
node "$HIGHLIGHTER" "$OUT" || true
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# frontmatter の title を pandoc が拾うと本文の先頭に見出しが増えるので落とす。
|
|
215
|
+
# ブラウザのタブ名には pagetitle を使っている
|
|
216
|
+
perl -0pi -e 's#<header id="title-block-header">.*?</header>\n?##s' "$OUT"
|
|
217
|
+
|
|
218
|
+
# 出力先が /tmp なので、元ファイルからの相対パス(画像・ローカルリンク)が
|
|
219
|
+
# そのままでは解決できない。ソースのあるディレクトリ基準の file:// に書き換える。
|
|
220
|
+
# 書き換えるのはタグの属性だけ。本文中の src="..." のようなインラインコードは触らない。
|
|
221
|
+
# スキーム付き(http(s) data: mailto: など)/絶対パス/ページ内アンカーはそのまま残す
|
|
222
|
+
local src_dir="$(cd "$(dirname "$src")" && pwd)"
|
|
223
|
+
MDBROWSE_SRC_DIR="$src_dir" perl -0pi -e '
|
|
224
|
+
BEGIN {
|
|
225
|
+
$base = $ENV{MDBROWSE_SRC_DIR};
|
|
226
|
+
# file:// の中で意味を持つ文字は先に逃がす(% は必ず最初)
|
|
227
|
+
$base =~ s/%/%25/g;
|
|
228
|
+
$base =~ s/#/%23/g;
|
|
229
|
+
$base =~ s/\?/%3F/g;
|
|
230
|
+
$base =~ s/ /%20/g;
|
|
231
|
+
}
|
|
232
|
+
s{(<(?:img|a|source|video|audio|embed|object|track)\b)([^>]*?)(/?>)}{
|
|
233
|
+
my ($tag, $attrs, $close) = ($1, $2, $3);
|
|
234
|
+
$attrs =~ s{((?:src|href|poster|data)=")(?![a-zA-Z][a-zA-Z0-9+.\-]*:|//|/|\#|")([^"]*)"}
|
|
235
|
+
{$1 . "file://" . $base . "/" . $2 . "\""}ge;
|
|
236
|
+
$tag . $attrs . $close;
|
|
237
|
+
}gse;
|
|
238
|
+
' "$OUT"
|
|
239
|
+
|
|
240
|
+
# 更新の目印。ブラウザはこれを見て、中身が変わったときだけ読み直す
|
|
241
|
+
printf 'window.__mdbrowseStamp="%s";\n' "$(date +%s)-$RANDOM" > "${OUT%.html}-stamp.js"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# ファイルの更新時刻とサイズ。同じ秒内の2回目の保存も拾えるようにサイズも見る
|
|
245
|
+
if stat -f %m . >/dev/null 2>&1; then
|
|
246
|
+
fingerprint() { stat -f '%m %z' "$1" 2>/dev/null; }
|
|
247
|
+
stat_time_name() { xargs -0 stat -f '%m %N' 2>/dev/null; }
|
|
248
|
+
else
|
|
249
|
+
fingerprint() { stat -c '%Y %s' "$1" 2>/dev/null; }
|
|
250
|
+
stat_time_name() { xargs -0 stat -c '%Y %n' 2>/dev/null; }
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# ディレクトリの中でいちばん最近保存された Markdown
|
|
254
|
+
newest_md() {
|
|
255
|
+
find "$1" \( -name .git -o -name node_modules -o -name .venv \) -prune \
|
|
256
|
+
-o -type f -name '*.md' -print0 2>/dev/null \
|
|
257
|
+
| stat_time_name | sort -rn | head -1 | cut -d' ' -f2-
|
|
258
|
+
}
|
|
94
259
|
|
|
95
|
-
|
|
260
|
+
# Spotlight で「家じゅうで直近に保存された Markdown」を引く。木を歩かないので速い
|
|
261
|
+
newest_global() {
|
|
262
|
+
mdfind 'kMDItemFSName == "*.md"c && kMDItemContentModificationDate >= $time.now(-1800)' 2>/dev/null \
|
|
263
|
+
| grep -v -e '/\.' -e '/node_modules/' -e '/Library/' \
|
|
264
|
+
| tr '\n' '\0' | stat_time_name | sort -rn | head -1 | cut -d' ' -f2-
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# 背景で動く本体。保存された Markdown を追いかけて変換し続ける
|
|
268
|
+
if [ "$daemon" -eq 1 ]; then
|
|
269
|
+
trap 'rm -f "$PIDFILE"' EXIT
|
|
270
|
+
use_spotlight=0
|
|
271
|
+
command -v mdfind >/dev/null 2>&1 && use_spotlight=1
|
|
272
|
+
[ "$use_spotlight" -eq 0 ] && root="$sync_cwd"
|
|
96
273
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
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"
|
|
274
|
+
pick() {
|
|
275
|
+
if [ "$use_spotlight" -eq 1 ]; then newest_global; else newest_md "$root"; fi
|
|
276
|
+
}
|
|
106
277
|
|
|
107
|
-
|
|
108
|
-
|
|
278
|
+
src="$(pick)"
|
|
279
|
+
if [ -n "$src" ] && [ -f "$src" ]; then render || true; fi
|
|
280
|
+
last="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
|
|
281
|
+
# 開いているファイルの保存は毎秒見る。別ファイルへの切り替えの検出は2秒ごと
|
|
282
|
+
tick=0
|
|
283
|
+
while sleep 1; do
|
|
284
|
+
tick=$(( tick + 1 ))
|
|
285
|
+
if [ $(( tick % 2 )) -eq 0 ]; then
|
|
286
|
+
cand="$(pick)"
|
|
287
|
+
[ -n "$cand" ] && src="$cand"
|
|
288
|
+
fi
|
|
289
|
+
[ -f "$src" ] || continue
|
|
290
|
+
now="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
|
|
291
|
+
[ "$now" = "$last" ] && continue
|
|
292
|
+
last="$now"
|
|
293
|
+
render || true
|
|
294
|
+
done
|
|
295
|
+
exit 0
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
if [ -n "$root" ]; then
|
|
299
|
+
src="$(newest_md "$root")"
|
|
300
|
+
if [ -z "$src" ]; then
|
|
301
|
+
echo "$PROG: no Markdown file under $root" >&2
|
|
302
|
+
exit 66
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
[ "$do_open" -eq 1 ] && open_tab
|
|
307
|
+
|
|
308
|
+
render
|
|
309
|
+
|
|
310
|
+
# 監視。保存のたびに変換し直す。スタイルシートを直したときも作り直す
|
|
311
|
+
if [ "$watch" -eq 1 ]; then
|
|
312
|
+
# ディレクトリを見る場合、木が大きいと走査が重い。1回の走査時間から
|
|
313
|
+
# 待ち時間を決めて、CPU を食い潰さないようにする
|
|
314
|
+
interval=1
|
|
315
|
+
if [ -n "$root" ]; then
|
|
316
|
+
t0="$(perl -MTime::HiRes=time -e 'printf "%d", time*1000')"
|
|
317
|
+
newest_md "$root" >/dev/null
|
|
318
|
+
t1="$(perl -MTime::HiRes=time -e 'printf "%d", time*1000')"
|
|
319
|
+
scan=$(( t1 - t0 ))
|
|
320
|
+
interval=$(( (scan * 5 + 999) / 1000 ))
|
|
321
|
+
[ "$interval" -lt 1 ] && interval=1
|
|
322
|
+
[ "$interval" -gt 10 ] && interval=10
|
|
323
|
+
if [ "$interval" -gt 1 ]; then
|
|
324
|
+
echo "$PROG: watching $root — save any .md under it (checking every ${interval}s; a smaller directory reacts faster) (Ctrl-C to stop)" >&2
|
|
325
|
+
else
|
|
326
|
+
echo "$PROG: watching $root — save any .md under it (Ctrl-C to stop)" >&2
|
|
327
|
+
fi
|
|
328
|
+
else
|
|
329
|
+
echo "$PROG: watching $src (Ctrl-C to stop)" >&2
|
|
330
|
+
fi
|
|
331
|
+
last="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
|
|
332
|
+
while sleep "$interval"; do
|
|
333
|
+
if [ -n "$root" ]; then
|
|
334
|
+
cand="$(newest_md "$root")"
|
|
335
|
+
[ -n "$cand" ] && src="$cand"
|
|
336
|
+
fi
|
|
337
|
+
now="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
|
|
338
|
+
[ "$now" = "$last" ] && continue
|
|
339
|
+
last="$now"
|
|
340
|
+
[ -f "$src" ] || continue
|
|
341
|
+
render || echo "$PROG: render failed" >&2
|
|
342
|
+
done
|
|
343
|
+
fi
|
|
@@ -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(/</g, '<').replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"').replace(/'/g, "'")
|
|
12
|
+
.replace(/&/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.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
}
|