md-pdf-studio 1.1.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 +208 -0
- package/bin/cli.js +186 -0
- package/package.json +58 -0
- package/src/browser/renderer.js +142 -0
- package/src/core/cover.js +40 -0
- package/src/core/frontmatter.js +54 -0
- package/src/core/index.js +80 -0
- package/src/core/parser.js +163 -0
- package/src/core/sanitizer.js +35 -0
- package/src/core/templates.js +192 -0
- package/src/core/themes/academic.js +20 -0
- package/src/core/themes/cover.js +14 -0
- package/src/core/themes/cyberpunk.js +21 -0
- package/src/core/themes/dark.js +21 -0
- package/src/core/themes/ebook.js +20 -0
- package/src/core/themes/github.js +21 -0
- package/src/core/themes/index.js +52 -0
- package/src/core/themes/minimal.js +20 -0
- package/src/core/themes/modern.js +34 -0
- package/src/core/toc.js +72 -0
- package/src/core/utils.js +85 -0
- package/src/index.js +2 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const coverCss = require('./cover');
|
|
2
|
+
const modernTheme = require('./modern');
|
|
3
|
+
const darkTheme = require('./dark');
|
|
4
|
+
const academicTheme = require('./academic');
|
|
5
|
+
const githubTheme = require('./github');
|
|
6
|
+
const ebookTheme = require('./ebook');
|
|
7
|
+
const cyberpunkTheme = require('./cyberpunk');
|
|
8
|
+
const minimalTheme = require('./minimal');
|
|
9
|
+
|
|
10
|
+
const THEMES = {
|
|
11
|
+
modern: modernTheme,
|
|
12
|
+
dark: darkTheme,
|
|
13
|
+
academic: academicTheme,
|
|
14
|
+
github: githubTheme,
|
|
15
|
+
ebook: ebookTheme,
|
|
16
|
+
cyberpunk: cyberpunkTheme,
|
|
17
|
+
minimal: minimalTheme
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds font override CSS string
|
|
22
|
+
* @param {string} font
|
|
23
|
+
* @returns {string} CSS snippet
|
|
24
|
+
*/
|
|
25
|
+
function getFontOverrideCss(font) {
|
|
26
|
+
if (!font) return '';
|
|
27
|
+
if (font === 'Lora' || font === 'Merriweather') {
|
|
28
|
+
return `body { font-family: '${font}', Georgia, serif !important; }\n`;
|
|
29
|
+
}
|
|
30
|
+
if (font === 'JetBrains Mono' || font === 'Fira Code') {
|
|
31
|
+
return `body { font-family: '${font}', monospace !important; }\n`;
|
|
32
|
+
}
|
|
33
|
+
return `body { font-family: '${font}', sans-serif !important; }\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns complete CSS theme string with cover page and font override
|
|
38
|
+
* @param {string} theme Name of theme
|
|
39
|
+
* @param {string} [font] Font family override
|
|
40
|
+
* @returns {string} Consolidated CSS string
|
|
41
|
+
*/
|
|
42
|
+
function getThemeStyles(theme, font = '') {
|
|
43
|
+
const themeCss = THEMES[theme] || THEMES.modern;
|
|
44
|
+
const fontOverrideCss = getFontOverrideCss(font);
|
|
45
|
+
|
|
46
|
+
return `${coverCss}\n${fontOverrideCss}${themeCss}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
getThemeStyles,
|
|
51
|
+
THEMES
|
|
52
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Theme Styles
|
|
3
|
+
*/
|
|
4
|
+
module.exports = `
|
|
5
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 13px; line-height: 1.5; color: #000000; background-color: #ffffff; padding: 10px; }
|
|
6
|
+
h1 { font-size: 20px; font-weight: 700; border-bottom: 2px solid #000; padding-bottom: 4px; break-after: avoid-page; }
|
|
7
|
+
h2 { font-size: 15px; font-weight: 700; margin-top: 18px; break-after: avoid-page; }
|
|
8
|
+
h3 { font-size: 13.5px; font-weight: 600; break-after: avoid-page; }
|
|
9
|
+
blockquote { border-left: 3px solid #000; padding-left: 12px; margin: 10px 0; }
|
|
10
|
+
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; }
|
|
11
|
+
tr { break-inside: avoid-page; }
|
|
12
|
+
th, td { border: 1px solid #000; padding: 6px; }
|
|
13
|
+
th { background: #f2f2f2; font-weight: 700; }
|
|
14
|
+
code { font-family: monospace; font-size: 11px; background: #f2f2f2; padding: 2px 4px; }
|
|
15
|
+
pre { font-family: monospace; font-size: 10.5px; background: #f9f9f9; border: 1px solid #ccc; padding: 10px; break-inside: avoid-page; }
|
|
16
|
+
.formula-box { border: 1px solid #000; padding: 8px; text-align: center; margin: 10px 0; }
|
|
17
|
+
.mermaid-container { display: block; text-align: center; margin: 12px 0; border: 1px solid #ccc; padding: 8px; overflow-x: auto; break-inside: avoid-page; }
|
|
18
|
+
.mermaid { display: block; text-align: center; width: 100%; }
|
|
19
|
+
.mermaid svg { max-width: 100% !important; height: auto !important; margin: 0 auto; display: inline-block; }
|
|
20
|
+
`;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern Theme Styles
|
|
3
|
+
*/
|
|
4
|
+
module.exports = `
|
|
5
|
+
body { font-family: 'Inter', 'Noto Sans JP', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 13px; line-height: 1.55; color: #1e293b; background-color: #ffffff; margin: 0; padding: 10px; }
|
|
6
|
+
h1 { font-size: 21px; font-weight: 700; color: #0f172a; border-bottom: 3px solid #2563eb; padding-bottom: 8px; margin-top: 0; margin-bottom: 16px; text-transform: uppercase; letter-spacing: 0.3px; break-after: avoid-page; page-break-after: avoid; }
|
|
7
|
+
h2 { font-size: 16px; font-weight: 700; color: #1e3a8a; background: #eff6ff; border-left: 5px solid #2563eb; padding: 6px 12px; margin-top: 22px; margin-bottom: 12px; border-radius: 0 4px 4px 0; break-after: avoid-page; page-break-after: avoid; }
|
|
8
|
+
h3 { font-size: 14.5px; font-weight: 600; color: #1e40af; margin-top: 16px; margin-bottom: 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px; break-after: avoid-page; page-break-after: avoid; }
|
|
9
|
+
h4 { font-size: 13.5px; font-weight: 600; color: #334155; margin-top: 12px; margin-bottom: 6px; break-after: avoid-page; page-break-after: avoid; }
|
|
10
|
+
.section-group { break-inside: avoid-page; page-break-inside: avoid; }
|
|
11
|
+
.formula-box { background: #f0f9ff; border: 1px solid #bae6fd; border-left: 4px solid #0284c7; padding: 10px 16px; border-radius: 4px; margin: 12px 0; font-size: 13.5px; color: #0369a1; text-align: center; }
|
|
12
|
+
.formula-box span { font-weight: 500; background: #ffffff; padding: 2px 8px; border-radius: 3px; border: 1px solid #e0f2fe; }
|
|
13
|
+
blockquote { margin: 12px 0; padding: 10px 16px; background-color: #f8fafc; border-left: 4px solid #3b82f6; border-radius: 4px; color: #334155; font-size: 12.5px; }
|
|
14
|
+
blockquote p { margin: 4px 0; }
|
|
15
|
+
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; break-inside: auto; }
|
|
16
|
+
tr { break-inside: avoid-page; page-break-inside: avoid; }
|
|
17
|
+
th { background-color: #1e293b; color: #ffffff; font-weight: 600; text-align: left; padding: 7px 9px; border: 1px solid #334155; font-size: 11.5px; letter-spacing: 0.2px; }
|
|
18
|
+
td { padding: 6px 9px; border: 1px solid #cbd5e1; vertical-align: top; }
|
|
19
|
+
tbody tr:nth-child(even) { background-color: #f8fafc; }
|
|
20
|
+
code { font-family: 'JetBrains Mono', Consolas, Monaco, monospace; font-size: 11.5px; background-color: #f1f5f9; color: #0f172a; padding: 1px 5px; border-radius: 3px; border: 1px solid #e2e8f0; }
|
|
21
|
+
pre { font-family: 'JetBrains Mono', Consolas, Monaco, monospace; font-size: 10.5px; line-height: 1.4; background-color: #0f172a; color: #f8fafc; padding: 10px 12px; border-radius: 6px; overflow-x: auto; margin: 10px 0; white-space: pre-wrap; word-break: break-all; break-inside: avoid-page; page-break-inside: avoid; }
|
|
22
|
+
pre code { background: none; color: inherit; padding: 0; border: none; font-size: inherit; }
|
|
23
|
+
.ui-mockup-container { margin: 12px 0; break-inside: avoid-page; page-break-inside: avoid; }
|
|
24
|
+
.ui-mockup { background-color: #1e1e2e; color: #a6adc8; border: 1px solid #313244; border-left: 4px solid #89b4fa; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
|
|
25
|
+
.ui-mockup code { color: #cdd6f4; }
|
|
26
|
+
.mermaid-container { display: block; text-align: center; margin: 14px 0; background-color: #ffffff; padding: 12px; border: 1px solid #e2e8f0; border-radius: 6px; overflow-x: auto; break-inside: avoid-page; page-break-inside: avoid; }
|
|
27
|
+
.mermaid { display: block; text-align: center; width: 100%; }
|
|
28
|
+
.mermaid svg { max-width: 100% !important; height: auto !important; margin: 0 auto; display: inline-block; }
|
|
29
|
+
hr { border: 0; height: 1px; background: #e2e8f0; margin: 16px 0; }
|
|
30
|
+
ul, ol { padding-left: 20px; margin: 6px 0; }
|
|
31
|
+
li { margin-bottom: 3px; }
|
|
32
|
+
strong { color: #0f172a; }
|
|
33
|
+
a { color: #2563eb; text-decoration: none; }
|
|
34
|
+
`;
|
package/src/core/toc.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table of Contents (TOC) Generator Utility
|
|
3
|
+
*/
|
|
4
|
+
const { escapeHtml } = require('./sanitizer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates a Table of Contents (TOC) HTML block from markdown headings with dotted leader lines
|
|
8
|
+
* @param {string} mdContent
|
|
9
|
+
* @returns {{ tocHtml: string, headingsMap: Map<string, string> }}
|
|
10
|
+
*/
|
|
11
|
+
function generateToc(mdContent) {
|
|
12
|
+
if (!mdContent) return { tocHtml: '', headingsMap: new Map() };
|
|
13
|
+
|
|
14
|
+
const lines = mdContent.split('\n');
|
|
15
|
+
const headings = [];
|
|
16
|
+
const headingsMap = new Map();
|
|
17
|
+
const slugCounts = {};
|
|
18
|
+
|
|
19
|
+
lines.forEach(line => {
|
|
20
|
+
const match = line.match(/^(#{1,3})\s+(.+)$/);
|
|
21
|
+
if (match) {
|
|
22
|
+
const level = match[1].length;
|
|
23
|
+
let title = match[2].trim();
|
|
24
|
+
|
|
25
|
+
// Clean inline markdown links/code from heading title
|
|
26
|
+
title = title.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
27
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
28
|
+
.replace(/\*+/g, '');
|
|
29
|
+
|
|
30
|
+
// Create unique slug
|
|
31
|
+
let slug = title.toLowerCase()
|
|
32
|
+
.replace(/[^\w\u00C0-\u024F\u1EA0-\u1EF9]+/g, '-')
|
|
33
|
+
.replace(/^-+|-+$/g, '');
|
|
34
|
+
|
|
35
|
+
if (!slug) slug = `heading-${headings.length + 1}`;
|
|
36
|
+
|
|
37
|
+
if (slugCounts[slug]) {
|
|
38
|
+
slugCounts[slug]++;
|
|
39
|
+
slug = `${slug}-${slugCounts[slug]}`;
|
|
40
|
+
} else {
|
|
41
|
+
slugCounts[slug] = 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
headings.push({ level, title, slug });
|
|
45
|
+
headingsMap.set(match[2].trim(), slug);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (headings.length === 0) return { tocHtml: '', headingsMap };
|
|
50
|
+
|
|
51
|
+
let tocHtml = `<div class="toc-container">
|
|
52
|
+
<div class="toc-title">đ Table of Contents</div>
|
|
53
|
+
<ul class="toc-list">\n`;
|
|
54
|
+
|
|
55
|
+
headings.forEach(h => {
|
|
56
|
+
const indentClass = `toc-item-h${h.level}`;
|
|
57
|
+
tocHtml += ` <li class="${indentClass}">
|
|
58
|
+
<a href="#${h.slug}">
|
|
59
|
+
<span class="toc-item-title">${escapeHtml(h.title)}</span>
|
|
60
|
+
<span class="toc-item-dots"></span>
|
|
61
|
+
</a>
|
|
62
|
+
</li>\n`;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
tocHtml += ` </ul>\n </div>\n`;
|
|
66
|
+
|
|
67
|
+
return { tocHtml, headingsMap };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
generateToc
|
|
72
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { parseFrontMatter } = require('./frontmatter');
|
|
4
|
+
const { generateToc } = require('./toc');
|
|
5
|
+
const { generateCoverPage } = require('./cover');
|
|
6
|
+
const { sanitizeHtml, escapeHtml } = require('./sanitizer');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Preprocesses Markdown content before parsing:
|
|
10
|
+
* - Transforms GitHub Callouts/Alerts (> [!NOTE], > [!TIP], etc.)
|
|
11
|
+
* - Transforms pagebreak comments (<!-- pagebreak --> or \pagebreak)
|
|
12
|
+
* - Converts simple LaTeX arrow/math symbols
|
|
13
|
+
* @param {string} mdContent
|
|
14
|
+
* @returns {string} Preprocessed markdown string
|
|
15
|
+
*/
|
|
16
|
+
function preprocessMarkdown(mdContent = '') {
|
|
17
|
+
if (!mdContent) return '';
|
|
18
|
+
|
|
19
|
+
return mdContent
|
|
20
|
+
// 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:** ')
|
|
26
|
+
// Pagebreaks
|
|
27
|
+
.replace(/<!--\s*page-?break\s*-->/gi, '<div class="page-break"></div>')
|
|
28
|
+
.replace(/\\pagebreak/gi, '<div class="page-break"></div>')
|
|
29
|
+
// LaTeX math symbols
|
|
30
|
+
.replace(/\\longrightarrow/g, 'âļ')
|
|
31
|
+
.replace(/\\rightarrow/g, 'â')
|
|
32
|
+
.replace(/\$â\$/g, 'â')
|
|
33
|
+
.replace(/\$\\rightarrow\$/g, 'â')
|
|
34
|
+
.replace(/\$\\longrightarrow\$/g, 'âļ')
|
|
35
|
+
.replace(/\$\\ge\$/g, 'âĨ')
|
|
36
|
+
.replace(/\$\\le\$/g, 'â¤')
|
|
37
|
+
.replace(/\$x\/y\$/g, 'x/y')
|
|
38
|
+
.replace(/\$x < y\$/g, 'x < y')
|
|
39
|
+
.replace(/\$x = y\$/g, 'x = y')
|
|
40
|
+
// Formula box transformation
|
|
41
|
+
.replace(/\$\$\s*\\text\{([^}]+)\}\s*=\s*\\text\{([^}]+)\}\s*-\s*\\text\{([^}]+)\}\s*\$\$/g,
|
|
42
|
+
'<div class="formula-box"><strong>$1</strong> = <span>$2</span> − <span>$3</span></div>')
|
|
43
|
+
.replace(/\$\$\s*([\s\S]+?)\s*\$\$/g, '<div class="formula-box">$1</div>');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Safely reads custom CSS file contents
|
|
48
|
+
* @param {string} cssFilePath
|
|
49
|
+
* @returns {string} CSS content
|
|
50
|
+
*/
|
|
51
|
+
function readCustomCss(cssFilePath) {
|
|
52
|
+
if (!cssFilePath) return '';
|
|
53
|
+
const absPath = resolvePath(cssFilePath);
|
|
54
|
+
if (fs.existsSync(absPath)) {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readFileSync(absPath, 'utf8');
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.warn(`[Warning] Could not read custom CSS file at: ${absPath}`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
console.warn(`[Warning] Custom CSS file not found at: ${absPath}`);
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves absolute path safely
|
|
68
|
+
* @param {string} filePath
|
|
69
|
+
* @returns {string} Absolute file path
|
|
70
|
+
*/
|
|
71
|
+
function resolvePath(filePath) {
|
|
72
|
+
if (!filePath) return '';
|
|
73
|
+
return path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
parseFrontMatter,
|
|
78
|
+
generateCoverPage,
|
|
79
|
+
preprocessMarkdown,
|
|
80
|
+
generateToc,
|
|
81
|
+
readCustomCss,
|
|
82
|
+
sanitizeHtml,
|
|
83
|
+
escapeHtml,
|
|
84
|
+
resolvePath
|
|
85
|
+
};
|
package/src/index.js
ADDED