@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 +5 -0
- package/README.md +5 -0
- package/assets/head.html +4 -1
- package/bin/mdbrowse +89 -25
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
*)
|
|
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
|
-
|
|
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
|
-
|
|
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" "$
|
|
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' "$
|
|
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
|
|
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
|
-
' "$
|
|
271
|
+
' "$tmp"
|
|
272
|
+
|
|
273
|
+
mv -f "$tmp" "$OUT"
|
|
239
274
|
|
|
240
|
-
#
|
|
241
|
-
|
|
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 '%
|
|
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 '%
|
|
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 |
|
|
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 |
|
|
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 ]
|
|
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 ||
|
|
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 %
|
|
348
|
+
if [ $(( tick % scan_gap )) -eq 0 ]; then
|
|
286
349
|
cand="$(pick)"
|
|
287
|
-
[ -n "$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 ||
|
|
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.
|
|
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",
|