@helping-ai-workflow/md2doc 2.0.1 → 2.1.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
@@ -49,6 +49,7 @@ md2doc --html --pdf foo.md # render both formats
49
49
  md2doc *.md # batch: each file → temp + open
50
50
 
51
51
  md2doc foo.md --out bar.html # write to a specific file (no auto-open)
52
+ md2doc foo.md --out report.pdf # --pdf inferred from the extension
52
53
  md2doc foo.md --out ./build/ # write to ./build/foo.html (no auto-open)
53
54
  md2doc *.md --out ./build/ # batch into ./build/
54
55
  md2doc foo.md --out ./build/ --open # explicit open with --out
@@ -62,9 +63,9 @@ also pass `--open`.
62
63
 
63
64
  | Flag | Meaning |
64
65
  |---|---|
65
- | `--html` | Render HTML (default if neither `--html` nor `--pdf` is given). |
66
+ | `--html` | Render HTML (default when neither `--html`/`--pdf` nor a `.pdf` file `--out` is given; directory targets always default to HTML). |
66
67
  | `--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
+ | `--out <path>` | Output path. Ends with `/` or an existing directory → directory mode. Ends with `.html` / `.pdf` → file mode (single input only). Without `--html`/`--pdf`, the extension selects the format. Implies `--no-open` unless `--open` is also passed. |
68
69
  | `--open` | Launch the platform viewer (`xdg-open` / `open` / `start`) after render. Default when `--out` is absent. |
69
70
  | `--no-open` | Skip the viewer launch. |
70
71
  | `--quiet` | Suppress per-file progress messages. |
package/bin/md2doc.js CHANGED
@@ -19,13 +19,15 @@ function printHelp() {
19
19
  ' md2doc --pdf <input.md>... Render to PDF instead',
20
20
  ' md2doc --html --pdf <input.md> Render both formats',
21
21
  ' md2doc <input.md> --out <file.html> Write to a specific file (no auto-open)',
22
+ ' md2doc <input.md> --out <file.pdf> Write a PDF (format inferred from extension)',
22
23
  ' md2doc <input.md>... --out <dir>/ Write each to <dir>/<stem>.html (no auto-open)',
23
24
  '',
24
25
  'Flags:',
25
- ' --html Render HTML (default if neither --html nor --pdf is given).',
26
+ ' --html Render HTML (default when neither --html/--pdf nor a .pdf file --out is given).',
26
27
  ' --pdf Render PDF.',
27
28
  ' --out <path> Output path. Ends with \'/\' or existing dir → directory mode.',
28
29
  ' Ends with .html/.pdf → file mode (single input only).',
30
+ ' Without --html/--pdf, the .html/.pdf extension selects the format.',
29
31
  ' Implies --no-open unless --open is also passed.',
30
32
  ' --open Launch the platform viewer after render (default when --out is absent).',
31
33
  ' --no-open Skip the viewer launch.',
@@ -85,7 +87,9 @@ function parseArgs(argv) {
85
87
  process.exit(2);
86
88
  }
87
89
 
88
- // Format defaults: neither flag → HTML only.
90
+ // Format defaults: neither flag → HTML only. formatsExplicit lets
91
+ // resolveOutputs infer the format from the --out extension instead.
92
+ const formatsExplicit = html || pdf;
89
93
  const formats = [];
90
94
  if (html || (!html && !pdf)) formats.push('html');
91
95
  if (pdf) formats.push('pdf');
@@ -98,7 +102,7 @@ function parseArgs(argv) {
98
102
  open = (out === null);
99
103
  }
100
104
 
101
- return { inputs, formats, out, open, quiet };
105
+ return { inputs, formats, formatsExplicit, out, open, quiet };
102
106
  }
103
107
 
104
108
  function shortHash(absPath) {
@@ -123,8 +127,11 @@ function classifyOut(outValue) {
123
127
  return { kind: 'dir', ext: null };
124
128
  }
125
129
  } 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' };
130
+ // Require a stem character before the extension: a basename that is just
131
+ // '.pdf'/'.html' is extensionless to path.extname() in the renderer, so
132
+ // treat it as ambiguous here instead of failing late in the child.
133
+ if (/[^/\\]\.html$/i.test(outValue)) return { kind: 'file', ext: 'html' };
134
+ if (/[^/\\]\.pdf$/i.test(outValue)) return { kind: 'file', ext: 'pdf' };
128
135
  return { kind: 'ambiguous', ext: null };
129
136
  }
130
137
 
@@ -144,27 +151,34 @@ function resolveOutputs(args) {
144
151
  if (cls.kind === 'ambiguous') {
145
152
  process.stderr.write(
146
153
  'error: --out \'' + args.out + '\' must end with \'/\' to mean a directory ' +
147
- 'or \'.html\'/\'.pdf\' to mean a file\n'
154
+ 'or \'.html\'/\'.pdf\' to mean a file ' +
155
+ '(the extension also selects the format when no --html/--pdf is given)\n'
148
156
  );
149
157
  process.exit(2);
150
158
  }
151
159
 
160
+ // No --html/--pdf given: the --out extension selects the format. classifyOut
161
+ // is the single authority for dir/file/ext — never re-derive from args.out here
162
+ // (an existing directory named e.g. foo.pdf must stay in dir mode).
163
+ const formats = (!args.formatsExplicit && cls.kind === 'file') ? [cls.ext] : args.formats;
164
+
152
165
  if (cls.kind === 'file') {
153
166
  if (args.inputs.length > 1) {
154
167
  process.stderr.write('error: --out file path is only valid with one input\n');
155
168
  process.exit(2);
156
169
  }
157
- if (args.formats.length > 1) {
158
- process.stderr.write('error: --out file path is not valid when producing both formats\n');
170
+ if (formats.length > 1) {
171
+ process.stderr.write('error: --out file path is not valid when producing both formats; use --out <dir>/ to write both\n');
159
172
  process.exit(2);
160
173
  }
161
- if (cls.ext !== args.formats[0]) {
174
+ if (cls.ext !== formats[0]) {
162
175
  process.stderr.write(
163
- 'error: --out \'' + args.out + '\' extension does not match selected format\n'
176
+ 'error: --out \'' + args.out + '\' extension does not match the --html/--pdf flag; ' +
177
+ 'drop the flag to infer the format from --out, or make them agree\n'
164
178
  );
165
179
  process.exit(2);
166
180
  }
167
- pairs.push({ input: args.inputs[0], format: args.formats[0], output: args.out });
181
+ pairs.push({ input: args.inputs[0], format: formats[0], output: args.out });
168
182
  return pairs;
169
183
  }
170
184
 
@@ -172,7 +186,7 @@ function resolveOutputs(args) {
172
186
  fs.mkdirSync(args.out, { recursive: true });
173
187
  for (const input of args.inputs) {
174
188
  const stem = path.basename(input).replace(/\.md$/i, '');
175
- for (const format of args.formats) {
189
+ for (const format of formats) {
176
190
  pairs.push({
177
191
  input,
178
192
  format,
package/lib/md2doc.js CHANGED
@@ -1515,7 +1515,9 @@ if (ext === '.html') {
1515
1515
  }
1516
1516
 
1517
1517
  // Write temporary HTML, launch headless Chromium, export PDF
1518
- const tmp = dst.replace(/\.pdf$/, '._tmp.html');
1518
+ // Case-insensitive: an uppercase .PDF dst must not make tmp === dst, or the
1519
+ // unlinkSync below deletes the freshly written PDF.
1520
+ const tmp = dst.replace(/\.pdf$/i, '._tmp.html');
1519
1521
  fs.writeFileSync(tmp, html, 'utf8');
1520
1522
 
1521
1523
  const browser = await puppeteer.launch({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, and Graphviz support",
5
5
  "keywords": [
6
6
  "markdown",