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,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Front Matter Parser Utility
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses YAML Front Matter from Markdown raw content
|
|
7
|
+
* @param {string} rawContent
|
|
8
|
+
* @returns {{ data: Object, content: string }}
|
|
9
|
+
*/
|
|
10
|
+
function parseFrontMatter(rawContent = '') {
|
|
11
|
+
if (!rawContent || typeof rawContent !== 'string') {
|
|
12
|
+
return { data: {}, content: '' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const frontMatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
|
|
16
|
+
const match = rawContent.match(frontMatterRegex);
|
|
17
|
+
|
|
18
|
+
if (!match) {
|
|
19
|
+
return { data: {}, content: rawContent };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const yamlBlock = match[1];
|
|
23
|
+
const content = rawContent.slice(match[0].length);
|
|
24
|
+
const data = {};
|
|
25
|
+
|
|
26
|
+
yamlBlock.split('\n').forEach(line => {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed || trimmed.startsWith('#')) return;
|
|
29
|
+
|
|
30
|
+
const colonIdx = trimmed.indexOf(':');
|
|
31
|
+
if (colonIdx === -1) return;
|
|
32
|
+
|
|
33
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
34
|
+
let val = trimmed.slice(colonIdx + 1).trim();
|
|
35
|
+
|
|
36
|
+
// Remove quotes
|
|
37
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
38
|
+
val = val.slice(1, -1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Convert booleans & numbers
|
|
42
|
+
if (val.toLowerCase() === 'true') val = true;
|
|
43
|
+
else if (val.toLowerCase() === 'false') val = false;
|
|
44
|
+
else if (!isNaN(val) && val !== '') val = Number(val);
|
|
45
|
+
|
|
46
|
+
data[key] = val;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return { data, content };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
parseFrontMatter
|
|
54
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { parseMarkdownToHtml } = require('./parser');
|
|
4
|
+
const { renderHtmlToPdf } = require('../browser/renderer');
|
|
5
|
+
const { resolvePath } = require('./utils');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Main function to convert Markdown file to PDF
|
|
9
|
+
* @param {string} inputPath Path to source .md file
|
|
10
|
+
* @param {string} [outputPath] Path to target .pdf file
|
|
11
|
+
* @param {Object} [options] Conversion options
|
|
12
|
+
* @returns {Promise<{ htmlPath: string, pdfPath: string }>}
|
|
13
|
+
*/
|
|
14
|
+
async function convertMarkdownToPdf(inputPath, outputPath, options = {}) {
|
|
15
|
+
const absInputPath = resolvePath(inputPath);
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(absInputPath)) {
|
|
18
|
+
throw new Error(`Input file not found: ${absInputPath}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const format = options.format ? options.format.toLowerCase() : 'pdf';
|
|
22
|
+
|
|
23
|
+
// Derive output path if omitted
|
|
24
|
+
let absOutputPath;
|
|
25
|
+
if (outputPath) {
|
|
26
|
+
absOutputPath = resolvePath(outputPath);
|
|
27
|
+
} else {
|
|
28
|
+
const parsed = path.parse(absInputPath);
|
|
29
|
+
absOutputPath = path.join(parsed.dir, `${parsed.name}.${format}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Read Markdown
|
|
33
|
+
const markdownContent = fs.readFileSync(absInputPath, 'utf8');
|
|
34
|
+
|
|
35
|
+
// Parse to HTML
|
|
36
|
+
const htmlContent = parseMarkdownToHtml(markdownContent, options);
|
|
37
|
+
|
|
38
|
+
if (format === 'html') {
|
|
39
|
+
fs.writeFileSync(absOutputPath, htmlContent, 'utf8');
|
|
40
|
+
return {
|
|
41
|
+
htmlPath: absOutputPath,
|
|
42
|
+
pdfPath: null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Temporary HTML path
|
|
47
|
+
const tempHtmlPath = path.join(path.parse(absOutputPath).dir, `_temp_${Date.now()}_${Math.random().toString(36).substring(7)}.html`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Save temporary HTML file
|
|
51
|
+
fs.writeFileSync(tempHtmlPath, htmlContent, 'utf8');
|
|
52
|
+
|
|
53
|
+
// Render HTML to PDF via headless browser
|
|
54
|
+
await renderHtmlToPdf(tempHtmlPath, absOutputPath, options);
|
|
55
|
+
|
|
56
|
+
// Clean up temporary HTML file unless explicitly requested to keep
|
|
57
|
+
if (!options.keepHtml) {
|
|
58
|
+
if (fs.existsSync(tempHtmlPath)) {
|
|
59
|
+
fs.unlinkSync(tempHtmlPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
htmlPath: tempHtmlPath,
|
|
65
|
+
pdfPath: absOutputPath
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Clean up on failure
|
|
69
|
+
if (fs.existsSync(tempHtmlPath)) {
|
|
70
|
+
try { fs.unlinkSync(tempHtmlPath); } catch (_) {}
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
convertMarkdownToPdf,
|
|
78
|
+
parseMarkdownToHtml,
|
|
79
|
+
renderHtmlToPdf
|
|
80
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const { marked } = require('marked');
|
|
2
|
+
const {
|
|
3
|
+
parseFrontMatter,
|
|
4
|
+
generateCoverPage,
|
|
5
|
+
preprocessMarkdown,
|
|
6
|
+
generateToc,
|
|
7
|
+
readCustomCss,
|
|
8
|
+
sanitizeHtml,
|
|
9
|
+
escapeHtml
|
|
10
|
+
} = require('./utils');
|
|
11
|
+
const { getHtmlTemplate } = require('./templates');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds marked custom renderer for diagrams, UI mockups, and headings
|
|
15
|
+
* @param {Map<string, string>} headingsMap
|
|
16
|
+
* @returns {marked.Renderer}
|
|
17
|
+
*/
|
|
18
|
+
function buildMarkedRenderer(headingsMap) {
|
|
19
|
+
const renderer = new marked.Renderer();
|
|
20
|
+
const originalCodeRenderer = renderer.code.bind(renderer);
|
|
21
|
+
|
|
22
|
+
renderer.code = function(codeArg, infostringArg, escapedArg) {
|
|
23
|
+
let text = '';
|
|
24
|
+
let lang = '';
|
|
25
|
+
|
|
26
|
+
if (typeof codeArg === 'object' && codeArg !== null) {
|
|
27
|
+
text = codeArg.text || '';
|
|
28
|
+
lang = codeArg.lang || '';
|
|
29
|
+
} else {
|
|
30
|
+
text = codeArg || '';
|
|
31
|
+
lang = infostringArg || '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (lang === 'mermaid') {
|
|
35
|
+
return `<div class="mermaid-container"><pre class="mermaid">${text}</pre></div>`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (text.includes('┌') && text.includes('└')) {
|
|
39
|
+
return `<div class="ui-mockup-container"><pre class="ui-mockup"><code>${escapeHtml(text)}</code></pre></div>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return originalCodeRenderer.call(this, codeArg, infostringArg, escapedArg);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
renderer.heading = function(text, level, raw, slugger) {
|
|
46
|
+
const rawClean = raw ? raw.trim() : text;
|
|
47
|
+
const slug = headingsMap.get(rawClean) || (slugger ? slugger.slug(raw) : `heading-${level}`);
|
|
48
|
+
return `<h${level} id="${slug}">${text}</h${level}>`;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return renderer;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Wraps section headers with section-group divs for clean page-break handling
|
|
56
|
+
* @param {string} html
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function wrapSectionGroups(html) {
|
|
60
|
+
let wrapped = html.replace(
|
|
61
|
+
/<h3>(.*?)<\/h3>\s*(<div class="ui-mockup-container">|<table|<div class="mermaid-container">|<ul|<ol)/g,
|
|
62
|
+
'<div class="section-group"><h3>$1</h3>$2'
|
|
63
|
+
);
|
|
64
|
+
return wrapped.replace(
|
|
65
|
+
/(<\/table>|<\/div>|<\/ul>|<\/ol>)\s*(?=<h[1-4]>|<hr|\$)/g,
|
|
66
|
+
'$1</div>'
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Converts Markdown content into a fully styled HTML string
|
|
72
|
+
* @param {string} rawMarkdownContent
|
|
73
|
+
* @param {Object} options
|
|
74
|
+
* @returns {string} HTML string
|
|
75
|
+
*/
|
|
76
|
+
function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
|
|
77
|
+
// 1. Extract Front Matter & Metadata
|
|
78
|
+
const { data: frontMatter, content: markdownContent } = parseFrontMatter(rawMarkdownContent);
|
|
79
|
+
const titleMatch = markdownContent.match(/^#\s+(.+)$/m);
|
|
80
|
+
const title = frontMatter.title || options.title || (titleMatch ? titleMatch[1].trim() : 'Converted Document');
|
|
81
|
+
const subtitle = frontMatter.subtitle || options.subtitle || '';
|
|
82
|
+
const author = frontMatter.author || options.author || '';
|
|
83
|
+
const date = frontMatter.date || options.date || '';
|
|
84
|
+
|
|
85
|
+
// 2. Merge options
|
|
86
|
+
const mergedOptions = {
|
|
87
|
+
theme: options.theme || frontMatter.theme || 'modern',
|
|
88
|
+
font: options.font || frontMatter.font || 'Inter',
|
|
89
|
+
pageSize: options.pageSize || frontMatter.pageSize || 'A4',
|
|
90
|
+
orientation: options.orientation || frontMatter.orientation || (frontMatter.landscape ? 'landscape' : 'portrait'),
|
|
91
|
+
margin: options.margin || frontMatter.margin || '14mm 12mm 16mm 12mm',
|
|
92
|
+
toc: options.toc !== undefined ? options.toc : (frontMatter.toc || false),
|
|
93
|
+
cover: options.cover !== undefined ? options.cover : (frontMatter.cover || false),
|
|
94
|
+
header: options.header !== undefined ? options.header : (frontMatter.header || true),
|
|
95
|
+
footer: options.footer !== undefined ? options.footer : (frontMatter.footer || true),
|
|
96
|
+
mermaid: options.mermaid !== undefined ? options.mermaid : (frontMatter.mermaid !== false),
|
|
97
|
+
katex: options.katex !== undefined ? options.katex : (frontMatter.katex !== false),
|
|
98
|
+
customCss: options.customCss || '',
|
|
99
|
+
cssFile: options.cssFile || frontMatter.cssFile || frontMatter.css
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// 3. Preprocess Markdown (LaTeX, pagebreaks, alerts)
|
|
103
|
+
const processedMd = preprocessMarkdown(markdownContent);
|
|
104
|
+
|
|
105
|
+
// 4. Generate TOC & Cover Page
|
|
106
|
+
let tocHtml = '';
|
|
107
|
+
let headingsMap = new Map();
|
|
108
|
+
if (mergedOptions.toc) {
|
|
109
|
+
const tocResult = generateToc(processedMd);
|
|
110
|
+
tocHtml = tocResult.tocHtml;
|
|
111
|
+
headingsMap = tocResult.headingsMap;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const coverHtml = generateCoverPage({
|
|
115
|
+
title,
|
|
116
|
+
subtitle,
|
|
117
|
+
author,
|
|
118
|
+
date,
|
|
119
|
+
cover: mergedOptions.cover
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 5. Read custom CSS file if specified
|
|
123
|
+
let customCss = mergedOptions.customCss || '';
|
|
124
|
+
if (mergedOptions.cssFile) {
|
|
125
|
+
customCss += '\n' + readCustomCss(mergedOptions.cssFile);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 6. Parse Markdown using custom renderer
|
|
129
|
+
marked.setOptions({ gfm: true, breaks: true });
|
|
130
|
+
const renderer = buildMarkedRenderer(headingsMap);
|
|
131
|
+
marked.use({ renderer });
|
|
132
|
+
|
|
133
|
+
let bodyHtml = marked.parse(processedMd);
|
|
134
|
+
bodyHtml = sanitizeHtml(bodyHtml);
|
|
135
|
+
|
|
136
|
+
// 7. Inject TOC, Cover Page & Section Groups
|
|
137
|
+
if (tocHtml) {
|
|
138
|
+
bodyHtml = bodyHtml.includes('</h1>')
|
|
139
|
+
? bodyHtml.replace('</h1>', '</h1>\n' + tocHtml)
|
|
140
|
+
: tocHtml + bodyHtml;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (coverHtml) {
|
|
144
|
+
bodyHtml = coverHtml + bodyHtml;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
bodyHtml = wrapSectionGroups(bodyHtml);
|
|
148
|
+
|
|
149
|
+
// 8. Generate and return full HTML template
|
|
150
|
+
return getHtmlTemplate({
|
|
151
|
+
title,
|
|
152
|
+
author,
|
|
153
|
+
bodyHtml,
|
|
154
|
+
options: {
|
|
155
|
+
...mergedOptions,
|
|
156
|
+
customCss
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
parseMarkdownToHtml
|
|
163
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Sanitizer & Escaping Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sanitizes HTML content by stripping dangerous inline script tags and inline handlers
|
|
7
|
+
* @param {string} html
|
|
8
|
+
* @returns {string} Clean HTML string
|
|
9
|
+
*/
|
|
10
|
+
function sanitizeHtml(html) {
|
|
11
|
+
if (!html) return '';
|
|
12
|
+
return html
|
|
13
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
14
|
+
.replace(/\s*on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escapes special HTML characters
|
|
19
|
+
* @param {string} str
|
|
20
|
+
* @returns {string} Escaped string
|
|
21
|
+
*/
|
|
22
|
+
function escapeHtml(str) {
|
|
23
|
+
if (!str) return '';
|
|
24
|
+
return str
|
|
25
|
+
.replace(/&/g, '&')
|
|
26
|
+
.replace(/</g, '<')
|
|
27
|
+
.replace(/>/g, '>')
|
|
28
|
+
.replace(/"/g, '"')
|
|
29
|
+
.replace(/'/g, ''');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
sanitizeHtml,
|
|
34
|
+
escapeHtml
|
|
35
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const { getThemeStyles } = require('./themes');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Escapes special XML characters in title/metadata
|
|
5
|
+
* @param {string} str
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
function escapeXml(str) {
|
|
9
|
+
if (!str) return 'Document';
|
|
10
|
+
return str.replace(/[<>&'"]/g, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns complete HTML document template with injected body, CSS, fonts, and scripts
|
|
15
|
+
* @param {Object} params
|
|
16
|
+
* @param {string} params.title Document title
|
|
17
|
+
* @param {string} [params.author] Document author
|
|
18
|
+
* @param {string} params.bodyHtml Document body HTML
|
|
19
|
+
* @param {Object} [params.options] Template options
|
|
20
|
+
* @returns {string} Full HTML string
|
|
21
|
+
*/
|
|
22
|
+
function getHtmlTemplate({ title, author = '', bodyHtml, options = {} }) {
|
|
23
|
+
const theme = options.theme || 'modern';
|
|
24
|
+
const enableMermaid = options.mermaid !== false;
|
|
25
|
+
const enableKatex = options.katex !== false;
|
|
26
|
+
const pageSize = options.pageSize || 'A4';
|
|
27
|
+
const orientation = options.orientation || 'portrait';
|
|
28
|
+
const margin = options.margin || '14mm 12mm 16mm 12mm';
|
|
29
|
+
const customCss = options.customCss || '';
|
|
30
|
+
|
|
31
|
+
const themeStyles = getThemeStyles(theme, options.font);
|
|
32
|
+
|
|
33
|
+
return `<!DOCTYPE html>
|
|
34
|
+
<html lang="vi">
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="UTF-8">
|
|
37
|
+
<title>${escapeXml(title)}</title>
|
|
38
|
+
|
|
39
|
+
<!--
|
|
40
|
+
Google Fonts Integration - Verified Open Source Licensing:
|
|
41
|
+
- Inter: SIL Open Font License 1.1 (100% Free / Open Source)
|
|
42
|
+
- Roboto: Apache License 2.0 (100% Free / Open Source)
|
|
43
|
+
- JetBrains Mono: SIL Open Font License 1.1 (100% Free / Open Source)
|
|
44
|
+
- Fira Code: SIL Open Font License 1.1 (100% Free / Open Source)
|
|
45
|
+
- Merriweather: SIL Open Font License 1.1 (100% Free / Open Source)
|
|
46
|
+
- Lora: SIL Open Font License 1.1 (100% Free / Open Source)
|
|
47
|
+
- Noto Sans JP: SIL Open Font License 1.1 (100% Free / Open Source)
|
|
48
|
+
-->
|
|
49
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
50
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
51
|
+
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Lora:ital,wght@0,400;0,600;1,400&family=Merriweather:wght@400;700&family=Noto+Sans+JP:wght@400;500;700&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
|
52
|
+
|
|
53
|
+
${enableKatex ? `
|
|
54
|
+
<!-- KaTeX for math rendering -->
|
|
55
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css">
|
|
56
|
+
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js"></script>
|
|
57
|
+
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js" onload="if(typeof renderMathInElement==='function'){renderMathInElement(document.body);}"></script>
|
|
58
|
+
` : ''}
|
|
59
|
+
|
|
60
|
+
${enableMermaid ? `
|
|
61
|
+
<!-- Mermaid.js for diagrams -->
|
|
62
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
63
|
+
<script>
|
|
64
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
65
|
+
if (typeof mermaid !== 'undefined') {
|
|
66
|
+
mermaid.initialize({
|
|
67
|
+
startOnLoad: true,
|
|
68
|
+
theme: '${theme === 'dark' ? 'dark' : 'base'}',
|
|
69
|
+
securityLevel: 'loose',
|
|
70
|
+
wrap: true,
|
|
71
|
+
useMaxWidth: true,
|
|
72
|
+
fontFamily: 'Inter, sans-serif',
|
|
73
|
+
sequence: {
|
|
74
|
+
useMaxWidth: true,
|
|
75
|
+
wrap: true,
|
|
76
|
+
boxMargin: 10,
|
|
77
|
+
noteMargin: 10,
|
|
78
|
+
messageMargin: 35
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
</script>
|
|
84
|
+
` : ''}
|
|
85
|
+
|
|
86
|
+
<style>
|
|
87
|
+
@page {
|
|
88
|
+
size: ${pageSize} ${orientation};
|
|
89
|
+
margin: ${margin};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
body {
|
|
93
|
+
-webkit-print-color-adjust: exact !important;
|
|
94
|
+
print-color-adjust: exact !important;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.page-break {
|
|
98
|
+
break-after: page;
|
|
99
|
+
page-break-after: always;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
img {
|
|
103
|
+
max-width: 100% !important;
|
|
104
|
+
height: auto !important;
|
|
105
|
+
object-fit: contain;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
input[type="checkbox"] {
|
|
109
|
+
-webkit-appearance: checkbox !important;
|
|
110
|
+
appearance: checkbox !important;
|
|
111
|
+
display: inline-block !important;
|
|
112
|
+
width: 14px !important;
|
|
113
|
+
height: 14px !important;
|
|
114
|
+
margin: 0 4px 0 0 !important;
|
|
115
|
+
vertical-align: middle !important;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
${themeStyles}
|
|
119
|
+
|
|
120
|
+
/* Table of Contents Container & Dotted Leader Lines */
|
|
121
|
+
.toc-container {
|
|
122
|
+
background: #f8fafc;
|
|
123
|
+
border: 1px solid #e2e8f0;
|
|
124
|
+
border-left: 4px solid #2563eb;
|
|
125
|
+
border-radius: 6px;
|
|
126
|
+
padding: 14px 18px;
|
|
127
|
+
margin: 16px 0 24px 0;
|
|
128
|
+
break-inside: avoid-page;
|
|
129
|
+
page-break-inside: avoid;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.toc-title {
|
|
133
|
+
font-size: 15px;
|
|
134
|
+
font-weight: 700;
|
|
135
|
+
color: #1e3a8a;
|
|
136
|
+
margin-bottom: 10px;
|
|
137
|
+
border-bottom: 1px solid #cbd5e1;
|
|
138
|
+
padding-bottom: 6px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.toc-list {
|
|
142
|
+
list-style: none;
|
|
143
|
+
padding-left: 0;
|
|
144
|
+
margin: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.toc-list li {
|
|
148
|
+
margin-bottom: 6px;
|
|
149
|
+
font-size: 13px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.toc-list a {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: baseline;
|
|
155
|
+
color: #2563eb;
|
|
156
|
+
text-decoration: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.toc-item-title {
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
max-width: 80%;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.toc-item-dots {
|
|
165
|
+
flex-grow: 1;
|
|
166
|
+
border-bottom: 1px dotted #94a3b8;
|
|
167
|
+
margin: 0 8px;
|
|
168
|
+
align-self: center;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.toc-list a:hover {
|
|
172
|
+
text-decoration: underline;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.toc-item-h1 { font-weight: 600; padding-left: 0; }
|
|
176
|
+
.toc-item-h2 { padding-left: 16px; }
|
|
177
|
+
.toc-item-h3 { padding-left: 32px; font-size: 12px; color: #64748b; }
|
|
178
|
+
|
|
179
|
+
/* Custom Injected CSS */
|
|
180
|
+
${customCss}
|
|
181
|
+
</style>
|
|
182
|
+
</head>
|
|
183
|
+
<body>
|
|
184
|
+
${bodyHtml}
|
|
185
|
+
</body>
|
|
186
|
+
</html>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
getHtmlTemplate,
|
|
191
|
+
getThemeStyles
|
|
192
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Academic Theme Styles
|
|
3
|
+
*/
|
|
4
|
+
module.exports = `
|
|
5
|
+
body { font-family: 'Merriweather', Georgia, serif; font-size: 12px; line-height: 1.7; color: #111827; background-color: #ffffff; padding: 10px; }
|
|
6
|
+
h1 { font-size: 20px; color: #111827; border-bottom: 2px solid #111827; padding-bottom: 6px; break-after: avoid-page; font-weight: 700; text-align: center; }
|
|
7
|
+
h2 { font-size: 15px; color: #1f2937; border-bottom: 1px solid #d1d5db; padding-bottom: 4px; margin-top: 20px; break-after: avoid-page; }
|
|
8
|
+
h3 { font-size: 13.5px; color: #374151; font-style: italic; break-after: avoid-page; }
|
|
9
|
+
blockquote { font-style: italic; border-left: 3px solid #6b7280; padding-left: 14px; margin: 12px 0; color: #4b5563; }
|
|
10
|
+
table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: 11.5px; }
|
|
11
|
+
tr { break-inside: avoid-page; }
|
|
12
|
+
th { background: #f3f4f6; color: #111827; padding: 6px; border: 1px solid #9ca3af; font-weight: 700; }
|
|
13
|
+
td { padding: 6px; border: 1px solid #d1d5db; }
|
|
14
|
+
code { font-family: 'JetBrains Mono', monospace; font-size: 11px; background: #f3f4f6; padding: 2px 4px; border-radius: 2px; }
|
|
15
|
+
pre { font-family: 'JetBrains Mono', monospace; font-size: 10px; background: #f9fafb; border: 1px solid #e5e7eb; padding: 10px; border-radius: 4px; break-inside: avoid-page; }
|
|
16
|
+
.formula-box { background: #f9fafb; border: 1px solid #d1d5db; padding: 8px 14px; border-radius: 4px; text-align: center; font-style: italic; }
|
|
17
|
+
.mermaid-container { display: block; text-align: center; margin: 14px 0; background: #ffffff; padding: 8px; border: 1px solid #e5e7eb; 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,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cover Page CSS Styles
|
|
3
|
+
*/
|
|
4
|
+
module.exports = `
|
|
5
|
+
.cover-page { min-height: 85vh; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: 40px 20px; break-after: page; page-break-after: always; }
|
|
6
|
+
.cover-page-inner { max-width: 650px; width: 100%; border: 2px solid #2563eb; padding: 40px 30px; border-radius: 8px; background: #ffffff; box-shadow: 0 10px 25px rgba(0,0,0,0.05); }
|
|
7
|
+
.cover-badge { display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: 1.5px; color: #2563eb; background: #eff6ff; padding: 4px 12px; border-radius: 20px; margin-bottom: 20px; }
|
|
8
|
+
.cover-title { font-size: 28px !important; font-weight: 800 !important; color: #0f172a !important; line-height: 1.3 !important; margin-bottom: 12px !important; border: none !important; padding: 0 !important; text-transform: none !important; }
|
|
9
|
+
.cover-subtitle { font-size: 16px !important; font-weight: 500 !important; color: #475569 !important; margin-bottom: 24px !important; border: none !important; background: none !important; padding: 0 !important; }
|
|
10
|
+
.cover-divider { height: 3px; width: 80px; background: linear-gradient(90deg, #2563eb, #38bdf8); margin: 0 auto 24px auto; border-radius: 2px; }
|
|
11
|
+
.cover-meta-container { display: flex; flex-direction: column; gap: 8px; font-size: 13px; color: #64748b; align-items: center; }
|
|
12
|
+
.cover-meta-item { display: flex; gap: 8px; }
|
|
13
|
+
.cover-meta-label { font-weight: 600; color: #334155; }
|
|
14
|
+
`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cyberpunk Theme Styles
|
|
3
|
+
*/
|
|
4
|
+
module.exports = `
|
|
5
|
+
body { font-family: 'Fira Code', 'JetBrains Mono', monospace; font-size: 12.5px; line-height: 1.6; color: #00f0ff; background-color: #0d1117; padding: 10px; }
|
|
6
|
+
h1 { font-size: 21px; color: #ff007f; text-shadow: 0 0 5px rgba(255,0,127,0.5); border-bottom: 2px solid #ff007f; padding-bottom: 6px; break-after: avoid-page; text-transform: uppercase; }
|
|
7
|
+
h2 { font-size: 16px; color: #00f0ff; background: #161b22; border-left: 4px solid #00f0ff; padding: 6px 12px; break-after: avoid-page; }
|
|
8
|
+
h3 { font-size: 14px; color: #ffe600; border-bottom: 1px solid #30363d; padding-bottom: 4px; break-after: avoid-page; }
|
|
9
|
+
blockquote { background: #161b22; border-left: 4px solid #ff007f; color: #8b949e; padding: 10px 16px; }
|
|
10
|
+
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 11.5px; }
|
|
11
|
+
tr { break-inside: avoid-page; }
|
|
12
|
+
th { background: #161b22; color: #00f0ff; padding: 8px; border: 1px solid #30363d; }
|
|
13
|
+
td { padding: 7px; border: 1px solid #30363d; color: #c9d1d9; }
|
|
14
|
+
tbody tr:nth-child(even) { background: #161b22; }
|
|
15
|
+
code { font-family: 'Fira Code', monospace; font-size: 11px; background: #161b22; color: #ff007f; padding: 2px 6px; border: 1px solid #30363d; }
|
|
16
|
+
pre { font-family: 'Fira Code', monospace; font-size: 10px; background: #010409; color: #00f0ff; padding: 12px; border-radius: 4px; border: 1px solid #30363d; overflow-x: auto; break-inside: avoid-page; }
|
|
17
|
+
.formula-box { background: #161b22; border: 1px solid #00f0ff; padding: 10px; text-align: center; color: #00f0ff; }
|
|
18
|
+
.mermaid-container { display: block; text-align: center; margin: 14px 0; background: #161b22; padding: 10px; border: 1px solid #ff007f; border-radius: 4px; overflow-x: auto; break-inside: avoid-page; }
|
|
19
|
+
.mermaid { display: block; text-align: center; width: 100%; }
|
|
20
|
+
.mermaid svg { max-width: 100% !important; height: auto !important; margin: 0 auto; display: inline-block; }
|
|
21
|
+
`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dark Theme Styles
|
|
3
|
+
*/
|
|
4
|
+
module.exports = `
|
|
5
|
+
body { font-family: 'Inter', sans-serif; font-size: 13px; line-height: 1.6; color: #e2e8f0; background-color: #0f172a; padding: 10px; }
|
|
6
|
+
h1 { font-size: 21px; color: #f8fafc; border-bottom: 3px solid #38bdf8; padding-bottom: 8px; break-after: avoid-page; }
|
|
7
|
+
h2 { font-size: 16px; color: #38bdf8; background: #1e293b; border-left: 5px solid #38bdf8; padding: 6px 12px; border-radius: 0 4px 4px 0; break-after: avoid-page; }
|
|
8
|
+
h3 { font-size: 14.5px; color: #7dd3fc; border-bottom: 1px solid #334155; padding-bottom: 4px; break-after: avoid-page; }
|
|
9
|
+
blockquote { background: #1e293b; border-left: 4px solid #38bdf8; color: #cbd5e1; padding: 10px 16px; border-radius: 4px; }
|
|
10
|
+
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; }
|
|
11
|
+
tr { break-inside: avoid-page; }
|
|
12
|
+
th { background: #1e293b; color: #38bdf8; padding: 8px; border: 1px solid #334155; }
|
|
13
|
+
td { padding: 7px; border: 1px solid #334155; }
|
|
14
|
+
tbody tr:nth-child(even) { background: #1e293b; }
|
|
15
|
+
code { font-family: 'JetBrains Mono', monospace; font-size: 11.5px; background: #1e293b; color: #f8fafc; padding: 2px 6px; border-radius: 3px; border: 1px solid #334155; }
|
|
16
|
+
pre { font-family: 'JetBrains Mono', monospace; font-size: 10.5px; background: #020617; color: #f8fafc; padding: 12px; border-radius: 6px; overflow-x: auto; break-inside: avoid-page; }
|
|
17
|
+
.formula-box { background: #1e293b; border: 1px solid #38bdf8; padding: 10px 16px; border-radius: 4px; text-align: center; color: #38bdf8; }
|
|
18
|
+
.mermaid-container { display: block; text-align: center; margin: 14px 0; background: #1e293b; padding: 12px; border-radius: 6px; overflow-x: auto; break-inside: avoid-page; }
|
|
19
|
+
.mermaid { display: block; text-align: center; width: 100%; }
|
|
20
|
+
.mermaid svg { max-width: 100% !important; height: auto !important; margin: 0 auto; display: inline-block; }
|
|
21
|
+
`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E-Book Theme Styles
|
|
3
|
+
*/
|
|
4
|
+
module.exports = `
|
|
5
|
+
body { font-family: 'Lora', Georgia, serif; font-size: 13.5px; line-height: 1.75; color: #2d3748; background: #fdfbf7; padding: 12px; }
|
|
6
|
+
h1 { font-size: 23px; font-weight: 700; color: #1a202c; text-align: center; margin-bottom: 20px; border-bottom: 2px solid #cbd5e0; padding-bottom: 8px; break-after: avoid-page; }
|
|
7
|
+
h2 { font-size: 17px; font-weight: 600; color: #2b6cb0; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px; margin-top: 22px; break-after: avoid-page; }
|
|
8
|
+
h3 { font-size: 14.5px; font-weight: 600; color: #2c5282; font-style: italic; break-after: avoid-page; }
|
|
9
|
+
blockquote { font-style: italic; border-left: 3px solid #3182ce; background: #edf2f7; padding: 12px 18px; margin: 14px 0; border-radius: 0 4px 4px 0; }
|
|
10
|
+
table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: 12px; }
|
|
11
|
+
tr { break-inside: avoid-page; }
|
|
12
|
+
th { background: #ebf8ff; color: #2b6cb0; padding: 8px; border: 1px solid #bee3f8; }
|
|
13
|
+
td { padding: 7px; border: 1px solid #e2e8f0; }
|
|
14
|
+
code { font-family: 'JetBrains Mono', monospace; font-size: 11.5px; background: #edf2f7; color: #2d3748; padding: 2px 5px; border-radius: 3px; }
|
|
15
|
+
pre { font-family: 'JetBrains Mono', monospace; font-size: 10.5px; background: #2d3748; color: #f7fafc; padding: 12px; border-radius: 6px; overflow-x: auto; break-inside: avoid-page; }
|
|
16
|
+
.formula-box { background: #ebf8ff; border: 1px solid #bee3f8; padding: 10px; text-align: center; color: #2b6cb0; border-radius: 6px; }
|
|
17
|
+
.mermaid-container { display: block; text-align: center; margin: 14px 0; background: #ffffff; padding: 10px; border: 1px solid #e2e8f0; border-radius: 6px; 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,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Theme Styles
|
|
3
|
+
*/
|
|
4
|
+
module.exports = `
|
|
5
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 13px; line-height: 1.6; color: #1f2328; background: #ffffff; padding: 10px; }
|
|
6
|
+
h1 { font-size: 22px; font-weight: 600; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; break-after: avoid-page; }
|
|
7
|
+
h2 { font-size: 17px; font-weight: 600; color: #1f2328; border-bottom: 1px solid #d0d7de; padding-bottom: 6px; margin-top: 20px; break-after: avoid-page; }
|
|
8
|
+
h3 { font-size: 14.5px; font-weight: 600; color: #1f2328; break-after: avoid-page; }
|
|
9
|
+
blockquote { border-left: 4px solid #d0d7de; color: #656d76; padding: 8px 16px; margin: 12px 0; }
|
|
10
|
+
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; }
|
|
11
|
+
tr { break-inside: avoid-page; }
|
|
12
|
+
th { background: #f6f8fa; color: #1f2328; font-weight: 600; padding: 8px; border: 1px solid #d0d7de; }
|
|
13
|
+
td { padding: 7px; border: 1px solid #d0d7de; }
|
|
14
|
+
tbody tr:nth-child(even) { background: #f6f8fa; }
|
|
15
|
+
code { font-family: 'Fira Code', monospace; font-size: 11.5px; background: rgba(175,184,193,0.2); color: #1f2328; padding: 2px 5px; border-radius: 4px; }
|
|
16
|
+
pre { font-family: 'Fira Code', monospace; font-size: 10.5px; background: #f6f8fa; color: #1f2328; padding: 12px; border-radius: 6px; border: 1px solid #d0d7de; overflow-x: auto; break-inside: avoid-page; }
|
|
17
|
+
.formula-box { background: #f6f8fa; border: 1px solid #d0d7de; padding: 10px; text-align: center; border-radius: 6px; }
|
|
18
|
+
.mermaid-container { display: block; text-align: center; margin: 14px 0; background: #ffffff; padding: 8px; border: 1px solid #d0d7de; border-radius: 6px; overflow-x: auto; break-inside: avoid-page; }
|
|
19
|
+
.mermaid { display: block; text-align: center; width: 100%; }
|
|
20
|
+
.mermaid svg { max-width: 100% !important; height: auto !important; margin: 0 auto; display: inline-block; }
|
|
21
|
+
`;
|