@helping-ai-workflow/md2doc 1.1.1 → 2.0.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.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support.
4
4
 
5
- Two global CLIs (`md2html`, `md2pdf`) you can call from any directory.
5
+ A single global CLI (`md2doc`) you can call from any directory.
6
6
 
7
7
  ## Install
8
8
 
@@ -43,25 +43,44 @@ npm install -g @helping-ai-workflow/md2doc
43
43
  ## Usage
44
44
 
45
45
  ```bash
46
- md2html foo.md # foo.html (next to source)
47
- md2html foo.md bar.md # batch render
48
- md2html foo.md --out custom.html # explicit output (single-file mode)
49
- md2html foo.md --open # render then launch viewer
50
- md2html foo.md --quiet # suppress progress output
46
+ md2doc foo.md # render HTML to OS temp dir, open viewer
47
+ md2doc --pdf foo.md # render PDF instead
48
+ md2doc --html --pdf foo.md # render both formats
49
+ md2doc *.md # batch: each file → temp + open
50
+
51
+ md2doc foo.md --out bar.html # write to a specific file (no auto-open)
52
+ md2doc foo.md --out ./build/ # write to ./build/foo.html (no auto-open)
53
+ md2doc *.md --out ./build/ # batch into ./build/
54
+ md2doc foo.md --out ./build/ --open # explicit open with --out
51
55
  ```
52
56
 
53
- `md2pdf` accepts the same four flags with PDF-output semantics.
57
+ By default, `md2doc` writes to your OS temp directory and launches the platform viewer.
58
+ Pass `--out <path>` to write somewhere specific; doing so disables auto-open unless you
59
+ also pass `--open`.
54
60
 
55
61
  ### Flags
56
62
 
57
63
  | Flag | Meaning |
58
64
  |---|---|
59
- | `--out <path>` | Explicit output path. Only valid with exactly one input. |
60
- | `--open` | Launch the platform viewer (`xdg-open` / `open` / `start`) after render. |
65
+ | `--html` | Render HTML (default if neither `--html` nor `--pdf` is given). |
66
+ | `--pdf` | Render PDF. Combine with `--html` to render both. |
67
+ | `--out <path>` | Output path. Ends with `/` or an existing directory → directory mode. Ends with `.html` / `.pdf` → file mode (single input only). Implies `--no-open` unless `--open` is also passed. |
68
+ | `--open` | Launch the platform viewer (`xdg-open` / `open` / `start`) after render. Default when `--out` is absent. |
69
+ | `--no-open` | Skip the viewer launch. |
61
70
  | `--quiet` | Suppress per-file progress messages. |
62
71
  | `--version`, `-v` | Print version. |
63
72
  | `--help`, `-h` | Print help. |
64
73
 
74
+ ### Migration from md2html / md2pdf (v1.x → v2.0.0)
75
+
76
+ | Old | New |
77
+ |---|---|
78
+ | `md2html foo.md` | `md2doc foo.md` |
79
+ | `md2pdf foo.md` | `md2doc --pdf foo.md` |
80
+ | `md2html foo.md --out f.html` | `md2doc foo.md --out f.html` |
81
+ | `md2html foo.md --open` | `md2doc foo.md` (open is default) |
82
+ | `md2html *.md` (output next to source) | `md2doc *.md --out ./build/` (or accept temp output) |
83
+
65
84
  ## Supported diagram types
66
85
 
67
86
  Embedded in fenced code blocks inside your Markdown:
