md-pdf-studio 2.2.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.
@@ -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');
@@ -18,11 +19,11 @@ function preprocessMarkdown(mdContent = '') {
18
19
 
19
20
  return mdContent
20
21
  // GitHub Callouts / Alerts
21
- .replace(/^>\s*\[!NOTE\]\s*\r?\n/gm, '> **ℹ️ NOTE:** ')
22
- .replace(/^>\s*\[!TIP\]\s*\r?\n/gm, '> **💡 TIP:** ')
23
- .replace(/^>\s*\[!WARNING\]\s*\r?\n/gm, '> **⚠️ WARNING:** ')
24
- .replace(/^>\s*\[!IMPORTANT\]\s*\r?\n/gm, '> **❗ IMPORTANT:** ')
25
- .replace(/^>\s*\[!CAUTION\]\s*\r?\n/gm, '> **🚫 CAUTION:** ')
22
+ .replace(/^>[ \t]*\[!NOTE\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **ℹ️ NOTE:** ')
23
+ .replace(/^>[ \t]*\[!TIP\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **💡 TIP:** ')
24
+ .replace(/^>[ \t]*\[!WARNING\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **⚠️ WARNING:** ')
25
+ .replace(/^>[ \t]*\[!IMPORTANT\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **❗ IMPORTANT:** ')
26
+ .replace(/^>[ \t]*\[!CAUTION\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **🚫 CAUTION:** ')
26
27
  // Pagebreaks
27
28
  .replace(/<!--\s*page-?break\s*-->/gi, '<div class="page-break"></div>')
28
29
  .replace(/\\pagebreak/gi, '<div class="page-break"></div>')
@@ -0,0 +1,78 @@
1
+ /*! SPDX-License-Identifier: MIT | Copyright (c) 2026 Van Nguyen | github.com/vannt-dev/convert-md-to-pdf */
2
+ const zlib = require('zlib');
3
+
4
+ /**
5
+ * A ZIP archive writer, enough for the two container formats built on ZIP
6
+ * that this tool exports: DOCX and EPUB.
7
+ *
8
+ * Entries are written in the order given, which EPUB relies on: its
9
+ * `mimetype` entry has to come first and uncompressed. Timestamps are fixed,
10
+ * so the same input gives the same bytes.
11
+ */
12
+
13
+ // 1980-01-01 00:00:00, the earliest date the format can express.
14
+ const DOS_TIME = 0;
15
+ const DOS_DATE = (0 << 9) | (1 << 5) | 1;
16
+
17
+ /**
18
+ * @param {{ name: string, data: Buffer | string, store?: boolean }[]} entries
19
+ * @returns {Buffer}
20
+ */
21
+ function createZip(entries) {
22
+ const localParts = [];
23
+ const centralParts = [];
24
+ let offset = 0;
25
+
26
+ for (const entry of entries) {
27
+ const name = Buffer.from(entry.name, 'utf8');
28
+ const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data, 'utf8');
29
+ const compressed = entry.store ? data : zlib.deflateRawSync(data, { level: 9 });
30
+ const method = entry.store ? 0 : 8;
31
+ const crc = zlib.crc32(data);
32
+
33
+ const local = Buffer.alloc(30);
34
+ local.writeUInt32LE(0x04034b50, 0);
35
+ local.writeUInt16LE(20, 4); // version needed
36
+ local.writeUInt16LE(0x0800, 6); // names are UTF-8
37
+ local.writeUInt16LE(method, 8);
38
+ local.writeUInt16LE(DOS_TIME, 10);
39
+ local.writeUInt16LE(DOS_DATE, 12);
40
+ local.writeUInt32LE(crc, 14);
41
+ local.writeUInt32LE(compressed.length, 18);
42
+ local.writeUInt32LE(data.length, 22);
43
+ local.writeUInt16LE(name.length, 26);
44
+ local.writeUInt16LE(0, 28);
45
+
46
+ const central = Buffer.alloc(46);
47
+ central.writeUInt32LE(0x02014b50, 0);
48
+ central.writeUInt16LE(20, 4); // version made by
49
+ central.writeUInt16LE(20, 6); // version needed
50
+ central.writeUInt16LE(0x0800, 8);
51
+ central.writeUInt16LE(method, 10);
52
+ central.writeUInt16LE(DOS_TIME, 12);
53
+ central.writeUInt16LE(DOS_DATE, 14);
54
+ central.writeUInt32LE(crc, 16);
55
+ central.writeUInt32LE(compressed.length, 20);
56
+ central.writeUInt32LE(data.length, 24);
57
+ central.writeUInt16LE(name.length, 28);
58
+ central.writeUInt32LE(offset, 42);
59
+
60
+ localParts.push(local, name, compressed);
61
+ centralParts.push(central, name);
62
+ offset += local.length + name.length + compressed.length;
63
+ }
64
+
65
+ const centralSize = centralParts.reduce((size, part) => size + part.length, 0);
66
+ const end = Buffer.alloc(22);
67
+ end.writeUInt32LE(0x06054b50, 0);
68
+ end.writeUInt16LE(entries.length, 8);
69
+ end.writeUInt16LE(entries.length, 10);
70
+ end.writeUInt32LE(centralSize, 12);
71
+ end.writeUInt32LE(offset, 16);
72
+
73
+ return Buffer.concat([...localParts, ...centralParts, end]);
74
+ }
75
+
76
+ module.exports = {
77
+ createZip
78
+ };
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');