@commte/mdbrowse 0.1.1 → 0.2.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
@@ -2,22 +2,21 @@
2
2
 
3
3
  # mdbrowse
4
4
 
5
- 編集中の Markdown を、自分の CSS でブラウザに表示する。シェルコマンドを実行できるエディタなら何でも使える
5
+ 編集中の Markdown を、自分の CSS でブラウザに表示する。エディタは何でもいい
6
6
 
7
- サーバーもポートも常駐プロセスも要らない。ショートカットを押すと現在のファイルが固定パスの HTML に変換され、開いたままのブラウザのタブがそれを拾う
7
+ `mdb` を1回実行するとタブが開き、以後はどのプロジェクトのどの Markdown を保存しても、そのタブに出る。設定は要らない。サーバーもポートも無い
8
8
 
9
9
  ![ライトテーマ](docs/preview-light.png)
10
10
 
11
11
  ## 何のために作ったか
12
12
 
13
- エディタ内蔵のプレビューは、エディタの中で描画されるので見た目に手を入れられない。サーバーを立てるツールなら自由になるが、常駐とポートを引き受けることになる。これはどちらでもない。pandoc の上に乗せたシェルスクリプトと、スタイルを収めた HTML が1枚あるだけ
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
 
@@ -52,29 +51,38 @@ cd mdbrowse
52
51
 
53
52
  </details>
54
53
 
55
- プレビュー用のタブを1回だけ開いて、そのままにしておく
54
+ あとは1回実行する
56
55
 
57
56
  ```sh
58
- mdb --open
57
+ mdb
59
58
  ```
60
59
 
61
- 必要なのは最初の1回だけ。以後は変換するたびに、そのタブの中身が入れ替わる。開き直すのは、タブを閉じたときとブラウザを再起動したときだけ
60
+ プレビュー用のタブが開き、小さな背景プロセスが動きだす。以後どの Markdown を保存しても、どのプロジェクトのものでも、そのタブに出る。止めるときは `mdb --stop`、再開はまた `mdb`
62
61
 
63
62
  ## 使い方
64
63
 
65
64
  ```sh
66
- mdb file.md # 変換する(出力 HTML を上書きする)
67
- mdb --open # プレビュー用のタブを開く
68
- mdb --path # 出力先の HTML のパスを表示する
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 のパスを表示する
69
71
  ```
70
72
 
71
- ファイルの監視はしない。保存しただけでは何も起きず、`mdb <file>` が走ったときにタブが切り替わる。エディタ側でキーに割り当てるか、保存時に走らせる
72
-
73
73
  | 環境変数 | 既定値 | |
74
74
  |---|---|---|
75
75
  | `MDBROWSE_OUT` | `/tmp/mdbrowse.html` | 出力先の HTML |
76
76
  | `MDBROWSE_HEAD` | `~/.config/mdbrowse/head.html` | スタイルとブラウザ側のスクリプト |
77
77
 
78
+ ### どうやって保存したファイルを見つけているか
79
+
80
+ Spotlight(`mdfind`)に「直近に保存された Markdown」を尋ねて、それを変換している。ディスクを歩き回らないので軽い。隠しディレクトリ、`node_modules`、`~/Library` の下は無視する。Spotlight が使えない環境では、`mdb` を実行したディレクトリを見る動きに落ちる
81
+
82
+ いま開いているファイルは毎秒そのまま見ているので、続けて保存したぶんはすぐ出る。別のファイルに移ったときだけ、Spotlight が気づくまで数秒かかる
83
+
84
+ 最後に保存されたものを追うので、別のプログラムが Markdown を書くとプレビューが持っていかれる。1つのファイルに固定したいときは `mdb -w そのファイル.md` を使う。指定したものだけを見て、`Ctrl-C` で止まる
85
+
78
86
  ## ページ内の操作
79
87
 
80
88
  右上に小さなバーが出る。テーマ、文字サイズ、本文幅、目次の表示を切り替えられる。選択はブラウザに保存されるので、再読み込みしても次に開くファイルにも引き継がれる。ディスクには何も書かず、サーバーも使わない
@@ -85,6 +93,8 @@ mdb --path # 出力先の HTML のパスを表示する
85
93
 
86
94
  ## エディタの設定
87
95
 
96
+ `mdb` が保存を追いかけるので、以下は要らない。背景で何も動かさず、キーを押して出したい場合はこちらを設定する。どれも1回だけ変換する `mdb <file>` を呼んでいる
97
+
88
98
  ### Zed
