@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 +102 -0
- package/mdview.d.ts +3 -0
- package/mdview.d.ts.map +1 -0
- package/mdview.js +270 -0
- package/mdview.js.map +1 -0
- package/mdview.ts +238 -0
- package/package.json +61 -0
- package/tsconfig.json +27 -0
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 `` 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
package/mdview.d.ts.map
ADDED
|
@@ -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":"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"}
|
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
|
+
}
|