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 +208 -0
- package/bin/cli.js +186 -0
- package/package.json +58 -0
- package/src/browser/renderer.js +142 -0
- package/src/core/cover.js +40 -0
- package/src/core/frontmatter.js +54 -0
- package/src/core/index.js +80 -0
- package/src/core/parser.js +163 -0
- package/src/core/sanitizer.js +35 -0
- package/src/core/templates.js +192 -0
- package/src/core/themes/academic.js +20 -0
- package/src/core/themes/cover.js +14 -0
- package/src/core/themes/cyberpunk.js +21 -0
- package/src/core/themes/dark.js +21 -0
- package/src/core/themes/ebook.js +20 -0
- package/src/core/themes/github.js +21 -0
- package/src/core/themes/index.js +52 -0
- package/src/core/themes/minimal.js +20 -0
- package/src/core/themes/modern.js +34 -0
- package/src/core/toc.js +72 -0
- package/src/core/utils.js +85 -0
- package/src/index.js +2 -0
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
|
+
[](https://chromewebstore.google.com/detail/markdown-to-pdf-converter/djikikejkmkklfeklfmddaadilhahamj)
|
|
6
|
+
[](https://chromewebstore.google.com/detail/markdown-to-pdf-converter/djikikejkmkklfeklfmddaadilhahamj)
|
|
7
|
+
[](https://www.npmjs.com/package/md-pdf-studio)
|
|
8
|
+
[](https://www.npmjs.com/package/md-pdf-studio)
|
|
9
|
+

|
|
10
|
+

|
|
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
|
+
};
|