89
99
 
90
100
  `install.sh` が `~/.config/zed/tasks.json` を作る。既存のファイルがある場合は上書きせず、貼り付ける内容を表示する。あとは `~/.config/zed/keymap.json` にキーを追加する
@@ -169,7 +179,7 @@ mdb sample.md
169
179
  ## 仕組み
170
180
 
171
181
  ```
172
- エディタのショートカット → mdb <file> → pandoc → /tmp/mdbrowse.html
182
+ ファイルを保存 → mdb(またはエディタのショートカット) → pandoc → /tmp/mdbrowse.html
173
183
  → /tmp/mdbrowse-stamp.js
174
184
 
175
185
  ブラウザがスタンプを見て、変化したときだけ再読み込み
@@ -181,6 +191,8 @@ YAML の frontmatter は本文に出さない。ファイル側の `title` が
181
191
 
182
192
  元ファイルからの相対パス(画像や隣のファイルへのリンク)は、変換時に絶対 `file://` へ書き換える。HTML が `/tmp` にあっても画像が表示されるのはこのため。書き換えるのは `img` や `a` などタグの属性だけなので、本文に `src="foo.png"` と書いても表示はそのまま。絶対パス、スキーム付きのもの(`http(s)`、`data:`、`mailto:` など)、`//` 始まり、ページ内アンカーには触らない。パスに `&` や `#`、空白が入っていても壊れない
183
193
 
194
+ 監視は同じ変換を繰り返しているだけ。背景プロセスは、開いているファイルの更新時刻とサイズを1秒ごとに、直近に保存された Markdown を2秒ごとに見て、変わったら変換し直す。待ち受けるものは無くポートも持たない。`mdb --stop` で終わり、`ps` に見えるシェルのプロセスが1つあるだけ
195
+
184
196
  ページは静的なので、待ち受けているものは何も無く、終了させる必要もない
185
197
 
186
198
  ## ライセンス
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 that can run a shell command.
5
+ Preview the Markdown file you are editing in a real browser, with your own CSS, from any editor.
6
6
 
7
- No server. No port. No daemon. One shortcut renders the current file to a fixed HTML path, and the browser tab you already have open picks it up.
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
  ![Light theme](docs/preview-light.png)
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 does neither: it is a shell script on top of `pandoc`, plus one HTML file holding the styles.
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
 
@@ -52,29 +51,38 @@ This installs `mdb` (and the longer `mdbrowse`) into `~/.local/bin`, and the sty
52
51
 
53
52
  </details>
54
53
 
55
- Open the preview tab once and leave it open:
54
+ Then run it once:
56
55
 
57
56
  ```sh
58
- mdb --open
57
+ mdb
59
58
  ```
60
59
 
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.
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.
62
61
 
63
62
  ## Usage
64
63
 
65
64
  ```sh
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
+ 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
69
71
  ```
70
72
 
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
-
73
73
  | Variable | Default | |
74
74
  |---|---|---|
75
75
  | `MDBROWSE_OUT` | `/tmp/mdbrowse.html` | output HTML path |
76
76
  | `MDBROWSE_HEAD` | `~/.config/mdbrowse/head.html` | stylesheet and browser script |
77
77
 
78
+ ### How the sync finds your file
79
+
80
+ 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.
81
+
82
+ 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.
83
+
84
+ 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`.
85
+
78
86
  ## In-page controls
79
87
 
80
88
  A small bar sits in the top-right corner: theme, font size, content width, and a toggle for the table of contents. Choices are stored in the browser, so they survive reloads and apply to every file you preview afterwards. Nothing is written to disk and no server is involved.
@@ -85,6 +93,8 @@ The table of contents is built from the headings of the current file and follows
85
93
 
86
94
  ## Editor setup
87
95
 
96
+ 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.
97
+
88
98
  ### Zed
89
99
 
90
100
  `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`:
@@ -169,7 +179,7 @@ mdb sample.md
169
179
  ## How it works
170
180
 
171
181
  ```
172
- editor shortcut → mdb <file> → pandoc → /tmp/mdbrowse.html
182
+ save a file → mdb (or an editor shortcut) → pandoc → /tmp/mdbrowse.html
173
183
  → /tmp/mdbrowse-stamp.js
174
184
 
175
185
  browser polls the stamp, reloads only on change
