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.
@@ -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, '&amp;')
26
+ .replace(/</g, '&lt;')
27
+ .replace(/>/g, '&gt;')
28
+ .replace(/"/g, '&quot;')
29
+ .replace(/'/g, '&#039;');
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
+ `;