@commte/mdbrowse 0.2.0 → 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
@@ -70,6 +70,11 @@ mdb -w file.md # 1つのファイルだけを前面で監視する(Ctrl-C
70
70
  mdb --path # 出力先の HTML のパスを表示する
71
71
  ```
72
72
 
73
+ | 環境変数 | 既定値 | |
74
+ |---|---|---|
75
+ | `MDBROWSE_OUT` | `/tmp/mdbrowse.html` | 出力先の HTML |
76
+ | `MDBROWSE_HEAD` | `~/.config/mdbrowse/head.html` | スタイルとブラウザ側のスクリプト |
77
+
73
78
  ### どうやって保存したファイルを見つけているか
74
79
 
75
80
  Spotlight(`mdfind`)に「直近に保存された Markdown」を尋ねて、それを変換している。ディスクを歩き回らないので軽い。隠しディレクトリ、`node_modules`、`~/Library` の下は無視する。Spotlight が使えない環境では、`mdb` を実行したディレクトリを見る動きに落ちる
package/README.md CHANGED
@@ -70,6 +70,11 @@ mdb -w file.md # follow one file in the foreground (Ctrl-C to stop)
70
70
  mdb --path # print the output HTML path
71
71
  ```
72
72
 
73
+ | Variable | Default | |
74
+ |---|---|---|
75
+ | `MDBROWSE_OUT` | `/tmp/mdbrowse.html` | output HTML path |
76
+ | `MDBROWSE_HEAD` | `~/.config/mdbrowse/head.html` | stylesheet and browser script |
77
+
73
78
  ### How the sync finds your file
74
79
 
75
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.
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.2.0"
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"
@@ -79,12 +79,15 @@ open_tab() {
79
79
  }
80
80
 
81
81
  PIDFILE="${OUT%.html}-sync.pid"
82
+ LOGFILE="${OUT%.html}-sync.log"
82
83
 
83
84
  running_pid() {
84
85
  [ -f "$PIDFILE" ] || return 1
85
86
  local pid; pid="$(cat "$PIDFILE" 2>/dev/null)"
86
87
  [ -n "$pid" ] || return 1
87
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
88
91
  echo "$pid"
89
92
  }
90
93
 
@@ -105,9 +108,20 @@ start_sync() {
105
108
  echo "$PROG: already in sync (mdb --stop to end it)"
106
109
  return 0
107
110
  fi
111
+ # PID は起動した本人に書かせる。すぐ落ちた場合に死んだ PID を残さないため
108
112
  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)"
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
111
125
  }
112
126
 
113
127
  case "${1:-}" in
@@ -147,13 +161,23 @@ while [ $# -gt 0 ]; do
147
161
  --sync-daemon) daemon=1; shift; sync_cwd="${1:-$PWD}" ;;
148
162
  --) shift; [ $# -gt 0 ] && src="$1"; break ;;
149
163
  -*) echo "$PROG: unknown option: $1" >&2; exit 2 ;;
150
- *) src="$1" ;;
164
+ *)
165
+ if [ -n "$src" ]; then
166
+ echo "$PROG: too many files: $1" >&2
167
+ exit 2
168
+ fi
169
+ src="$1" ;;
151
170
  esac
152
- shift
171
+ shift || break
153
172
  done
154
173
 
155
174
  # 引数なし(または --open だけ)が既定の使い方。タブを開いて、以後は勝手に追従する
156
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
157
181
  if ! command -v pandoc >/dev/null 2>&1; then
158
182
  echo "$PROG: pandoc not found. Install it first (macOS: brew install pandoc)" >&2
159
183
  exit 127
@@ -192,34 +216,41 @@ if command -v node >/dev/null 2>&1 && [ -f "$HIGHLIGHTER" ]; then
192
216
  use_shiki=1
193
217
  fi
194
218
 
195
- # シンタックスハイライトの指定は pandoc 3.9 で名前が変わった
219
+ # シンタックスハイライトの指定は pandoc 3.9 で名前が変わった。
220
+ # sort -V は POSIX に無いので、major と minor を数として比べる
196
221
  pv="$(pandoc --version | head -1 | awk '{print $2}')"
197
- 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
198
225
  if [ "$use_shiki" -eq 1 ]; then hl=(--syntax-highlighting none); else hl=(--syntax-highlighting breezedark); fi
199
226
  else
200
227
  if [ "$use_shiki" -eq 1 ]; then hl=(--no-highlight); else hl=(--highlight-style breezedark); fi
201
228
  fi
202
229
 