package/bin/md2doc.js ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const crypto = require('crypto');
8
+ const { spawnSync } = require('child_process');
9
+
10
+ const LIB = path.resolve(__dirname, '..', 'lib', 'md2doc.js');
11
+ const VERSION = require('../package.json').version;
12
+
13
+ function printHelp() {
14
+ process.stdout.write([
15
+ 'md2doc — render Markdown to HTML / PDF (WaveDrom / Mermaid / Graphviz supported)',
16
+ '',
17
+ 'Usage:',
18
+ ' md2doc <input.md>... Render to HTML, write to OS temp dir, open viewer',
19
+ ' md2doc --pdf <input.md>... Render to PDF instead',
20
+ ' md2doc --html --pdf <input.md> Render both formats',
21
+ ' md2doc <input.md> --out <file.html> Write to a specific file (no auto-open)',
22
+ ' md2doc <input.md>... --out <dir>/ Write each to <dir>/<stem>.html (no auto-open)',
23
+ '',
24
+ 'Flags:',
25
+ ' --html Render HTML (default if neither --html nor --pdf is given).',
26
+ ' --pdf Render PDF.',
27
+ ' --out <path> Output path. Ends with \'/\' or existing dir → directory mode.',
28
+ ' Ends with .html/.pdf → file mode (single input only).',
29
+ ' Implies --no-open unless --open is also passed.',
30
+ ' --open Launch the platform viewer after render (default when --out is absent).',
31
+ ' --no-open Skip the viewer launch.',
32
+ ' --quiet Suppress per-file progress messages.',
33
+ ' --version, -v Print version.',
34
+ ' --help, -h Print this help.',
35
+ ''
36
+ ].join('\n'));
37
+ }
38
+
39
+ function parseArgs(argv) {
40
+ const inputs = [];
41
+ let html = false;
42
+ let pdf = false;
43
+ let out = null;
44
+ let openExplicit = null; // null = unset; true/false = user-specified
45
+ let quiet = false;
46
+
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const a = argv[i];
49
+ if (a === '--version' || a === '-v') {
50
+ process.stdout.write(VERSION + '\n');
51
+ process.exit(0);
52
+ }
53
+ if (a === '--help' || a === '-h') {
54
+ printHelp();
55
+ process.exit(0);
56
+ }
57
+ if (a === '--html') { html = true; continue; }
58
+ if (a === '--pdf') { pdf = true; continue; }
59
+ if (a === '--open') { openExplicit = true; continue; }
60
+ if (a === '--no-open') { openExplicit = false; continue; }
61
+ if (a === '--quiet') { quiet = true; continue; }
62
+ if (a === '--out') {
63
+ if (out !== null) {
64
+ process.stderr.write('error: --out specified more than once\n');
65
+ process.exit(2);
66
+ }
67
+ i++;
68
+ if (i >= argv.length) {
69
+ process.stderr.write('error: --out requires a value\n');
70
+ process.exit(2);
71
+ }
72
+ out = argv[i];
73
+ continue;
74
+ }
75
+ if (a.startsWith('-')) {
76
+ process.stderr.write('error: unknown flag ' + a + '\n');
77
+ process.exit(2);
78
+ }
79
+ inputs.push(a);
80
+ }
81
+
82
+ if (inputs.length === 0) {
83
+ process.stderr.write('error: no input file\n');
84
+ printHelp();
85
+ process.exit(2);
86
+ }
87
+
88
+ // Format defaults: neither flag → HTML only.
89
+ const formats = [];
90
+ if (html || (!html && !pdf)) formats.push('html');
91
+ if (pdf) formats.push('pdf');
92
+
93
+ // Open default: ON when --out absent, OFF when --out present; user can override either way.
94
+ let open;
95
+ if (openExplicit !== null) {
96
+ open = openExplicit;
97
+ } else {
98
+ open = (out === null);
99
+ }
100
+
101
+ return { inputs, formats, out, open, quiet };
102
+ }
103
+
104
+ function shortHash(absPath) {
105
+ return crypto.createHash('sha1').update(absPath).digest('hex').slice(0, 6);
106
+ }
107
+
108
+ function defaultOutputPath(input, format) {
109
+ const abs = path.resolve(input);
110
+ const stem = path.basename(input).replace(/\.md$/i, '');
111
+ const dir = path.join(os.tmpdir(), 'md2doc');
112
+ fs.mkdirSync(dir, { recursive: true });
113
+ return path.join(dir, stem + '-' + shortHash(abs) + '.' + format);
114
+ }
115
+
116
+ function classifyOut(outValue) {
117
+ // Returns { kind: 'dir' | 'file', ext: 'html'|'pdf'|null }
118
+ if (outValue.endsWith('/') || outValue.endsWith(path.sep)) {
119
+ return { kind: 'dir', ext: null };
120
+ }
121
+ try {
122
+ if (fs.existsSync(outValue) && fs.statSync(outValue).isDirectory()) {
123
+ return { kind: 'dir', ext: null };
124
+ }
125
+ } catch (_) { /* fall through */ }
126
+ if (/\.html$/i.test(outValue)) return { kind: 'file', ext: 'html' };
127
+ if (/\.pdf$/i.test(outValue)) return { kind: 'file', ext: 'pdf' };
128
+ return { kind: 'ambiguous', ext: null };
129
+ }
130
+
131
+ function resolveOutputs(args) {
132
+ const pairs = [];
133
+ if (args.out === null) {
134
+ for (const input of args.inputs) {
135
+ for (const format of args.formats) {
136
+ pairs.push({ input, format, output: defaultOutputPath(input, format) });
137
+ }
138
+ }
139
+ return pairs;
140
+ }
141
+
142
+ const cls = classifyOut(args.out);
143
+
144
+ if (cls.kind === 'ambiguous') {
145
+ process.stderr.write(
146
+ 'error: --out \'' + args.out + '\' must end with \'/\' to mean a directory ' +
147
+ 'or \'.html\'/\'.pdf\' to mean a file\n'
148
+ );
149
+ process.exit(2);
150
+ }
151
+
152
+ if (cls.kind === 'file') {
153
+ if (args.inputs.length > 1) {
154
+ process.stderr.write('error: --out file path is only valid with one input\n');
155
+ process.exit(2);
156
+ }
157
+ if (args.formats.length > 1) {
158
+ process.stderr.write('error: --out file path is not valid when producing both formats\n');
159
+ process.exit(2);
160
+ }
161
+ if (cls.ext !== args.formats[0]) {
162
+ process.stderr.write(
163
+ 'error: --out \'' + args.out + '\' extension does not match selected format\n'
164
+ );
165
+ process.exit(2);
166
+ }
167
+ pairs.push({ input: args.inputs[0], format: args.formats[0], output: args.out });
168
+ return pairs;
169
+ }
170
+
171
+ // cls.kind === 'dir'
172
+ fs.mkdirSync(args.out, { recursive: true });
173
+ for (const input of args.inputs) {
174
+ const stem = path.basename(input).replace(/\.md$/i, '');
175
+ for (const format of args.formats) {
176
+ pairs.push({
177
+ input,
178
+ format,
179
+ output: path.join(args.out, stem + '.' + format),
180
+ });
181
+ }
182
+ }
183
+ return pairs;
184
+ }
185
+
186
+ function isWSL() {
187
+ if (process.platform !== 'linux') return false;
188
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
189
+ try {
190
+ return fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop');
191
+ } catch (_) {
192
+ return false;
193
+ }
194
+ }
195
+
196
+ function openViewer(filePath) {
197
+ const platform = process.platform;
198
+ let cmd, args;
199
+ if (platform === 'darwin') {
200
+ cmd = 'open'; args = [filePath];
201
+ } else if (platform === 'win32') {
202
+ cmd = 'cmd'; args = ['/c', 'start', '""', filePath];
203
+ } else if (isWSL()) {
204
+ const r = spawnSync('wslpath', ['-w', filePath], { encoding: 'utf8' });
205
+ if (r.status === 0 && r.stdout) {
206
+ cmd = 'explorer.exe'; args = [r.stdout.trim()];
207
+ } else {
208
+ cmd = 'xdg-open'; args = [filePath];
209
+ }
210
+ } else {
211
+ cmd = 'xdg-open'; args = [filePath];
212
+ }
213
+ const r = spawnSync(cmd, args, { stdio: 'ignore' });
214
+ if (r.error) {
215
+ process.stderr.write('warning: could not launch viewer for ' + filePath + ': ' + r.error.message + '\n');
216
+ }
217
+ }
218
+
219
+ function main() {
220
+ const args = parseArgs(process.argv.slice(2));
221
+
222
+ for (const input of args.inputs) {
223
+ if (!fs.existsSync(input)) {
224
+ process.stderr.write('error: input not found: ' + input + '\n');
225
+ process.exit(1);
226
+ }
227
+ }
228
+
229
+ const pairs = resolveOutputs(args);
230
+ const outputs = [];
231
+ for (const { input, output } of pairs) {
232
+ const stdio = args.quiet ? ['inherit', 'ignore', 'inherit'] : 'inherit';
233
+ const r = spawnSync(process.execPath, [LIB, input, output], { stdio });
234
+ if (r.status !== 0) {
235
+ process.stderr.write('error: render failed for ' + input + ' (exit ' + r.status + ')\n');
236
+ process.exit(r.status || 1);
237
+ }
238
+ if (!args.quiet) {
239
+ process.stdout.write(output + '\n');
240
+ }
241
+ outputs.push(output);
242
+ }
243
+
244
+ if (args.open) {
245
+ for (const o of outputs) openViewer(o);
246
+ }
247
+ }
248
+
249
+ main();
package/lib/md2doc.js CHANGED
@@ -412,9 +412,19 @@ ${itemsHtml}
412
412
  marked.setOptions({ gfm: true, breaks: false, renderer });
