md-pdf-studio 2.2.0 β†’ 2.4.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
@@ -8,9 +8,13 @@
8
8
  [![npm downloads](https://img.shields.io/npm/d18m/md-pdf-studio)](https://www.npmjs.com/package/md-pdf-studio)
9
9
  ![Browser Extension](https://img.shields.io/badge/Extension-Manifest_V3-green)
10
10
  ![License](https://img.shields.io/badge/license-MIT-green)
11
+ [![Official vannt-dev build](https://vannt-dev.github.io/badges/official.svg)](https://vannt-dev.github.io/official.html)
11
12
 
12
13
  ---
13
14
 
15
+ Official builds are listed at [vannt-dev.github.io/official.html](https://vannt-dev.github.io/official.html);
16
+ anything else is not published by vannt-dev.
17
+
14
18
  ## ✨ Features
15
19
 
16
20
  - 🧩 **Browser Extension (Manifest V3)**:
@@ -19,7 +23,9 @@
19
23
  - Convert active web page or GitHub raw Markdown files with 1 click.
20
24
  - Context menu integration: Right-click selected Markdown text or any page -> "Convert to PDF".
21
25
  - Full-page Print Preview with live theme switching, Font Selector, Auto TOC, Custom CSS, and native PDF/HTML export.
22
- - πŸ“‘ **Auto Cover Page Generator**: Generate elegant document report cover pages via YAML Front Matter (`cover: true`) or `--cover` flag.
26
+ - Page numbers and a running header/footer, set in the popup (off by default).
27
+ - πŸ“‘ **Auto Cover Page Generator**: Generate elegant document report cover pages via YAML Front Matter (`cover: true`) or `--cover` flag. The author and date labels are in English, or in Vietnamese with `lang: vi` in the front matter.
28
+ - πŸ”’ **Page Numbers & Running Header/Footer**: `--page-numbers` prints "Page 3 of 12" at the foot of each page (`--page-numbers simple` prints "3"). `--header "text"` and `--footer "text"` print a line of text at the top and foot of each page, with `{title}`, `{author}`, `{date}`, `{page}` and `{pages}` filled in (`--header "{title}"` prints the document title, `""` prints nothing). Both need their text right after the flag: `md2pdf --header README.md` is refused, because `README.md` is an input file. `--page-numbers` takes `full` or `simple` as its value and nothing else, so `md2pdf --page-numbers README.md` converts `README.md`. With both a footer and page numbers, the footer text sits left and the numbers right. The cover page is printed without them, but counts as page 1. All off by default; front matter keys `header`, `footer` and `pageNumbers` do the same. PDF only (the CLI and the extension); Word and EPUB leave them out.
23
29
  - πŸ“Œ **Dotted Leader Line Table of Contents (TOC)**: Auto-generates publication-grade TOC with dotted leader lines (`...`) and clickable anchor links (`--toc`).
24
30
  - 🎨 **7 Preset Themes**: `modern` (default), `dark`, `academic`, `github`, `ebook`, `cyberpunk`, and `minimal`.
25
31
  - πŸ”€ **Open-Source Font Selector**: 100% free under SIL OFL / Apache 2.0 (`Inter`, `Roboto`, `Lora`, `Merriweather`, `JetBrains Mono`, `Fira Code`).
@@ -30,6 +36,9 @@
30
36
  - 🌈 **Syntax Highlighting**: Fenced code blocks that name a language (` ```js `, ` ```python `, ` ```diff `, …) are coloured at build time with highlight.js β€” no CDN, so it also works offline and in static HTML export. Colours follow the theme's code block background. Works in the CLI and in the extension preview.
31
37
  - πŸ“ **LaTeX Math Support**: Mathematical equations rendered with KaTeX.
32
38
  - πŸ“ **Footnotes**: `text[^1]` with `[^1]: the note` anywhere in the file. Notes are numbered in the order they are first referenced, gathered at the end of the document, and linked both ways. Works in the CLI and in the extension preview.
39
+ - πŸ“˜ **Word & EPUB Export (CLI)**: `-f docx` writes a Word document and `-f epub` an EPUB 3 e-book, straight from the Markdown and without a browser. Headings, lists (nested, numbered from any start, task lists), tables with their alignment, code blocks, quotes, footnotes, links and local images become the real thing in each format β€” Word styles you can restyle, a chapter per top-level heading and a navigation menu in the EPUB. `--toc`, `--cover`, page size, orientation and margins apply to Word; `--toc` and `--cover` to EPUB. Themes, custom CSS, Mermaid diagrams and KaTeX math are for PDF and HTML only: a diagram or formula is kept as its source. An image on the web is not downloaded β€” it is named in Word and becomes a link in the EPUB β€” and a link to a neighbouring file keeps its text only.
40
+ - πŸ”– **PDF Bookmarks (CLI)**: The PDF gets an outline (bookmarks) built from its headings, nested by level, so a PDF viewer's sidebar can jump to any section. On by default; `--no-outline` or `outline: false` in the front matter leaves it out. The extension prints through the browser's print dialog, which does not add bookmarks.
41
+ - πŸ“Ž **File Includes (CLI)**: A line that holds only `@import "chapters/intro.md"` is replaced by that file, relative to the file the line is in. A Markdown file is inserted as Markdown (without its front matter) and may import further files; any other file β€” `@import "src/app.js"` β€” is inserted as a code block labelled with its extension. Lines inside a code block are left alone, a missing file or files that import each other stop the conversion with the file and line, and watch mode follows the imported files. `--no-include` turns it off.
33
42
  - πŸ’‘ **GitHub Callout Box Alerts**: Support for `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!IMPORTANT]`, and `> [!CAUTION]`.
34
43
  - πŸ’» **ASCII UI Mockups**: Styled dark-theme boxes for terminal output & ASCII wireframe mockups.
35
44
 
@@ -83,6 +92,29 @@ npx md-pdf-studio file1.md file2.md -o output_dir/
83
92
 
84
93
  # Convert entire directory of .md files
85
94
  npx md-pdf-studio ./docs/ -o ./dist/
95
+
96
+ # Word document with a cover page and a table of contents
97
+ npx md-pdf-studio report.md -f docx --cover --toc
98
+
99
+ # EPUB e-book, one chapter per top-level heading
100
+ npx md-pdf-studio book.md -f epub -o book.epub
101
+ ```
102
+
103
+ A book kept as one file per chapter is one document with includes:
104
+
105
+ ```markdown
106
+ ---
107
+ title: Handbook
108
+ toc: true
109
+ ---
110
+ # Handbook
111
+
112
+ @import "chapters/01-intro.md"
113
+ @import "chapters/02-setup.md"
114
+
115
+ ## Example config
116
+
117
+ @import "examples/config.yml"
86
118
  ```
87
119
 
88
120
  ---
@@ -91,13 +123,16 @@ npx md-pdf-studio ./docs/ -o ./dist/
91
123
 
92
124
  | Flag | Alias | Description | Default |
93
125
  | :--- | :--- | :--- | :--- |
94
- | `-o, --output <path>`| | Output PDF/HTML file path or target directory | Auto-derived |
95
- | `-f, --format <format>`| | Output format: `pdf`, `html` | `pdf` |
126
+ | `-o, --output <path>`| | Output file path or target directory | Auto-derived |
127
+ | `-f, --format <format>`| | Output format: `pdf`, `html`, `docx`, `epub` | `pdf` |
96
128
  | `-w, --watch` | | Live watch input file and auto-recompile on change | `false` |
97
129
  | `-t, --theme <theme>` | | Theme: `modern`, `dark`, `academic`, `github`, `ebook`, `cyberpunk`, `minimal` | `modern` |
98
130
  | `--font <font>` | | Font family: `Inter`, `Roboto`, `Lora`, `Merriweather`, `JetBrains Mono`, `Fira Code` | `Inter` |
99
131
  | `--cover` | | Auto-generate document report Cover Page | `false` |
100
132
  | `--toc` | | Auto-generate Table of Contents with Dotted Leader Lines | `false` |
133
+ | `--page-numbers [style]` | | Page numbers at the foot of each page: `full` ("Page 3 of 12") or `simple` ("3"); any other next argument stays an input | off (`full` when given alone) |
134
+ | `--header <text>` | | Running header; `{title}`, `{author}`, `{date}`, `{page}`, `{pages}` are filled in; `""` for none | off |
135
+ | `--footer <text>` | | Running footer, same placeholders; `""` for none | off |
101
136
  | `-p, --page-size <size>`| | Paper size: `A4`, `Letter`, `A3`, `Legal` | `A4` |
102
137
  | `-l, --landscape` | | Use landscape page orientation | `false` |
103
138
  | `-m, --margin <margin>`| | Custom page margin (e.g. `14mm 12mm 16mm 12mm`) | `14mm 12mm 16mm 12mm` |
@@ -106,6 +141,8 @@ npx md-pdf-studio ./docs/ -o ./dist/
106
141
  | `--no-mermaid` | | Disable Mermaid diagram rendering | `false` |
107
142
  | `--no-katex` | | Disable KaTeX math formula rendering | `false` |
108
143
  | `--no-highlight` | | Disable syntax highlighting of code blocks | `false` |
144
+ | `--no-include` | | Leave `@import "file"` lines as they are instead of inserting the file | `false` |
145
+ | `--no-outline` | | Leave out the PDF bookmarks (outline) made from the headings | `false` |
109
146
 
110
147
  ---
111
148
 
@@ -126,6 +163,13 @@ font: Inter
126
163
  pageSize: A4
127
164
  landscape: false
128
165
  highlight: true
166
+ # document language (default en): cover labels and the page's lang
167
+ lang: en
168
+ # running header and footer; header: true also means the title
169
+ header: "{title}"
170
+ footer: "Engineering Team"
171
+ # full ("Page 3 of 12"), simple ("3") or false
172
+ pageNumbers: full
129
173
  ---
130
174
  # Document Executive Summary
131
175
  ...
@@ -133,13 +177,10 @@ highlight: true
133
177
 
134
178
  ---
135
179
 
136
- ## 🌿 Git Flow & Automated Release Workflow
137
-
138
- This project follows standard **Git Flow** branching and automated release publishing:
180
+ ## 🌿 Branching & Automated Release Workflow
139
181
 
140
182
  ### πŸ”€ Branching Strategy
141
- - **`main`**: Production-ready branch. Stores stable releases. Pushing a version tag `v*` from `main` triggers a release.
142
- - **`develop`**: Active development branch. Daily commits, feature additions, and refactoring take place here.
183
+ - **`main`**: The default branch. Changes arrive through pull requests from short-lived feature branches, and pushing a version tag `v*` from `main` triggers a release.
143
184
 
144
185
  ### πŸš€ Automated Release Pipeline
145
186
  Whenever a version tag `v*` is pushed, GitHub Actions ([`.github/workflows/release.yml`](.github/workflows/release.yml)) builds from that tag and automatically:
@@ -181,24 +222,32 @@ convert-md-to-pdf/
181
222
  β”‚ β”‚ β”‚ β”œβ”€β”€ cyberpunk.js # Cyberpunk neon dark theme
182
223
  β”‚ β”‚ β”‚ └── minimal.js # Minimal monochrome theme
183
224
  β”‚ β”‚ β”œβ”€β”€ frontmatter.js # YAML front matter parser
225
+ β”‚ β”‚ β”œβ”€β”€ include.js # @import "file" expansion
226
+ β”‚ β”‚ β”œβ”€β”€ docmodel.js # Format-neutral document model (blocks of flat runs)
227
+ β”‚ β”‚ β”œβ”€β”€ docx.js # Word (.docx) writer
228
+ β”‚ β”‚ β”œβ”€β”€ epub.js # EPUB 3 writer
229
+ β”‚ β”‚ β”œβ”€β”€ images.js # Local image reading & sizes
230
+ β”‚ β”‚ β”œβ”€β”€ zip.js # ZIP container writer for docx/epub
184
231
  β”‚ β”‚ β”œβ”€β”€ cover.js # Cover page generator
185
232
  β”‚ β”‚ β”œβ”€β”€ toc.js # Dotted leader line TOC generator
186
233
  β”‚ β”‚ β”œβ”€β”€ sanitizer.js # HTML sanitizer & escaping
187
234
  β”‚ β”‚ β”œβ”€β”€ utils.js # Preprocessors & helper re-exports
188
235
  β”‚ β”‚ β”œβ”€β”€ parser.js # Pipeline parser (Markdown -> HTML)
189
236
  β”‚ β”‚ β”œβ”€β”€ templates.js # HTML document template builder
237
+ β”‚ β”‚ β”œβ”€β”€ pagemarks.js # Page numbers & running header/footer CSS (shared with the extension)
190
238
  β”‚ β”‚ └── index.js # Main library API entrypoint
239
+ β”‚ β”œβ”€β”€ cli/ # Command line option checks (options.js)
191
240
  β”‚ └── browser/ # Headless browser rendering engine
192
- β”‚ └── renderer.js # Puppeteer Chrome/Edge launcher & PDF worker pool
241
+ β”‚ └── renderer.js # Runs installed Chrome/Edge headless (--print-to-pdf) & PDF worker pool
193
242
  β”œβ”€β”€ extension/ # Browser Extension (Manifest V3)
194
243
  β”‚ β”œβ”€β”€ manifest.json
195
244
  β”‚ β”œβ”€β”€ background/ # Service worker & context menus
196
245
  β”‚ β”œβ”€β”€ content/ # Content script for raw MD pages
197
246
  β”‚ β”œβ”€β”€ popup/ # Extension Popup UI (Batch file, Paste, Page, History)
198
247
  β”‚ β”œβ”€β”€ preview/ # Print Preview & PDF/HTML export page
199
- β”‚ β”œβ”€β”€ lib/ # Vendor libraries (marked, mermaid, katex)
248
+ β”‚ β”œβ”€β”€ lib/ # Vendor libraries (marked, mermaid, katex) & code shared with the CLI
200
249
  β”‚ └── icons/ # Extension icons (16, 48, 128)
201
- β”œβ”€β”€ tests/ # Standalone test suite (7/7 PASS)
250
+ β”œβ”€β”€ tests/ # Standalone test suite (`npm test`)
202
251
  β”‚ └── index.test.js
203
252
  β”œβ”€β”€ dist/ # Built Chrome Extension zip artifacts
204
253
  β”œβ”€β”€ examples/ # Usage examples & custom CSS templates
package/bin/cli.js CHANGED
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
2
3
 
3
4
  const { Command } = require('commander');
4
5
  const path = require('path');
5
6
  const fs = require('fs');
6
7
  const { convertMarkdownToPdf } = require('../src/core/index');
8
+ const { normalizePageNumbersArgs, parseMarginText } = require('../src/cli/options');
7
9
  const packageJson = require('../package.json');
8
10
 
11
+ const OUTPUT_FORMATS = ['pdf', 'html', 'docx', 'epub'];
12
+ // An --output that names a file rather than a folder.
13
+ const OUTPUT_FILE = /\.(pdf|html|docx|epub)$/i;
14
+
9
15
  const program = new Command();
10
16
 
11
17
  program
@@ -14,8 +20,8 @@ program
14
20
  .description('High-quality Markdown to PDF converter CLI with Mermaid, Math, TOC, Custom CSS, and Batch conversion.')
15
21
  .version(packageJson.version, '-v, --version', 'Output tool version')
16
22
  .argument('<inputs...>', 'Input Markdown file(s) or wildcard pattern (.md)')
17
- .option('-o, --output <path>', 'Output PDF/HTML file path or destination directory')
18
- .option('-f, --format <format>', 'Output format: pdf (default), html', 'pdf')
23
+ .option('-o, --output <path>', 'Output file path or destination directory')
24
+ .option('-f, --format <format>', 'Output format: pdf (default), html, docx, epub', 'pdf')
19
25
  .option('-w, --watch', 'Watch input file(s) and recompile automatically on changes', false)
20
26
  .option('-t, --theme <theme>', 'Theme name: modern (default), dark, academic, minimal, github, ebook, cyberpunk', 'modern')
21
27
  .option('--font <font>', 'Font family: Inter (default), Roboto, Lora, Merriweather, JetBrains Mono, Fira Code', 'Inter')
@@ -24,14 +30,22 @@ program
24
30
  .option('-l, --landscape', 'Use landscape orientation', false)
25
31
  .option('-m, --margin <margin>', 'Page margin (e.g. 14mm 12mm 16mm 12mm)', '14mm 12mm 16mm 12mm')
26
32
  .option('--toc', 'Auto-generate Table of Contents (TOC)', false)
33
+ .option('--page-numbers [style]', 'Print page numbers in the footer: full ("Page 3 of 12", default) or simple ("3")')
34
+ .option('--header <text>', 'Running header text; {title}, {author}, {date}, {page}, {pages} are filled in ("" for none)', parseMarginText)
35
+ .option('--footer <text>', 'Running footer text, same placeholders ("" for none)', parseMarginText)
27
36
  .option('-c, --css <path>', 'Custom CSS stylesheet file path to inject')
28
37
  .option('-k, --keep-html', 'Keep temporary HTML file after conversion', false)
29
38
  .option('--no-mermaid', 'Disable Mermaid diagram rendering')
30
39
  .option('--no-katex', 'Disable KaTeX math formula rendering')
31
40
  .option('--no-highlight', 'Disable syntax highlighting of code blocks')
41
+ .option('--no-include', 'Leave @import "file" lines as they are instead of inserting the file')
42
+ .option('--no-outline', 'Leave out the PDF bookmarks (outline) made from the headings')
32
43
  .option('-b, --browser <path>', 'Custom Chrome/Edge executable path')
33
44
  .action(async (inputArgs, options) => {
34
45
  try {
46
+ // Files pulled in with @import, so watch mode can follow them too.
47
+ const importedFiles = new Set();
48
+
35
49
  const runConversion = async () => {
36
50
  const startTime = Date.now();
37
51
  const inputFiles = expandInputFiles(inputArgs);
@@ -43,18 +57,22 @@ program
43
57
  }
44
58
 
45
59
  const targetFormat = (options.format || 'pdf').toLowerCase();
60
+ if (!OUTPUT_FORMATS.includes(targetFormat)) {
61
+ throw new Error(`Unknown format "${options.format}". Use one of: ${OUTPUT_FORMATS.join(', ')}`);
62
+ }
46
63
  console.log(`\n\x1b[36mπŸš€ Converting ${inputFiles.length} file(s) to ${targetFormat.toUpperCase()}...\x1b[0m`);
47
64
  console.log(` Theme: ${options.theme}`);
48
65
  console.log(` Font: ${options.font}`);
49
66
  console.log(` Paper: ${options.pageSize} (${options.landscape ? 'Landscape' : 'Portrait'})`);
50
67
  if (options.cover) console.log(` Cover: Auto-generated Cover Page Enabled`);
51
68
  if (options.toc) console.log(` TOC: Auto-generated Table of Contents Enabled`);
69
+ if (options.pageNumbers) console.log(` Pages: Page numbers (${options.pageNumbers === true ? 'full' : options.pageNumbers})`);
52
70
  if (options.css) console.log(` CSS: ${path.resolve(options.css)}`);
53
71
 
54
72
  let targetOutputDir = null;
55
73
  if (options.output) {
56
74
  const resolvedOut = path.resolve(options.output);
57
- if (inputFiles.length === 1 && (resolvedOut.endsWith('.pdf') || resolvedOut.endsWith('.html'))) {
75
+ if (inputFiles.length === 1 && OUTPUT_FILE.test(resolvedOut)) {
58
76
  targetOutputDir = null;
59
77
  } else {
60
78
  targetOutputDir = resolvedOut;
@@ -71,7 +89,7 @@ program
71
89
 
72
90
  if (targetOutputDir) {
73
91
  fileOutPath = path.join(targetOutputDir, `${fileBaseName}.${targetFormat}`);
74
- } else if (options.output && (options.output.endsWith('.pdf') || options.output.endsWith('.html'))) {
92
+ } else if (options.output && OUTPUT_FILE.test(options.output)) {
75
93
  fileOutPath = options.output;
76
94
  }
77
95
 
@@ -86,15 +104,22 @@ program
86
104
  margin: options.margin,
87
105
  toc: options.toc,
88
106
  cover: options.cover,
107
+ header: options.header,
108
+ footer: options.footer,
109
+ pageNumbers: options.pageNumbers,
89
110
  cssFile: options.css,
90
111
  keepHtml: options.keepHtml,
91
112
  mermaid: options.mermaid,
92
113
  katex: options.katex,
93
114
  highlight: options.highlight,
115
+ include: options.include,
116
+ // Only an explicit --no-outline overrides `outline:` in the front matter.
117
+ outline: options.outline === false ? false : undefined,
94
118
  executablePath: options.browser
95
119
  });
96
120
 
97
- const outPathDisplay = result.pdfPath || result.htmlPath;
121
+ result.includedFiles.forEach(includedFile => importedFiles.add(includedFile));
122
+ const outPathDisplay = result.outputPath;
98
123
  console.log(` \x1b[32mβœ” File saved:\x1b[0m ${outPathDisplay}`);
99
124
  count++;
100
125
  }
@@ -112,27 +137,35 @@ program
112
137
  console.log('\n\x1b[35mπŸ‘€ Watch mode enabled. Monitoring files for changes...\x1b[0m');
113
138
 
114
139
  let debounceTimer = null;
140
+ let watchImports = () => {};
115
141
  const triggerRecompile = (filename) => {
116
142
  if (debounceTimer) clearTimeout(debounceTimer);
117
143
  debounceTimer = setTimeout(async () => {
118
144
  console.log(`\n\x1b[33mπŸ”„ Change detected in ${filename}. Recompiling...\x1b[0m`);
119
145
  try {
120
146
  await runConversion();
147
+ watchImports();
121
148
  } catch (err) {
122
149
  console.error(`\x1b[31mβœ– Error during watch recompile:\x1b[0m ${err.message}`);
123
150
  }
124
151
  }, 300);
125
152
  };
126
153
 
127
- inputFiles.forEach(filePath => {
128
- if (fs.existsSync(filePath)) {
129
- fs.watch(filePath, (eventType) => {
130
- if (eventType === 'change') {
131
- triggerRecompile(path.basename(filePath));
132
- }
133
- });
134
- }
135
- });
154
+ const watched = new Set();
155
+ const watchFile = (filePath) => {
156
+ if (watched.has(filePath) || !fs.existsSync(filePath)) return;
157
+ watched.add(filePath);
158
+ fs.watch(filePath, (eventType) => {
159
+ if (eventType === 'change') {
160
+ triggerRecompile(path.basename(filePath));
161
+ }
162
+ });
163
+ };
164
+ // An edit can add an @import, so the list is read again after every run.
165
+ watchImports = () => importedFiles.forEach(watchFile);
166
+
167
+ inputFiles.forEach(watchFile);
168
+ watchImports();
136
169
 
137
170
  if (options.css && fs.existsSync(options.css)) {
138
171
  fs.watch(options.css, (eventType) => {
@@ -185,4 +218,4 @@ function expandInputFiles(args) {
185
218
  return Array.from(filesSet);
186
219
  }
187
220
 
188
- program.parse(process.argv);
221
+ program.parse(normalizePageNumbersArgs(process.argv));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "md-pdf-studio",
3
- "version": "2.2.0",
4
- "description": "A powerful CLI tool and Manifest V3 Browser Extension to convert Markdown files, text, or web pages into beautifully styled PDFs with Mermaid diagrams, LaTeX math, TOC, and themes.",
3
+ "version": "2.4.0",
4
+ "description": "A powerful CLI tool and Manifest V3 Browser Extension to convert Markdown files, text, or web pages into beautifully styled PDFs with Mermaid diagrams, LaTeX math, TOC, and themes. The CLI also writes Word (.docx) and EPUB.",
5
5
  "main": "src/core/index.js",
6
6
  "bin": {
7
7
  "md-pdf-studio": "bin/cli.js",
@@ -33,7 +33,9 @@
33
33
  "md2pdf",
34
34
  "marked",
35
35
  "md-pdf-studio",
36
- "markdown-to-pdf"
36
+ "markdown-to-pdf",
37
+ "docx",
38
+ "epub"
37
39
  ],
38
40
  "author": "vannt-dev",
39
41
  "license": "MIT",
@@ -57,6 +59,6 @@
57
59
  "@commitlint/cli": "^21.2.3",
58
60
  "@commitlint/config-conventional": "^21.2.3",
59
61
  "adm-zip": "^0.6.0",
60
- "lefthook": "^2.1.14"
62
+ "lefthook": "^2.2.1"
61
63
  }
62
64
  }
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const os = require('os');
@@ -48,6 +49,29 @@ function findBrowserExecutable() {
48
49
  return null;
49
50
  }
50
51
 
52
+ /**
53
+ * The command line that prints a page to PDF with headless Chrome/Edge.
54
+ * @param {string} fileUrl Page to print
55
+ * @param {string} outputPath PDF to write
56
+ * @param {Object} [options]
57
+ * @param {boolean} [options.legacyHeadless] Use `--headless` for browsers without `--headless=new`
58
+ * @param {boolean} [options.outline] Add PDF bookmarks from the headings (default true)
59
+ * @returns {string[]}
60
+ */
61
+ function buildBrowserArgs(fileUrl, outputPath, options = {}) {
62
+ return [
63
+ options.legacyHeadless ? '--headless' : '--headless=new',
64
+ '--disable-gpu',
65
+ '--no-sandbox',
66
+ '--no-pdf-header-footer',
67
+ // Bookmarks from h1-h6. A browser that does not know the switch ignores it.
68
+ ...(options.outline === false ? [] : ['--generate-pdf-document-outline']),
69
+ '--virtual-time-budget=8000',
70
+ `--print-to-pdf=${outputPath}`,
71
+ fileUrl
72
+ ];
73
+ }
74
+
51
75
  /**
52
76
  * Renders HTML file to PDF using headless browser
53
77
  * @param {string} htmlPath
@@ -64,28 +88,12 @@ function renderHtmlToPdf(htmlPath, outputPath, options = {}) {
64
88
  }
65
89
 
66
90
  const fileUrl = 'file:///' + path.resolve(htmlPath).replace(/\\/g, '/');
67
- const args = [
68
- '--headless=new',
69
- '--disable-gpu',
70
- '--no-sandbox',
71
- '--no-pdf-header-footer',
72
- '--virtual-time-budget=8000',
73
- `--print-to-pdf=${outputPath}`,
74
- fileUrl
75
- ];
91
+ const args = buildBrowserArgs(fileUrl, outputPath, { outline: options.outline });
76
92
 
77
93
  execFile(browserPath, args, { timeout: 30000 }, (error) => {
78
94
  if (error) {
79
95
  // Fallback for older chrome versions that don't support --headless=new
80
- const fallbackArgs = [
81
- '--headless',
82
- '--disable-gpu',
83
- '--no-sandbox',
84
- '--no-pdf-header-footer',
85
- '--virtual-time-budget=8000',
86
- `--print-to-pdf=${outputPath}`,
87
- fileUrl
88
- ];
96
+ const fallbackArgs = buildBrowserArgs(fileUrl, outputPath, { outline: options.outline, legacyHeadless: true });
89
97
 
90
98
  execFile(browserPath, fallbackArgs, { timeout: 30000 }, (err2) => {
91
99
  if (err2) {
@@ -135,6 +143,7 @@ async function renderBatchHtmlToPdf(itemPairs, options = {}) {
135
143
  }
136
144
 
137
145
  module.exports = {
146
+ buildBrowserArgs,
138
147
  findBrowserExecutable,
139
148
  renderHtmlToPdf,
140
149
  renderBatchHtmlToPdf
@@ -0,0 +1,61 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
2
+ /**
3
+ * Command line helpers for options whose value could be mistaken for an input file.
4
+ */
5
+ const fs = require('fs');
6
+ const { InvalidArgumentError } = require('commander');
7
+
8
+ const PAGE_NUMBER_STYLES = ['full', 'simple'];
9
+ const INPUT_FILE = /\.(md|markdown|txt)$/i;
10
+
11
+ /**
12
+ * `--page-numbers` takes an optional style. Only a known style is read as its value, so in
13
+ * `--page-numbers README.md` the file stays an input: the flag is given its default instead.
14
+ * @param {string[]} argv process.argv
15
+ * @returns {string[]}
16
+ */
17
+ function normalizePageNumbersArgs(argv) {
18
+ const out = [];
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const arg = argv[i];
21
+ if (arg === '--') {
22
+ out.push(...argv.slice(i));
23
+ break;
24
+ }
25
+ if (arg === '--page-numbers') {
26
+ const next = argv[i + 1];
27
+ if (next !== undefined && PAGE_NUMBER_STYLES.includes(next.toLowerCase())) {
28
+ out.push(`--page-numbers=${next.toLowerCase()}`);
29
+ i++;
30
+ } else {
31
+ out.push('--page-numbers=full');
32
+ }
33
+ continue;
34
+ }
35
+ out.push(arg);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /**
41
+ * Value check for `--header <text>` and `--footer <text>`. An empty string turns them off.
42
+ * A value that names an existing Markdown file is refused: it is almost always an input file
43
+ * that followed the flag, as in `md2pdf --header README.md`.
44
+ * @param {string} value
45
+ * @returns {string}
46
+ */
47
+ function parseMarginText(value) {
48
+ if (INPUT_FILE.test(value) && fs.existsSync(value) && fs.statSync(value).isFile()) {
49
+ throw new InvalidArgumentError(
50
+ `"${value}" is an input file, not header/footer text. Give the text right after the flag, ` +
51
+ `e.g. --header "{title}" ${value}`
52
+ );
53
+ }
54
+ return value;
55
+ }
56
+
57
+ module.exports = {
58
+ PAGE_NUMBER_STYLES,
59
+ normalizePageNumbersArgs,
60
+ parseMarginText
61
+ };
package/src/core/cover.js CHANGED
@@ -1,15 +1,32 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Cover Page Generator Utility
3
4
  */
4
5
  const { escapeHtml } = require('./sanitizer');
5
6
 
7
+ // Cover labels by document language (front matter `lang`); any other language gets English.
8
+ const COVER_LABELS = {
9
+ en: { author: 'Author', date: 'Date' },
10
+ vi: { author: 'TΓ‘c giαΊ£', date: 'NgΓ y lαΊ­p' }
11
+ };
12
+
13
+ /**
14
+ * The cover labels for a language tag such as `en`, `vi` or `vi-VN`.
15
+ * @param {string} [language]
16
+ * @returns {{ author: string, date: string }}
17
+ */
18
+ function coverLabels(language) {
19
+ const primary = String(language || 'en').toLowerCase().split(/[-_]/)[0];
20
+ return COVER_LABELS[primary] || COVER_LABELS.en;
21
+ }
22
+
6
23
  /**
7
24
  * Generates a Cover Page HTML block if requested in front matter or options
8
25
  * @param {Object} metadata
9
26
  * @returns {string} Cover page HTML string
10
27
  */
11
28
  function generateCoverPage(metadata = {}) {
12
- const { title, subtitle, author, date, cover } = metadata;
29
+ const { title, subtitle, author, date, cover, language } = metadata;
13
30
 
14
31
  if (!cover && !subtitle && !author && !date) {
15
32
  return '';
@@ -19,6 +36,7 @@ function generateCoverPage(metadata = {}) {
19
36
  const docSubtitle = subtitle || '';
20
37
  const docAuthor = author || '';
21
38
  const docDate = date || '';
39
+ const labels = coverLabels(language);
22
40
 
23
41
  return `<div class="cover-page">
24
42
  <div class="cover-page-inner">
@@ -27,14 +45,14 @@ function generateCoverPage(metadata = {}) {
27
45
  ${docSubtitle ? `<h2 class="cover-subtitle">${escapeHtml(docSubtitle)}</h2>` : ''}
28
46
  <div class="cover-divider"></div>
29
47
  <div class="cover-meta-container">
30
- ${docAuthor ? `<div class="cover-meta-item"><span class="cover-meta-label">TΓ‘c giαΊ£:</span> <span class="cover-meta-val">${escapeHtml(docAuthor)}</span></div>` : ''}
31
- ${docDate ? `<div class="cover-meta-item"><span class="cover-meta-label">NgΓ y lαΊ­p:</span> <span class="cover-meta-val">${escapeHtml(docDate)}</span></div>` : ''}
48
+ ${docAuthor ? `<div class="cover-meta-item"><span class="cover-meta-label">${labels.author}:</span> <span class="cover-meta-val">${escapeHtml(docAuthor)}</span></div>` : ''}
49
+ ${docDate ? `<div class="cover-meta-item"><span class="cover-meta-label">${labels.date}:</span> <span class="cover-meta-val">${escapeHtml(docDate)}</span></div>` : ''}
32
50
  </div>
33
51
  </div>
34
- </div>
35
- <div class="page-break"></div>\n`;
52
+ </div>\n`;
36
53
  }
37
54
 
38
55
  module.exports = {
56
+ coverLabels,
39
57
  generateCoverPage
40
58
  };