@commte/mdbrowse 0.2.0 → 0.3.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 +15 -0
- package/README.md +15 -0
- package/assets/head.html +18 -4
- package/bin/mdbrowse +235 -38
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -59,17 +59,32 @@ mdb
|
|
|
59
59
|
|
|
60
60
|
プレビュー用のタブが開き、小さな背景プロセスが動きだす。以後どの Markdown を保存しても、どのプロジェクトのものでも、そのタブに出る。止めるときは `mdb --stop`、再開はまた `mdb`
|
|
61
61
|
|
|
62
|
+
ログアウトや再起動で消える。macOS なら、勝手に戻ってくるようにもできる
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
mdb --install-agent
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`~/Library/LaunchAgents` に LaunchAgent を1枚置くだけで、ログインした時点から追従が動いている。外すときは `mdb --uninstall-agent`。macOS のログイン項目には「開発元を識別できない項目」として出る。スクリプトに署名していないため
|
|
69
|
+
|
|
62
70
|
## 使い方
|
|
63
71
|
|
|
64
72
|
```sh
|
|
65
73
|
mdb # タブを開いて、以後ずっと追従させる
|
|
66
74
|
mdb --stop # 背景の追従を止める
|
|
67
75
|
mdb --status # いま何が出ているかを表示する
|
|
76
|
+
mdb --install-agent # ログイン時に自動で始めるようにする(macOS)
|
|
77
|
+
mdb --uninstall-agent # それを外す
|
|
68
78
|
mdb file.md # そのファイルを1回だけ変換する
|
|
69
79
|
mdb -w file.md # 1つのファイルだけを前面で監視する(Ctrl-C で終了)
|
|
70
80
|
mdb --path # 出力先の HTML のパスを表示する
|
|
71
81
|
```
|
|
72
82
|
|
|
83
|
+
| 環境変数 | 既定値 | |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `MDBROWSE_OUT` | `/tmp/mdbrowse.html` | 出力先の HTML |
|
|
86
|
+
| `MDBROWSE_HEAD` | `~/.config/mdbrowse/head.html` | スタイルとブラウザ側のスクリプト |
|
|
87
|
+
|
|
73
88
|
### どうやって保存したファイルを見つけているか
|
|
74
89
|
|
|
75
90
|
Spotlight(`mdfind`)に「直近に保存された Markdown」を尋ねて、それを変換している。ディスクを歩き回らないので軽い。隠しディレクトリ、`node_modules`、`~/Library` の下は無視する。Spotlight が使えない環境では、`mdb` を実行したディレクトリを見る動きに落ちる
|
package/README.md
CHANGED
|
@@ -59,17 +59,32 @@ mdb
|
|
|
59
59
|
|
|
60
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
61
|
|
|
62
|
+
The process is gone after a logout or a reboot. On macOS you can have it come back on its own:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
mdb --install-agent
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That writes a LaunchAgent under `~/Library/LaunchAgents` and nothing else, so the sync is running from the moment you log in. `mdb --uninstall-agent` removes it. macOS lists it under Login Items as an unidentified developer — the script is not code-signed.
|
|
69
|
+
|
|
62
70
|
## Usage
|
|
63
71
|
|
|
64
72
|
```sh
|
|
65
73
|
mdb # open the tab and keep it in sync
|
|
66
74
|
mdb --stop # stop the background sync
|
|
67
75
|
mdb --status # show what is being previewed
|
|
76
|
+
mdb --install-agent # start the sync at every login (macOS)
|
|
77
|
+
mdb --uninstall-agent # remove it
|
|
68
78
|
mdb file.md # render one file, once
|
|
69
79
|
mdb -w file.md # follow one file in the foreground (Ctrl-C to stop)
|
|
70
80
|
mdb --path # print the output HTML path
|
|
71
81
|
```
|
|
72
82
|
|
|
83
|
+
| Variable | Default | |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `MDBROWSE_OUT` | `/tmp/mdbrowse.html` | output HTML path |
|
|
86
|
+
| `MDBROWSE_HEAD` | `~/.config/mdbrowse/head.html` | stylesheet and browser script |
|
|
87
|
+
|
|
73
88
|
### How the sync finds your file
|
|
74
89
|
|
|
75
90
|
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
|
@@ -184,8 +184,17 @@
|
|
|
184
184
|
var THEMES = ['light', 'dark'];
|
|
185
185
|
var WIDTHS = [800, 680, 960, 1200];
|
|
186
186
|
var printing = false;
|
|
187
|
-
|
|
188
|
-
addEventListener('
|
|
187
|
+
var printTimer = null;
|
|
188
|
+
addEventListener('beforeprint', function () {
|
|
189
|
+
printing = true;
|
|
190
|
+
// afterprint が来ないブラウザや、途中で閉じられた場合に止まったままにならないようにする
|
|
191
|
+
clearTimeout(printTimer);
|
|
192
|
+
printTimer = setTimeout(function () { printing = false; }, 60000);
|
|
193
|
+
});
|
|
194
|
+
addEventListener('afterprint', function () {
|
|
195
|
+
printing = false;
|
|
196
|
+
clearTimeout(printTimer);
|
|
197
|
+
});
|
|
189
198
|
|
|
190
199
|
function save() { try { localStorage.setItem('mdbrowse', JSON.stringify(s)); } catch (e) {} }
|
|
191
200
|
function fontSize() { return s.fontSize || parseInt(getComputedStyle(r).getPropertyValue('--font-size')) || 16; }
|
|
@@ -255,7 +264,8 @@
|
|
|
255
264
|
}
|
|
256
265
|
heads = heads.filter(function (h) { return lv(h) <= top + 1; });
|
|
257
266
|
}
|
|
258
|
-
|
|
267
|
+
try {
|
|
268
|
+
if (heads.length >= 3 && typeof IntersectionObserver === 'function') {
|
|
259
269
|
var topLevel = Math.min.apply(null, heads.map(function (h) { return +h.tagName.slice(1); }));
|
|
260
270
|
var toc = document.createElement('nav');
|
|
261
271
|
toc.id = 'mdtoc';
|
|
@@ -292,6 +302,7 @@
|
|
|
292
302
|
}, { rootMargin: '0px 0px -75% 0px' });
|
|
293
303
|
heads.forEach(function (h) { io.observe(h); });
|
|
294
304
|
}
|
|
305
|
+
} catch (e) { /* 目次が組めなくても、更新の監視は動かす */ }
|
|
295
306
|
|
|
296
307
|
// スクロール位置はブラウザ標準の復元に任せる(手で保存するとカクつく)
|
|
297
308
|
var lastScroll = 0;
|
|
@@ -316,6 +327,7 @@
|
|
|
316
327
|
var stampUrl = location.pathname.replace(/\.html$/, '-stamp.js');
|
|
317
328
|
var known = null;
|
|
318
329
|
function checkStamp() {
|
|
330
|
+
if (printing) return;
|
|
319
331
|
var el = document.createElement('script');
|
|
320
332
|
el.src = stampUrl + '?t=' + Date.now();
|
|
321
333
|
el.onload = function () {
|
|
@@ -333,8 +345,10 @@
|
|
|
333
345
|
addEventListener('focus', function () { checkStamp(); });
|
|
334
346
|
// バーを操作している間は再読み込みを止める。状態を持たずにその場で見る
|
|
335
347
|
// (持つと、バーの上にポインタを置いたままウィンドウを離れたときに止まったままになる)
|
|
348
|
+
var hoverable = typeof window.matchMedia !== 'function'
|
|
349
|
+
|| window.matchMedia('(hover: hover)').matches;
|
|
336
350
|
setInterval(function () {
|
|
337
|
-
if (printing || bar.matches(':hover')) return;
|
|
351
|
+
if (printing || (hoverable && bar.matches(':hover'))) return;
|
|
338
352
|
if (Date.now() - lastScroll < 900) return;
|
|
339
353
|
checkStamp();
|
|
340
354
|
}, 1000);
|
package/bin/mdbrowse
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
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.3.0"
|
|
7
7
|
PROG="$(basename "$0")"
|
|
8
8
|
OUT="${MDBROWSE_OUT:-/tmp/mdbrowse.html}"
|
|
9
|
+
case "$OUT" in
|
|
10
|
+
*.html) ;;
|
|
11
|
+
*) echo "$(basename "$0"): MDBROWSE_OUT must end with .html (got: $OUT)" >&2; exit 2 ;;
|
|
12
|
+
esac
|
|
13
|
+
STAMP="${OUT%.html}-stamp.js"
|
|
9
14
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/mdbrowse"
|
|
10
15
|
|
|
11
16
|
# スタイルシートの探索順
|
|
@@ -40,6 +45,8 @@ Usage:
|
|
|
40
45
|
mdb <file.md> render that file once
|
|
41
46
|
mdb --stop stop the background sync
|
|
42
47
|
mdb --status show what is being previewed
|
|
48
|
+
mdb --install-agent start the sync automatically at every login (macOS)
|
|
49
|
+
mdb --uninstall-agent remove it
|
|
43
50
|
mdb -w <file|dir> watch in the foreground instead (Ctrl-C to stop)
|
|
44
51
|
mdb --eject copy the bundled stylesheet into your config directory
|
|
45
52
|
mdb --path print the output HTML path
|
|
@@ -47,7 +54,12 @@ Usage:
|
|
|
47
54
|
|
|
48
55
|
With no arguments, mdb opens the preview tab and starts a small background
|
|
49
56
|
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
|
|
57
|
+
any editor — shows up in that tab. Nothing else to set up. `mdb --stop` ends it,
|
|
58
|
+
and it is gone after a logout or a reboot.
|
|
59
|
+
|
|
60
|
+
`mdb --install-agent` makes it start on its own at every login, so you never
|
|
61
|
+
have to think about starting or stopping it. It writes a LaunchAgent under
|
|
62
|
+
~/Library/LaunchAgents and nothing else; `mdb --uninstall-agent` removes it.
|
|
51
63
|
|
|
52
64
|
Finding the file you just saved uses Spotlight (mdfind). Files under hidden
|
|
53
65
|
directories, node_modules and ~/Library are ignored. Without Spotlight, mdb
|
|
@@ -72,32 +84,61 @@ open_url() {
|
|
|
72
84
|
fi
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
# まだ何も変換していないときの仮ページ。スタイルシートごと入れるので、
|
|
88
|
+
# 更新の監視も目次もバーも、ふつうのページと同じように動く
|
|
89
|
+
placeholder() {
|
|
90
|
+
printf '%s\n' '<!doctype html><meta charset="utf-8"><title>mdbrowse</title>'
|
|
91
|
+
[ -f "$HEAD" ] && cat "$HEAD"
|
|
92
|
+
printf '%s\n' '<p>No preview yet. Save a Markdown file.'
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
open_tab() {
|
|
76
|
-
[ -f "$OUT" ] ||
|
|
77
|
-
[ -f "$
|
|
96
|
+
[ -f "$OUT" ] || placeholder > "$OUT"
|
|
97
|
+
[ -f "$STAMP" ] || printf 'window.__mdbrowseStamp="0";\n' > "$STAMP"
|
|
78
98
|
open_url
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
|
|
101
|
+
# PID・ログ・ロックは自分だけが書ける場所に置く。共有の /tmp に固定名で置くと、
|
|
102
|
+
# 先に symlink を作られたときに書き込み先を差し替えられる
|
|
103
|
+
RUNTIME_DIR="${TMPDIR:-/tmp}/mdbrowse-$(id -u)"
|
|
104
|
+
( umask 077; mkdir -p "$RUNTIME_DIR" ) 2>/dev/null || true
|
|
105
|
+
LOGFILE="$RUNTIME_DIR/sync.log"
|
|
106
|
+
LOCKDIR="$RUNTIME_DIR/sync.lock"
|
|
107
|
+
PIDFILE="$LOCKDIR/pid"
|
|
108
|
+
LEGACY_PIDFILE="${OUT%.html}-sync.pid"
|
|
82
109
|
|
|
83
110
|
running_pid() {
|
|
84
111
|
[ -f "$PIDFILE" ] || return 1
|
|
85
112
|
local pid; pid="$(cat "$PIDFILE" 2>/dev/null)"
|
|
86
113
|
[ -n "$pid" ] || return 1
|
|
87
114
|
kill -0 "$pid" 2>/dev/null || return 1
|
|
115
|
+
# PID は使い回される。中身が自分のプロセスか確かめてから扱う
|
|
116
|
+
ps -p "$pid" -o command= 2>/dev/null | grep -q -- '--sync-daemon' || return 1
|
|
88
117
|
echo "$pid"
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
stop_sync() {
|
|
92
|
-
local pid
|
|
121
|
+
local pid stopped=0 i=0
|
|
93
122
|
if pid="$(running_pid)"; then
|
|
94
123
|
kill "$pid" 2>/dev/null || true
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
while [ $i -lt 30 ]; do
|
|
125
|
+
kill -0 "$pid" 2>/dev/null || break
|
|
126
|
+
i=$(( i + 1 ))
|
|
127
|
+
sleep 0.1
|
|
128
|
+
done
|
|
129
|
+
stopped=1
|
|
100
130
|
fi
|
|
131
|
+
# 0.2.x が置いた古い場所のものも拾う
|
|
132
|
+
if [ -f "$LEGACY_PIDFILE" ]; then
|
|
133
|
+
local old; old="$(cat "$LEGACY_PIDFILE" 2>/dev/null || true)"
|
|
134
|
+
if [ -n "$old" ] && kill -0 "$old" 2>/dev/null; then
|
|
135
|
+
kill "$old" 2>/dev/null || true
|
|
136
|
+
stopped=1
|
|
137
|
+
fi
|
|
138
|
+
rm -f "$LEGACY_PIDFILE"
|
|
139
|
+
fi
|
|
140
|
+
rm -rf "$LOCKDIR"
|
|
141
|
+
if [ "$stopped" -eq 1 ]; then echo "$PROG: stopped"; else echo "$PROG: not running"; fi
|
|
101
142
|
}
|
|
102
143
|
|
|
103
144
|
start_sync() {
|
|
@@ -105,13 +146,90 @@ start_sync() {
|
|
|
105
146
|
echo "$PROG: already in sync (mdb --stop to end it)"
|
|
106
147
|
return 0
|
|
107
148
|
fi
|
|
149
|
+
# PID は起動した本人に書かせる。すぐ落ちた場合に死んだ PID を残さないため
|
|
108
150
|
nohup "$0" --sync-daemon "$PWD" >/dev/null 2>&1 &
|
|
109
|
-
|
|
110
|
-
|
|
151
|
+
local i=0
|
|
152
|
+
while [ $i -lt 20 ]; do
|
|
153
|
+
running_pid >/dev/null && break
|
|
154
|
+
i=$(( i + 1 ))
|
|
155
|
+
sleep 0.1
|
|
156
|
+
done
|
|
157
|
+
if running_pid >/dev/null; then
|
|
158
|
+
echo "$PROG: in sync — save any .md and it shows up here (mdb --stop to end it)"
|
|
159
|
+
else
|
|
160
|
+
echo "$PROG: could not start the background sync (see $LOGFILE)" >&2
|
|
161
|
+
return 1
|
|
162
|
+
fi
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
AGENT_LABEL="com.commte.mdbrowse"
|
|
166
|
+
AGENT_PLIST="$HOME/Library/LaunchAgents/$AGENT_LABEL.plist"
|
|
167
|
+
|
|
168
|
+
agent_installed() { [ -f "$AGENT_PLIST" ]; }
|
|
169
|
+
|
|
170
|
+
install_agent() {
|
|
171
|
+
if [ "$(uname -s)" != "Darwin" ]; then
|
|
172
|
+
echo "$PROG: --install-agent is macOS only" >&2
|
|
173
|
+
return 1
|
|
174
|
+
fi
|
|
175
|
+
local self; self="$(resolve_dir "$0")/$(basename "$(readlink "$0" 2>/dev/null || echo "$0")")"
|
|
176
|
+
[ -x "$self" ] || self="$0"
|
|
177
|
+
|
|
178
|
+
# launchd の PATH は最小限なので、pandoc と node の場所を書き込んでおく
|
|
179
|
+
local paths="/usr/bin:/bin:/usr/sbin:/sbin"
|
|
180
|
+
local d
|
|
181
|
+
for d in "$(command -v pandoc)" "$(command -v node)" "$(command -v mdfind)"; do
|
|
182
|
+
[ -n "$d" ] && paths="$(dirname "$d"):$paths"
|
|
183
|
+
done
|
|
184
|
+
|
|
185
|
+
mkdir -p "$HOME/Library/LaunchAgents"
|
|
186
|
+
{
|
|
187
|
+
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>'
|
|
188
|
+
printf '%s\n' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
|
|
189
|
+
printf '%s\n' '<plist version="1.0">'
|
|
190
|
+
printf '%s\n' '<dict>'
|
|
191
|
+
printf ' <key>Label</key><string>%s</string>\n' "$AGENT_LABEL"
|
|
192
|
+
printf '%s\n' ' <key>ProgramArguments</key>'
|
|
193
|
+
printf '%s\n' ' <array>'
|
|
194
|
+
printf ' <string>%s</string>\n' "$self"
|
|
195
|
+
printf '%s\n' ' <string>--sync-daemon</string>'
|
|
196
|
+
printf ' <string>%s</string>\n' "$HOME"
|
|
197
|
+
printf '%s\n' ' </array>'
|
|
198
|
+
printf '%s\n' ' <key>RunAtLoad</key><true/>'
|
|
199
|
+
printf '%s\n' ' <key>KeepAlive</key><false/>'
|
|
200
|
+
printf ' <key>StandardErrorPath</key><string>%s</string>\n' "$LOGFILE"
|
|
201
|
+
printf '%s\n' ' <key>EnvironmentVariables</key>'
|
|
202
|
+
printf '%s\n' ' <dict>'
|
|
203
|
+
printf ' <key>PATH</key><string>%s</string>\n' "$paths"
|
|
204
|
+
[ -n "${MDBROWSE_OUT:-}" ] && printf ' <key>MDBROWSE_OUT</key><string>%s</string>\n' "$MDBROWSE_OUT"
|
|
205
|
+
[ -n "${MDBROWSE_HEAD:-}" ] && printf ' <key>MDBROWSE_HEAD</key><string>%s</string>\n' "$MDBROWSE_HEAD"
|
|
206
|
+
printf '%s\n' ' </dict>'
|
|
207
|
+
printf '%s\n' '</dict>'
|
|
208
|
+
printf '%s\n' '</plist>'
|
|
209
|
+
} > "$AGENT_PLIST"
|
|
210
|
+
|
|
211
|
+
launchctl bootout "gui/$(id -u)/$AGENT_LABEL" 2>/dev/null || true
|
|
212
|
+
if ! launchctl bootstrap "gui/$(id -u)" "$AGENT_PLIST" 2>/dev/null; then
|
|
213
|
+
launchctl load -w "$AGENT_PLIST" 2>/dev/null || true
|
|
214
|
+
fi
|
|
215
|
+
echo "$PROG: starts at login now ($AGENT_PLIST)"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
uninstall_agent() {
|
|
219
|
+
launchctl bootout "gui/$(id -u)/$AGENT_LABEL" 2>/dev/null \
|
|
220
|
+
|| launchctl unload -w "$AGENT_PLIST" 2>/dev/null || true
|
|
221
|
+
if [ -f "$AGENT_PLIST" ]; then
|
|
222
|
+
rm -f "$AGENT_PLIST"
|
|
223
|
+
echo "$PROG: will not start at login any more"
|
|
224
|
+
else
|
|
225
|
+
echo "$PROG: not installed"
|
|
226
|
+
fi
|
|
111
227
|
}
|
|
112
228
|
|
|
113
229
|
case "${1:-}" in
|
|
114
230
|
-h|--help) usage; exit 0 ;;
|
|
231
|
+
--install-agent) install_agent; exit $? ;;
|
|
232
|
+
--uninstall-agent) uninstall_agent; exit 0 ;;
|
|
115
233
|
-v|--version) echo "mdbrowse $VERSION"; exit 0 ;;
|
|
116
234
|
--path) echo "$OUT"; exit 0 ;;
|
|
117
235
|
--stop) stop_sync; exit 0 ;;
|
|
@@ -121,6 +239,11 @@ case "${1:-}" in
|
|
|
121
239
|
else
|
|
122
240
|
echo "$PROG: not running"
|
|
123
241
|
fi
|
|
242
|
+
if agent_installed; then
|
|
243
|
+
echo "starts at login: yes ($AGENT_PLIST)"
|
|
244
|
+
else
|
|
245
|
+
echo "starts at login: no (mdb --install-agent to change that)"
|
|
246
|
+
fi
|
|
124
247
|
[ -f "$OUT" ] && echo "showing: $(grep -o '<title>[^<]*' "$OUT" 2>/dev/null | sed 's/<title>//')"
|
|
125
248
|
exit 0 ;;
|
|
126
249
|
--eject)
|
|
@@ -147,13 +270,23 @@ while [ $# -gt 0 ]; do
|
|
|
147
270
|
--sync-daemon) daemon=1; shift; sync_cwd="${1:-$PWD}" ;;
|
|
148
271
|
--) shift; [ $# -gt 0 ] && src="$1"; break ;;
|
|
149
272
|
-*) echo "$PROG: unknown option: $1" >&2; exit 2 ;;
|
|
150
|
-
*)
|
|
273
|
+
*)
|
|
274
|
+
if [ -n "$src" ]; then
|
|
275
|
+
echo "$PROG: too many files: $1" >&2
|
|
276
|
+
exit 2
|
|
277
|
+
fi
|
|
278
|
+
src="$1" ;;
|
|
151
279
|
esac
|
|
152
|
-
shift
|
|
280
|
+
shift || break
|
|
153
281
|
done
|
|
154
282
|
|
|
155
283
|
# 引数なし(または --open だけ)が既定の使い方。タブを開いて、以後は勝手に追従する
|
|
156
284
|
if [ -z "$src" ] && [ "$watch" -eq 0 ] && [ "$daemon" -eq 0 ]; then
|
|
285
|
+
# --open だけを渡された場合はタブを開くだけ(エディタのタスクから呼ばれる経路)
|
|
286
|
+
if [ "$do_open" -eq 1 ]; then
|
|
287
|
+
open_tab
|
|
288
|
+
exit 0
|
|
289
|
+
fi
|
|
157
290
|
if ! command -v pandoc >/dev/null 2>&1; then
|
|
158
291
|
echo "$PROG: pandoc not found. Install it first (macOS: brew install pandoc)" >&2
|
|
159
292
|
exit 127
|
|
@@ -192,34 +325,47 @@ if command -v node >/dev/null 2>&1 && [ -f "$HIGHLIGHTER" ]; then
|
|
|
192
325
|
use_shiki=1
|
|
193
326
|
fi
|
|
194
327
|
|
|
195
|
-
# シンタックスハイライトの指定は pandoc 3.9
|
|
328
|
+
# シンタックスハイライトの指定は pandoc 3.9 で名前が変わった。
|
|
329
|
+
# sort -V は POSIX に無いので、major と minor を数として比べる
|
|
196
330
|
pv="$(pandoc --version | head -1 | awk '{print $2}')"
|
|
197
|
-
|
|
331
|
+
pv_major="${pv%%.*}"; pv_rest="${pv#*.}"; pv_minor="${pv_rest%%.*}"
|
|
332
|
+
case "$pv_major$pv_minor" in *[!0-9]*|'') pv_major=0; pv_minor=0 ;; esac
|
|
333
|
+
if [ "$pv_major" -gt 3 ] || { [ "$pv_major" -eq 3 ] && [ "$pv_minor" -ge 9 ]; }; then
|
|
198
334
|
if [ "$use_shiki" -eq 1 ]; then hl=(--syntax-highlighting none); else hl=(--syntax-highlighting breezedark); fi
|
|
199
335
|
else
|
|
200
336
|
if [ "$use_shiki" -eq 1 ]; then hl=(--no-highlight); else hl=(--highlight-style breezedark); fi
|
|
201
337
|
fi
|
|
202
338
|
|
|
203
339
|
render() {
|
|
204
|
-
|
|
340
|
+
# 途中経過をタブに見せないため、別名で組み立ててから置き換える。
|
|
341
|
+
# 名前は毎回変える。固定名だと、同時に走ったときに互いの中間結果を踏む
|
|
342
|
+
local tmp
|
|
343
|
+
tmp="$(mktemp "$OUT.part.XXXXXX")" || return 1
|
|
344
|
+
|
|
345
|
+
# 出力先が .part なので、拡張子から形式を推測させず明示する
|
|
346
|
+
local args=(--standalone --from gfm --to html5 "${hl[@]}" --variable "pagetitle=$(basename "$src")" --output "$tmp")
|
|
205
347
|
[ -f "$HEAD" ] && args+=(--include-in-header "$HEAD")
|
|
206
348
|
|
|
207
|
-
|
|
349
|
+
# 途中で失敗したら、そこで止めて古い表示を残す。
|
|
350
|
+
# 関数を `render || ...` の形で呼ぶと関数の中では set -e が効かないので、各段で判定する
|
|
351
|
+
pandoc "$src" "${args[@]}" || { rm -f "$tmp"; return 1; }
|
|
208
352
|
|
|
209
353
|
# コードブロックを Shiki で色付けし直す。失敗しても変換自体は成立させる
|
|
210
354
|
if [ "$use_shiki" -eq 1 ]; then
|
|
211
|
-
node "$HIGHLIGHTER" "$
|
|
355
|
+
node "$HIGHLIGHTER" "$tmp" || true
|
|
212
356
|
fi
|
|
213
357
|
|
|
214
358
|
# frontmatter の title を pandoc が拾うと本文の先頭に見出しが増えるので落とす。
|
|
215
359
|
# ブラウザのタブ名には pagetitle を使っている
|
|
216
|
-
perl -0pi -e 's#<header id="title-block-header">.*?</header>\n?##s' "$
|
|
360
|
+
perl -0pi -e 's#<header id="title-block-header">.*?</header>\n?##s' "$tmp" \
|
|
361
|
+
|| { rm -f "$tmp"; return 1; }
|
|
217
362
|
|
|
218
363
|
# 出力先が /tmp なので、元ファイルからの相対パス(画像・ローカルリンク)が
|
|
219
364
|
# そのままでは解決できない。ソースのあるディレクトリ基準の file:// に書き換える。
|
|
220
365
|
# 書き換えるのはタグの属性だけ。本文中の src="..." のようなインラインコードは触らない。
|
|
221
366
|
# スキーム付き(http(s) data: mailto: など)/絶対パス/ページ内アンカーはそのまま残す
|
|
222
|
-
local src_dir
|
|
367
|
+
local src_dir
|
|
368
|
+
src_dir="$(cd "$(dirname "$src")" && pwd)" || { rm -f "$tmp"; return 1; }
|
|
223
369
|
MDBROWSE_SRC_DIR="$src_dir" perl -0pi -e '
|
|
224
370
|
BEGIN {
|
|
225
371
|
$base = $ENV{MDBROWSE_SRC_DIR};
|
|
@@ -227,70 +373,121 @@ render() {
|
|
|
227
373
|
$base =~ s/%/%25/g;
|
|
228
374
|
$base =~ s/#/%23/g;
|
|
229
375
|
$base =~ s/\?/%3F/g;
|
|
376
|
+
$base =~ s/&/%26/g;
|
|
377
|
+
$base =~ s/"/%22/g;
|
|
230
378
|
$base =~ s/ /%20/g;
|
|
231
379
|
}
|
|
232
|
-
|
|
380
|
+
# 属性値の中の > でタグを切らないよう、引用符の中はまとめて食う
|
|
381
|
+
s{(<(?:img|a|source|video|audio|embed|object|track)\b)((?:[^>"\x27]|"[^"]*"|\x27[^\x27]*\x27)*)(>)}{
|
|
233
382
|
my ($tag, $attrs, $close) = ($1, $2, $3);
|
|
234
383
|
$attrs =~ s{((?:src|href|poster|data)=")(?![a-zA-Z][a-zA-Z0-9+.\-]*:|//|/|\#|")([^"]*)"}
|
|
235
384
|
{$1 . "file://" . $base . "/" . $2 . "\""}ge;
|
|
385
|
+
$attrs =~ s{((?:src|href|poster|data)=\x27)(?![a-zA-Z][a-zA-Z0-9+.\-]*:|//|/|\#|\x27)([^\x27]*)\x27}
|
|
386
|
+
{$1 . "file://" . $base . "/" . $2 . "\x27"}ge;
|
|
236
387
|
$tag . $attrs . $close;
|
|
237
388
|
}gse;
|
|
238
|
-
' "$
|
|
389
|
+
' "$tmp" || { rm -f "$tmp"; return 1; }
|
|
239
390
|
|
|
240
|
-
|
|
241
|
-
|
|
391
|
+
mv -f "$tmp" "$OUT" || { rm -f "$tmp"; return 1; }
|
|
392
|
+
|
|
393
|
+
# 更新の目印。ブラウザはこれを見て、中身が変わったときだけ読み直す。
|
|
394
|
+
# 本体の差し替えが済んでから書く
|
|
395
|
+
local stamp_tmp
|
|
396
|
+
stamp_tmp="$(mktemp "$STAMP.part.XXXXXX")" || return 1
|
|
397
|
+
printf 'window.__mdbrowseStamp="%s";\n' "$(date +%s)-$RANDOM" > "$stamp_tmp" \
|
|
398
|
+
|| { rm -f "$stamp_tmp"; return 1; }
|
|
399
|
+
mv -f "$stamp_tmp" "$STAMP" || { rm -f "$stamp_tmp"; return 1; }
|
|
242
400
|
}
|
|
243
401
|
|
|
244
402
|
# ファイルの更新時刻とサイズ。同じ秒内の2回目の保存も拾えるようにサイズも見る
|
|
245
403
|
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; }
|
|
404
|
+
fingerprint() { stat -f '%Fm %z %i' "$1" 2>/dev/null || true; }
|
|
405
|
+
stat_time_name() { xargs -0 stat -f '%m %N' 2>/dev/null || true; }
|
|
248
406
|
else
|
|
249
|
-
fingerprint() { stat -c '%
|
|
250
|
-
stat_time_name() { xargs -0 stat -c '%Y %n' 2>/dev/null; }
|
|
407
|
+
fingerprint() { stat -c '%.9Y %s %i' "$1" 2>/dev/null || true; }
|
|
408
|
+
stat_time_name() { xargs -0 stat -c '%Y %n' 2>/dev/null || true; }
|
|
251
409
|
fi
|
|
252
410
|
|
|
411
|
+
# 「更新時刻 パス」の行から、いちばん新しい1件を返す。
|
|
412
|
+
# sort と head の組み合わせは、件数が多いと head の SIGPIPE で pipefail に引っかかる
|
|
413
|
+
pick_newest() {
|
|
414
|
+
awk '$1 > m { m = $1; sub(/^[0-9]+ /, ""); f = $0 } END { if (f != "") print f }'
|
|
415
|
+
}
|
|
416
|
+
|
|
253
417
|
# ディレクトリの中でいちばん最近保存された Markdown
|
|
254
418
|
newest_md() {
|
|
255
419
|
find "$1" \( -name .git -o -name node_modules -o -name .venv \) -prune \
|
|
256
420
|
-o -type f -name '*.md' -print0 2>/dev/null \
|
|
257
|
-
| stat_time_name |
|
|
421
|
+
| stat_time_name | pick_newest || true
|
|
258
422
|
}
|
|
259
423
|
|
|
260
424
|
# Spotlight で「家じゅうで直近に保存された Markdown」を引く。木を歩かないので速い
|
|
425
|
+
# 該当が無いときは grep が 1 を返す。pipefail に拾わせると背景プロセスが即死するので握る
|
|
261
426
|
newest_global() {
|
|
262
427
|
mdfind 'kMDItemFSName == "*.md"c && kMDItemContentModificationDate >= $time.now(-1800)' 2>/dev/null \
|
|
263
428
|
| grep -v -e '/\.' -e '/node_modules/' -e '/Library/' \
|
|
264
|
-
| tr '\n' '\0' | stat_time_name |
|
|
429
|
+
| tr '\n' '\0' | stat_time_name | pick_newest || true
|
|
265
430
|
}
|
|
266
431
|
|
|
267
432
|
# 背景で動く本体。保存された Markdown を追いかけて変換し続ける
|
|
268
433
|
if [ "$daemon" -eq 1 ]; then
|
|
269
|
-
|
|
434
|
+
# 確認と取得を1操作にする。mkdir は原子的なので、同時に叩かれても1つしか通らない
|
|
435
|
+
if ! mkdir "$LOCKDIR" 2>/dev/null; then
|
|
436
|
+
# 取れなかった側は、持ち主が名乗るのを少し待つ。
|
|
437
|
+
# すぐ「持ち主なし」と判断すると、書き込み前の相手からロックを奪ってしまう
|
|
438
|
+
i=0
|
|
439
|
+
while [ $i -lt 30 ]; do
|
|
440
|
+
running_pid >/dev/null && exit 0
|
|
441
|
+
i=$(( i + 1 ))
|
|
442
|
+
sleep 0.1
|
|
443
|
+
done
|
|
444
|
+
# 3秒待っても名乗らないなら、置き去りのロックとして回収する
|
|
445
|
+
rm -rf "$LOCKDIR"
|
|
446
|
+
mkdir "$LOCKDIR" 2>/dev/null || exit 0
|
|
447
|
+
fi
|
|
448
|
+
echo $$ > "$PIDFILE"
|
|
449
|
+
# 自分が持っているときだけ外す。あとから起きたものの分を消さないため
|
|
450
|
+
trap 'if [ "$(cat "$PIDFILE" 2>/dev/null)" = "$$" ]; then rm -rf "$LOCKDIR"; fi' EXIT
|
|
270
451
|
use_spotlight=0
|
|
271
452
|
command -v mdfind >/dev/null 2>&1 && use_spotlight=1
|
|
272
|
-
[ "$use_spotlight" -eq 0 ]
|
|
453
|
+
if [ "$use_spotlight" -eq 0 ]; then root="$sync_cwd"; fi
|
|
273
454
|
|
|
274
455
|
pick() {
|
|
275
456
|
if [ "$use_spotlight" -eq 1 ]; then newest_global; else newest_md "$root"; fi
|
|
276
457
|
}
|
|
277
458
|
|
|
459
|
+
# 変換の失敗は画面に出ないので、ログに残す
|
|
460
|
+
log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOGFILE"; }
|
|
461
|
+
|
|
462
|
+
# Spotlight を使えない場合は木を歩く。大きな木では間隔を伸ばす
|
|
463
|
+
scan_gap=2
|
|
464
|
+
if [ "$use_spotlight" -eq 0 ]; then
|
|
465
|
+
t0="$(perl -MTime::HiRes=time -e 'printf "%d", time*1000')"
|
|
466
|
+
newest_md "$root" >/dev/null
|
|
467
|
+
t1="$(perl -MTime::HiRes=time -e 'printf "%d", time*1000')"
|
|
468
|
+
scan_gap=$(( ( (t1 - t0) * 5 + 999) / 1000 ))
|
|
469
|
+
[ "$scan_gap" -lt 2 ] && scan_gap=2
|
|
470
|
+
[ "$scan_gap" -gt 10 ] && scan_gap=10
|
|
471
|
+
log "watching $root every ${scan_gap}s (no Spotlight)"
|
|
472
|
+
fi
|
|
473
|
+
|
|
278
474
|
src="$(pick)"
|
|
279
|
-
if [ -n "$src" ] && [ -f "$src" ]; then render ||
|
|
475
|
+
if [ -n "$src" ] && [ -f "$src" ]; then render || log "render failed: $src"; fi
|
|
280
476
|
last="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
|
|
281
477
|
# 開いているファイルの保存は毎秒見る。別ファイルへの切り替えの検出は2秒ごと
|
|
282
478
|
tick=0
|
|
283
479
|
while sleep 1; do
|
|
284
480
|
tick=$(( tick + 1 ))
|
|
285
|
-
if [ $(( tick %
|
|
481
|
+
if [ $(( tick % scan_gap )) -eq 0 ]; then
|
|
286
482
|
cand="$(pick)"
|
|
287
|
-
[ -n "$cand" ]
|
|
483
|
+
if [ -n "$cand" ]; then src="$cand"; fi
|
|
288
484
|
fi
|
|
485
|
+
[ -n "$src" ] || continue
|
|
289
486
|
[ -f "$src" ] || continue
|
|
290
487
|
now="$src $(fingerprint "$src") $(fingerprint "$HEAD")"
|
|
291
488
|
[ "$now" = "$last" ] && continue
|
|
292
489
|
last="$now"
|
|
293
|
-
render ||
|
|
490
|
+
render || log "render failed: $src"
|
|
294
491
|
done
|
|
295
492
|
exit 0
|
|
296
493
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commte/mdbrowse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
6
|
"mdbrowse": "bin/mdbrowse",
|