@@ -179,6 +189,8 @@ YAML front matter is consumed rather than printed: the file's own `title` does n
179
189
 
180
190
  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.
181
191
 
192
+ 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`.
193
+
182
194
  The page is static, so nothing is listening and nothing needs to be shut down.
183
195
 
184
196
  ## License
package/assets/head.html CHANGED
@@ -186,6 +186,8 @@
186
186
  var printing = false;
187
187
  addEventListener('beforeprint', function () { printing = true; });
188
188
  addEventListener('afterprint', function () { printing = false; });
189
+ // 印刷がキャンセルされて afterprint が来ない場合に、止まったままにならないようにする
190
+ addEventListener('visibilitychange', function () { if (!document.hidden) printing = false; });
189
191
 
190
192
  function save() { try { localStorage.setItem('mdbrowse', JSON.stringify(s)); } catch (e) {} }
191
193
  function fontSize() { return s.fontSize || parseInt(getComputedStyle(r).getPropertyValue('--font-size')) || 16; }
@@ -333,8 +335,9 @@
333
335
  addEventListener('focus', function () { checkStamp(); });
334
336
  // バーを操作している間は再読み込みを止める。状態を持たずにその場で見る
335
337
  // (持つと、バーの上にポインタを置いたままウィンドウを離れたときに止まったままになる)
338
+ var hoverable = !matchMedia || matchMedia('(hover: hover)').matches;
336
339
  setInterval(function () {
337
- if (printing || bar.matches(':hover')) return;
340
+ if (printing || (hoverable && bar.matches(':hover'))) return;
338
341
  if (Date.now() - lastScroll < 900) return;
339
342
  checkStamp();
340
343
  }, 1000);
package/bin/mdbrowse CHANGED
@@ -3,7 +3,7 @@
3
3
  # browser tab can stay open and just swap its contents.
4
4
  set -euo pipefail
5
5
 
6
- VERSION="0.1.1"
6
+ VERSION="0.2.1"
7
7
  PROG="$(basename "$0")"
8
8
  OUT="${MDBROWSE_OUT:-/tmp/mdbrowse.html}"
9
9
  CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/mdbrowse"
@@ -36,11 +36,22 @@ usage() {
36
36
  mdb — render Markdown to a fixed HTML file for browser preview
37
37
 
38
38
  Usage:
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
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.
44
55
 
45
56
  The same command is also installed under its full name, `mdbrowse`.
46
57
 
@@ -61,10 +72,71 @@ open_url() {
61
72
  fi
62
73
  }
63
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
+ LOGFILE="${OUT%.html}-sync.log"
83
+
84
+ running_pid() {
85
+ [ -f "$PIDFILE" ] || return 1
86
+ local pid; pid="$(cat "$PIDFILE" 2>/dev/null)"
87
+ [ -n "$pid" ] || return 1
88
+ kill -0 "$pid" 2>/dev/null || return 1
89
+ # PID は使い回される。中身が自分のプロセスか確かめてから扱う
90
+ ps -p "$pid" -o command= 2>/dev/null | grep -q -- '--sync-daemon' || return 1
91
+ echo "$pid"
92
+ }
93
+
94
+ stop_sync() {
95
+ local pid
96
+ if pid="$(running_pid)"; then
97
+ kill "$pid" 2>/dev/null || true
98
+ rm -f "$PIDFILE"
99
+ echo "$PROG: stopped"
100
+ else
101
+ rm -f "$PIDFILE"
102
+ echo "$PROG: not running"
103
+ fi
104
+ }
105
+
106
+ start_sync() {
107
+ if running_pid >/dev/null; then
108
+ echo "$PROG: already in sync (mdb --stop to end it)"
109
+ return 0
110
+ fi
111
+ # PID は起動した本人に書かせる。すぐ落ちた場合に死んだ PID を残さないため
112
+ nohup "$0" --sync-daemon "$PWD" >/dev/null 2>&1 &
113
+ local i=0
114
+ while [ $i -lt 20 ]; do
115
+ running_pid >/dev/null && break
116
+ i=$(( i + 1 ))
117
+ sleep 0.1
118
+ done
119
+ if running_pid >/dev/null; then
120
+ echo "$PROG: in sync — save any .md and it shows up here (mdb --stop to end it)"
121
+ else
122
+ echo "$PROG: could not start the background sync (see $LOGFILE)" >&2
123
+ return 1
124
+ fi
125
+ }
126
+
64
127
  case "${1:-}" in
65
128
  -h|--help) usage; exit 0 ;;
66
129
  -v|--version) echo "mdbrowse $VERSION"; exit 0 ;;
67
130
  --path) echo "$OUT"; exit 0 ;;
131
+ --stop) stop_sync; exit 0 ;;
132
+ --status)
133
+ if running_pid >/dev/null; then
134
+ echo "$PROG: in sync (pid $(running_pid))"
135
+ else
136
+ echo "$PROG: not running"
137
+ fi
138
+ [ -f "$OUT" ] && echo "showing: $(grep -o '<title>[^<]*' "$OUT" 2>/dev/null | sed 's/<title>//')"
139
+ exit 0 ;;
68
140
  --eject)
69
141
  mkdir -p "$CONFIG_DIR"
70
142
  if [ -f "$CONFIG_DIR/head.html" ] && [ "${2:-}" != "--force" ]; then
@@ -74,20 +146,64 @@ case "${1:-}" in
74
146
  cp "$BUNDLED_HEAD" "$CONFIG_DIR/head.html"
75
147
  echo "$CONFIG_DIR/head.html"
76
148
  exit 0 ;;
77
- --open)
78
- [ -f "$OUT" ] || printf '<!doctype html><meta charset="utf-8"><p>No preview yet.' > "$OUT"
79
- [ -f "${OUT%.html}-stamp.js" ] || printf 'window.__mdbrowseStamp="0";\n' > "${OUT%.html}-stamp.js"
80
- open_url; exit 0 ;;
81
- "") usage; exit 2 ;;
82
149
  esac
83
150
 
84
- src="$1"
151
+ # 残りは「オプション+ファイル」
152
+ watch=0
153
+ do_open=0
154
+ daemon=0
155
+ sync_cwd=""
156
+ src=""
157
+ while [ $# -gt 0 ]; do
158
+ case "$1" in
159
+ -w|--watch) watch=1 ;;
160
+ -o|--open) do_open=1 ;;
161
+ --sync-daemon) daemon=1; shift; sync_cwd="${1:-$PWD}" ;;
162
+ --) shift; [ $# -gt 0 ] && src="$1"; break ;;
163
+ -*) echo "$PROG: unknown option: $1" >&2; exit 2 ;;
164
+ *)
165
+ if [ -n "$src" ]; then
166
+ echo "$PROG: too many files: $1" >&2
167
+ exit 2
168
+ fi
169
+ src="$1" ;;
170
+ esac
171
+ shift || break
172
+ done
173
+
174
+ # 引数なし(または --open だけ)が既定の使い方。タブを開いて、以後は勝手に追従する
175
+ if [ -z "$src" ] && [ "$watch" -eq 0 ] && [ "$daemon" -eq 0 ]; then
176
+ # --open だけを渡された場合はタブを開くだけ(エディタのタスクから呼ばれる経路)
177
+ if [ "$do_open" -eq 1 ]; then
178
+ open_tab
179
+ exit 0
180
+ fi
181
+ if ! command -v pandoc >/dev/null 2>&1; then
182
+ echo "$PROG: pandoc not found. Install it first (macOS: brew install pandoc)" >&2
183
+ exit 127
184
+ fi
185
+ open_tab
186
+ start_sync
187
+ exit 0
188
+ fi
189
+
190
+ # -w だけならカレントディレクトリを見る
191
+ [ -z "$src" ] && [ "$watch" -eq 1 ] && src="."
85
192
 
86
193
  if ! command -v pandoc >/dev/null 2>&1; then
87
194
  echo "$PROG: pandoc not found. Install it first (macOS: brew install pandoc)" >&2
88
195
  exit 127
89
196
  fi
90
- if [ ! -f "$src" ]; then
197
+ root=""
198
+ if [ "$daemon" -eq 1 ]; then
199
+ :
200
+ elif [ -d "$src" ]; then
201
+ if [ "$watch" -eq 0 ]; then
202
+ echo "$PROG: $src is a directory (pass -w to watch it)" >&2
203
+ exit 2
204
+ fi
205
+ root="$src"
206
+ elif [ ! -f "$src" ]; then
91
207
  echo "$PROG: no such file: $src" >&2
92
208
  exit 66
93
209
  fi
@@ -100,40 +216,50 @@ if command -v node >/dev/null 2>&1 && [ -f "$HIGHLIGHTER" ]; then
100
216
  use_shiki=1
101
217
  fi
102
218
 
103
- # シンタックスハイライトの指定は pandoc 3.9 で名前が変わった
219
+ # シンタックスハイライトの指定は pandoc 3.9 で名前が変わった。
220
+ # sort -V は POSIX に無いので、major と minor を数として比べる
104
221
  pv="$(pandoc --version | head -1 | awk '{print $2}')"
105
- if [ "$(printf '%s\n3.9\n' "$pv" | sort -V | head -1)" = "3.9" ]; then
222
+ pv_major="${pv%%.*}"; pv_rest="${pv#*.}"; pv_minor="${pv_rest%%.*}"
223
+ case "$pv_major$pv_minor" in *[!0-9]*|'') pv_major=0; pv_minor=0 ;; esac
224
+ if [ "$pv_major" -gt 3 ] || { [ "$pv_major" -eq 3 ] && [ "$pv_minor" -ge 9 ]; }; then
106
225
  if [ "$use_shiki" -eq 1 ]; then hl=(--syntax-highlighting none); else hl=(--syntax-highlighting breezedark); fi
107
226
  else
108
227
  if [ "$use_shiki" -eq 1 ]; then hl=(--no-highlight); else hl=(--highlight-style breezedark); fi
109
228
  fi
110
229
 
111
- args=(--standalone --from gfm "${hl[@]}" --variable "pagetitle=$(basename "$src")" --output "$OUT")
112
- [ -f "$HEAD" ] && args+=(--include-in-header "$HEAD")
230
+ render() {
231
+ # 途中経過をタブに見せないため、別名で組み立ててから置き換える
232
+ local tmp="$OUT.part"
233
+ # 出力先が .part なので、拡張子から形式を推測させず明示する
234
+ local args=(--standalone --from gfm --to html5 "${hl[@]}" --variable "pagetitle=$(basename "$src")" --output "$tmp")
235
+ [ -f "$HEAD" ] && args+=(--include-in-header "$HEAD")
113
236
 
114
- pandoc "$src" "${args[@]}"
237
+ pandoc "$src" "${args[@]}"
115
238
 
116
- # コードブロックを Shiki で色付けし直す。失敗しても変換自体は成立させる
117
- if [ "$use_shiki" -eq 1 ]; then
118
- node "$HIGHLIGHTER" "$OUT" || true
119
- fi
239
+ # コードブロックを Shiki で色付けし直す。失敗しても変換自体は成立させる
240
+ if [ "$use_shiki" -eq 1 ]; then
241
+ node "$HIGHLIGHTER" "$tmp" || true
242
+ fi
120
243
 
121
- # frontmatter の title を pandoc が拾うと本文の先頭に見出しが増えるので落とす。
122
- # ブラウザのタブ名には pagetitle を使っている
123
- perl -0pi -e 's#<header id="title-block-header">.*?</header>\n?##s' "$OUT"
244
+ # frontmatter の title を pandoc が拾うと本文の先頭に見出しが増えるので落とす。
245
+ # ブラウザのタブ名には pagetitle を使っている
246
+ perl -0pi -e 's#<header id="title-block-header">.*?</header>\n?##s' "$tmp"
124
247
 
125
- # 出力先が /tmp なので、元ファイルからの相対パス(画像・ローカルリンク)が
126
- # そのままでは解決できない。ソースのあるディレクトリ基準の file:// に書き換える。
127
- # 書き換えるのはタグの属性だけ。本文中の src="..." のようなインラインコードは触らない。
128
- # スキーム付き(http(s) data: mailto: など)/絶対パス/ページ内アンカーはそのまま残す
129
- src_dir="$(cd "$(dirname "$src")" && pwd)"
130
- MDBROWSE_SRC_DIR="$src_dir" perl -0pi -e '
248
+ # 出力先が /tmp なので、元ファイルからの相対パス(画像・ローカルリンク)が
249
+ # そのままでは解決できない。ソースのあるディレクトリ基準の file:// に書き換える。
250
+ # 書き換えるのはタグの属性だけ。本文中の src="..." のようなインラインコードは触らない。
251
+ # スキーム付き(http(s) data: mailto: など)/絶対パス/ページ内アンカーはそのまま残す
252
+ local src_dir
253
+ src_dir="$(cd "$(dirname "$src")" && pwd)" || return 1
254
+ MDBROWSE_SRC_DIR="$src_dir" perl -0pi -e '
131
255
  BEGIN {
132
256
  $base = $ENV{MDBROWSE_SRC_DIR};
133
257
  # file:// の中で意味を持つ文字は先に逃がす(% は必ず最初)
134
258
  $base =~ s/%/%25/g;
135
259
  $base =~ s/#/%23/g;
136
260
  $base =~ s/\?/%3F/g;
261
+ $base =~ s/&/%26/g;
262
+ $base =~ s/"/%22/g;
137
263
  $base =~ s/ /%20/g;
138
264
  }
139
265
  s{(<(?:img|a|source|video|audio|embed|object|track)\b)([^>]*?)(/?>)}{
@@ -142,7 +268,140 @@ MDBROWSE_SRC_DIR="$src_dir" perl -0pi -e '
142
268
  {$1 . "file://" . $base . "/" . $2 . "\""}ge;
143
269
  $tag . $attrs . $close;
144
270
  }gse;
145
- ' "$OUT"
271
+ ' "$tmp"
272
+
273
+ mv -f "$tmp" "$OUT"
146
274
 
147
- # 更新の目印。ブラウザはこれを見て、中身が変わったときだけ読み直す
148
- printf 'window.__mdbrowseStamp="%s";\n' "$(date +%s)-$RANDOM" > "${OUT%.html}-stamp.js"
275
+ # 更新の目印。ブラウザはこれを見て、中身が変わったときだけ読み直す。
276
+ # 本体の差し替えが済んでから書く
277
+ local stamp="${OUT%.html}-stamp.js"
278
+ printf 'window.__mdbrowseStamp="%s";\n' "$(date +%s)-$RANDOM" > "$stamp.part"
279
+ mv -f "$stamp.part" "$stamp"
280
+ }
281
+
282
+ # ファイルの更新時刻とサイズ。同じ秒内の2回目の保存も拾えるようにサイズも見る
283
+ if stat -f %m . >/dev/null 2>&1; then
284
+ fingerprint() { stat -f '%Fm %z %i' "$1" 2>/dev/null || true; }
285
+ stat_time_name() { xargs -0 stat -f '%m %N' 2>/dev/null || true; }
286
+ else
287
+ fingerprint() { stat -c '%.9Y %s %i' "$1" 2>/dev/null || true; }
288
+ stat_time_name() { xargs -0 stat -c '%Y %n' 2>/dev/null || true; }
289
+ fi
290
+
291
+ # 「更新時刻 パス」の行から、いちばん新しい1件を返す。
292
+ # sort と head の組み合わせは、件数が多いと head の SIGPIPE で pipefail に引っかかる
293
+ pick_newest() {
294
+ awk '$1 > m { m = $1; sub(/^[0-9]+ /, ""); f = $0 } END { if (f != "") print f }'
295
+ }
296
+
297
+ # ディレクトリの中でいちばん最近保存された Markdown
298
+ newest_md() {
299
+ find "$1" \( -name .git -o -name node_modules -o -name .venv \) -prune \
300
+ -o -type f -name '*.md' -print0 2>/dev/null \
301
+ | stat_time_name | pick_newest || true
302
+ }
303
+
304
+ # Spotlight で「家じゅうで直近に保存された Markdown」を引く。木を歩かないので速い
305
+ # 該当が無いときは grep が 1 を返す。pipefail に拾わせると背景プロセスが即死するので握る
306
+ newest_global() {
307
+ mdfind 'kMDItemFSName == "*.md"c && kMDItemContentModificationDate >= $time.now(-1800)' 2>/dev/null \
308
+ | grep -v -e '/\.' -e '/node_modules/' -e '/Library/' \
309
+ | tr '\n' '\0' | stat_time_name | pick_newest || true
310
+ }
311
+
312
+ # 背景で動く本体。保存された Markdown を追いかけて変換し続ける
313
+ if [ "$daemon" -eq 1 ]; then
314
+ # 先に別のものが動いていたら黙って降りる(同時に叩かれた場合の取り合いを避ける)
315
+ if running_pid >/dev/null; then exit 0; fi
316
+ echo $$ > "$PIDFILE"
317
+ trap 'rm -f "$PIDFILE"' EXIT
318
+ use_spotlight=0
319
+ command -v mdfind >/dev/null 2>&1 && use_spotlight=1
320
+ if [ "$use_spotlight" -eq 0 ]; then root="$sync_cwd"; fi
321
+
322
+ pick() {
323
+ if [ "$use_spotlight" -eq 1 ]; then newest_global; else newest_md "$root"; fi
324
+ }
325
+
326
+ # 変換の失敗は画面に出ないので、ログに残す
327
+ log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOGFILE"; }
328
+
329
+ # Spotlight を使えない場合は木を歩く。大きな木では間隔を伸ばす
330
+ scan_gap=2
331
+ if [ "$use_spotlight" -eq 0 ]; then
332
+ t0="$(perl -MTime::HiRes=time -e 'printf "%d", time*1000')"
333
+ newest_md "$root" >/dev/null
334
+ t1="$(perl -MTime::HiRes=time -e 'printf "%d", time*1000')"
335
+ scan_gap=$(( ( (t1 - t0) * 5 + 999) / 1000 ))
336
+ [ "$scan_gap" -lt 2 ] && scan_gap=2
337
+ [ "$scan_gap" -gt 10 ] && scan_gap=10
338
+ log "watching $root every ${scan_gap}s (no Spotlight)"
339
+ fi
340
+
341
+ src="$(pick)"
342
+ if [ -n "$src" ] && [ -f "$src" ]; then render || log "render failed: $src"; fi
343
+ last="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
344
+ # 開いているファイルの保存は毎秒見る。別ファイルへの切り替えの検出は2秒ごと
345
+ tick=0
346
+ while sleep 1; do
347
+ tick=$(( tick + 1 ))
348
+ if [ $(( tick % scan_gap )) -eq 0 ]; then
349
+ cand="$(pick)"
350
+ if [ -n "$cand" ]; then src="$cand"; fi
351
+ fi
352
+ [ -n "$src" ] || continue
353
+ [ -f "$src" ] || continue
354
+ now="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
355
+ [ "$now" = "$last" ] && continue
356
+ last="$now"
357
+ render || log "render failed: $src"
358
+ done
359
+ exit 0
360
+ fi
361
+
362
+ if [ -n "$root" ]; then
363
+ src="$(newest_md "$root")"
364
+ if [ -z "$src" ]; then
365
+ echo "$PROG: no Markdown file under $root" >&2
366
+ exit 66
367
+ fi
368
+ fi
369
+
370
+ [ "$do_open" -eq 1 ] && open_tab
371
+
372
+ render
373
+
374
+ # 監視。保存のたびに変換し直す。スタイルシートを直したときも作り直す
375
+ if [ "$watch" -eq 1 ]; then
376
+ # ディレクトリを見る場合、木が大きいと走査が重い。1回の走査時間から
377
+ # 待ち時間を決めて、CPU を食い潰さないようにする
378
+ interval=1
379
+ if [ -n "$root" ]; then
380
+ t0="$(perl -MTime::HiRes=time -e 'printf "%d", time*1000')"
381
+ newest_md "$root" >/dev/null
382
+ t1="$(perl -MTime::HiRes=time -e 'printf "%d", time*1000')"
383
+ scan=$(( t1 - t0 ))
384
+ interval=$(( (scan * 5 + 999) / 1000 ))
385
+ [ "$interval" -lt 1 ] && interval=1
386
+ [ "$interval" -gt 10 ] && interval=10
387
+ if [ "$interval" -gt 1 ]; then
388
+ echo "$PROG: watching $root — save any .md under it (checking every ${interval}s; a smaller directory reacts faster) (Ctrl-C to stop)" >&2
389
+ else
390
+ echo "$PROG: watching $root — save any .md under it (Ctrl-C to stop)" >&2
391
+ fi
392
+ else
393
+ echo "$PROG: watching $src (Ctrl-C to stop)" >&2
394
+ fi
395
+ last="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
396
+ while sleep "$interval"; do
397
+ if [ -n "$root" ]; then
398
+ cand="$(newest_md "$root")"
399
+ [ -n "$cand" ] && src="$cand"
400
+ fi
401
+ now="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
402
+ [ "$now" = "$last" ] && continue
403
+ last="$now"
404
+ [ -f "$src" ] || continue
405
+ render || echo "$PROG: render failed" >&2
406
+ done
407
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commte/mdbrowse",
3
- "version": "0.1.1",
3
+ "version": "0.2.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
6
  "mdbrowse": "bin/mdbrowse",