@bobfrankston/mdview 0.1.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.md ADDED
@@ -0,0 +1,102 @@
1
+ # mdview
2
+
3
+ Render a markdown file (or stdin) and display it in a window via [msger](../msger/README.md) (Rust/wry, default) or [msgview](../msgview/README.md) (Electron).
4
+
5
+ ## Why
6
+
7
+ [msger](../msger/README.md) and [msgview](../msgview/README.md) are HTML viewers — they don't auto-render markdown. mdview is the preprocessor: it renders the `.md` to a self-contained HTML document (via [msgcommon's `renderMarkdown`](../msgcommon/markdown.ts)), injects a `<base href>` so relative links and images resolve against the source file's directory, and hands the HTML to whichever host you pick.
8
+
9
+ The HTML is loaded **raw** (`-raw` / `rawHtml: true`) — no msger/msgview template, no buttons. Just the rendered page in a window.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g @bobfrankston/mdview
15
+ ```
16
+
17
+ This pulls msger as a dependency, so the default host works out of the box. To use the `-host msgview` backend you must install msgview separately:
18
+
19
+ ```bash
20
+ npm install -g @bobfrankston/msgview
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ mdview [options] <file.md> [-- <host-flags>...]
27
+ cat foo.md | mdview -stream [options] [-- <host-flags>...]
28
+ ```
29
+
30
+ ### Options
31
+
32
+ | Flag | Description |
33
+ |---|---|
34
+ | `-host msger\|msgview` | Backend host. Default: `msger`. Persisted to `~/.mdview.json` so you don't have to re-specify. |
35
+ | `-w`, `-watch` | Re-render and reload when the file changes on disk. (File mode only.) |
36
+ | `-stream` | Read markdown from stdin instead of a file. |
37
+ | `-save <file>` | Render to `<file>` (HTML) and exit; don't open a window. |
38
+ | `-h`, `-help` | Show help. |
39
+
40
+ Both `-foo` and `--foo` forms are accepted.
41
+
42
+ ### Host pass-through flags
43
+
44
+ Anything after `--` is forwarded to the host:
45
+
46
+ ```bash
47
+ mdview README.md -- -size 900,700 -ontop
48
+ ```
49
+
50
+ For msger: a small subset (`-size`, `-pos`, `-zoom`, `-ontop`, `-dev`) is parsed and translated to the API. Unknown flags are dropped with a warning.
51
+
52
+ For msgview: passthrough is forwarded verbatim to the `msgview` CLI.
53
+
54
+ ## Examples
55
+
56
+ ```bash
57
+ # Default (msger), one-shot
58
+ mdview README.md
59
+
60
+ # Switch to msgview and remember the choice
61
+ mdview -host msgview notes.md
62
+ mdview notes.md # subsequent runs use msgview until you switch back
63
+
64
+ # Watch mode — reloads on save
65
+ mdview -w draft.md
66
+
67
+ # Render and save HTML, no window
68
+ mdview README.md -save out.html
69
+
70
+ # Pipe from stdin
71
+ cat foo.md | mdview -stream
72
+ gh pr view --json body -q .body | mdview -stream
73
+
74
+ # Pin position/size for the window
75
+ mdview README.md -- -pos 100,100 -size 1000,800 -ontop
76
+ ```
77
+
78
+ ## How it works
79
+
80
+ 1. Read the markdown source (file or stdin)
81
+ 2. Render to a complete HTML document via `renderMarkdown(md, title, baseHref)` from msgcommon
82
+ 3. `<base href="file:///<source-dir>/">` is injected into `<head>` — relative `[link](./other.md)` and `![img](./pic.png)` resolve against the source directory
83
+ 4. Pass HTML + `rawHtml: true` to the host:
84
+ - **msger**: programmatic `showMessageBoxEx` API → stdin JSON to the Rust binary (no command-line length limit)
85
+ - **msgview**: `spawn('msgview', ['-raw', '-html', html, ...])` (subject to Windows cmd.exe ~8k arg limit; use msger for large docs)
86
+
87
+ ## Limits
88
+
89
+ - **Watch mode** respawns the window — geometry resets each reload. Pin it with `-- -pos x,y -size w,h`.
90
+ - **msgview + huge HTML** can hit the 8k cmd.exe arg limit on Windows. Switch to msger if rendering is silently truncated.
91
+ - **Cross-file `[x](other.md)` links** navigate to the raw `.md` file, not the rendered version. The host loads it as text. (Future: intercept `.md` navigation and re-render.)
92
+ - **Explicit refresh** is editor-driven via watch mode — `touch` the file to force a reload without changing it.
93
+
94
+ ## Bind to `.md` files (Windows)
95
+
96
+ In Explorer, right-click any `.md` → Open With → Choose another app → `mdview.cmd` (in `%APPDATA%\npm\`). Tick "Always use this app".
97
+
98
+ ## Related
99
+
100
+ - [msger](../msger/README.md) — Rust/wry host, default backend
101
+ - [msgview](../msgview/README.md) — Electron host, opt-in backend
102
+ - [msgcommon](../msgcommon/README.md) — shared CLI parser and the `renderMarkdown` helper mdview uses
package/mdview.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=mdview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mdview.d.ts","sourceRoot":"","sources":["mdview.ts"],"names":[],"mappings":""}
package/mdview.js ADDED
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ // mdview: render a .md file and display it via msger or msgview.
3
+ //
4
+ // The hosts load the rendered HTML verbatim via rawHtml:true (no template,
5
+ // no buttons, no data: URL) so msger's wry IPC handler doesn't panic on an
6
+ // empty URI. Relative links/images work because we emit <base href="file:///..."/>
7
+ // in the rendered head pointing at the source file's directory (or cwd in
8
+ // stream mode).
9
+ //
10
+ // Host strategy:
11
+ // - msger: use the programmatic API (`showMessageBoxEx`) — no CLI-length
12
+ // limit, rendered HTML goes via stdin JSON to the Rust binary.
13
+ // - msgview: spawn the `msgview` CLI with `-raw -html <html>`. The Windows
14
+ // cmd.exe wrapper imposes an ~8k char limit on the full command;
15
+ // very large markdown files may be truncated. Use msger for big docs.
16
+ import { spawn } from 'child_process';
17
+ import fs from 'fs';
18
+ import os from 'os';
19
+ import path from 'path';
20
+ import { pathToFileURL } from 'url';
21
+ import { renderMarkdown } from '@bobfrankston/msgcommon/markdown';
22
+ const HELP = `
23
+ mdview — render a markdown file and display via msger or msgview
24
+
25
+ Usage:
26
+ mdview [options] <file.md> [-- <host-flags>...]
27
+ cat foo.md | mdview -stream [options] [-- <host-flags>...]
28
+
29
+ Options:
30
+ -host msger|msgview Backend host (default: msger; remembered for next run)
31
+ -w, -watch Reload when the file changes on disk (file mode only)
32
+ -stream Read markdown from stdin instead of a file
33
+ -save <file> Render to <file>.html and exit; don't open a window
34
+ -h, -help Show this help
35
+
36
+ Single- and double-dash forms are accepted (-host == --host).
37
+
38
+ Host flags after \`--\` are forwarded (msger: parsed and translated; msgview:
39
+ passed through to the CLI). Example:
40
+ mdview README.md -- -size 900,700 -ontop
41
+
42
+ Defaults:
43
+ Host preference is saved to ~/.mdview.json on first explicit -host and
44
+ reused on subsequent runs.
45
+ `;
46
+ const CONFIG_PATH = path.join(os.homedir(), '.mdview.json');
47
+ function loadDefaultHost() {
48
+ try {
49
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
50
+ if (cfg.host === 'msger' || cfg.host === 'msgview')
51
+ return cfg.host;
52
+ }
53
+ catch { /* missing or malformed — fall through */ }
54
+ return 'msger';
55
+ }
56
+ function saveDefaultHost(host) {
57
+ try {
58
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify({ host }, null, 2));
59
+ }
60
+ catch (e) {
61
+ console.warn(`mdview: could not save default host: ${e.message}`);
62
+ }
63
+ }
64
+ function parseArgs(argv) {
65
+ const out = { file: null, host: null, watch: false, stream: false, save: null, passthrough: [], help: false };
66
+ const args = [...argv];
67
+ const norm = (s) => s.replace(/^--/, '-'); // accept both -foo and --foo
68
+ while (args.length > 0) {
69
+ const raw = args.shift();
70
+ if (raw === '--' || raw === '-') {
71
+ out.passthrough.push(...args);
72
+ break;
73
+ }
74
+ const a = norm(raw);
75
+ if (a === '-h' || a === '-help')
76
+ out.help = true;
77
+ else if (a === '-w' || a === '-watch')
78
+ out.watch = true;
79
+ else if (a === '-stream')
80
+ out.stream = true;
81
+ else if (a === '-host') {
82
+ const v = args.shift();
83
+ if (v !== 'msger' && v !== 'msgview')
84
+ throw new Error(`-host must be msger or msgview (got ${v})`);
85
+ out.host = v;
86
+ }
87
+ else if (a === '-save') {
88
+ const v = args.shift();
89
+ if (!v)
90
+ throw new Error('-save requires a file path');
91
+ out.save = v;
92
+ }
93
+ else if (a.startsWith('-'))
94
+ throw new Error(`Unknown option: ${raw} (put host flags after --)`);
95
+ else if (!out.file)
96
+ out.file = raw;
97
+ else
98
+ throw new Error(`Unexpected extra argument: ${raw}`);
99
+ }
100
+ return out;
101
+ }
102
+ function readStdinSync() {
103
+ // Block until stdin EOF. Used in -stream mode.
104
+ return fs.readFileSync(0, 'utf-8');
105
+ }
106
+ function renderSource(args) {
107
+ let md, baseDir, title;
108
+ if (args.stream) {
109
+ md = readStdinSync();
110
+ baseDir = process.cwd();
111
+ title = args.file ? path.basename(args.file) : 'stdin';
112
+ }
113
+ else {
114
+ const absPath = path.resolve(args.file);
115
+ if (!fs.existsSync(absPath))
116
+ throw new Error(`file not found: ${absPath}`);
117
+ md = fs.readFileSync(absPath, 'utf-8');
118
+ baseDir = path.dirname(absPath);
119
+ title = path.basename(absPath);
120
+ }
121
+ const baseHref = pathToFileURL(baseDir + path.sep).href;
122
+ return { html: renderMarkdown(md, title, baseHref), title };
123
+ }
124
+ // Translate a tiny subset of common passthrough flags into MessageBoxOptions.
125
+ // Anything unrecognised is dropped with a warning — full parity with msger CLI
126
+ // isn't a goal; for rich flag control, invoke msger directly with -raw -html.
127
+ // Returns a loosely-typed bag that gets spread into MessageBoxOptions at the
128
+ // call site; exhaustive typing here would duplicate msger's option interface.
129
+ function passthroughToOptions(passthrough) {
130
+ const opts = {};
131
+ for (let i = 0; i < passthrough.length; i++) {
132
+ const a = passthrough[i];
133
+ if (a === '-ontop' || a === '--ontop' || a === '-alwaysontop' || a === '--alwaysontop')
134
+ opts.alwaysOnTop = true;
135
+ else if ((a === '-size' || a === '--size') && passthrough[i + 1]) {
136
+ const [w, h] = passthrough[++i].split(',').map(s => parseInt(s.trim(), 10));
137
+ if (!isNaN(w) && !isNaN(h))
138
+ opts.size = { width: w, height: h };
139
+ }
140
+ else if ((a === '-pos' || a === '--pos') && passthrough[i + 1]) {
141
+ const parts = passthrough[++i].split(',').map(s => parseInt(s.trim(), 10));
142
+ const pos = { x: parts[0], y: parts[1] };
143
+ if (!isNaN(parts[2]))
144
+ pos.screen = parts[2];
145
+ opts.pos = pos;
146
+ }
147
+ else if ((a === '-zoom' || a === '--zoom') && passthrough[i + 1])
148
+ opts.zoom = parseFloat(passthrough[++i]);
149
+ else if (a === '-dev' || a === '--dev')
150
+ opts.dev = true;
151
+ else
152
+ console.warn(`mdview: dropping unsupported passthrough flag: ${a}`);
153
+ }
154
+ return opts;
155
+ }
156
+ async function launchMsger(html, title, passthrough) {
157
+ const msger = await import('@bobfrankston/msger');
158
+ const opts = { title, html, rawHtml: true, ...passthroughToOptions(passthrough) };
159
+ return msger.showMessageBoxEx(opts);
160
+ }
161
+ function launchMsgview(html, title, passthrough) {
162
+ const args = ['-raw', '-title', title, '-html', html, ...passthrough];
163
+ return spawn('msgview', args, { stdio: ['ignore', 'inherit', 'inherit'], shell: true });
164
+ }
165
+ async function main() {
166
+ let args;
167
+ try {
168
+ args = parseArgs(process.argv.slice(2));
169
+ }
170
+ catch (e) {
171
+ console.error(`mdview: ${e.message}`);
172
+ console.error(HELP);
173
+ process.exit(1);
174
+ }
175
+ if (args.help) {
176
+ console.log(HELP);
177
+ process.exit(0);
178
+ }
179
+ if (!args.file && !args.stream) {
180
+ console.log(HELP);
181
+ process.exit(1);
182
+ }
183
+ if (args.watch && args.stream) {
184
+ console.error('mdview: -watch and -stream are mutually exclusive');
185
+ process.exit(1);
186
+ }
187
+ if (args.watch && !args.file) {
188
+ console.error('mdview: -watch needs a file');
189
+ process.exit(1);
190
+ }
191
+ // Persist host preference when the user supplies -host explicitly.
192
+ if (args.host)
193
+ saveDefaultHost(args.host);
194
+ const host = args.host ?? loadDefaultHost();
195
+ let rendered;
196
+ try {
197
+ rendered = renderSource(args);
198
+ }
199
+ catch (e) {
200
+ console.error(`mdview: ${e.message}`);
201
+ process.exit(1);
202
+ }
203
+ // -save: write HTML and exit without opening a window.
204
+ if (args.save) {
205
+ try {
206
+ fs.writeFileSync(args.save, rendered.html, 'utf-8');
207
+ process.exit(0);
208
+ }
209
+ catch (e) {
210
+ console.error(`mdview: failed to save ${args.save}: ${e.message}`);
211
+ process.exit(1);
212
+ }
213
+ }
214
+ // One-shot: render, show, exit when host exits.
215
+ if (!args.watch) {
216
+ if (host === 'msger') {
217
+ const handle = await launchMsger(rendered.html, rendered.title, args.passthrough);
218
+ await handle.result;
219
+ process.exit(0);
220
+ }
221
+ else {
222
+ const child = launchMsgview(rendered.html, rendered.title, args.passthrough);
223
+ child.on('exit', (code) => process.exit(code ?? 0));
224
+ }
225
+ return;
226
+ }
227
+ // Watch mode: stay alive, kill + respawn on file change.
228
+ const absPath = path.resolve(args.file);
229
+ let msgerHandle = null;
230
+ let msgviewChild = null;
231
+ let reloadTimer = null;
232
+ const relaunch = async () => {
233
+ try {
234
+ const r = renderSource(args);
235
+ if (host === 'msger') {
236
+ if (msgerHandle && !msgerHandle.closed)
237
+ msgerHandle.close();
238
+ msgerHandle = await launchMsger(r.html, r.title, args.passthrough);
239
+ }
240
+ else {
241
+ if (msgviewChild && !msgviewChild.killed)
242
+ msgviewChild.kill();
243
+ msgviewChild = launchMsgview(r.html, r.title, args.passthrough);
244
+ msgviewChild.on('exit', () => { msgviewChild = null; });
245
+ }
246
+ }
247
+ catch (e) {
248
+ console.error(`mdview: render failed: ${e.message}`);
249
+ }
250
+ };
251
+ // Initial launch uses the already-rendered HTML to avoid a redundant read.
252
+ if (host === 'msger')
253
+ msgerHandle = await launchMsger(rendered.html, rendered.title, args.passthrough);
254
+ else
255
+ msgviewChild = launchMsgview(rendered.html, rendered.title, args.passthrough);
256
+ fs.watch(absPath, { persistent: true }, () => {
257
+ if (reloadTimer)
258
+ clearTimeout(reloadTimer);
259
+ reloadTimer = setTimeout(relaunch, 150);
260
+ });
261
+ process.on('SIGINT', () => {
262
+ if (msgerHandle && !msgerHandle.closed)
263
+ msgerHandle.close();
264
+ if (msgviewChild)
265
+ msgviewChild.kill();
266
+ process.exit(0);
267
+ });
268
+ }
269
+ main();
270
+ //# sourceMappingURL=mdview.js.map
package/mdview.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mdview.js","sourceRoot":"","sources":["mdview.ts"],"names":[],"mappings":";AACA,iEAAiE;AACjE,EAAE;AACF,2EAA2E;AAC3E,2EAA2E;AAC3E,mFAAmF;AACnF,0EAA0E;AAC1E,gBAAgB;AAChB,EAAE;AACF,iBAAiB;AACjB,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,8EAA8E;AAC9E,mFAAmF;AAEnF,OAAO,EAAE,KAAK,EAAgB,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAalE,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBZ,CAAC;AAEF,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;AAE5D,SAAS,eAAe;IACpB,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9D,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC,CAAC,yCAAyC,CAAC,CAAC;IACrD,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,SAAS,eAAe,CAAC,IAAyB;IAC9C,IAAI,CAAC;QAAC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IACzE,OAAO,CAAM,EAAE,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IAAC,CAAC;AACzF,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAC7B,MAAM,GAAG,GAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACpH,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAK,6BAA6B;IACpF,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QACzB,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;YAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;YAAC,MAAM;QAAC,CAAC;QAC1E,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,OAAO;YAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;aAC5C,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,QAAQ;YAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC;aACnD,IAAI,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;aACvC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;YACrB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACvB,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,SAAS;gBAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,GAAG,CAAC,CAAC;YACnG,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;QACjB,CAAC;aACI,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;YACrB,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACvB,IAAI,CAAC,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;YACtD,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;QACjB,CAAC;aACI,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,4BAA4B,CAAC,CAAC;aAC3F,IAAI,CAAC,GAAG,CAAC,IAAI;YAAE,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC;;YAC9B,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED,SAAS,aAAa;IAClB,+CAA+C;IAC/C,OAAO,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,YAAY,CAAC,IAAU;IAC5B,IAAI,EAAU,EAAE,OAAe,EAAE,KAAa,CAAC;IAC/C,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,EAAE,GAAG,aAAa,EAAE,CAAC;QACrB,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;QACxB,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC3D,CAAC;SAAM,CAAC;QACJ,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC;QAC3E,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACvC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IACD,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;IACxD,OAAO,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;AAChE,CAAC;AAED,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,6EAA6E;AAC7E,8EAA8E;AAC9E,SAAS,oBAAoB,CAAC,WAAqB;IAC/C,MAAM,IAAI,GAA4B,EAAE,CAAC;IACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,cAAc,IAAI,CAAC,KAAK,eAAe;YAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;aAC3G,IAAI,CAAC,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,QAAQ,CAAC,IAAI,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC/D,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;YAC5E,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;gBAAE,IAAI,CAAC,IAAI,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QACpE,CAAC;aACI,IAAI,CAAC,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC7D,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;YAC3E,MAAM,GAAG,GAA2B,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAAE,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAC5C,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACnB,CAAC;aACI,IAAI,CAAC,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,QAAQ,CAAC,IAAI,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC;YAAE,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aACtG,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO;YAAE,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC;;YACnD,OAAO,CAAC,IAAI,CAAC,kDAAkD,CAAC,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,KAAa,EAAE,WAAqB;IACzE,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,oBAAoB,CAAC,WAAW,CAAC,EAAE,CAAC;IAClF,OAAO,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC;AAED,SAAS,aAAa,CAAC,IAAY,EAAE,KAAa,EAAE,WAAqB;IACrE,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC,CAAC;IACtE,OAAO,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5F,CAAC;AAED,KAAK,UAAU,IAAI;IACf,IAAI,IAAU,CAAC;IACf,IAAI,CAAC;QAAC,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IAChD,OAAO,CAAM,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IAE/F,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IACtD,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IACvE,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IACvH,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IAEhG,mEAAmE;IACnE,IAAI,IAAI,CAAC,IAAI;QAAE,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,eAAe,EAAE,CAAC;IAE5C,IAAI,QAAyC,CAAC;IAC9C,IAAI,CAAC;QAAC,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IACtC,OAAO,CAAM,EAAE,CAAC;QAAC,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IAE1E,uDAAuD;IACvD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC;YACD,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAAC,OAAO,CAAC,KAAK,CAAC,0BAA0B,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAAC,CAAC;IAC7G,CAAC;IAED,gDAAgD;IAChD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACd,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YAClF,MAAM,MAAM,CAAC,MAAM,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;aAAM,CAAC;YACJ,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YAC7E,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC;QACD,OAAO;IACX,CAAC;IAED,yDAAyD;IACzD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,IAAI,WAAW,GAAqB,IAAI,CAAC;IACzC,IAAI,YAAY,GAAiB,IAAI,CAAC;IACtC,IAAI,WAAW,GAAmB,IAAI,CAAC;IAEvC,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QACxB,IAAI,CAAC;YACD,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;YAC7B,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;gBACnB,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,MAAM;oBAAE,WAAW,CAAC,KAAK,EAAE,CAAC;gBAC5D,WAAW,GAAG,MAAM,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YACvE,CAAC;iBAAM,CAAC;gBACJ,IAAI,YAAY,IAAI,CAAC,YAAY,CAAC,MAAM;oBAAE,YAAY,CAAC,IAAI,EAAE,CAAC;gBAC9D,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;gBAChE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5D,CAAC;QACL,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAAC,OAAO,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAAC,CAAC;IAC9E,CAAC,CAAC;IAEF,2EAA2E;IAC3E,IAAI,IAAI,KAAK,OAAO;QAAE,WAAW,GAAG,MAAM,WAAW,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;;QAClG,YAAY,GAAG,aAAa,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAEnF,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE;QACzC,IAAI,WAAW;YAAE,YAAY,CAAC,WAAW,CAAC,CAAC;QAC3C,WAAW,GAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACtB,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,MAAM;YAAE,WAAW,CAAC,KAAK,EAAE,CAAC;QAC5D,IAAI,YAAY;YAAE,YAAY,CAAC,IAAI,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;AACP,CAAC;AAED,IAAI,EAAE,CAAC"}
package/mdview.ts ADDED
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ // mdview: render a .md file and display it via msger or msgview.
3
+ //
4
+ // The hosts load the rendered HTML verbatim via rawHtml:true (no template,
5
+ // no buttons, no data: URL) so msger's wry IPC handler doesn't panic on an
6
+ // empty URI. Relative links/images work because we emit <base href="file:///..."/>
7
+ // in the rendered head pointing at the source file's directory (or cwd in
8
+ // stream mode).
9
+ //
10
+ // Host strategy:
11
+ // - msger: use the programmatic API (`showMessageBoxEx`) — no CLI-length
12
+ // limit, rendered HTML goes via stdin JSON to the Rust binary.
13
+ // - msgview: spawn the `msgview` CLI with `-raw -html <html>`. The Windows
14
+ // cmd.exe wrapper imposes an ~8k char limit on the full command;
15
+ // very large markdown files may be truncated. Use msger for big docs.
16
+
17
+ import { spawn, ChildProcess } from 'child_process';
18
+ import fs from 'fs';
19
+ import os from 'os';
20
+ import path from 'path';
21
+ import { pathToFileURL } from 'url';
22
+ import { renderMarkdown } from '@bobfrankston/msgcommon/markdown';
23
+ import { MessageBoxHandle } from '@bobfrankston/msger';
24
+
25
+ interface Args {
26
+ file: string; // null when -stream is used
27
+ host: 'msger' | 'msgview'; // null = use persisted default from config
28
+ watch: boolean;
29
+ stream: boolean;
30
+ save: string; // null when not saving
31
+ passthrough: string[];
32
+ help: boolean;
33
+ }
34
+
35
+ const HELP = `
36
+ mdview — render a markdown file and display via msger or msgview
37
+
38
+ Usage:
39
+ mdview [options] <file.md> [-- <host-flags>...]
40
+ cat foo.md | mdview -stream [options] [-- <host-flags>...]
41
+
42
+ Options:
43
+ -host msger|msgview Backend host (default: msger; remembered for next run)
44
+ -w, -watch Reload when the file changes on disk (file mode only)
45
+ -stream Read markdown from stdin instead of a file
46
+ -save <file> Render to <file>.html and exit; don't open a window
47
+ -h, -help Show this help
48
+
49
+ Single- and double-dash forms are accepted (-host == --host).
50
+
51
+ Host flags after \`--\` are forwarded (msger: parsed and translated; msgview:
52
+ passed through to the CLI). Example:
53
+ mdview README.md -- -size 900,700 -ontop
54
+
55
+ Defaults:
56
+ Host preference is saved to ~/.mdview.json on first explicit -host and
57
+ reused on subsequent runs.
58
+ `;
59
+
60
+ const CONFIG_PATH = path.join(os.homedir(), '.mdview.json');
61
+
62
+ function loadDefaultHost(): 'msger' | 'msgview' {
63
+ try {
64
+ const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
65
+ if (cfg.host === 'msger' || cfg.host === 'msgview') return cfg.host;
66
+ } catch { /* missing or malformed — fall through */ }
67
+ return 'msger';
68
+ }
69
+
70
+ function saveDefaultHost(host: 'msger' | 'msgview'): void {
71
+ try { fs.writeFileSync(CONFIG_PATH, JSON.stringify({ host }, null, 2)); }
72
+ catch (e: any) { console.warn(`mdview: could not save default host: ${e.message}`); }
73
+ }
74
+
75
+ function parseArgs(argv: string[]): Args {
76
+ const out: Args = { file: null, host: null, watch: false, stream: false, save: null, passthrough: [], help: false };
77
+ const args = [...argv];
78
+ const norm = (s: string) => s.replace(/^--/, '-'); // accept both -foo and --foo
79
+ while (args.length > 0) {
80
+ const raw = args.shift();
81
+ if (raw === '--' || raw === '-') { out.passthrough.push(...args); break; }
82
+ const a = norm(raw);
83
+ if (a === '-h' || a === '-help') out.help = true;
84
+ else if (a === '-w' || a === '-watch') out.watch = true;
85
+ else if (a === '-stream') out.stream = true;
86
+ else if (a === '-host') {
87
+ const v = args.shift();
88
+ if (v !== 'msger' && v !== 'msgview') throw new Error(`-host must be msger or msgview (got ${v})`);
89
+ out.host = v;
90
+ }
91
+ else if (a === '-save') {
92
+ const v = args.shift();
93
+ if (!v) throw new Error('-save requires a file path');
94
+ out.save = v;
95
+ }
96
+ else if (a.startsWith('-')) throw new Error(`Unknown option: ${raw} (put host flags after --)`);
97
+ else if (!out.file) out.file = raw;
98
+ else throw new Error(`Unexpected extra argument: ${raw}`);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ function readStdinSync(): string {
104
+ // Block until stdin EOF. Used in -stream mode.
105
+ return fs.readFileSync(0, 'utf-8');
106
+ }
107
+
108
+ function renderSource(args: Args) {
109
+ let md: string, baseDir: string, title: string;
110
+ if (args.stream) {
111
+ md = readStdinSync();
112
+ baseDir = process.cwd();
113
+ title = args.file ? path.basename(args.file) : 'stdin';
114
+ } else {
115
+ const absPath = path.resolve(args.file);
116
+ if (!fs.existsSync(absPath)) throw new Error(`file not found: ${absPath}`);
117
+ md = fs.readFileSync(absPath, 'utf-8');
118
+ baseDir = path.dirname(absPath);
119
+ title = path.basename(absPath);
120
+ }
121
+ const baseHref = pathToFileURL(baseDir + path.sep).href;
122
+ return { html: renderMarkdown(md, title, baseHref), title };
123
+ }
124
+
125
+ // Translate a tiny subset of common passthrough flags into MessageBoxOptions.
126
+ // Anything unrecognised is dropped with a warning — full parity with msger CLI
127
+ // isn't a goal; for rich flag control, invoke msger directly with -raw -html.
128
+ // Returns a loosely-typed bag that gets spread into MessageBoxOptions at the
129
+ // call site; exhaustive typing here would duplicate msger's option interface.
130
+ function passthroughToOptions(passthrough: string[]) {
131
+ const opts: Record<string, unknown> = {};
132
+ for (let i = 0; i < passthrough.length; i++) {
133
+ const a = passthrough[i];
134
+ if (a === '-ontop' || a === '--ontop' || a === '-alwaysontop' || a === '--alwaysontop') opts.alwaysOnTop = true;
135
+ else if ((a === '-size' || a === '--size') && passthrough[i + 1]) {
136
+ const [w, h] = passthrough[++i].split(',').map(s => parseInt(s.trim(), 10));
137
+ if (!isNaN(w) && !isNaN(h)) opts.size = { width: w, height: h };
138
+ }
139
+ else if ((a === '-pos' || a === '--pos') && passthrough[i + 1]) {
140
+ const parts = passthrough[++i].split(',').map(s => parseInt(s.trim(), 10));
141
+ const pos: Record<string, number> = { x: parts[0], y: parts[1] };
142
+ if (!isNaN(parts[2])) pos.screen = parts[2];
143
+ opts.pos = pos;
144
+ }
145
+ else if ((a === '-zoom' || a === '--zoom') && passthrough[i + 1]) opts.zoom = parseFloat(passthrough[++i]);
146
+ else if (a === '-dev' || a === '--dev') opts.dev = true;
147
+ else console.warn(`mdview: dropping unsupported passthrough flag: ${a}`);
148
+ }
149
+ return opts;
150
+ }
151
+
152
+ async function launchMsger(html: string, title: string, passthrough: string[]): Promise<MessageBoxHandle> {
153
+ const msger = await import('@bobfrankston/msger');
154
+ const opts = { title, html, rawHtml: true, ...passthroughToOptions(passthrough) };
155
+ return msger.showMessageBoxEx(opts);
156
+ }
157
+
158
+ function launchMsgview(html: string, title: string, passthrough: string[]): ChildProcess {
159
+ const args = ['-raw', '-title', title, '-html', html, ...passthrough];
160
+ return spawn('msgview', args, { stdio: ['ignore', 'inherit', 'inherit'], shell: true });
161
+ }
162
+
163
+ async function main(): Promise<void> {
164
+ let args: Args;
165
+ try { args = parseArgs(process.argv.slice(2)); }
166
+ catch (e: any) { console.error(`mdview: ${e.message}`); console.error(HELP); process.exit(1); }
167
+
168
+ if (args.help) { console.log(HELP); process.exit(0); }
169
+ if (!args.file && !args.stream) { console.log(HELP); process.exit(1); }
170
+ if (args.watch && args.stream) { console.error('mdview: -watch and -stream are mutually exclusive'); process.exit(1); }
171
+ if (args.watch && !args.file) { console.error('mdview: -watch needs a file'); process.exit(1); }
172
+
173
+ // Persist host preference when the user supplies -host explicitly.
174
+ if (args.host) saveDefaultHost(args.host);
175
+ const host = args.host ?? loadDefaultHost();
176
+
177
+ let rendered: { html: string; title: string };
178
+ try { rendered = renderSource(args); }
179
+ catch (e: any) { console.error(`mdview: ${e.message}`); process.exit(1); }
180
+
181
+ // -save: write HTML and exit without opening a window.
182
+ if (args.save) {
183
+ try {
184
+ fs.writeFileSync(args.save, rendered.html, 'utf-8');
185
+ process.exit(0);
186
+ } catch (e: any) { console.error(`mdview: failed to save ${args.save}: ${e.message}`); process.exit(1); }
187
+ }
188
+
189
+ // One-shot: render, show, exit when host exits.
190
+ if (!args.watch) {
191
+ if (host === 'msger') {
192
+ const handle = await launchMsger(rendered.html, rendered.title, args.passthrough);
193
+ await handle.result;
194
+ process.exit(0);
195
+ } else {
196
+ const child = launchMsgview(rendered.html, rendered.title, args.passthrough);
197
+ child.on('exit', (code) => process.exit(code ?? 0));
198
+ }
199
+ return;
200
+ }
201
+
202
+ // Watch mode: stay alive, kill + respawn on file change.
203
+ const absPath = path.resolve(args.file);
204
+ let msgerHandle: MessageBoxHandle = null;
205
+ let msgviewChild: ChildProcess = null;
206
+ let reloadTimer: NodeJS.Timeout = null;
207
+
208
+ const relaunch = async () => {
209
+ try {
210
+ const r = renderSource(args);
211
+ if (host === 'msger') {
212
+ if (msgerHandle && !msgerHandle.closed) msgerHandle.close();
213
+ msgerHandle = await launchMsger(r.html, r.title, args.passthrough);
214
+ } else {
215
+ if (msgviewChild && !msgviewChild.killed) msgviewChild.kill();
216
+ msgviewChild = launchMsgview(r.html, r.title, args.passthrough);
217
+ msgviewChild.on('exit', () => { msgviewChild = null; });
218
+ }
219
+ } catch (e: any) { console.error(`mdview: render failed: ${e.message}`); }
220
+ };
221
+
222
+ // Initial launch uses the already-rendered HTML to avoid a redundant read.
223
+ if (host === 'msger') msgerHandle = await launchMsger(rendered.html, rendered.title, args.passthrough);
224
+ else msgviewChild = launchMsgview(rendered.html, rendered.title, args.passthrough);
225
+
226
+ fs.watch(absPath, { persistent: true }, () => {
227
+ if (reloadTimer) clearTimeout(reloadTimer);
228
+ reloadTimer = setTimeout(relaunch, 150);
229
+ });
230
+
231
+ process.on('SIGINT', () => {
232
+ if (msgerHandle && !msgerHandle.closed) msgerHandle.close();
233
+ if (msgviewChild) msgviewChild.kill();
234
+ process.exit(0);
235
+ });
236
+ }
237
+
238
+ main();
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@bobfrankston/mdview",
3
+ "version": "0.1.1",
4
+ "description": "Render a markdown file and display it via msger or msgview",
5
+ "type": "module",
6
+ "main": "mdview.js",
7
+ "types": "mdview.d.ts",
8
+ "bin": {
9
+ "mdview": "mdview.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "watch": "tsc -w",
14
+ "prerelease:local": "git add -A && (git diff-index --quiet HEAD || git commit -m \"Pre-release commit\")",
15
+ "preversion": "npm run build && git add -A",
16
+ "release": "npm run prerelease:local && npm version patch && npm publish --quiet",
17
+ "postversion": "git push && git push --tags"
18
+ },
19
+ "keywords": [
20
+ "markdown",
21
+ "viewer",
22
+ "msger",
23
+ "msgview"
24
+ ],
25
+ "author": "Bob Frankston",
26
+ "license": "MIT",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^25.2.1",
32
+ "typescript": "^5.7.2"
33
+ },
34
+ "dependencies": {
35
+ "@bobfrankston/msgcommon": "^0.1.30",
36
+ "@bobfrankston/msger": "^0.1.346"
37
+ },
38
+ "files": [
39
+ "*.js",
40
+ "*.d.ts",
41
+ "*.d.ts.map",
42
+ "*.js.map",
43
+ "*.ts",
44
+ "tsconfig.json",
45
+ "README.md"
46
+ ],
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git@github.com:BobFrankston/mdview.git"
50
+ },
51
+ ".dependencies": {
52
+ "@bobfrankston/msgcommon": "file:../msgcommon",
53
+ "@bobfrankston/msger": "file:../msger"
54
+ },
55
+ ".transformedSnapshot": {
56
+ "dependencies": {
57
+ "@bobfrankston/msgcommon": "^0.1.30",
58
+ "@bobfrankston/msger": "^0.1.346"
59
+ }
60
+ }
61
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./",
8
+ "rootDir": "./",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "strictNullChecks": false,
14
+ "esModuleInterop": true,
15
+ "skipLibCheck": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "resolveJsonModule": true,
18
+ "allowSyntheticDefaultImports": true
19
+ },
20
+ "include": [
21
+ "*.ts"
22
+ ],
23
+ "exclude": [
24
+ "node_modules",
25
+ "*.d.ts"
26
+ ]
27
+ }