203
230
  render() {
204
- local args=(--standalone --from gfm "${hl[@]}" --variable "pagetitle=$(basename "$src")" --output "$OUT")
231
+ # 途中経過をタブに見せないため、別名で組み立ててから置き換える
232
+ local tmp="$OUT.part"
233
+ # 出力先が .part なので、拡張子から形式を推測させず明示する
234
+ local args=(--standalone --from gfm --to html5 "${hl[@]}" --variable "pagetitle=$(basename "$src")" --output "$tmp")
205
235
  [ -f "$HEAD" ] && args+=(--include-in-header "$HEAD")
206
236
 
207
237
  pandoc "$src" "${args[@]}"
208
238
 
209
239
  # コードブロックを Shiki で色付けし直す。失敗しても変換自体は成立させる
210
240
  if [ "$use_shiki" -eq 1 ]; then
211
- node "$HIGHLIGHTER" "$OUT" || true
241
+ node "$HIGHLIGHTER" "$tmp" || true
212
242
  fi
213
243
 
214
244
  # frontmatter の title を pandoc が拾うと本文の先頭に見出しが増えるので落とす。
215
245
  # ブラウザのタブ名には pagetitle を使っている
216
- perl -0pi -e 's#<header id="title-block-header">.*?</header>\n?##s' "$OUT"
246
+ perl -0pi -e 's#<header id="title-block-header">.*?</header>\n?##s' "$tmp"
217
247
 
218
248
  # 出力先が /tmp なので、元ファイルからの相対パス(画像・ローカルリンク)が
219
249
  # そのままでは解決できない。ソースのあるディレクトリ基準の file:// に書き換える。
220
250
  # 書き換えるのはタグの属性だけ。本文中の src="..." のようなインラインコードは触らない。
221
251
  # スキーム付き(http(s) data: mailto: など)/絶対パス/ページ内アンカーはそのまま残す
222
- local src_dir="$(cd "$(dirname "$src")" && pwd)"
252
+ local src_dir
253
+ src_dir="$(cd "$(dirname "$src")" && pwd)" || return 1
223
254
  MDBROWSE_SRC_DIR="$src_dir" perl -0pi -e '
224
255
  BEGIN {
225
256
  $base = $ENV{MDBROWSE_SRC_DIR};
@@ -227,6 +258,8 @@ render() {
227
258
  $base =~ s/%/%25/g;
228
259
  $base =~ s/#/%23/g;
229
260
  $base =~ s/\?/%3F/g;
261
+ $base =~ s/&/%26/g;
262
+ $base =~ s/"/%22/g;
230
263
  $base =~ s/ /%20/g;
231
264
  }
232
265
  s{(<(?:img|a|source|video|audio|embed|object|track)\b)([^>]*?)(/?>)}{
@@ -235,62 +268,93 @@ render() {
235
268
  {$1 . "file://" . $base . "/" . $2 . "\""}ge;
236
269
  $tag . $attrs . $close;
237
270
  }gse;
238
- ' "$OUT"
271
+ ' "$tmp"
272
+
273
+ mv -f "$tmp" "$OUT"
239
274
 
240
- # 更新の目印。ブラウザはこれを見て、中身が変わったときだけ読み直す
241
- 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"
242
280
  }
243
281
 
244
282
  # ファイルの更新時刻とサイズ。同じ秒内の2回目の保存も拾えるようにサイズも見る
245
283
  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; }
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; }
248
286
  else
249
- fingerprint() { stat -c '%Y %s' "$1" 2>/dev/null; }
250
- stat_time_name() { xargs -0 stat -c '%Y %n' 2>/dev/null; }
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; }
251
289
  fi
252
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
+
253
297
  # ディレクトリの中でいちばん最近保存された Markdown
254
298
  newest_md() {
255
299
  find "$1" \( -name .git -o -name node_modules -o -name .venv \) -prune \
256
300
  -o -type f -name '*.md' -print0 2>/dev/null \
257
- | stat_time_name | sort -rn | head -1 | cut -d' ' -f2-
301
+ | stat_time_name | pick_newest || true
258
302
  }
259
303
 
260
304
  # Spotlight で「家じゅうで直近に保存された Markdown」を引く。木を歩かないので速い
305
+ # 該当が無いときは grep が 1 を返す。pipefail に拾わせると背景プロセスが即死するので握る
261
306
  newest_global() {
262
307
  mdfind 'kMDItemFSName == "*.md"c && kMDItemContentModificationDate >= $time.now(-1800)' 2>/dev/null \
263
308
  | grep -v -e '/\.' -e '/node_modules/' -e '/Library/' \
264
- | tr '\n' '\0' | stat_time_name | sort -rn | head -1 | cut -d' ' -f2-
309
+ | tr '\n' '\0' | stat_time_name | pick_newest || true
265
310
  }
266
311
 
267
312
  # 背景で動く本体。保存された Markdown を追いかけて変換し続ける
268
313
  if [ "$daemon" -eq 1 ]; then
314
+ # 先に別のものが動いていたら黙って降りる(同時に叩かれた場合の取り合いを避ける)
315
+ if running_pid >/dev/null; then exit 0; fi
316
+ echo $$ > "$PIDFILE"
269
317
  trap 'rm -f "$PIDFILE"' EXIT
270
318
  use_spotlight=0
271
319
  command -v mdfind >/dev/null 2>&1 && use_spotlight=1
272
- [ "$use_spotlight" -eq 0 ] && root="$sync_cwd"
320
+ if [ "$use_spotlight" -eq 0 ]; then root="$sync_cwd"; fi
273
321
 
274
322
  pick() {
275
323
  if [ "$use_spotlight" -eq 1 ]; then newest_global; else newest_md "$root"; fi
276
324
  }
277
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
+
278
341
  src="$(pick)"
279
- if [ -n "$src" ] && [ -f "$src" ]; then render || true; fi
342
+ if [ -n "$src" ] && [ -f "$src" ]; then render || log "render failed: $src"; fi
280
343
  last="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
281
344
  # 開いているファイルの保存は毎秒見る。別ファイルへの切り替えの検出は2秒ごと
282
345
  tick=0
283
346
  while sleep 1; do
284
347
  tick=$(( tick + 1 ))
285
- if [ $(( tick % 2 )) -eq 0 ]; then
348
+ if [ $(( tick % scan_gap )) -eq 0 ]; then
286
349
  cand="$(pick)"
287
- [ -n "$cand" ] && src="$cand"
350
+ if [ -n "$cand" ]; then src="$cand"; fi
288
351
  fi
352
+ [ -n "$src" ] || continue
289
353
  [ -f "$src" ] || continue
290
354
  now="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
291
355
  [ "$now" = "$last" ] && continue
292
356
  last="$now"
293
- render || true
357
+ render || log "render failed: $src"
294
358
  done
295
359
  exit 0
296
360
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commte/mdbrowse",
3
- "version": "0.2.0",
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",