@brahim.ariani/md2pdf-cli 1.1.0 → 1.2.1
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 +58 -4
- package/bin/md2pdf.js +18 -0
- package/lib/index.js +99 -7
- package/lib/mermaid.js +16 -0
- package/lib/styles.js +163 -105
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -7,13 +7,13 @@ GitHub-flavored Markdown, tables, code blocks, blockquotes, images **and LaTeX m
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install -g md2pdf-cli
|
|
10
|
+
npm install -g @brahim.ariani/md2pdf-cli
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Or use it directly in a project:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install md2pdf-cli
|
|
16
|
+
npm install brahim.ariani/md2pdf-cli
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## CLI
|
|
@@ -30,12 +30,15 @@ If `output.pdf` is omitted, the output filename is derived from the input (e.g.
|
|
|
30
30
|
|------------------------|------------------------------------------------------------|
|
|
31
31
|
| `--title <text>` | Document title (defaults to the input filename) |
|
|
32
32
|
| `--css <file>` | Path to a custom CSS file (replaces the default styles) |
|
|
33
|
+
| `--theme <name>` | Built-in theme: `default`, `academic`, `latex` |
|
|
33
34
|
| `--format <size>` | Page format: `A4`, `Letter`, `Legal`, ... Default: `A4` |
|
|
34
35
|
| `--toc` | Prepend an auto-generated table of contents |
|
|
35
36
|
| `--toc-depth <n>` | Deepest heading level included in the TOC. Default: `3` |
|
|
36
37
|
| `--toc-title <text>` | TOC heading text. Default: `Contents` |
|
|
37
38
|
| `--highlight` | Syntax-highlight fenced code blocks with Shiki |
|
|
38
39
|
| `--code-theme <name>` | Shiki theme for code blocks. Default: `github-light` |
|
|
40
|
+
| `--mermaid` | Render ` ```mermaid ` code blocks as diagrams |
|
|
41
|
+
| `--mermaid-theme <t>` | Mermaid theme: `base`, `default`, `neutral`, `dark`, `forest`. Default: `base` |
|
|
39
42
|
| `--cover` | Render a title page from YAML front matter |
|
|
40
43
|
| `--no-cover` | Never render a title page (overrides front matter) |
|
|
41
44
|
| `--no-page-numbers` | Disable the page-number footer |
|
|
@@ -54,6 +57,7 @@ md2pdf notes.md notes.pdf --format Letter --no-page-numbers
|
|
|
54
57
|
md2pdf book.md book.pdf --toc --toc-depth 2 --toc-title "Table of Contents"
|
|
55
58
|
md2pdf code.md code.pdf --highlight --code-theme github-dark
|
|
56
59
|
md2pdf paper.md paper.pdf --cover --toc
|
|
60
|
+
md2pdf thesis.md thesis.pdf --theme academic --toc
|
|
57
61
|
md2pdf paper.md paper.pdf # equations rendered by default
|
|
58
62
|
md2pdf draft.md draft.pdf --no-math # treat $...$ as literal text
|
|
59
63
|
```
|
|
@@ -87,6 +91,27 @@ md2pdf report.md report.pdf --toc --toc-title "Sommaire"
|
|
|
87
91
|
The TOC is placed on its own page (it ends with a page break). You can fully
|
|
88
92
|
restyle it via `--css` by targeting `nav.toc`, `nav.toc .toc-title`, etc.
|
|
89
93
|
|
|
94
|
+
## Themes
|
|
95
|
+
|
|
96
|
+
Pick a built-in look with `--theme`:
|
|
97
|
+
|
|
98
|
+
| Theme | Description |
|
|
99
|
+
|------------|----------------------------------------------------------------------|
|
|
100
|
+
| `default` | Clean sans-serif, navy headings, zebra tables (the original look) |
|
|
101
|
+
| `academic` | Georgia serif, justified & indented paragraphs, centered title |
|
|
102
|
+
| `latex` | Classic LaTeX `article` look: Computer Modern serif, booktabs tables |
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
md2pdf report.md report.pdf --theme academic
|
|
106
|
+
md2pdf thesis.md thesis.pdf --theme latex --toc
|
|
107
|
+
md2pdf design.md design.pdf --mermaid --highlight --cover
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Every theme keeps the same structural rules (math, TOC, cover page, tables,
|
|
111
|
+
page-break safety) and only swaps typography and colors. An unknown theme name
|
|
112
|
+
falls back to `default` with a warning. For full control, `--css` still
|
|
113
|
+
replaces all styles entirely.
|
|
114
|
+
|
|
90
115
|
## Front matter & cover page
|
|
91
116
|
|
|
92
117
|
Markdown files may start with a YAML front-matter block. It is parsed, stripped
|
|
@@ -138,6 +163,32 @@ fast. Use any Shiki theme name (e.g. `github-light`, `github-dark`, `nord`,
|
|
|
138
163
|
`dracula`, `min-light`). Unknown languages fall back to a plain, escaped code
|
|
139
164
|
block, and an unknown theme falls back to `github-light`.
|
|
140
165
|
|
|
166
|
+
## Mermaid diagrams
|
|
167
|
+
|
|
168
|
+
With `--mermaid`, fenced code blocks tagged `mermaid` are rendered into vector
|
|
169
|
+
diagrams with [Mermaid](https://mermaid.js.org/) (flowcharts, sequence diagrams,
|
|
170
|
+
Gantt charts, etc.):
|
|
171
|
+
|
|
172
|
+
````markdown
|
|
173
|
+
```mermaid
|
|
174
|
+
flowchart LR
|
|
175
|
+
A[Start] --> B{OK?}
|
|
176
|
+
B -- Yes --> C[Ship]
|
|
177
|
+
B -- No --> A
|
|
178
|
+
```
|
|
179
|
+
````
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
md2pdf design.md design.pdf --mermaid
|
|
183
|
+
md2pdf design.md design.pdf --mermaid --mermaid-theme neutral
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Diagrams are rendered inside the same headless Chromium used for printing, so
|
|
187
|
+
the resulting SVG is embedded directly in the PDF — no network access or extra
|
|
188
|
+
tooling required. Mermaid runs with `securityLevel: 'strict'`, and a diagram
|
|
189
|
+
with invalid syntax is skipped rather than aborting the whole conversion.
|
|
190
|
+
Without `--mermaid`, ` ```mermaid ` blocks are left as plain code.
|
|
191
|
+
|
|
141
192
|
## Security / HTML sanitization
|
|
142
193
|
|
|
143
194
|
Markdown allows raw HTML, which means an untrusted `.md` file can embed
|
|
@@ -193,8 +244,9 @@ await convert({
|
|
|
193
244
|
| `input` | `string` | — | Path to a Markdown file (required) |
|
|
194
245
|
| `output` | `string` | — | Path to the output PDF (required) |
|
|
195
246
|
| `title` | `string` | input basename | `<title>` of the generated HTML |
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
247
|
+
| `theme` | `string` | `'default'` | Built-in theme: `default`/`academic`/`latex` |
|
|
248
|
+
| `css` | `string` | bundled default | Inline CSS string (overrides `theme`) |
|
|
249
|
+
| `cssFile` | `string` | — | Path to a CSS file (overrides `css` and `theme`) |
|
|
198
250
|
| `format` | `string` | `'A4'` | Puppeteer page format |
|
|
199
251
|
| `margin` | `object` | 22mm / 18mm | `{ top, bottom, left, right }` |
|
|
200
252
|
| `pageNumbers` | `boolean` | `true` | Render `n / total` in the footer |
|
|
@@ -205,6 +257,8 @@ await convert({
|
|
|
205
257
|
| `tocTitle` | `string` | `'Contents'` | TOC heading text |
|
|
206
258
|
| `highlight` | `boolean` | `false` | Syntax-highlight code blocks with Shiki |
|
|
207
259
|
| `codeTheme` | `string` | `'github-light'` | Shiki theme name for code blocks |
|
|
260
|
+
| `mermaid` | `boolean` | `false` | Render `mermaid` code blocks as diagrams |
|
|
261
|
+
| `mermaidTheme` | `string` | `'base'` | Mermaid theme name (light by default) |
|
|
208
262
|
| `cover` | `boolean` | front matter | Render a title page (`true`/`false` overrides YAML) |
|
|
209
263
|
| `headerTemplate` | `string` | empty | Puppeteer header HTML |
|
|
210
264
|
| `footerTemplate` | `string` | page numbers | Puppeteer footer HTML |
|
package/bin/md2pdf.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { convert } = require('../lib/index');
|
|
6
|
+
const { listThemes } = require('../lib/styles');
|
|
6
7
|
|
|
7
8
|
function printUsage() {
|
|
8
9
|
console.log(`Usage:
|
|
@@ -11,12 +12,15 @@ function printUsage() {
|
|
|
11
12
|
Options:
|
|
12
13
|
--title <text> Document title (defaults to input filename)
|
|
13
14
|
--css <file> Path to a custom CSS file (replaces the default styles)
|
|
15
|
+
--theme <name> Built-in theme: default, academic, latex
|
|
14
16
|
--format <size> Page format (A4, Letter, ...). Default: A4
|
|
15
17
|
--toc Prepend an auto-generated table of contents
|
|
16
18
|
--toc-depth <n> Max heading level included in the TOC. Default: 3
|
|
17
19
|
--toc-title <text> TOC heading text. Default: "Contents"
|
|
18
20
|
--highlight Syntax-highlight fenced code blocks (Shiki)
|
|
19
21
|
--code-theme <name> Shiki theme for code blocks. Default: github-light
|
|
22
|
+
--mermaid Render mermaid fenced code blocks as diagrams
|
|
23
|
+
--mermaid-theme <t> Mermaid theme: base, default, neutral, dark, forest. Default: base
|
|
20
24
|
--cover Render a title page from YAML front matter
|
|
21
25
|
--no-cover Never render a title page (overrides front matter)
|
|
22
26
|
--no-page-numbers Disable footer page numbers
|
|
@@ -43,14 +47,17 @@ function parseArgs(argv) {
|
|
|
43
47
|
if (a === '--keep-html') { args.flags.keepHtml = true; continue; }
|
|
44
48
|
if (a === '--toc') { args.flags.toc = true; continue; }
|
|
45
49
|
if (a === '--highlight') { args.flags.highlight = true; continue; }
|
|
50
|
+
if (a === '--mermaid') { args.flags.mermaid = true; continue; }
|
|
46
51
|
if (a === '--cover') { args.flags.cover = true; continue; }
|
|
47
52
|
if (a === '--no-cover') { args.flags.cover = false; continue; }
|
|
48
53
|
if (a === '--title') { args.flags.title = argv[++i]; continue; }
|
|
49
54
|
if (a === '--css') { args.flags.cssFile = argv[++i]; continue; }
|
|
55
|
+
if (a === '--theme') { args.flags.theme = argv[++i]; continue; }
|
|
50
56
|
if (a === '--format') { args.flags.format = argv[++i]; continue; }
|
|
51
57
|
if (a === '--toc-depth') { args.flags.tocDepth = parseInt(argv[++i], 10); continue; }
|
|
52
58
|
if (a === '--toc-title') { args.flags.tocTitle = argv[++i]; continue; }
|
|
53
59
|
if (a === '--code-theme') { args.flags.codeTheme = argv[++i]; continue; }
|
|
60
|
+
if (a === '--mermaid-theme') { args.flags.mermaidTheme = argv[++i]; continue; }
|
|
54
61
|
if (a.startsWith('--')) {
|
|
55
62
|
console.error(`Unknown option: ${a}`);
|
|
56
63
|
process.exit(2);
|
|
@@ -73,12 +80,21 @@ function parseArgs(argv) {
|
|
|
73
80
|
args.positional[1] ||
|
|
74
81
|
input.replace(/\.md$/i, '') + '.pdf';
|
|
75
82
|
|
|
83
|
+
if (args.flags.theme && !listThemes().includes(args.flags.theme)) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`WARNING: unknown theme "${args.flags.theme}", falling back to "default". ` +
|
|
86
|
+
`Available: ${listThemes().join(', ')}`
|
|
87
|
+
);
|
|
88
|
+
args.flags.theme = 'default';
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
try {
|
|
77
92
|
const result = await convert({
|
|
78
93
|
input,
|
|
79
94
|
output,
|
|
80
95
|
title: args.flags.title,
|
|
81
96
|
cssFile: args.flags.cssFile,
|
|
97
|
+
theme: args.flags.theme || 'default',
|
|
82
98
|
format: args.flags.format || 'A4',
|
|
83
99
|
pageNumbers: args.flags.pageNumbers !== false,
|
|
84
100
|
math: args.flags.math !== false,
|
|
@@ -88,6 +104,8 @@ function parseArgs(argv) {
|
|
|
88
104
|
tocTitle: args.flags.tocTitle,
|
|
89
105
|
highlight: !!args.flags.highlight,
|
|
90
106
|
codeTheme: args.flags.codeTheme,
|
|
107
|
+
mermaid: !!args.flags.mermaid,
|
|
108
|
+
mermaidTheme: args.flags.mermaidTheme || 'base',
|
|
91
109
|
cover: args.flags.cover,
|
|
92
110
|
keepHtml: !!args.flags.keepHtml,
|
|
93
111
|
});
|
package/lib/index.js
CHANGED
|
@@ -5,11 +5,12 @@ const path = require('path');
|
|
|
5
5
|
const { Marked } = require('marked');
|
|
6
6
|
const markedKatex = require('marked-katex-extension');
|
|
7
7
|
const puppeteer = require('puppeteer');
|
|
8
|
-
const { defaultCss, katexCssLink } = require('./styles');
|
|
8
|
+
const { defaultCss, katexCssLink, getThemeCss } = require('./styles');
|
|
9
9
|
const { sanitizeHtml } = require('./sanitize');
|
|
10
10
|
const { collectHeadings, buildTocHtml, slugify } = require('./toc');
|
|
11
11
|
const { createCodeHighlighter } = require('./highlight');
|
|
12
12
|
const { parseFrontMatter, buildCoverHtml } = require('./frontmatter');
|
|
13
|
+
const { getMermaidScript } = require('./mermaid');
|
|
13
14
|
|
|
14
15
|
// Recursively collect the fenced-code languages used anywhere in the document
|
|
15
16
|
// (including inside lists/blockquotes) so only those grammars are loaded.
|
|
@@ -27,7 +28,7 @@ function collectCodeLangs(tokens, out = new Set()) {
|
|
|
27
28
|
|
|
28
29
|
// Render Markdown to HTML using an isolated Marked instance so per-call state
|
|
29
30
|
// (heading ids, KaTeX extension, highlighter) never leaks across invocations.
|
|
30
|
-
async function renderMarkdown({ md, math, toc, tocDepth, tocTitle, highlight, codeTheme }) {
|
|
31
|
+
async function renderMarkdown({ md, math, toc, tocDepth, tocTitle, highlight, codeTheme, mermaid }) {
|
|
31
32
|
const m = new Marked();
|
|
32
33
|
m.setOptions({ gfm: true, breaks: false });
|
|
33
34
|
if (math) {
|
|
@@ -51,13 +52,17 @@ async function renderMarkdown({ md, math, toc, tocDepth, tocTitle, highlight, co
|
|
|
51
52
|
return `<h${level} id="${slug}">${text}</h${level}>\n`;
|
|
52
53
|
},
|
|
53
54
|
};
|
|
54
|
-
if (highlighter) {
|
|
55
|
+
if (highlighter || mermaid) {
|
|
55
56
|
renderer.code = (text, infostring) => {
|
|
56
57
|
const lang = (infostring || '').trim().split(/\s+/)[0];
|
|
57
|
-
if (
|
|
58
|
+
if (mermaid && lang === 'mermaid') {
|
|
59
|
+
return `<pre class="mermaid">${escapeHtml(text)}</pre>\n`;
|
|
60
|
+
}
|
|
61
|
+
if (highlighter && highlighter.supports(lang)) {
|
|
58
62
|
return highlighter.toHtml(text, lang);
|
|
59
63
|
}
|
|
60
|
-
|
|
64
|
+
const cls = lang ? ` class="language-${lang}"` : '';
|
|
65
|
+
return `<pre><code${cls}>${escapeHtml(text)}</code></pre>\n`;
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
m.use({ renderer });
|
|
@@ -135,6 +140,52 @@ function normalizeBlockMath(src) {
|
|
|
135
140
|
return out.join('\n');
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
// Page dimensions in millimetres (portrait) for the formats Puppeteer accepts.
|
|
144
|
+
const PAGE_SIZES_MM = {
|
|
145
|
+
a0: [841, 1189],
|
|
146
|
+
a1: [594, 841],
|
|
147
|
+
a2: [420, 594],
|
|
148
|
+
a3: [297, 420],
|
|
149
|
+
a4: [210, 297],
|
|
150
|
+
a5: [148, 210],
|
|
151
|
+
a6: [105, 148],
|
|
152
|
+
letter: [215.9, 279.4],
|
|
153
|
+
legal: [215.9, 355.6],
|
|
154
|
+
tabloid: [279.4, 431.8],
|
|
155
|
+
ledger: [431.8, 279.4],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const PX_PER_MM = 96 / 25.4;
|
|
159
|
+
|
|
160
|
+
function lengthToMm(value, fallbackMm) {
|
|
161
|
+
if (value == null) return fallbackMm;
|
|
162
|
+
const m = String(value).trim().match(/^([\d.]+)\s*(mm|cm|in|px|pt)?$/i);
|
|
163
|
+
if (!m) return fallbackMm;
|
|
164
|
+
const n = parseFloat(m[1]);
|
|
165
|
+
switch ((m[2] || 'mm').toLowerCase()) {
|
|
166
|
+
case 'cm': return n * 10;
|
|
167
|
+
case 'in': return n * 25.4;
|
|
168
|
+
case 'pt': return (n / 72) * 25.4;
|
|
169
|
+
case 'px': return n / PX_PER_MM;
|
|
170
|
+
default: return n;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Computes the usable content area (inside the page margins) in CSS pixels, so
|
|
175
|
+
// oversized diagrams can be scaled down to fit a single page.
|
|
176
|
+
function printableAreaPx(format, margin = {}) {
|
|
177
|
+
const size = PAGE_SIZES_MM[String(format || 'A4').toLowerCase()] || PAGE_SIZES_MM.a4;
|
|
178
|
+
const [wMm, hMm] = size;
|
|
179
|
+
const left = lengthToMm(margin.left, 0);
|
|
180
|
+
const right = lengthToMm(margin.right, 0);
|
|
181
|
+
const top = lengthToMm(margin.top, 0);
|
|
182
|
+
const bottom = lengthToMm(margin.bottom, 0);
|
|
183
|
+
return {
|
|
184
|
+
width: Math.max(1, (wMm - left - right) * PX_PER_MM),
|
|
185
|
+
height: Math.max(1, (hMm - top - bottom) * PX_PER_MM),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
138
189
|
async function convert(options) {
|
|
139
190
|
const {
|
|
140
191
|
input,
|
|
@@ -157,6 +208,9 @@ async function convert(options) {
|
|
|
157
208
|
highlight = false,
|
|
158
209
|
codeTheme = 'github-light',
|
|
159
210
|
cover,
|
|
211
|
+
theme = 'default',
|
|
212
|
+
mermaid = false,
|
|
213
|
+
mermaidTheme = 'base',
|
|
160
214
|
} = options;
|
|
161
215
|
|
|
162
216
|
if (!input) throw new Error('`input` is required');
|
|
@@ -180,6 +234,7 @@ async function convert(options) {
|
|
|
180
234
|
tocTitle,
|
|
181
235
|
highlight,
|
|
182
236
|
codeTheme,
|
|
237
|
+
mermaid,
|
|
183
238
|
});
|
|
184
239
|
|
|
185
240
|
const wantCover =
|
|
@@ -188,9 +243,13 @@ async function convert(options) {
|
|
|
188
243
|
const fullBody = [coverHtml, tocHtml, parsedBody].filter(Boolean).join('\n');
|
|
189
244
|
const body = sanitize ? sanitizeHtml(fullBody) : fullBody;
|
|
190
245
|
|
|
191
|
-
let resolvedCss
|
|
246
|
+
let resolvedCss;
|
|
192
247
|
if (cssFile) {
|
|
193
248
|
resolvedCss = fs.readFileSync(path.resolve(cssFile), 'utf8');
|
|
249
|
+
} else if (css) {
|
|
250
|
+
resolvedCss = css;
|
|
251
|
+
} else {
|
|
252
|
+
resolvedCss = getThemeCss(theme);
|
|
194
253
|
}
|
|
195
254
|
|
|
196
255
|
const docTitle =
|
|
@@ -228,6 +287,39 @@ async function convert(options) {
|
|
|
228
287
|
.map((img) => img.src)
|
|
229
288
|
);
|
|
230
289
|
|
|
290
|
+
if (mermaid && (await page.$('.mermaid'))) {
|
|
291
|
+
await page.addScriptTag({ content: getMermaidScript() });
|
|
292
|
+
const area = printableAreaPx(format, margin);
|
|
293
|
+
await page.evaluate(async (opts) => {
|
|
294
|
+
const { themeName, maxW, maxH } = opts;
|
|
295
|
+
window.mermaid.initialize({
|
|
296
|
+
startOnLoad: false,
|
|
297
|
+
securityLevel: 'strict',
|
|
298
|
+
theme: themeName,
|
|
299
|
+
});
|
|
300
|
+
await window.mermaid.run({
|
|
301
|
+
querySelector: '.mermaid',
|
|
302
|
+
suppressErrors: true,
|
|
303
|
+
});
|
|
304
|
+
// Scale any diagram larger than one page down to fit, keeping ratio.
|
|
305
|
+
document.querySelectorAll('.mermaid svg').forEach((svg) => {
|
|
306
|
+
const vb = svg.viewBox && svg.viewBox.baseVal;
|
|
307
|
+
let w = vb && vb.width;
|
|
308
|
+
let h = vb && vb.height;
|
|
309
|
+
if (!w || !h) {
|
|
310
|
+
const rect = svg.getBoundingClientRect();
|
|
311
|
+
w = rect.width;
|
|
312
|
+
h = rect.height;
|
|
313
|
+
}
|
|
314
|
+
if (!w || !h) return;
|
|
315
|
+
const scale = Math.min((maxW * 0.99) / w, (maxH * 0.98) / h, 1);
|
|
316
|
+
svg.style.maxWidth = 'none';
|
|
317
|
+
svg.style.width = `${Math.floor(w * scale)}px`;
|
|
318
|
+
svg.style.height = `${Math.floor(h * scale)}px`;
|
|
319
|
+
});
|
|
320
|
+
}, { themeName: mermaidTheme, maxW: area.width, maxH: area.height });
|
|
321
|
+
}
|
|
322
|
+
|
|
231
323
|
const pdfOptions = {
|
|
232
324
|
path: outputAbs,
|
|
233
325
|
format,
|
|
@@ -255,4 +347,4 @@ async function convert(options) {
|
|
|
255
347
|
}
|
|
256
348
|
}
|
|
257
349
|
|
|
258
|
-
module.exports = { convert, defaultCss };
|
|
350
|
+
module.exports = { convert, defaultCss, renderMarkdown, printableAreaPx };
|
package/lib/mermaid.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// Mermaid's bundled UMD build is large (~3 MB); read it lazily and cache it so
|
|
6
|
+
// it is only loaded into memory when a document actually uses diagrams.
|
|
7
|
+
let cachedScript = null;
|
|
8
|
+
|
|
9
|
+
function getMermaidScript() {
|
|
10
|
+
if (cachedScript != null) return cachedScript;
|
|
11
|
+
const scriptPath = require.resolve('mermaid/dist/mermaid.min.js');
|
|
12
|
+
cachedScript = fs.readFileSync(scriptPath, 'utf8');
|
|
13
|
+
return cachedScript;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = { getMermaidScript };
|
package/lib/styles.js
CHANGED
|
@@ -13,22 +13,72 @@ function katexCssLink() {
|
|
|
13
13
|
return `<link rel="stylesheet" href="${fileUrl}">`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Structural rules required by the converter's features (KaTeX, TOC, cover
|
|
17
|
+
// page, images, page-break safety). These are theme-agnostic and always
|
|
18
|
+
// applied; per-theme skins below only handle typography and colors.
|
|
19
|
+
const baseCss = `
|
|
17
20
|
@page { size: A4; margin: 22mm 18mm 22mm 18mm; }
|
|
18
21
|
* { box-sizing: border-box; }
|
|
22
|
+
body { margin: 0; max-width: 100%; }
|
|
23
|
+
h1, h2, h3, h4 { page-break-after: avoid; break-after: avoid; }
|
|
24
|
+
img {
|
|
25
|
+
max-width: 100%;
|
|
26
|
+
height: auto;
|
|
27
|
+
display: block;
|
|
28
|
+
margin: 12px auto;
|
|
29
|
+
page-break-inside: avoid;
|
|
30
|
+
break-inside: avoid;
|
|
31
|
+
}
|
|
32
|
+
table {
|
|
33
|
+
width: 100%;
|
|
34
|
+
border-collapse: collapse;
|
|
35
|
+
margin: 12px 0 18px 0;
|
|
36
|
+
page-break-inside: avoid;
|
|
37
|
+
}
|
|
38
|
+
pre { overflow-x: auto; page-break-inside: avoid; }
|
|
39
|
+
.mermaid {
|
|
40
|
+
text-align: center;
|
|
41
|
+
margin: 14px 0;
|
|
42
|
+
background: #ffffff;
|
|
43
|
+
color: #000;
|
|
44
|
+
padding: 8px 0;
|
|
45
|
+
border: none;
|
|
46
|
+
page-break-inside: avoid;
|
|
47
|
+
break-inside: avoid;
|
|
48
|
+
}
|
|
49
|
+
.mermaid svg { max-width: 100%; height: auto; background: #ffffff; }
|
|
50
|
+
.katex { font-size: 1.05em; }
|
|
51
|
+
.katex-display { margin: 14px 0; overflow-x: auto; overflow-y: hidden; page-break-inside: avoid; }
|
|
52
|
+
.katex-display > .katex { display: inline-block; text-align: center; max-width: 100%; }
|
|
53
|
+
nav.toc { page-break-after: always; break-after: page; margin-bottom: 8px; }
|
|
54
|
+
nav.toc .toc-title { margin-top: 0; padding-bottom: 4px; }
|
|
55
|
+
nav.toc ul { list-style: none; margin: 4px 0; padding-left: 18px; }
|
|
56
|
+
nav.toc > ul { padding-left: 0; }
|
|
57
|
+
nav.toc li { margin-bottom: 3px; }
|
|
58
|
+
section.cover {
|
|
59
|
+
page-break-after: always;
|
|
60
|
+
break-after: page;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
text-align: center;
|
|
66
|
+
min-height: 80vh;
|
|
67
|
+
}
|
|
68
|
+
section.cover .cover-title { font-size: 30pt; border: none; margin: 0 0 8px 0; padding: 0; }
|
|
69
|
+
section.cover .cover-subtitle { font-size: 15pt; margin: 0 0 28px 0; }
|
|
70
|
+
section.cover .cover-author { font-size: 13pt; margin: 0 0 6px 0; }
|
|
71
|
+
section.cover .cover-date { font-size: 11pt; margin: 0; }
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const defaultSkin = `
|
|
19
75
|
body {
|
|
20
76
|
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
21
77
|
font-size: 11pt;
|
|
22
78
|
line-height: 1.55;
|
|
23
79
|
color: #1a1a1a;
|
|
24
|
-
max-width: 100%;
|
|
25
|
-
margin: 0;
|
|
26
|
-
}
|
|
27
|
-
h1, h2, h3, h4 {
|
|
28
|
-
color: #102a43;
|
|
29
|
-
page-break-after: avoid;
|
|
30
|
-
break-after: avoid;
|
|
31
80
|
}
|
|
81
|
+
h1, h2, h3, h4 { color: #102a43; }
|
|
32
82
|
h1 { font-size: 22pt; border-bottom: 2px solid #102a43; padding-bottom: 6px; margin-top: 0; }
|
|
33
83
|
h2 { font-size: 16pt; border-bottom: 1px solid #bcccdc; padding-bottom: 4px; margin-top: 28px; }
|
|
34
84
|
h3 { font-size: 13pt; margin-top: 22px; }
|
|
@@ -45,111 +95,119 @@ code {
|
|
|
45
95
|
font-size: 9.5pt;
|
|
46
96
|
color: #b91c1c;
|
|
47
97
|
}
|
|
48
|
-
pre {
|
|
49
|
-
background: #0f172a;
|
|
50
|
-
color: #f1f5f9;
|
|
51
|
-
padding: 12px;
|
|
52
|
-
border-radius: 5px;
|
|
53
|
-
overflow-x: auto;
|
|
54
|
-
font-size: 9pt;
|
|
55
|
-
}
|
|
98
|
+
pre { background: #0f172a; color: #f1f5f9; padding: 12px; border-radius: 5px; font-size: 9pt; }
|
|
56
99
|
pre code { background: transparent; color: inherit; padding: 0; }
|
|
57
|
-
table {
|
|
58
|
-
|
|
59
|
-
border-collapse: collapse;
|
|
60
|
-
margin: 12px 0 18px 0;
|
|
61
|
-
page-break-inside: avoid;
|
|
62
|
-
font-size: 9.5pt;
|
|
63
|
-
}
|
|
64
|
-
th, td {
|
|
65
|
-
border: 1px solid #cbd5e1;
|
|
66
|
-
padding: 6px 9px;
|
|
67
|
-
text-align: left;
|
|
68
|
-
vertical-align: top;
|
|
69
|
-
}
|
|
100
|
+
table { font-size: 9.5pt; }
|
|
101
|
+
th, td { border: 1px solid #cbd5e1; padding: 6px 9px; text-align: left; vertical-align: top; }
|
|
70
102
|
th { background: #e2e8f0; color: #0b2447; font-weight: 600; }
|
|
71
103
|
tr:nth-child(even) td { background: #f8fafc; }
|
|
72
104
|
hr { border: none; border-top: 1px solid #cbd5e1; margin: 24px 0; }
|
|
73
|
-
blockquote {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
color: #475569;
|
|
77
|
-
background: #f8fafc;
|
|
78
|
-
margin: 10px 0;
|
|
79
|
-
}
|
|
80
|
-
img {
|
|
81
|
-
max-width: 100%;
|
|
82
|
-
height: auto;
|
|
83
|
-
display: block;
|
|
84
|
-
margin: 12px auto;
|
|
85
|
-
border: 1px solid #e2e8f0;
|
|
86
|
-
border-radius: 4px;
|
|
87
|
-
page-break-inside: avoid;
|
|
88
|
-
break-inside: avoid;
|
|
89
|
-
}
|
|
90
|
-
img + em, p > em {
|
|
91
|
-
display: block;
|
|
92
|
-
text-align: center;
|
|
93
|
-
color: #475569;
|
|
94
|
-
font-size: 9.5pt;
|
|
95
|
-
margin-bottom: 14px;
|
|
96
|
-
}
|
|
105
|
+
blockquote { border-left: 4px solid #64748b; padding: 4px 12px; color: #475569; background: #f8fafc; margin: 10px 0; }
|
|
106
|
+
img { border: 1px solid #e2e8f0; border-radius: 4px; }
|
|
107
|
+
img + em, p > em { display: block; text-align: center; color: #475569; font-size: 9.5pt; margin-bottom: 14px; }
|
|
97
108
|
a { color: #1d4ed8; text-decoration: none; }
|
|
98
|
-
nav.toc {
|
|
99
|
-
page-break-after: always;
|
|
100
|
-
break-after: page;
|
|
101
|
-
margin-bottom: 8px;
|
|
102
|
-
}
|
|
103
|
-
nav.toc .toc-title {
|
|
104
|
-
margin-top: 0;
|
|
105
|
-
border-bottom: 1px solid #bcccdc;
|
|
106
|
-
padding-bottom: 4px;
|
|
107
|
-
}
|
|
108
|
-
nav.toc ul {
|
|
109
|
-
list-style: none;
|
|
110
|
-
margin: 4px 0;
|
|
111
|
-
padding-left: 18px;
|
|
112
|
-
}
|
|
113
|
-
nav.toc > ul { padding-left: 0; }
|
|
114
|
-
nav.toc li { margin-bottom: 3px; }
|
|
109
|
+
nav.toc .toc-title { border-bottom: 1px solid #bcccdc; }
|
|
115
110
|
nav.toc a { color: #102a43; }
|
|
116
|
-
section.cover {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
111
|
+
section.cover .cover-subtitle { color: #475569; }
|
|
112
|
+
section.cover .cover-author { color: #102a43; }
|
|
113
|
+
section.cover .cover-date { color: #64748b; }
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
// Emulates the classic LaTeX `article` look: Computer Modern serif, justified
|
|
117
|
+
// and indented paragraphs, booktabs-style rules, centered title.
|
|
118
|
+
const latexSkin = `
|
|
119
|
+
body {
|
|
120
|
+
font-family: "Latin Modern Roman", "CMU Serif", "Computer Modern", "Georgia", "Times New Roman", serif;
|
|
121
|
+
font-size: 11pt;
|
|
122
|
+
line-height: 1.5;
|
|
123
|
+
color: #000;
|
|
124
|
+
}
|
|
125
|
+
h1, h2, h3, h4 { color: #000; font-weight: 700; }
|
|
126
|
+
h1 { font-size: 19pt; text-align: center; margin-top: 0; margin-bottom: 20px; }
|
|
127
|
+
h2 { font-size: 14pt; margin-top: 24px; }
|
|
128
|
+
h3 { font-size: 12pt; margin-top: 18px; }
|
|
129
|
+
h4 { font-size: 11pt; margin-top: 14px; font-style: italic; font-weight: 600; }
|
|
130
|
+
p { text-align: justify; margin: 0 0 2px 0; text-indent: 1.5em; }
|
|
131
|
+
p:first-of-type, h1 + p, h2 + p, h3 + p, h4 + p { text-indent: 0; }
|
|
132
|
+
ul, ol { margin: 6px 0 10px 0; padding-left: 24px; }
|
|
133
|
+
li { margin-bottom: 2px; }
|
|
134
|
+
code {
|
|
135
|
+
background: #f4f4f4;
|
|
136
|
+
padding: 1px 4px;
|
|
137
|
+
border-radius: 2px;
|
|
138
|
+
font-family: "Consolas", "Courier New", monospace;
|
|
139
|
+
font-size: 9.5pt;
|
|
140
|
+
color: #222;
|
|
131
141
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
pre { background: #f4f4f4; color: #1a1a1a; padding: 12px; border: 1px solid #ddd; border-radius: 3px; font-size: 9pt; }
|
|
143
|
+
pre code { background: transparent; color: inherit; padding: 0; }
|
|
144
|
+
table { font-size: 10pt; margin: 14px auto; border-top: 1.5px solid #000; border-bottom: 1.5px solid #000; }
|
|
145
|
+
th, td { border: none; padding: 5px 12px; text-align: left; vertical-align: top; }
|
|
146
|
+
th { border-bottom: 1px solid #000; font-weight: 700; }
|
|
147
|
+
hr { border: none; border-top: 1px solid #000; margin: 22px 0; }
|
|
148
|
+
blockquote { border-left: 2px solid #000; padding: 2px 14px; color: #1a1a1a; margin: 10px 24px; }
|
|
149
|
+
img + em, p > em { display: block; text-align: center; color: #333; font-size: 9.5pt; margin-bottom: 14px; }
|
|
150
|
+
a { color: #000; text-decoration: none; }
|
|
151
|
+
nav.toc .toc-title { border-bottom: 1px solid #000; text-align: center; }
|
|
152
|
+
nav.toc a { color: #000; }
|
|
153
|
+
section.cover .cover-subtitle { color: #333; }
|
|
154
|
+
section.cover .cover-date { color: #333; }
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const academicSkin = `
|
|
158
|
+
body {
|
|
159
|
+
font-family: "Georgia", "Times New Roman", "Cambria", serif;
|
|
160
|
+
font-size: 11.5pt;
|
|
161
|
+
line-height: 1.6;
|
|
162
|
+
color: #161616;
|
|
163
|
+
}
|
|
164
|
+
h1, h2, h3, h4 { color: #161616; font-weight: 700; font-family: "Georgia", "Times New Roman", serif; }
|
|
165
|
+
h1 { font-size: 20pt; text-align: center; margin-top: 0; margin-bottom: 18px; }
|
|
166
|
+
h2 { font-size: 15pt; margin-top: 26px; }
|
|
167
|
+
h3 { font-size: 12.5pt; margin-top: 20px; font-style: italic; }
|
|
168
|
+
h4 { font-size: 11.5pt; margin-top: 16px; }
|
|
169
|
+
p { text-align: justify; margin: 0 0 4px 0; text-indent: 1.6em; }
|
|
170
|
+
p:first-of-type, h1 + p, h2 + p, h3 + p, h4 + p { text-indent: 0; }
|
|
171
|
+
ul, ol { margin: 8px 0 12px 0; padding-left: 26px; }
|
|
172
|
+
li { margin-bottom: 3px; }
|
|
173
|
+
code {
|
|
174
|
+
background: #f2f2f0;
|
|
175
|
+
padding: 1px 4px;
|
|
176
|
+
border-radius: 2px;
|
|
177
|
+
font-family: "Consolas", "Courier New", monospace;
|
|
178
|
+
font-size: 9.5pt;
|
|
179
|
+
color: #333;
|
|
137
180
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
181
|
+
pre { background: #f2f2f0; color: #1a1a1a; padding: 12px; border: 1px solid #ddd; border-radius: 3px; font-size: 9pt; }
|
|
182
|
+
pre code { background: transparent; color: inherit; padding: 0; }
|
|
183
|
+
table { font-size: 10pt; margin: 14px auto; }
|
|
184
|
+
th, td { border: 1px solid #999; padding: 5px 10px; text-align: left; vertical-align: top; }
|
|
185
|
+
th { background: #ececec; font-weight: 700; }
|
|
186
|
+
hr { border: none; border-top: 1px solid #999; margin: 22px 0; }
|
|
187
|
+
blockquote { border-left: 3px solid #888; padding: 2px 14px; color: #333; font-style: italic; margin: 10px 20px; }
|
|
188
|
+
img + em, p > em { display: block; text-align: center; color: #444; font-size: 9.5pt; margin-bottom: 14px; }
|
|
189
|
+
a { color: #1a1a1a; text-decoration: underline; }
|
|
190
|
+
nav.toc .toc-title { border-bottom: 1px solid #999; text-align: center; }
|
|
191
|
+
nav.toc a { color: #161616; }
|
|
192
|
+
section.cover .cover-subtitle { color: #444; font-style: italic; }
|
|
193
|
+
section.cover .cover-date { color: #555; }
|
|
194
|
+
`;
|
|
195
|
+
|
|
196
|
+
const themes = {
|
|
197
|
+
default: defaultSkin,
|
|
198
|
+
academic: academicSkin,
|
|
199
|
+
latex: latexSkin,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
function listThemes() {
|
|
203
|
+
return Object.keys(themes);
|
|
143
204
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
text-align: center;
|
|
205
|
+
|
|
206
|
+
function getThemeCss(name) {
|
|
207
|
+
const skin = themes[name] || themes.default;
|
|
208
|
+
return `${baseCss}\n${skin}`;
|
|
149
209
|
}
|
|
150
|
-
.katex { font-size: 1.05em; }
|
|
151
|
-
.katex-display { margin: 14px 0; overflow-x: auto; overflow-y: hidden; page-break-inside: avoid; }
|
|
152
|
-
.katex-display > .katex { display: inline-block; text-align: center; max-width: 100%; }
|
|
153
|
-
`;
|
|
154
210
|
|
|
155
|
-
|
|
211
|
+
const defaultCss = getThemeCss('default');
|
|
212
|
+
|
|
213
|
+
module.exports = { defaultCss, katexCssLink, getThemeCss, listThemes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brahim.ariani/md2pdf-cli",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Convert Markdown files to beautifully styled PDFs using marked and puppeteer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -31,11 +31,13 @@
|
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"test": "node test/sanitize.js && node test/toc.js && node test/highlight.js && node test/frontmatter.js && node test/smoke.js",
|
|
34
|
+
"test": "node test/sanitize.js && node test/toc.js && node test/highlight.js && node test/frontmatter.js && node test/themes.js && node test/mermaid.js && node test/smoke.js",
|
|
35
35
|
"test:sanitize": "node test/sanitize.js",
|
|
36
36
|
"test:toc": "node test/toc.js",
|
|
37
37
|
"test:highlight": "node test/highlight.js",
|
|
38
|
-
"test:frontmatter": "node test/frontmatter.js"
|
|
38
|
+
"test:frontmatter": "node test/frontmatter.js",
|
|
39
|
+
"test:themes": "node test/themes.js",
|
|
40
|
+
"test:mermaid": "node test/mermaid.js"
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
41
43
|
"dompurify": "^3.4.7",
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
"katex": "^0.16.9",
|
|
45
47
|
"marked": "^12.0.0",
|
|
46
48
|
"marked-katex-extension": "^5.0.0",
|
|
49
|
+
"mermaid": "^11.15.0",
|
|
47
50
|
"puppeteer": "^24.15.0",
|
|
48
51
|
"shiki": "^4.1.0"
|
|
49
52
|
},
|