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 +60 -11
- package/bin/cli.js +48 -15
- package/package.json +6 -4
- package/src/browser/renderer.js +27 -18
- package/src/cli/options.js +61 -0
- package/src/core/cover.js +23 -5
- package/src/core/docmodel.js +324 -0
- package/src/core/docx.js +493 -0
- package/src/core/epub.js +338 -0
- package/src/core/footnotes.js +1 -0
- package/src/core/frontmatter.js +1 -0
- package/src/core/images.js +70 -0
- package/src/core/include.js +105 -0
- package/src/core/index.js +80 -12
- package/src/core/pagemarks.js +142 -0
- package/src/core/parser.js +20 -3
- package/src/core/sanitizer.js +1 -0
- package/src/core/templates.js +4 -2
- package/src/core/themes/academic.js +1 -0
- package/src/core/themes/cover.js +1 -0
- package/src/core/themes/cyberpunk.js +1 -0
- package/src/core/themes/dark.js +1 -0
- package/src/core/themes/ebook.js +1 -0
- package/src/core/themes/github.js +1 -0
- package/src/core/themes/highlight.js +1 -0
- package/src/core/themes/index.js +1 -0
- package/src/core/themes/minimal.js +1 -0
- package/src/core/themes/modern.js +1 -0
- package/src/core/toc.js +1 -0
- package/src/core/utils.js +6 -5
- package/src/core/zip.js +78 -0
- package/src/index.js +1 -0
package/README.md
CHANGED
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/md-pdf-studio)
|
|
9
9
|

|
|
10
10
|

|
|
11
|
+
[](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
|
-
-
|
|
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
|
|
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
|
-
## πΏ
|
|
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`**:
|
|
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 #
|
|
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 (
|
|
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
|
|
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 &&
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
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
|
|
62
|
+
"lefthook": "^2.2.1"
|
|
61
63
|
}
|
|
62
64
|
}
|
package/src/browser/renderer.js
CHANGED
|
@@ -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"
|
|
31
|
-
${docDate ? `<div class="cover-meta-item"><span class="cover-meta-label"
|
|
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
|
};
|