@brahim.ariani/md2pdf-cli 1.1.0 → 1.2.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 +58 -4
- package/bin/md2pdf.js +18 -0
- package/lib/index.js +35 -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 });
|
|
@@ -157,6 +162,9 @@ async function convert(options) {
|
|
|
157
162
|
highlight = false,
|
|
158
163
|
codeTheme = 'github-light',
|
|
159
164
|
cover,
|
|
165
|
+
theme = 'default',
|
|
166
|
+
mermaid = false,
|
|
167
|
+
mermaidTheme = 'base',
|
|
160
168
|
} = options;
|
|
161
169
|
|
|
162
170
|
if (!input) throw new Error('`input` is required');
|
|
@@ -180,6 +188,7 @@ async function convert(options) {
|
|
|
180
188
|
tocTitle,
|
|
181
189
|
highlight,
|
|
182
190
|
codeTheme,
|
|
191
|
+
mermaid,
|
|
183
192
|
});
|
|
184
193
|
|
|
185
194
|
const wantCover =
|
|
@@ -188,9 +197,13 @@ async function convert(options) {
|
|
|
188
197
|
const fullBody = [coverHtml, tocHtml, parsedBody].filter(Boolean).join('\n');
|
|
189
198
|
const body = sanitize ? sanitizeHtml(fullBody) : fullBody;
|
|
190
199
|
|
|
191
|
-
let resolvedCss
|
|
200
|
+
let resolvedCss;
|
|
192
201
|
if (cssFile) {
|
|
193
202
|
resolvedCss = fs.readFileSync(path.resolve(cssFile), 'utf8');
|
|
203
|
+
} else if (css) {
|
|
204
|
+
resolvedCss = css;
|
|
205
|
+
} else {
|
|
206
|
+
resolvedCss = getThemeCss(theme);
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
const docTitle =
|
|
@@ -228,6 +241,21 @@ async function convert(options) {
|
|
|
228
241
|
.map((img) => img.src)
|
|
229
242
|
);
|
|
230
243
|
|
|
244
|
+
if (mermaid && (await page.$('.mermaid'))) {
|
|
245
|
+
await page.addScriptTag({ content: getMermaidScript() });
|
|
246
|
+
await page.evaluate(async (themeName) => {
|
|
247
|
+
window.mermaid.initialize({
|
|
248
|
+
startOnLoad: false,
|
|
249
|
+
securityLevel: 'strict',
|
|
250
|
+
theme: themeName,
|
|
251
|
+
});
|
|
252
|
+
await window.mermaid.run({
|
|
253
|
+
querySelector: '.mermaid',
|
|
254
|
+
suppressErrors: true,
|
|
255
|
+
});
|
|
256
|
+
}, mermaidTheme);
|
|
257
|
+
}
|
|
258
|
+
|
|
231
259
|
const pdfOptions = {
|
|
232
260
|
path: outputAbs,
|
|
233
261
|
format,
|
|
@@ -255,4 +283,4 @@ async function convert(options) {
|
|
|
255
283
|
}
|
|
256
284
|
}
|
|
257
285
|
|
|
258
|
-
module.exports = { convert, defaultCss };
|
|
286
|
+
module.exports = { convert, defaultCss, renderMarkdown };
|
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.
|
|
3
|
+
"version": "1.2.0",
|
|
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
|
},
|