@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 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
- | `css` | `string` | bundled default | Inline CSS string |
197
- | `cssFile` | `string` | | Path to a CSS file (overrides `css`) |
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 (highlighter.supports(lang)) {
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
- return `<pre><code>${escapeHtml(text)}</code></pre>\n`;
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 = css || defaultCss;
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
- const defaultCss = `
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
- width: 100%;
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
- border-left: 4px solid #64748b;
75
- padding: 4px 12px;
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
- page-break-after: always;
118
- break-after: page;
119
- display: flex;
120
- flex-direction: column;
121
- align-items: center;
122
- justify-content: center;
123
- text-align: center;
124
- min-height: 80vh;
125
- }
126
- section.cover .cover-title {
127
- font-size: 30pt;
128
- border: none;
129
- margin: 0 0 8px 0;
130
- padding: 0;
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
- section.cover .cover-subtitle {
133
- font-size: 15pt;
134
- color: #475569;
135
- margin: 0 0 28px 0;
136
- text-align: center;
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
- section.cover .cover-author {
139
- font-size: 13pt;
140
- color: #102a43;
141
- margin: 0 0 6px 0;
142
- text-align: center;
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
- section.cover .cover-date {
145
- font-size: 11pt;
146
- color: #64748b;
147
- margin: 0;
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
- module.exports = { defaultCss, katexCssLink };
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.0",
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
  },