md-pdf-studio 2.3.0 → 2.4.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
@@ -8,9 +8,13 @@
8
8
  [![npm downloads](https://img.shields.io/npm/d18m/md-pdf-studio)](https://www.npmjs.com/package/md-pdf-studio)
9
9
  ![Browser Extension](https://img.shields.io/badge/Extension-Manifest_V3-green)
10
10
  ![License](https://img.shields.io/badge/license-MIT-green)
11
+ [![Official vannt-dev build](https://vannt-dev.github.io/badges/official.svg)](https://vannt-dev.github.io/official.html)
11
12
 
12
13
  ---
13
14
 
15
+ Official builds are listed at [vannt-dev.github.io/official.html](https://vannt-dev.github.io/official.html);
16
+ anything else is not published by vannt-dev.
17
+
14
18
  ## ✨ Features
15
19
 
16
20
  - 🧩 **Browser Extension (Manifest V3)**:
@@ -19,7 +23,9 @@
19
23
  - Convert active web page or GitHub raw Markdown files with 1 click.
20
24
  - Context menu integration: Right-click selected Markdown text or any page -> "Convert to PDF".
21
25
  - 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.
26
+ - Page numbers and a running header/footer, set in the popup (off by default).
27
+ - 📑 **Auto Cover Page Generator**: Generate elegant document report cover pages via YAML Front Matter (`cover: true`) or `--cover` flag. The author and date labels are in English, or in Vietnamese with `lang: vi` in the front matter.
28
+ - 🔢 **Page Numbers & Running Header/Footer**: `--page-numbers` prints "Page 3 of 12" at the foot of each page (`--page-numbers simple` prints "3"). `--header "text"` and `--footer "text"` print a line of text at the top and foot of each page, with `{title}`, `{author}`, `{date}`, `{page}` and `{pages}` filled in (`--header "{title}"` prints the document title, `""` prints nothing). Both need their text right after the flag: `md2pdf --header README.md` is refused, because `README.md` is an input file. `--page-numbers` takes `full` or `simple` as its value and nothing else, so `md2pdf --page-numbers README.md` converts `README.md`. With both a footer and page numbers, the footer text sits left and the numbers right. The cover page is printed without them, but counts as page 1. All off by default; front matter keys `header`, `footer` and `pageNumbers` do the same. PDF only (the CLI and the extension); Word and EPUB leave them out.
23
29
  - 📌 **Dotted Leader Line Table of Contents (TOC)**: Auto-generates publication-grade TOC with dotted leader lines (`...`) and clickable anchor links (`--toc`).
24
30
  - 🎨 **7 Preset Themes**: `modern` (default), `dark`, `academic`, `github`, `ebook`, `cyberpunk`, and `minimal`.
25
31
  - 🔤 **Open-Source Font Selector**: 100% free under SIL OFL / Apache 2.0 (`Inter`, `Roboto`, `Lora`, `Merriweather`, `JetBrains Mono`, `Fira Code`).
@@ -31,6 +37,7 @@
31
37
  - 📐 **LaTeX Math Support**: Mathematical equations rendered with KaTeX.
32
38
  - 📝 **Footnotes**: `text[^1]` with `[^1]: the note` anywhere in the file. Notes are numbered in the order they are first referenced, gathered at the end of the document, and linked both ways. Works in the CLI and in the extension preview.
33
39
  - 📘 **Word & EPUB Export (CLI)**: `-f docx` writes a Word document and `-f epub` an EPUB 3 e-book, straight from the Markdown and without a browser. Headings, lists (nested, numbered from any start, task lists), tables with their alignment, code blocks, quotes, footnotes, links and local images become the real thing in each format — Word styles you can restyle, a chapter per top-level heading and a navigation menu in the EPUB. `--toc`, `--cover`, page size, orientation and margins apply to Word; `--toc` and `--cover` to EPUB. Themes, custom CSS, Mermaid diagrams and KaTeX math are for PDF and HTML only: a diagram or formula is kept as its source. An image on the web is not downloaded — it is named in Word and becomes a link in the EPUB — and a link to a neighbouring file keeps its text only.
40
+ - 🔖 **PDF Bookmarks (CLI)**: The PDF gets an outline (bookmarks) built from its headings, nested by level, so a PDF viewer's sidebar can jump to any section. On by default; `--no-outline` or `outline: false` in the front matter leaves it out. The extension prints through the browser's print dialog, which does not add bookmarks.
34
41
  - 📎 **File Includes (CLI)**: A line that holds only `@import "chapters/intro.md"` is replaced by that file, relative to the file the line is in. A Markdown file is inserted as Markdown (without its front matter) and may import further files; any other file — `@import "src/app.js"` — is inserted as a code block labelled with its extension. Lines inside a code block are left alone, a missing file or files that import each other stop the conversion with the file and line, and watch mode follows the imported files. `--no-include` turns it off.
35
42
  - 💡 **GitHub Callout Box Alerts**: Support for `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!IMPORTANT]`, and `> [!CAUTION]`.
36
43
  - 💻 **ASCII UI Mockups**: Styled dark-theme boxes for terminal output & ASCII wireframe mockups.
@@ -123,6 +130,9 @@ toc: true
123
130
  | `--font <font>` | | Font family: `Inter`, `Roboto`, `Lora`, `Merriweather`, `JetBrains Mono`, `Fira Code` | `Inter` |
124
131
  | `--cover` | | Auto-generate document report Cover Page | `false` |
125
132
  | `--toc` | | Auto-generate Table of Contents with Dotted Leader Lines | `false` |
133
+ | `--page-numbers [style]` | | Page numbers at the foot of each page: `full` ("Page 3 of 12") or `simple` ("3"); any other next argument stays an input | off (`full` when given alone) |
134
+ | `--header <text>` | | Running header; `{title}`, `{author}`, `{date}`, `{page}`, `{pages}` are filled in; `""` for none | off |
135
+ | `--footer <text>` | | Running footer, same placeholders; `""` for none | off |
126
136
  | `-p, --page-size <size>`| | Paper size: `A4`, `Letter`, `A3`, `Legal` | `A4` |
127
137
  | `-l, --landscape` | | Use landscape page orientation | `false` |
128
138
  | `-m, --margin <margin>`| | Custom page margin (e.g. `14mm 12mm 16mm 12mm`) | `14mm 12mm 16mm 12mm` |
@@ -132,6 +142,7 @@ toc: true
132
142
  | `--no-katex` | | Disable KaTeX math formula rendering | `false` |
133
143
  | `--no-highlight` | | Disable syntax highlighting of code blocks | `false` |
134
144
  | `--no-include` | | Leave `@import "file"` lines as they are instead of inserting the file | `false` |
145
+ | `--no-outline` | | Leave out the PDF bookmarks (outline) made from the headings | `false` |
135
146
 
136
147
  ---
137
148
 
@@ -152,6 +163,13 @@ font: Inter
152
163
  pageSize: A4
153
164
  landscape: false
154
165
  highlight: true
166
+ # document language (default en): cover labels and the page's lang
167
+ lang: en
168
+ # running header and footer; header: true also means the title
169
+ header: "{title}"
170
+ footer: "Engineering Team"
171
+ # full ("Page 3 of 12"), simple ("3") or false
172
+ pageNumbers: full
155
173
  ---
156
174
  # Document Executive Summary
157
175
  ...
@@ -159,13 +177,10 @@ highlight: true
159
177
 
160
178
  ---
161
179
 
162
- ## 🌿 Git Flow & Automated Release Workflow
163
-
164
- This project follows standard **Git Flow** branching and automated release publishing:
180
+ ## 🌿 Branching & Automated Release Workflow
165
181
 
166
182
  ### 🔀 Branching Strategy
167
- - **`main`**: Production-ready branch. Stores stable releases. Pushing a version tag `v*` from `main` triggers a release.
168
- - **`develop`**: Active development branch. Daily commits, feature additions, and refactoring take place here.
183
+ - **`main`**: The default branch. Changes arrive through pull requests from short-lived feature branches, and pushing a version tag `v*` from `main` triggers a release.
169
184
 
170
185
  ### 🚀 Automated Release Pipeline
171
186
  Whenever a version tag `v*` is pushed, GitHub Actions ([`.github/workflows/release.yml`](.github/workflows/release.yml)) builds from that tag and automatically:
@@ -219,18 +234,20 @@ convert-md-to-pdf/
219
234
  │ │ ├── utils.js # Preprocessors & helper re-exports
220
235
  │ │ ├── parser.js # Pipeline parser (Markdown -> HTML)
221
236
  │ │ ├── templates.js # HTML document template builder
237
+ │ │ ├── pagemarks.js # Page numbers & running header/footer CSS (shared with the extension)
222
238
  │ │ └── index.js # Main library API entrypoint
239
+ │ ├── cli/ # Command line option checks (options.js)
223
240
  │ └── browser/ # Headless browser rendering engine
224
- │ └── renderer.js # Puppeteer Chrome/Edge launcher & PDF worker pool
241
+ │ └── renderer.js # Runs installed Chrome/Edge headless (--print-to-pdf) & PDF worker pool
225
242
  ├── extension/ # Browser Extension (Manifest V3)
226
243
  │ ├── manifest.json
227
244
  │ ├── background/ # Service worker & context menus
228
245
  │ ├── content/ # Content script for raw MD pages
229
246
  │ ├── popup/ # Extension Popup UI (Batch file, Paste, Page, History)
230
247
  │ ├── preview/ # Print Preview & PDF/HTML export page
231
- │ ├── lib/ # Vendor libraries (marked, mermaid, katex)
248
+ │ ├── lib/ # Vendor libraries (marked, mermaid, katex) & code shared with the CLI
232
249
  │ └── icons/ # Extension icons (16, 48, 128)
233
- ├── tests/ # Standalone test suite (7/7 PASS)
250
+ ├── tests/ # Standalone test suite (`npm test`)
234
251
  │ └── index.test.js
235
252
  ├── dist/ # Built Chrome Extension zip artifacts
236
253
  ├── examples/ # Usage examples & custom CSS templates
package/bin/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
2
3
 
3
4
  const { Command } = require('commander');
4
5
  const path = require('path');
5
6
  const fs = require('fs');
6
7
  const { convertMarkdownToPdf } = require('../src/core/index');
8
+ const { normalizePageNumbersArgs, parseMarginText } = require('../src/cli/options');
7
9
  const packageJson = require('../package.json');
8
10
 
9
11
  const OUTPUT_FORMATS = ['pdf', 'html', 'docx', 'epub'];
@@ -28,12 +30,16 @@ program
28
30
  .option('-l, --landscape', 'Use landscape orientation', false)
29
31
  .option('-m, --margin <margin>', 'Page margin (e.g. 14mm 12mm 16mm 12mm)', '14mm 12mm 16mm 12mm')
30
32
  .option('--toc', 'Auto-generate Table of Contents (TOC)', false)
33
+ .option('--page-numbers [style]', 'Print page numbers in the footer: full ("Page 3 of 12", default) or simple ("3")')
34
+ .option('--header <text>', 'Running header text; {title}, {author}, {date}, {page}, {pages} are filled in ("" for none)', parseMarginText)
35
+ .option('--footer <text>', 'Running footer text, same placeholders ("" for none)', parseMarginText)
31
36
  .option('-c, --css <path>', 'Custom CSS stylesheet file path to inject')
32
37
  .option('-k, --keep-html', 'Keep temporary HTML file after conversion', false)
33
38
  .option('--no-mermaid', 'Disable Mermaid diagram rendering')
34
39
  .option('--no-katex', 'Disable KaTeX math formula rendering')
35
40
  .option('--no-highlight', 'Disable syntax highlighting of code blocks')
36
41
  .option('--no-include', 'Leave @import "file" lines as they are instead of inserting the file')
42
+ .option('--no-outline', 'Leave out the PDF bookmarks (outline) made from the headings')
37
43
  .option('-b, --browser <path>', 'Custom Chrome/Edge executable path')
38
44
  .action(async (inputArgs, options) => {
39
45
  try {
@@ -60,6 +66,7 @@ program
60
66
  console.log(` Paper: ${options.pageSize} (${options.landscape ? 'Landscape' : 'Portrait'})`);
61
67
  if (options.cover) console.log(` Cover: Auto-generated Cover Page Enabled`);
62
68
  if (options.toc) console.log(` TOC: Auto-generated Table of Contents Enabled`);
69
+ if (options.pageNumbers) console.log(` Pages: Page numbers (${options.pageNumbers === true ? 'full' : options.pageNumbers})`);
63
70
  if (options.css) console.log(` CSS: ${path.resolve(options.css)}`);
64
71
 
65
72
  let targetOutputDir = null;
@@ -97,12 +104,17 @@ program
97
104
  margin: options.margin,
98
105
  toc: options.toc,
99
106
  cover: options.cover,
107
+ header: options.header,
108
+ footer: options.footer,
109
+ pageNumbers: options.pageNumbers,
100
110
  cssFile: options.css,
101
111
  keepHtml: options.keepHtml,
102
112
  mermaid: options.mermaid,
103
113
  katex: options.katex,
104
114
  highlight: options.highlight,
105
115
  include: options.include,
116
+ // Only an explicit --no-outline overrides `outline:` in the front matter.
117
+ outline: options.outline === false ? false : undefined,
106
118
  executablePath: options.browser
107
119
  });
108
120
 
@@ -206,4 +218,4 @@ function expandInputFiles(args) {
206
218
  return Array.from(filesSet);
207
219
  }
208
220
 
209
- program.parse(process.argv);
221
+ program.parse(normalizePageNumbersArgs(process.argv));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md-pdf-studio",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
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. The CLI also writes Word (.docx) and EPUB.",
5
5
  "main": "src/core/index.js",
6
6
  "bin": {
@@ -59,6 +59,6 @@
59
59
  "@commitlint/cli": "^21.2.3",
60
60
  "@commitlint/config-conventional": "^21.2.3",
61
61
  "adm-zip": "^0.6.0",
62
- "lefthook": "^2.1.14"
62
+ "lefthook": "^2.2.1"
63
63
  }
64
64
  }
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const os = require('os');
@@ -48,6 +49,29 @@ function findBrowserExecutable() {
48
49
  return null;
49
50
  }
50
51
 
52
+ /**
53
+ * The command line that prints a page to PDF with headless Chrome/Edge.
54
+ * @param {string} fileUrl Page to print
55
+ * @param {string} outputPath PDF to write
56
+ * @param {Object} [options]
57
+ * @param {boolean} [options.legacyHeadless] Use `--headless` for browsers without `--headless=new`
58
+ * @param {boolean} [options.outline] Add PDF bookmarks from the headings (default true)
59
+ * @returns {string[]}
60
+ */
61
+ function buildBrowserArgs(fileUrl, outputPath, options = {}) {
62
+ return [
63
+ options.legacyHeadless ? '--headless' : '--headless=new',
64
+ '--disable-gpu',
65
+ '--no-sandbox',
66
+ '--no-pdf-header-footer',
67
+ // Bookmarks from h1-h6. A browser that does not know the switch ignores it.
68
+ ...(options.outline === false ? [] : ['--generate-pdf-document-outline']),
69
+ '--virtual-time-budget=8000',
70
+ `--print-to-pdf=${outputPath}`,
71
+ fileUrl
72
+ ];
73
+ }
74
+
51
75
  /**
52
76
  * Renders HTML file to PDF using headless browser
53
77
  * @param {string} htmlPath
@@ -64,28 +88,12 @@ function renderHtmlToPdf(htmlPath, outputPath, options = {}) {
64
88
  }
65
89
 
66
90
  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
- ];
91
+ const args = buildBrowserArgs(fileUrl, outputPath, { outline: options.outline });
76
92
 
77
93
  execFile(browserPath, args, { timeout: 30000 }, (error) => {
78
94
  if (error) {
79
95
  // 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
- ];
96
+ const fallbackArgs = buildBrowserArgs(fileUrl, outputPath, { outline: options.outline, legacyHeadless: true });
89
97
 
90
98
  execFile(browserPath, fallbackArgs, { timeout: 30000 }, (err2) => {
91
99
  if (err2) {
@@ -135,6 +143,7 @@ async function renderBatchHtmlToPdf(itemPairs, options = {}) {
135
143
  }
136
144
 
137
145
  module.exports = {
146
+ buildBrowserArgs,
138
147
  findBrowserExecutable,
139
148
  renderHtmlToPdf,
140
149
  renderBatchHtmlToPdf
@@ -0,0 +1,61 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
2
+ /**
3
+ * Command line helpers for options whose value could be mistaken for an input file.
4
+ */
5
+ const fs = require('fs');
6
+ const { InvalidArgumentError } = require('commander');
7
+
8
+ const PAGE_NUMBER_STYLES = ['full', 'simple'];
9
+ const INPUT_FILE = /\.(md|markdown|txt)$/i;
10
+
11
+ /**
12
+ * `--page-numbers` takes an optional style. Only a known style is read as its value, so in
13
+ * `--page-numbers README.md` the file stays an input: the flag is given its default instead.
14
+ * @param {string[]} argv process.argv
15
+ * @returns {string[]}
16
+ */
17
+ function normalizePageNumbersArgs(argv) {
18
+ const out = [];
19
+ for (let i = 0; i < argv.length; i++) {
20
+ const arg = argv[i];
21
+ if (arg === '--') {
22
+ out.push(...argv.slice(i));
23
+ break;
24
+ }
25
+ if (arg === '--page-numbers') {
26
+ const next = argv[i + 1];
27
+ if (next !== undefined && PAGE_NUMBER_STYLES.includes(next.toLowerCase())) {
28
+ out.push(`--page-numbers=${next.toLowerCase()}`);
29
+ i++;
30
+ } else {
31
+ out.push('--page-numbers=full');
32
+ }
33
+ continue;
34
+ }
35
+ out.push(arg);
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /**
41
+ * Value check for `--header <text>` and `--footer <text>`. An empty string turns them off.
42
+ * A value that names an existing Markdown file is refused: it is almost always an input file
43
+ * that followed the flag, as in `md2pdf --header README.md`.
44
+ * @param {string} value
45
+ * @returns {string}
46
+ */
47
+ function parseMarginText(value) {
48
+ if (INPUT_FILE.test(value) && fs.existsSync(value) && fs.statSync(value).isFile()) {
49
+ throw new InvalidArgumentError(
50
+ `"${value}" is an input file, not header/footer text. Give the text right after the flag, ` +
51
+ `e.g. --header "{title}" ${value}`
52
+ );
53
+ }
54
+ return value;
55
+ }
56
+
57
+ module.exports = {
58
+ PAGE_NUMBER_STYLES,
59
+ normalizePageNumbersArgs,
60
+ parseMarginText
61
+ };
package/src/core/cover.js CHANGED
@@ -1,15 +1,32 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Cover Page Generator Utility
3
4
  */
4
5
  const { escapeHtml } = require('./sanitizer');
5
6
 
7
+ // Cover labels by document language (front matter `lang`); any other language gets English.
8
+ const COVER_LABELS = {
9
+ en: { author: 'Author', date: 'Date' },
10
+ vi: { author: 'Tác giả', date: 'Ngày lập' }
11
+ };
12
+
13
+ /**
14
+ * The cover labels for a language tag such as `en`, `vi` or `vi-VN`.
15
+ * @param {string} [language]
16
+ * @returns {{ author: string, date: string }}
17
+ */
18
+ function coverLabels(language) {
19
+ const primary = String(language || 'en').toLowerCase().split(/[-_]/)[0];
20
+ return COVER_LABELS[primary] || COVER_LABELS.en;
21
+ }
22
+
6
23
  /**
7
24
  * Generates a Cover Page HTML block if requested in front matter or options
8
25
  * @param {Object} metadata
9
26
  * @returns {string} Cover page HTML string
10
27
  */
11
28
  function generateCoverPage(metadata = {}) {
12
- const { title, subtitle, author, date, cover } = metadata;
29
+ const { title, subtitle, author, date, cover, language } = metadata;
13
30
 
14
31
  if (!cover && !subtitle && !author && !date) {
15
32
  return '';
@@ -19,6 +36,7 @@ function generateCoverPage(metadata = {}) {
19
36
  const docSubtitle = subtitle || '';
20
37
  const docAuthor = author || '';
21
38
  const docDate = date || '';
39
+ const labels = coverLabels(language);
22
40
 
23
41
  return `<div class="cover-page">
24
42
  <div class="cover-page-inner">
@@ -27,13 +45,14 @@ function generateCoverPage(metadata = {}) {
27
45
  ${docSubtitle ? `<h2 class="cover-subtitle">${escapeHtml(docSubtitle)}</h2>` : ''}
28
46
  <div class="cover-divider"></div>
29
47
  <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>` : ''}
48
+ ${docAuthor ? `<div class="cover-meta-item"><span class="cover-meta-label">${labels.author}:</span> <span class="cover-meta-val">${escapeHtml(docAuthor)}</span></div>` : ''}
49
+ ${docDate ? `<div class="cover-meta-item"><span class="cover-meta-label">${labels.date}:</span> <span class="cover-meta-val">${escapeHtml(docDate)}</span></div>` : ''}
32
50
  </div>
33
51
  </div>
34
52
  </div>\n`;
35
53
  }
36
54
 
37
55
  module.exports = {
56
+ coverLabels,
38
57
  generateCoverPage
39
58
  };
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const { marked } = require('marked');
2
3
  const { parseFrontMatter } = require('./frontmatter');
3
4
  const { applyFootnotes } = require('./footnotes');
package/src/core/docx.js CHANGED
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const { buildDocument, collectHeadings, isPortableLink, plainText } = require('./docmodel');
2
3
  const { readLocalImage } = require('./images');
3
4
  const { createZip } = require('./zip');
package/src/core/epub.js CHANGED
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const crypto = require('crypto');
2
3
  const hljs = require('highlight.js/lib/common');
3
4
  const { buildDocument, collectHeadings, isPortableLink, plainText } = require('./docmodel');
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Markdown footnotes: `text[^id]` in the body, `[^id]: note` anywhere on a line of its own.
3
4
  *
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Front Matter Parser Utility
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
 
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { parseFrontMatter } = require('./frontmatter');
package/src/core/index.js CHANGED
@@ -1,8 +1,9 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { parseMarkdownToHtml } = require('./parser');
4
5
  const { renderHtmlToPdf } = require('../browser/renderer');
5
- const { resolvePath } = require('./utils');
6
+ const { resolvePath, parseFrontMatter } = require('./utils');
6
7
  const { expandIncludes } = require('./include');
7
8
  const { markdownToDocx } = require('./docx');
8
9
  const { markdownToEpub } = require('./epub');
@@ -29,6 +30,18 @@ function temporaryHtmlPath(absInputPath, absOutputPath) {
29
30
  }
30
31
  }
31
32
 
33
+ /**
34
+ * Whether the PDF gets bookmarks from its headings: on unless `outline: false` is passed
35
+ * or set in the front matter (an option wins over the front matter).
36
+ * @param {string} markdown
37
+ * @param {Object} options
38
+ * @returns {boolean}
39
+ */
40
+ function pdfOutline(markdown, options = {}) {
41
+ if (options.outline !== undefined) return options.outline !== false;
42
+ return parseFrontMatter(markdown).data.outline !== false;
43
+ }
44
+
32
45
  /**
33
46
  * Main function to convert a Markdown file: to PDF by default, or to the
34
47
  * format named in `options.format` (`html`, `docx`, `epub`)
@@ -98,8 +111,8 @@ async function convertMarkdownToPdf(inputPath, outputPath, options = {}) {
98
111
  // Save temporary HTML file
99
112
  fs.writeFileSync(tempHtmlPath, htmlContent, 'utf8');
100
113
 
101
- // Render HTML to PDF via headless browser
102
- await renderHtmlToPdf(tempHtmlPath, absOutputPath, options);
114
+ // Render HTML to PDF via headless browser, with bookmarks unless turned off
115
+ await renderHtmlToPdf(tempHtmlPath, absOutputPath, { ...options, outline: pdfOutline(markdownContent, options) });
103
116
 
104
117
  // Clean up temporary HTML file unless explicitly requested to keep
105
118
  if (!options.keepHtml) {
@@ -129,6 +142,7 @@ module.exports = {
129
142
  markdownToDocx,
130
143
  markdownToEpub,
131
144
  parseMarkdownToHtml,
145
+ pdfOutline,
132
146
  renderHtmlToPdf,
133
147
  temporaryHtmlPath
134
148
  };
@@ -0,0 +1,142 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
2
+ /**
3
+ * Page numbers and a running header/footer, printed in the page margins with CSS `@page`
4
+ * margin boxes. Chrome and Edge draw these both for `--print-to-pdf` and in their print
5
+ * dialog, so the CLI and the extension share this file: it needs nothing from Node and is
6
+ * shipped in the extension as extension/lib/pagemarks.js (a test keeps the two identical).
7
+ *
8
+ * Header and footer are text with placeholders: {page}, {pages}, {title}, {author}, {date}.
9
+ */
10
+ (function (scope) {
11
+ const NUMBER_STYLES = {
12
+ full: 'Page {page} of {pages}',
13
+ simple: '{page}'
14
+ };
15
+ const DEFAULT_HEADER = '{title}';
16
+ const PLACEHOLDER = /\{(page|pages|title|author|date)\}/g;
17
+
18
+ /**
19
+ * The text for a header or footer option: a string as it is, `true` for the default.
20
+ * @param {*} value
21
+ * @param {string} fallback Text used for `true`
22
+ * @returns {string} '' when off
23
+ */
24
+ function textOf(value, fallback) {
25
+ if (value === true) return fallback;
26
+ if (typeof value === 'number') return String(value);
27
+ return typeof value === 'string' ? value.trim() : '';
28
+ }
29
+
30
+ /**
31
+ * The page number text for the `pageNumbers` option.
32
+ * @param {*} value true, 'full', 'simple', or false/empty for none
33
+ * @returns {string} '' when off
34
+ */
35
+ function numbersOf(value) {
36
+ if (value === undefined || value === null || value === false || value === '') return '';
37
+ if (value === true) return NUMBER_STYLES.full;
38
+ const style = String(value).trim().toLowerCase();
39
+ if (style === 'true') return NUMBER_STYLES.full;
40
+ if (style === 'false' || style === 'none' || style === 'off') return '';
41
+ if (!NUMBER_STYLES[style]) {
42
+ throw new Error(`Unknown page number style "${value}". Use one of: ${Object.keys(NUMBER_STYLES).join(', ')}`);
43
+ }
44
+ return NUMBER_STYLES[style];
45
+ }
46
+
47
+ /**
48
+ * A CSS string literal that cannot end early or close the <style> element.
49
+ * @param {string} text
50
+ * @returns {string}
51
+ */
52
+ function cssString(text) {
53
+ const safe = text
54
+ .replace(/\\/g, '\\\\')
55
+ .replace(/"/g, '\\"')
56
+ .replace(/[\r\n]+/g, ' ')
57
+ .replace(/</g, '\\3C ');
58
+ return `"${safe}"`;
59
+ }
60
+
61
+ /**
62
+ * Turns header/footer text into the value of a CSS `content` property.
63
+ * @param {string} text
64
+ * @param {{ title?: string, author?: string, date?: string }} meta
65
+ * @returns {string}
66
+ */
67
+ function contentValue(text, meta = {}) {
68
+ const parts = [];
69
+ let literal = '';
70
+ let last = 0;
71
+ for (const match of text.matchAll(PLACEHOLDER)) {
72
+ literal += text.slice(last, match.index);
73
+ last = match.index + match[0].length;
74
+ const name = match[1];
75
+ if (name === 'page' || name === 'pages') {
76
+ if (literal) parts.push(cssString(literal));
77
+ literal = '';
78
+ parts.push(`counter(${name})`);
79
+ } else {
80
+ literal += meta[name] ? String(meta[name]) : '';
81
+ }
82
+ }
83
+ literal += text.slice(last);
84
+ if (literal) parts.push(cssString(literal));
85
+ return parts.length ? parts.join(' ') : '""';
86
+ }
87
+
88
+ /**
89
+ * Where each text goes. The header sits top centre. The footer sits bottom centre, unless
90
+ * page numbers are also on: then the footer text goes left and the numbers right.
91
+ * @param {{ header?: *, footer?: *, pageNumbers?: * }} options
92
+ * @returns {Object<string, string>} margin box name -> text
93
+ */
94
+ function pageMarkBoxes(options = {}) {
95
+ const header = textOf(options.header, DEFAULT_HEADER);
96
+ const numbers = numbersOf(options.pageNumbers);
97
+ const footer = textOf(options.footer, NUMBER_STYLES.full);
98
+ const boxes = {};
99
+ if (header) boxes['top-center'] = header;
100
+ if (footer && numbers && footer !== numbers) {
101
+ boxes['bottom-left'] = footer;
102
+ boxes['bottom-right'] = numbers;
103
+ } else if (footer || numbers) {
104
+ boxes['bottom-center'] = footer || numbers;
105
+ }
106
+ return boxes;
107
+ }
108
+
109
+ /**
110
+ * CSS for the page numbers and running header/footer, or '' when none is on.
111
+ * A cover page (.cover-page) is printed without them.
112
+ * @param {{ header?: *, footer?: *, pageNumbers?: *, font?: string }} options
113
+ * @param {{ title?: string, author?: string, date?: string }} [meta]
114
+ * @returns {string}
115
+ */
116
+ function pageMarksCss(options = {}, meta = {}) {
117
+ const boxes = pageMarkBoxes(options);
118
+ const names = Object.keys(boxes);
119
+ if (!names.length) return '';
120
+
121
+ const font = options.font ? `${cssString(String(options.font))}, sans-serif` : 'sans-serif';
122
+ const style = `font-family: ${font}; font-size: 9px; color: #64748b;`;
123
+ const rules = names.map(
124
+ (name) => ` @${name} { content: ${contentValue(boxes[name], meta)}; ${style} }`
125
+ );
126
+ const blank = names.map((name) => ` @${name} { content: none; }`);
127
+ return [
128
+ '/* Page numbers and running header/footer */',
129
+ '@page {',
130
+ ...rules,
131
+ '}',
132
+ '.cover-page { page: cover; }',
133
+ '@page cover {',
134
+ ...blank,
135
+ '}'
136
+ ].join('\n');
137
+ }
138
+
139
+ const api = { pageMarksCss, pageMarkBoxes, contentValue };
140
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
141
+ else scope.MdPdfPageMarks = api;
142
+ })(typeof globalThis !== 'undefined' ? globalThis : this);
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const { marked } = require('marked');
2
3
  const hljs = require('highlight.js/lib/common');
3
4
  const {
@@ -11,6 +12,7 @@ const {
11
12
  } = require('./utils');
12
13
  const { getHtmlTemplate } = require('./templates');
13
14
  const { applyFootnotes } = require('./footnotes');
15
+ const { pageMarksCss } = require('./pagemarks');
14
16
 
15
17
  /**
16
18
  * Builds marked custom renderer for diagrams, UI mockups, and headings
@@ -82,6 +84,8 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
82
84
  const subtitle = frontMatter.subtitle || options.subtitle || '';
83
85
  const author = frontMatter.author || options.author || '';
84
86
  const date = frontMatter.date || options.date || '';
87
+ // Document language, as for EPUB: sets the cover labels and the page's lang attribute
88
+ const language = String(frontMatter.lang || frontMatter.language || options.language || 'en');
85
89
 
86
90
  // 2. Merge options
87
91
  const mergedOptions = {
@@ -92,8 +96,10 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
92
96
  margin: options.margin || frontMatter.margin || '14mm 12mm 16mm 12mm',
93
97
  toc: options.toc !== undefined ? options.toc : (frontMatter.toc || false),
94
98
  cover: options.cover !== undefined ? options.cover : (frontMatter.cover || false),
95
- header: options.header !== undefined ? options.header : (frontMatter.header || true),
96
- footer: options.footer !== undefined ? options.footer : (frontMatter.footer || true),
99
+ // Running header/footer and page numbers: off unless asked for (see pagemarks.js)
100
+ header: options.header !== undefined ? options.header : frontMatter.header,
101
+ footer: options.footer !== undefined ? options.footer : frontMatter.footer,
102
+ pageNumbers: options.pageNumbers !== undefined ? options.pageNumbers : frontMatter.pageNumbers,
97
103
  mermaid: options.mermaid !== undefined ? options.mermaid : (frontMatter.mermaid !== false),
98
104
  katex: options.katex !== undefined ? options.katex : (frontMatter.katex !== false),
99
105
  highlight: options.highlight !== undefined ? options.highlight : (frontMatter.highlight !== false),
@@ -118,7 +124,8 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
118
124
  subtitle,
119
125
  author,
120
126
  date,
121
- cover: mergedOptions.cover
127
+ cover: mergedOptions.cover,
128
+ language
122
129
  });
123
130
 
124
131
  // 5. Read custom CSS file if specified
@@ -127,6 +134,15 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
127
134
  customCss += '\n' + readCustomCss(mergedOptions.cssFile);
128
135
  }
129
136
 
137
+ // Page numbers and running header/footer, printed in the page margins
138
+ const pageCss = pageMarksCss(
139
+ { header: mergedOptions.header, footer: mergedOptions.footer, pageNumbers: mergedOptions.pageNumbers, font: mergedOptions.font },
140
+ { title, author, date }
141
+ );
142
+ if (pageCss) {
143
+ customCss = pageCss + '\n' + customCss;
144
+ }
145
+
130
146
  // 6. Parse Markdown using custom renderer
131
147
  marked.setOptions({ gfm: true, breaks: true });
132
148
  const renderer = buildMarkedRenderer(headingsMap, mergedOptions.highlight);
@@ -152,6 +168,7 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
152
168
  return getHtmlTemplate({
153
169
  title,
154
170
  author,
171
+ language,
155
172
  bodyHtml,
156
173
  options: {
157
174
  ...mergedOptions,
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * HTML Sanitizer & Escaping Utilities
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const { getThemeStyles } = require('./themes');
2
3
 
3
4
  /**
@@ -15,11 +16,12 @@ function escapeXml(str) {
15
16
  * @param {Object} params
16
17
  * @param {string} params.title Document title
17
18
  * @param {string} [params.author] Document author
19
+ * @param {string} [params.language] Document language tag (default en)
18
20
  * @param {string} params.bodyHtml Document body HTML
19
21
  * @param {Object} [params.options] Template options
20
22
  * @returns {string} Full HTML string
21
23
  */
22
- function getHtmlTemplate({ title, author = '', bodyHtml, options = {} }) {
24
+ function getHtmlTemplate({ title, author = '', language = 'en', bodyHtml, options = {} }) {
23
25
  const theme = options.theme || 'modern';
24
26
  const enableMermaid = options.mermaid !== false;
25
27
  const enableKatex = options.katex !== false;
@@ -31,7 +33,7 @@ function getHtmlTemplate({ title, author = '', bodyHtml, options = {} }) {
31
33
  const themeStyles = getThemeStyles(theme, options.font);
32
34
 
33
35
  return `<!DOCTYPE html>
34
- <html lang="vi">
36
+ <html lang="${escapeXml(language)}">
35
37
  <head>
36
38
  <meta charset="UTF-8">
37
39
  <title>${escapeXml(title)}</title>
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Academic Theme Styles
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Cover Page CSS Styles
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Cyberpunk Theme Styles
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Dark Theme Styles
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * E-Book Theme Styles
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * GitHub Theme Styles
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Syntax Highlighting Token Colors
3
4
  * Two palettes, picked by the background each theme gives its code blocks.
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const coverCss = require('./cover');
2
3
  const modernTheme = require('./modern');
3
4
  const darkTheme = require('./dark');
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Minimal Theme Styles
3
4
  */
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Modern Theme Styles
3
4
  */
package/src/core/toc.js CHANGED
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  /**
2
3
  * Table of Contents (TOC) Generator Utility
3
4
  */
package/src/core/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { parseFrontMatter } = require('./frontmatter');
package/src/core/zip.js CHANGED
@@ -1,3 +1,4 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  const zlib = require('zlib');
2
3
 
3
4
  /**
package/src/index.js CHANGED
@@ -1,2 +1,3 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
1
2
  // Re-export core API module
2
3
  module.exports = require('./core/index');