413
413
 
414
414
  // Pre-process non-standard inline syntax before marked parses
415
+ const escAttr = (s) => String(s)
416
+ .replace(/&/g, '&amp;')
417
+ .replace(/</g, '&lt;')
418
+ .replace(/>/g, '&gt;')
419
+ .replace(/"/g, '&quot;');
415
420
  const mdPre = md
416
421
  .replace(/\^([^^]+)\^/g, '<sup>$1</sup>') // ^a^ → <sup>a</sup>
417
- .replace(/~([^~]+)~/g, '<sub>$1</sub>'); // ~a~ → <sub>a</sub>
422
+ .replace(/~([^~]+)~/g, '<sub>$1</sub>') // ~a~ → <sub>a</sub>
423
+ .replace(/\[\[([^\]\n]+)\]\]/g, (_, inner) => { // [[ref-id, §sub]] → clickable citation
424
+ const body = inner.trim();
425
+ const slug = body.split(',', 1)[0].trim();
426
+ return `<a href="#${escAttr(slug)}">[${escAttr(body)}]</a>`;
427
+ });
418
428
 
419
429
  bodyHtml = marked.parse(mdPre);
420
430
  serializedSections = JSON.stringify(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
5
5
  "keywords": [
6
6
  "markdown",
@@ -13,8 +13,7 @@
13
13
  ],
14
14
  "main": "lib/md2doc.js",
15
15
  "bin": {
16
- "md2html": "bin/md2html.js",
17
- "md2pdf": "bin/md2pdf.js"
16
+ "md2doc": "bin/md2doc.js"
18
17
  },
19
18
  "files": [
20
19
  "lib/",
@@ -32,7 +31,7 @@
32
31
  },
33
32
  "scripts": {
34
33
  "preinstall": "node scripts/preinstall.js",
35
- "test": "node test/md2doc.test.js"
34
+ "test": "node test/md2doc.test.js && node test/cli.test.js"
36
35
  },
37
36
  "repository": {
38
37
  "type": "git",
package/bin/md2html.js DELETED
@@ -1,156 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const path = require('path');
5
- const fs = require('fs');
6
- const { spawnSync } = require('child_process');
7
-
8
- const FORMAT = 'html';
9
- const LIB = path.resolve(__dirname, '..', 'lib', 'md2doc.js');
10
- const VERSION = require('../package.json').version;
11
-
12
- function printHelp() {
13
- process.stdout.write([
14
- `md2${FORMAT} — render Markdown to ${FORMAT.toUpperCase()} (WaveDrom / Mermaid / Graphviz supported)`,
15
- '',
16
- 'Usage:',
17
- ` md2${FORMAT} <input.md> [<input2.md> ...] # render each to <stem>_gen.${FORMAT} next to source`,
18
- ` md2${FORMAT} <input.md> --out <path.${FORMAT}> # explicit output (single-file mode)`,
19
- ` md2${FORMAT} <input.md> --open # render then open viewer`,
20
- ` md2${FORMAT} <input.md> --quiet # suppress progress output`,
21
- '',
22
- 'Flags:',
23
- ' --out <path> Explicit output path. Only valid with exactly one input.',
24
- ' --open Launch the platform viewer after render.',
25
- ' --quiet Suppress per-file progress messages.',
26
- ' --version, -v Print version.',
27
- ' --help, -h Print this help.',
28
- ''
29
- ].join('\n'));
30
- }
31
-
32
- function parseArgs(argv) {
33
- const inputs = [];
34
- let out = null;
35
- let open = false;
36
- let quiet = false;
37
-
38
- for (let i = 0; i < argv.length; i++) {
39
- const a = argv[i];
40
- if (a === '--version' || a === '-v') {
41
- process.stdout.write(VERSION + '\n');
42
- process.exit(0);
43
- }
44
- if (a === '--help' || a === '-h') {
45
- printHelp();
46
- process.exit(0);
47
- }
48
- if (a === '--out') {
49
- if (out !== null) {
50
- process.stderr.write('error: --out specified more than once\n');
51
- process.exit(2);
52
- }
53
- i++;
54
- if (i >= argv.length) {
55
- process.stderr.write('error: --out requires a value\n');
56
- process.exit(2);
57
- }
58
- out = argv[i];
59
- continue;
60
- }
61
- if (a === '--open') { open = true; continue; }
62
- if (a === '--quiet') { quiet = true; continue; }
63
- if (a.startsWith('-')) {
64
- process.stderr.write(`error: unknown flag ${a}\n`);
65
- process.exit(2);
66
- }
67
- inputs.push(a);
68
- }
69
-
70
- if (inputs.length === 0) {
71
- printHelp();
72
- process.exit(2);
73
- }
74
- if (out !== null && inputs.length !== 1) {
75
- process.stderr.write('error: --out is only valid with exactly one input file\n');
76
- process.exit(2);
77
- }
78
- return { inputs, out, open, quiet };
79
- }
80
-
81
- function deriveOutput(input, format) {
82
- // Default suffix matches the workspace Makefile convention: <stem>_gen.<format>.
83
- const stem = input.replace(/\.md$/i, '');
84
- if (stem === input) {
85
- // No .md extension — append _gen.<format>.
86
- return input + '_gen.' + format;
87
- }
88
- return stem + '_gen.' + format;
89
- }
90
-
91
- function isWSL() {
92
- if (process.platform !== 'linux') return false;
93
- if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
94
- try {
95
- return fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop');
96
- } catch (_) {
97
- return false;
98
- }
99
- }
100
-
101
- function openViewer(filePath) {
102
- const platform = process.platform;
103
- let cmd, args;
104
- if (platform === 'darwin') {
105
- cmd = 'open'; args = [filePath];
106
- } else if (platform === 'win32') {
107
- cmd = 'cmd'; args = ['/c', 'start', '""', filePath];
108
- } else if (isWSL()) {
109
- // Convert the WSL Linux path to a Windows-side path then launch via explorer.exe,
110
- // which uses Windows file associations (.html → default browser, .pdf → default reader).
111
- const r = spawnSync('wslpath', ['-w', filePath], { encoding: 'utf8' });
112
- if (r.status === 0 && r.stdout) {
113
- cmd = 'explorer.exe'; args = [r.stdout.trim()];
114
- } else {
115
- cmd = 'xdg-open'; args = [filePath];
116
- }
117
- } else {
118
- cmd = 'xdg-open'; args = [filePath];
119
- }
120
- const r = spawnSync(cmd, args, { stdio: 'ignore' });
121
- // explorer.exe returns exit code 1 on success (Windows quirk via WSL interop),
122
- // so a non-zero exit is not necessarily a failure when launching via explorer.
123
- if (r.error) {
124
- process.stderr.write(`warning: could not launch viewer for ${filePath}: ${r.error.message}\n`);
125
- }
126
- }
127
-
128
- function main() {
129
- const args = parseArgs(process.argv.slice(2));
130
- const outputs = [];
131
-
132
- for (const input of args.inputs) {
133
- if (!fs.existsSync(input)) {
134
- process.stderr.write(`error: input not found: ${input}\n`);
135
- process.exit(1);
136
- }
137
- const output = (args.out !== null) ? args.out : deriveOutput(input, FORMAT);
138
- // lib/md2doc.js prints its own "[FORMAT] input → output" line; redirect its stdout
139
- // when --quiet so neither layer emits progress.
140
- const stdio = args.quiet ? ['inherit', 'ignore', 'inherit'] : 'inherit';
141
- const r = spawnSync(process.execPath, [LIB, input, output], { stdio });
142
- if (r.status !== 0) {
143
- process.stderr.write(`error: render failed for ${input} (exit ${r.status})\n`);
144
- process.exit(r.status || 1);
145
- }
146
- outputs.push(output);
147
- }
148
-
149
- if (args.open) {
150
- for (const o of outputs) {
151
- openViewer(o);
152
- }
153
- }
154
- }
155
-
156
- main();
package/bin/md2pdf.js DELETED
@@ -1,153 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const path = require('path');
5
- const fs = require('fs');
6
- const { spawnSync } = require('child_process');
7
-
8
- const FORMAT = 'pdf';
9
- const LIB = path.resolve(__dirname, '..', 'lib', 'md2doc.js');
10
- const VERSION = require('../package.json').version;
11
-
12
- function printHelp() {
13
- process.stdout.write([
14
- `md2${FORMAT} — render Markdown to ${FORMAT.toUpperCase()} (WaveDrom / Mermaid / Graphviz supported)`,
15
- '',
16
- 'Usage:',
17
- ` md2${FORMAT} <input.md> [<input2.md> ...] # render each to <stem>_gen.${FORMAT} next to source`,
18
- ` md2${FORMAT} <input.md> --out <path.${FORMAT}> # explicit output (single-file mode)`,
19
- ` md2${FORMAT} <input.md> --open # render then open viewer`,
20
- ` md2${FORMAT} <input.md> --quiet # suppress progress output`,
21
- '',
22
- 'Flags:',
23
- ' --out <path> Explicit output path. Only valid with exactly one input.',
24
- ' --open Launch the platform viewer after render.',
25
- ' --quiet Suppress per-file progress messages.',
26
- ' --version, -v Print version.',
27
- ' --help, -h Print this help.',
28
- ''
29
- ].join('\n'));
30
- }
31
-
32
- function parseArgs(argv) {
33
- const inputs = [];
34
- let out = null;
35
- let open = false;
36
- let quiet = false;
37
-
38
- for (let i = 0; i < argv.length; i++) {
39
- const a = argv[i];
40
- if (a === '--version' || a === '-v') {
41
- process.stdout.write(VERSION + '\n');
42
- process.exit(0);
43
- }
44
- if (a === '--help' || a === '-h') {
45
- printHelp();
46
- process.exit(0);
47
- }
48
- if (a === '--out') {
49
- if (out !== null) {
50
- process.stderr.write('error: --out specified more than once\n');
51
- process.exit(2);
52
- }
53
- i++;
54
- if (i >= argv.length) {
55
- process.stderr.write('error: --out requires a value\n');
56
- process.exit(2);
57
- }
58
- out = argv[i];
59
- continue;
60
- }
61
- if (a === '--open') { open = true; continue; }
62
- if (a === '--quiet') { quiet = true; continue; }
63
- if (a.startsWith('-')) {
64
- process.stderr.write(`error: unknown flag ${a}\n`);
65
- process.exit(2);
66
- }
67
- inputs.push(a);
68
- }
69
-
70
- if (inputs.length === 0) {
71
- printHelp();
72
- process.exit(2);
73
- }
74
- if (out !== null && inputs.length !== 1) {
75
- process.stderr.write('error: --out is only valid with exactly one input file\n');
76
- process.exit(2);
77
- }
78
- return { inputs, out, open, quiet };
79
- }
80
-
81
- function deriveOutput(input, format) {
82
- // Default suffix matches the workspace Makefile convention: <stem>_gen.<format>.
83
- const stem = input.replace(/\.md$/i, '');
84
- if (stem === input) {
85
- return input + '_gen.' + format;
86
- }
87
- return stem + '_gen.' + format;
88
- }
89
-
90
- function isWSL() {
91
- if (process.platform !== 'linux') return false;
92
- if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
93
- try {
94
- return fs.existsSync('/proc/sys/fs/binfmt_misc/WSLInterop');
95
- } catch (_) {
96
- return false;
97
- }
98
- }
99
-
100
- function openViewer(filePath) {
101
- const platform = process.platform;
102
- let cmd, args;
103
- if (platform === 'darwin') {
104
- cmd = 'open'; args = [filePath];
105
- } else if (platform === 'win32') {
106
- cmd = 'cmd'; args = ['/c', 'start', '""', filePath];
107
- } else if (isWSL()) {
108
- // Convert the WSL Linux path to a Windows-side path then launch via explorer.exe,
109
- // which uses Windows file associations (.html → default browser, .pdf → default reader).
110
- const r = spawnSync('wslpath', ['-w', filePath], { encoding: 'utf8' });
111
- if (r.status === 0 && r.stdout) {
112
- cmd = 'explorer.exe'; args = [r.stdout.trim()];
113
- } else {
114
- cmd = 'xdg-open'; args = [filePath];
115
- }
116
- } else {
117
- cmd = 'xdg-open'; args = [filePath];
118
- }
119
- const r = spawnSync(cmd, args, { stdio: 'ignore' });
120
- if (r.error) {
121
- process.stderr.write(`warning: could not launch viewer for ${filePath}: ${r.error.message}\n`);
122
- }
123
- }
124
-
125
- function main() {
126
- const args = parseArgs(process.argv.slice(2));
127
- const outputs = [];
128
-
129
- for (const input of args.inputs) {
130
- if (!fs.existsSync(input)) {
131
- process.stderr.write(`error: input not found: ${input}\n`);
132
- process.exit(1);
133
- }
134
- const output = (args.out !== null) ? args.out : deriveOutput(input, FORMAT);
135
- // lib/md2doc.js prints its own "[FORMAT] input → output" line; redirect its stdout
136
- // when --quiet so neither layer emits progress.
137
- const stdio = args.quiet ? ['inherit', 'ignore', 'inherit'] : 'inherit';
138
- const r = spawnSync(process.execPath, [LIB, input, output], { stdio });
139
- if (r.status !== 0) {
140
- process.stderr.write(`error: render failed for ${input} (exit ${r.status})\n`);
141
- process.exit(r.status || 1);
142
- }
143
- outputs.push(output);
144
- }
145
-
146
- if (args.open) {
147
- for (const o of outputs) {
148
- openViewer(o);
149
- }
150
- }
151
- }
152
-
153
- main();