md-pdf-studio 1.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 ADDED
@@ -0,0 +1,208 @@
1
+ # 📄 convert-md-to-pdf
2
+
3
+ > A powerful CLI tool and **Browser Extension (Manifest V3)** to convert Markdown (`.md`) files, raw text, or web pages into beautifully styled, high-quality PDF & static HTML documents. Built with support for **Cover Pages**, **Dotted Leader Line Table of Contents (TOC)**, **Mermaid.js diagrams**, **LaTeX Math**, **GitHub Callout Alerts**, **Custom CSS Injection**, **7 Preset Themes**, and **Open-Source Font Selection**.
4
+
5
+ [![Chrome Web Store](https://img.shields.io/badge/Chrome_Web_Store-Install_Extension-4285F4?logo=googlechrome&logoColor=white)](https://chromewebstore.google.com/detail/markdown-to-pdf-converter/djikikejkmkklfeklfmddaadilhahamj)
6
+ [![Chrome Web Store users](https://img.shields.io/chrome-web-store/users/djikikejkmkklfeklfmddaadilhahamj?label=Chrome%20users)](https://chromewebstore.google.com/detail/markdown-to-pdf-converter/djikikejkmkklfeklfmddaadilhahamj)
7
+ [![npm version](https://img.shields.io/npm/v/md-pdf-studio?color=blue)](https://www.npmjs.com/package/md-pdf-studio)
8
+ [![npm downloads](https://img.shields.io/npm/d18m/md-pdf-studio)](https://www.npmjs.com/package/md-pdf-studio)
9
+ ![Browser Extension](https://img.shields.io/badge/Extension-Manifest_V3-green)
10
+ ![License](https://img.shields.io/badge/license-MIT-green)
11
+
12
+ ---
13
+
14
+ ## ✨ Features
15
+
16
+ - 🧩 **Browser Extension (Manifest V3)**:
17
+ - Drag & drop single or **multiple `.md` files** directly into the popup.
18
+ - Auto-saved draft text in popup & conversion history list with 1-click re-open.
19
+ - Convert active web page or GitHub raw Markdown files with 1 click.
20
+ - Context menu integration: Right-click selected Markdown text or any page -> "Convert to PDF".
21
+ - 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.
23
+ - 📌 **Dotted Leader Line Table of Contents (TOC)**: Auto-generates publication-grade TOC with dotted leader lines (`...`) and clickable anchor links (`--toc`).
24
+ - 🎨 **7 Preset Themes**: `modern` (default), `dark`, `academic`, `github`, `ebook`, `cyberpunk`, and `minimal`.
25
+ - 🔤 **Open-Source Font Selector**: 100% free under SIL OFL / Apache 2.0 (`Inter`, `Roboto`, `Lora`, `Merriweather`, `JetBrains Mono`, `Fira Code`).
26
+ - ⚡ **Live Watch Mode & HTML Export**: Real-time auto-recompilation on file changes (`-w, --watch`) and direct static HTML export (`-f, --format html`).
27
+ - 🚀 **CLI & Batch Conversion**: Convert single files or batch process directories with wildcard patterns.
28
+ - 🎨 **Custom CSS Injection**: Inject custom branding CSS stylesheets (`-c, --css custom.css`).
29
+ - 📊 **Mermaid Diagrams**: Sequence diagrams (with auto note wrapping), flowcharts, class diagrams, and gantt charts.
30
+ - 📐 **LaTeX Math Support**: Mathematical equations rendered with KaTeX.
31
+ - 💡 **GitHub Callout Box Alerts**: Support for `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!IMPORTANT]`, and `> [!CAUTION]`.
32
+ - 💻 **ASCII UI Mockups**: Styled dark-theme boxes for terminal output & ASCII wireframe mockups.
33
+
34
+ ---
35
+
36
+ ## 🌐 Extension Installation & Usage
37
+
38
+ ### 🛒 Direct Install from Chrome Web Store (Recommended)
39
+ You can install the official published extension directly with 1-click:
40
+ 👉 **[Install Markdown to PDF Converter on Chrome Web Store](https://chromewebstore.google.com/detail/markdown-to-pdf-converter/djikikejkmkklfeklfmddaadilhahamj)**
41
+
42
+ ### 🛠️ Developer Unpacked Installation (Local Build):
43
+ 1. Clone or download this repository:
44
+ ```bash
45
+ git clone https://github.com/vannt-dev/convert-md-to-pdf.git
46
+ ```
47
+ 2. Open your browser and navigate to `chrome://extensions` (or `edge://extensions`).
48
+ 3. Enable **Developer mode** (toggle in the top-right corner).
49
+ 4. Click **Load unpacked** (Tải tiện ích đã giải nén).
50
+ 5. Select the **`extension`** directory inside this repository (or load `dist/extension-latest.zip`).
51
+ 6. The **Markdown to PDF Converter** icon will appear in your browser toolbar!
52
+
53
+ ---
54
+
55
+ ## 💻 CLI Installation & Usage
56
+
57
+ ### Global Installation via npm / npx
58
+ The CLI is published on npm as [`md-pdf-studio`](https://www.npmjs.com/package/md-pdf-studio):
59
+ ```bash
60
+ npm install -g md-pdf-studio
61
+ ```
62
+ or run directly with `npx`:
63
+ ```bash
64
+ npx md-pdf-studio input.md
65
+ ```
66
+ A global install provides the `md-pdf-studio` command and the `md2pdf` and `convert-md-to-pdf` aliases.
67
+ PDF output uses a locally installed Google Chrome, Microsoft Edge or Chromium.
68
+
69
+ > The npm package named `convert-md-to-pdf` is an unrelated project; install `md-pdf-studio` for this tool.
70
+
71
+ ### CLI Examples:
72
+ ```bash
73
+ # Convert a Markdown file with Cover Page, Auto TOC & Cyberpunk Theme
74
+ npx md-pdf-studio input.md --cover --toc -t cyberpunk --font "Fira Code" -o output.pdf
75
+
76
+ # Watch file and auto-recompile on save
77
+ npx md-pdf-studio input.md -w -f html
78
+
79
+ # Batch convert multiple files to an output directory
80
+ npx md-pdf-studio file1.md file2.md -o output_dir/
81
+
82
+ # Convert entire directory of .md files
83
+ npx md-pdf-studio ./docs/ -o ./dist/
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 🛠️ CLI Options
89
+
90
+ | Flag | Alias | Description | Default |
91
+ | :--- | :--- | :--- | :--- |
92
+ | `-o, --output <path>`| | Output PDF/HTML file path or target directory | Auto-derived |
93
+ | `-f, --format <format>`| | Output format: `pdf`, `html` | `pdf` |
94
+ | `-w, --watch` | | Live watch input file and auto-recompile on change | `false` |
95
+ | `-t, --theme <theme>` | | Theme: `modern`, `dark`, `academic`, `github`, `ebook`, `cyberpunk`, `minimal` | `modern` |
96
+ | `--font <font>` | | Font family: `Inter`, `Roboto`, `Lora`, `Merriweather`, `JetBrains Mono`, `Fira Code` | `Inter` |
97
+ | `--cover` | | Auto-generate document report Cover Page | `false` |
98
+ | `--toc` | | Auto-generate Table of Contents with Dotted Leader Lines | `false` |
99
+ | `-p, --page-size <size>`| | Paper size: `A4`, `Letter`, `A3`, `Legal` | `A4` |
100
+ | `-l, --landscape` | | Use landscape page orientation | `false` |
101
+ | `-m, --margin <margin>`| | Custom page margin (e.g. `14mm 12mm 16mm 12mm`) | `14mm 12mm 16mm 12mm` |
102
+ | `-c, --css <path>` | | Custom CSS stylesheet file path to inject | |
103
+ | `-k, --keep-html` | | Keep temporary HTML file after conversion | `false` |
104
+ | `--no-mermaid` | | Disable Mermaid diagram rendering | `false` |
105
+ | `--no-katex` | | Disable KaTeX math formula rendering | `false` |
106
+
107
+ ---
108
+
109
+ ## 📄 Front Matter Options
110
+
111
+ You can specify conversion settings directly inside your `.md` files using YAML Front Matter:
112
+
113
+ ```yaml
114
+ ---
115
+ title: "Quarterly Financial Report"
116
+ subtitle: "Q3 Fiscal Summary"
117
+ author: "Engineering Team"
118
+ date: "2026-08-11"
119
+ cover: true
120
+ toc: true
121
+ theme: github
122
+ font: Inter
123
+ pageSize: A4
124
+ landscape: false
125
+ ---
126
+ # Document Executive Summary
127
+ ...
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 🌿 Git Flow & Automated Release Workflow
133
+
134
+ This project follows standard **Git Flow** branching and automated release publishing:
135
+
136
+ ### 🔀 Branching Strategy
137
+ - **`main`**: Production-ready branch. Stores stable releases. Merging code into `main` automatically triggers a GitHub Release.
138
+ - **`develop`**: Active development branch. Daily commits, feature additions, and refactoring take place here.
139
+
140
+ ### 🚀 Automated Release Pipeline
141
+ Whenever code is merged from `develop` into `main` (or a version tag `v*` is pushed), GitHub Actions ([`.github/workflows/release.yml`](.github/workflows/release.yml)) automatically:
142
+ 1. Runs full unit test suite (`npm test`).
143
+ 2. Bundles cross-platform extension zip artifacts (`dist/extension-vX.Y.Z.zip`).
144
+ 3. Auto-generates release notes from commit history.
145
+ 4. Publishes a new official **[GitHub Release](https://github.com/vannt-dev/convert-md-to-pdf/releases)** with downloadable ZIP binaries attached!
146
+
147
+ ### 💡 1-Click CLI Release Command
148
+ You can also bump version and trigger a release with a single CLI command:
149
+ ```bash
150
+ npm run release <version|patch|minor|major>
151
+ # Example: npm run release 1.2.0
152
+ ```
153
+
154
+ Please review our [Git Commit & Push Workflow Guide (GIT_WORKFLOW.md)](GIT_WORKFLOW.md) for conventional commit guidelines and pre-commit checks.
155
+
156
+ ---
157
+
158
+ ## 📁 Project Architecture
159
+
160
+ ```
161
+ convert-md-to-pdf/
162
+ ├── bin/
163
+ │ └── cli.js # CLI executable with watch & format options
164
+ ├── src/
165
+ │ ├── core/ # Shared modular conversion engine
166
+ │ │ ├── themes/ # Modular Theme CSS collection
167
+ │ │ │ ├── index.js # Theme registry & font override builder
168
+ │ │ │ ├── cover.js # Cover page CSS
169
+ │ │ │ ├── modern.js # Modern blue theme
170
+ │ │ │ ├── dark.js # Dark slate theme
171
+ │ │ │ ├── academic.js # Academic serif theme
172
+ │ │ │ ├── github.js # GitHub light theme
173
+ │ │ │ ├── ebook.js # E-Book paper theme
174
+ │ │ │ ├── cyberpunk.js # Cyberpunk neon dark theme
175
+ │ │ │ └── minimal.js # Minimal monochrome theme
176
+ │ │ ├── frontmatter.js # YAML front matter parser
177
+ │ │ ├── cover.js # Cover page generator
178
+ │ │ ├── toc.js # Dotted leader line TOC generator
179
+ │ │ ├── sanitizer.js # HTML sanitizer & escaping
180
+ │ │ ├── utils.js # Preprocessors & helper re-exports
181
+ │ │ ├── parser.js # Pipeline parser (Markdown -> HTML)
182
+ │ │ ├── templates.js # HTML document template builder
183
+ │ │ └── index.js # Main library API entrypoint
184
+ │ └── browser/ # Headless browser rendering engine
185
+ │ └── renderer.js # Puppeteer Chrome/Edge launcher & PDF worker pool
186
+ ├── extension/ # Browser Extension (Manifest V3)
187
+ │ ├── manifest.json
188
+ │ ├── background/ # Service worker & context menus
189
+ │ ├── content/ # Content script for raw MD pages
190
+ │ ├── popup/ # Extension Popup UI (Batch file, Paste, Page, History)
191
+ │ ├── preview/ # Print Preview & PDF/HTML export page
192
+ │ ├── lib/ # Vendor libraries (marked, mermaid, katex)
193
+ │ └── icons/ # Extension icons (16, 48, 128)
194
+ ├── tests/ # Standalone test suite (7/7 PASS)
195
+ │ └── index.test.js
196
+ ├── dist/ # Built Chrome Extension zip artifacts
197
+ ├── examples/ # Usage examples & custom CSS templates
198
+ ├── package.json
199
+ ├── README.md
200
+ ├── PRIVACY_POLICY.md
201
+ └── GIT_WORKFLOW.md
202
+ ```
203
+
204
+ ---
205
+
206
+ ## 📝 License
207
+
208
+ Distributed under the MIT License. See [LICENSE](LICENSE) for details.
package/bin/cli.js ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const { convertMarkdownToPdf } = require('../src/core/index');
7
+ const packageJson = require('../package.json');
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('md-pdf-studio')
13
+ .alias('md2pdf')
14
+ .description('High-quality Markdown to PDF converter CLI with Mermaid, Math, TOC, Custom CSS, and Batch conversion.')
15
+ .version(packageJson.version, '-v, --version', 'Output tool version')
16
+ .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')
19
+ .option('-w, --watch', 'Watch input file(s) and recompile automatically on changes', false)
20
+ .option('-t, --theme <theme>', 'Theme name: modern (default), dark, academic, minimal, github, ebook, cyberpunk', 'modern')
21
+ .option('--font <font>', 'Font family: Inter (default), Roboto, Lora, Merriweather, JetBrains Mono, Fira Code', 'Inter')
22
+ .option('--cover', 'Auto-generate elegant document Cover Page', false)
23
+ .option('-p, --page-size <size>', 'Paper size: A4, Letter, A3, Legal', 'A4')
24
+ .option('-l, --landscape', 'Use landscape orientation', false)
25
+ .option('-m, --margin <margin>', 'Page margin (e.g. 14mm 12mm 16mm 12mm)', '14mm 12mm 16mm 12mm')
26
+ .option('--toc', 'Auto-generate Table of Contents (TOC)', false)
27
+ .option('-c, --css <path>', 'Custom CSS stylesheet file path to inject')
28
+ .option('-k, --keep-html', 'Keep temporary HTML file after conversion', false)
29
+ .option('--no-mermaid', 'Disable Mermaid diagram rendering')
30
+ .option('--no-katex', 'Disable KaTeX math formula rendering')
31
+ .option('-b, --browser <path>', 'Custom Chrome/Edge executable path')
32
+ .action(async (inputArgs, options) => {
33
+ try {
34
+ const runConversion = async () => {
35
+ const startTime = Date.now();
36
+ const inputFiles = expandInputFiles(inputArgs);
37
+
38
+ if (inputFiles.length === 0) {
39
+ console.error('\x1b[31m✖ Error:\x1b[0m No valid Markdown files found matching input.');
40
+ if (!options.watch) process.exit(1);
41
+ return;
42
+ }
43
+
44
+ const targetFormat = (options.format || 'pdf').toLowerCase();
45
+ console.log(`\n\x1b[36m🚀 Converting ${inputFiles.length} file(s) to ${targetFormat.toUpperCase()}...\x1b[0m`);
46
+ console.log(` Theme: ${options.theme}`);
47
+ console.log(` Font: ${options.font}`);
48
+ console.log(` Paper: ${options.pageSize} (${options.landscape ? 'Landscape' : 'Portrait'})`);
49
+ if (options.cover) console.log(` Cover: Auto-generated Cover Page Enabled`);
50
+ if (options.toc) console.log(` TOC: Auto-generated Table of Contents Enabled`);
51
+ if (options.css) console.log(` CSS: ${path.resolve(options.css)}`);
52
+
53
+ let targetOutputDir = null;
54
+ if (options.output) {
55
+ const resolvedOut = path.resolve(options.output);
56
+ if (inputFiles.length === 1 && (resolvedOut.endsWith('.pdf') || resolvedOut.endsWith('.html'))) {
57
+ targetOutputDir = null;
58
+ } else {
59
+ targetOutputDir = resolvedOut;
60
+ if (!fs.existsSync(targetOutputDir)) {
61
+ fs.mkdirSync(targetOutputDir, { recursive: true });
62
+ }
63
+ }
64
+ }
65
+
66
+ let count = 0;
67
+ for (const file of inputFiles) {
68
+ const fileBaseName = path.parse(file).name;
69
+ let fileOutPath = null;
70
+
71
+ if (targetOutputDir) {
72
+ fileOutPath = path.join(targetOutputDir, `${fileBaseName}.${targetFormat}`);
73
+ } else if (options.output && (options.output.endsWith('.pdf') || options.output.endsWith('.html'))) {
74
+ fileOutPath = options.output;
75
+ }
76
+
77
+ console.log(`[\x1b[33m${count + 1}/${inputFiles.length}\x1b[0m] Processing: ${file}`);
78
+
79
+ const result = await convertMarkdownToPdf(file, fileOutPath, {
80
+ format: targetFormat,
81
+ theme: options.theme,
82
+ font: options.font,
83
+ pageSize: options.pageSize,
84
+ orientation: options.landscape ? 'landscape' : 'portrait',
85
+ margin: options.margin,
86
+ toc: options.toc,
87
+ cover: options.cover,
88
+ cssFile: options.css,
89
+ keepHtml: options.keepHtml,
90
+ mermaid: options.mermaid,
91
+ katex: options.katex,
92
+ executablePath: options.browser
93
+ });
94
+
95
+ const outPathDisplay = result.pdfPath || result.htmlPath;
96
+ console.log(` \x1b[32m✔ File saved:\x1b[0m ${outPathDisplay}`);
97
+ count++;
98
+ }
99
+
100
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
101
+ console.log(`\x1b[32m✨ Conversion complete!\x1b[0m ${count} file(s) generated in ${duration}s.`);
102
+ };
103
+
104
+ // Initial conversion
105
+ await runConversion();
106
+
107
+ // Watch mode implementation
108
+ if (options.watch) {
109
+ const inputFiles = expandInputFiles(inputArgs);
110
+ console.log('\n\x1b[35m👀 Watch mode enabled. Monitoring files for changes...\x1b[0m');
111
+
112
+ let debounceTimer = null;
113
+ const triggerRecompile = (filename) => {
114
+ if (debounceTimer) clearTimeout(debounceTimer);
115
+ debounceTimer = setTimeout(async () => {
116
+ console.log(`\n\x1b[33m🔄 Change detected in ${filename}. Recompiling...\x1b[0m`);
117
+ try {
118
+ await runConversion();
119
+ } catch (err) {
120
+ console.error(`\x1b[31m✖ Error during watch recompile:\x1b[0m ${err.message}`);
121
+ }
122
+ }, 300);
123
+ };
124
+
125
+ inputFiles.forEach(filePath => {
126
+ if (fs.existsSync(filePath)) {
127
+ fs.watch(filePath, (eventType) => {
128
+ if (eventType === 'change') {
129
+ triggerRecompile(path.basename(filePath));
130
+ }
131
+ });
132
+ }
133
+ });
134
+
135
+ if (options.css && fs.existsSync(options.css)) {
136
+ fs.watch(options.css, (eventType) => {
137
+ if (eventType === 'change') {
138
+ triggerRecompile(path.basename(options.css));
139
+ }
140
+ });
141
+ }
142
+ }
143
+ } catch (err) {
144
+ console.error(`\x1b[31m✖ Error:\x1b[0m ${err.message}`);
145
+ process.exit(1);
146
+ }
147
+ });
148
+
149
+ function expandInputFiles(args) {
150
+ const filesSet = new Set();
151
+
152
+ args.forEach(arg => {
153
+ // Check if direct file exists
154
+ if (fs.existsSync(arg)) {
155
+ const stat = fs.statSync(arg);
156
+ if (stat.isFile() && (arg.endsWith('.md') || arg.endsWith('.markdown') || arg.endsWith('.txt'))) {
157
+ filesSet.add(path.resolve(arg));
158
+ } else if (stat.isDirectory()) {
159
+ const dirFiles = fs.readdirSync(arg);
160
+ dirFiles.forEach(f => {
161
+ if (f.endsWith('.md') || f.endsWith('.markdown')) {
162
+ filesSet.add(path.resolve(arg, f));
163
+ }
164
+ });
165
+ }
166
+ } else {
167
+ // Handle simple wildcard pattern like *.md or docs/*.md
168
+ const dir = path.dirname(arg) || '.';
169
+ const pattern = path.basename(arg);
170
+
171
+ if (fs.existsSync(dir)) {
172
+ const regexPattern = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$', 'i');
173
+ const dirFiles = fs.readdirSync(dir);
174
+ dirFiles.forEach(f => {
175
+ if (regexPattern.test(f) && (f.endsWith('.md') || f.endsWith('.markdown'))) {
176
+ filesSet.add(path.resolve(dir, f));
177
+ }
178
+ });
179
+ }
180
+ }
181
+ });
182
+
183
+ return Array.from(filesSet);
184
+ }
185
+
186
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "md-pdf-studio",
3
+ "version": "1.1.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.",
5
+ "main": "src/core/index.js",
6
+ "bin": {
7
+ "md-pdf-studio": "bin/cli.js",
8
+ "md2pdf": "bin/cli.js",
9
+ "convert-md-to-pdf": "bin/cli.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "src/"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/cli.js",
17
+ "build:extension": "node scripts/build-extension.js",
18
+ "fetch:libs": "node scripts/fetch-vendor-libs.js",
19
+ "pre-commit": "node scripts/pre-commit.js",
20
+ "check:commit": "node scripts/check-commit.js",
21
+ "release": "node scripts/release.js",
22
+ "test": "node tests/index.test.js",
23
+ "test:sample": "node bin/cli.js examples/sample.md --toc -c examples/custom.css -o output_test.pdf"
24
+ },
25
+ "keywords": [
26
+ "markdown",
27
+ "pdf",
28
+ "cli",
29
+ "converter",
30
+ "chrome-extension",
31
+ "mermaid",
32
+ "katex",
33
+ "md2pdf",
34
+ "marked",
35
+ "md-pdf-studio",
36
+ "markdown-to-pdf"
37
+ ],
38
+ "author": "vannt-dev",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/vannt-dev/convert-md-to-pdf.git"
43
+ },
44
+ "homepage": "https://github.com/vannt-dev/convert-md-to-pdf#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/vannt-dev/convert-md-to-pdf/issues"
47
+ },
48
+ "dependencies": {
49
+ "commander": "^12.1.0",
50
+ "marked": "^12.0.2"
51
+ },
52
+ "engines": {
53
+ "node": ">=16.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "adm-zip": "^0.6.0"
57
+ }
58
+ }
@@ -0,0 +1,142 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execFile } = require('child_process');
5
+
6
+ /**
7
+ * Finds available Chrome or Edge binary path on system
8
+ * @returns {string|null}
9
+ */
10
+ function findBrowserExecutable() {
11
+ const platform = os.platform();
12
+ const candidates = [];
13
+
14
+ if (platform === 'win32') {
15
+ const programFiles = process.env['PROGRAMFILES'] || 'C:\\Program Files';
16
+ const programFilesX86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
17
+ const localAppData = process.env['LOCALAPPDATA'] || path.join(os.homedir(), 'AppData', 'Local');
18
+
19
+ candidates.push(
20
+ path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
21
+ path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
22
+ path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
23
+ path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
24
+ path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe')
25
+ );
26
+ } else if (platform === 'darwin') {
27
+ candidates.push(
28
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
29
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
30
+ '/Applications/Chromium.app/Contents/MacOS/Chromium'
31
+ );
32
+ } else {
33
+ // Linux / Unix
34
+ candidates.push(
35
+ '/usr/bin/google-chrome',
36
+ '/usr/bin/microsoft-edge',
37
+ '/usr/bin/chromium-browser',
38
+ '/usr/bin/chromium'
39
+ );
40
+ }
41
+
42
+ for (const binPath of candidates) {
43
+ if (fs.existsSync(binPath)) {
44
+ return binPath;
45
+ }
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Renders HTML file to PDF using headless browser
53
+ * @param {string} htmlPath
54
+ * @param {string} outputPath
55
+ * @param {Object} options
56
+ * @returns {Promise<string>}
57
+ */
58
+ function renderHtmlToPdf(htmlPath, outputPath, options = {}) {
59
+ return new Promise((resolve, reject) => {
60
+ const browserPath = options.executablePath || findBrowserExecutable();
61
+
62
+ if (!browserPath) {
63
+ return reject(new Error('No installed Chromium browser (Edge/Chrome) was found on your system. Please install Microsoft Edge or Google Chrome.'));
64
+ }
65
+
66
+ 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
+ ];
76
+
77
+ execFile(browserPath, args, { timeout: 30000 }, (error) => {
78
+ if (error) {
79
+ // 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
+ ];
89
+
90
+ execFile(browserPath, fallbackArgs, { timeout: 30000 }, (err2) => {
91
+ if (err2) {
92
+ return reject(new Error(`Failed to render PDF using browser: ${err2.message}`));
93
+ }
94
+ if (fs.existsSync(outputPath)) {
95
+ resolve(outputPath);
96
+ } else {
97
+ reject(new Error('Browser finished but output PDF file was not created.'));
98
+ }
99
+ });
100
+ return;
101
+ }
102
+
103
+ if (fs.existsSync(outputPath)) {
104
+ resolve(outputPath);
105
+ } else {
106
+ reject(new Error('Browser finished but output PDF file was not created.'));
107
+ }
108
+ });
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Renders multiple HTML files to PDF using concurrent browser execution pool
114
+ * @param {Array<{ htmlPath: string, outputPath: string }>} itemPairs
115
+ * @param {Object} options
116
+ * @returns {Promise<Array<string>>}
117
+ */
118
+ async function renderBatchHtmlToPdf(itemPairs, options = {}) {
119
+ const concurrency = Math.min(options.concurrency || 4, itemPairs.length);
120
+ const results = [];
121
+ const queue = [...itemPairs];
122
+
123
+ async function worker() {
124
+ while (queue.length > 0) {
125
+ const item = queue.shift();
126
+ if (!item) break;
127
+ const pdfPath = await renderHtmlToPdf(item.htmlPath, item.outputPath, options);
128
+ results.push(pdfPath);
129
+ }
130
+ }
131
+
132
+ const workers = Array.from({ length: concurrency }, () => worker());
133
+ await Promise.all(workers);
134
+ return results;
135
+ }
136
+
137
+ module.exports = {
138
+ findBrowserExecutable,
139
+ renderHtmlToPdf,
140
+ renderBatchHtmlToPdf
141
+ };
142
+
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Cover Page Generator Utility
3
+ */
4
+ const { escapeHtml } = require('./sanitizer');
5
+
6
+ /**
7
+ * Generates a Cover Page HTML block if requested in front matter or options
8
+ * @param {Object} metadata
9
+ * @returns {string} Cover page HTML string
10
+ */
11
+ function generateCoverPage(metadata = {}) {
12
+ const { title, subtitle, author, date, cover } = metadata;
13
+
14
+ if (!cover && !subtitle && !author && !date) {
15
+ return '';
16
+ }
17
+
18
+ const docTitle = title || 'Document Report';
19
+ const docSubtitle = subtitle || '';
20
+ const docAuthor = author || '';
21
+ const docDate = date || '';
22
+
23
+ return `<div class="cover-page">
24
+ <div class="cover-page-inner">
25
+ <div class="cover-badge">DOCUMENT REPORT</div>
26
+ <h1 class="cover-title">${escapeHtml(docTitle)}</h1>
27
+ ${docSubtitle ? `<h2 class="cover-subtitle">${escapeHtml(docSubtitle)}</h2>` : ''}
28
+ <div class="cover-divider"></div>
29
+ <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>` : ''}
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div class="page-break"></div>\n`;
36
+ }
37
+
38
+ module.exports = {
39
+ generateCoverPage
40
+ };