md-pdf-studio 1.1.0 → 2.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/LICENSE +21 -0
- package/README.md +214 -208
- package/bin/cli.js +188 -186
- package/package.json +62 -58
- package/src/browser/renderer.js +142 -142
- package/src/core/cover.js +40 -40
- package/src/core/frontmatter.js +54 -54
- package/src/core/index.js +80 -80
- package/src/core/parser.js +164 -163
- package/src/core/sanitizer.js +57 -35
- package/src/core/templates.js +192 -192
- package/src/core/themes/academic.js +20 -20
- package/src/core/themes/cover.js +14 -14
- package/src/core/themes/cyberpunk.js +21 -21
- package/src/core/themes/dark.js +21 -21
- package/src/core/themes/ebook.js +20 -20
- package/src/core/themes/github.js +21 -21
- package/src/core/themes/highlight.js +38 -0
- package/src/core/themes/index.js +58 -52
- package/src/core/themes/minimal.js +20 -20
- package/src/core/themes/modern.js +34 -34
- package/src/core/toc.js +72 -72
- package/src/core/utils.js +85 -85
- package/src/index.js +2 -2
package/src/browser/renderer.js
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const { execFile } = require('child_process');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Finds available Chrome or Edge binary path on system
|
|
8
|
-
* @returns {string|null}
|
|
9
|
-
*/
|
|
10
|
-
function findBrowserExecutable() {
|
|
11
|
-
const platform = os.platform();
|
|
12
|
-
const candidates = [];
|
|
13
|
-
|
|
14
|
-
if (platform === 'win32') {
|
|
15
|
-
const programFiles = process.env['PROGRAMFILES'] || 'C:\\Program Files';
|
|
16
|
-
const programFilesX86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
|
|
17
|
-
const localAppData = process.env['LOCALAPPDATA'] || path.join(os.homedir(), 'AppData', 'Local');
|
|
18
|
-
|
|
19
|
-
candidates.push(
|
|
20
|
-
path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
21
|
-
path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
22
|
-
path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
23
|
-
path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
24
|
-
path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe')
|
|
25
|
-
);
|
|
26
|
-
} else if (platform === 'darwin') {
|
|
27
|
-
candidates.push(
|
|
28
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
29
|
-
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
30
|
-
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
|
31
|
-
);
|
|
32
|
-
} else {
|
|
33
|
-
// Linux / Unix
|
|
34
|
-
candidates.push(
|
|
35
|
-
'/usr/bin/google-chrome',
|
|
36
|
-
'/usr/bin/microsoft-edge',
|
|
37
|
-
'/usr/bin/chromium-browser',
|
|
38
|
-
'/usr/bin/chromium'
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const binPath of candidates) {
|
|
43
|
-
if (fs.existsSync(binPath)) {
|
|
44
|
-
return binPath;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Renders HTML file to PDF using headless browser
|
|
53
|
-
* @param {string} htmlPath
|
|
54
|
-
* @param {string} outputPath
|
|
55
|
-
* @param {Object} options
|
|
56
|
-
* @returns {Promise<string>}
|
|
57
|
-
*/
|
|
58
|
-
function renderHtmlToPdf(htmlPath, outputPath, options = {}) {
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
const browserPath = options.executablePath || findBrowserExecutable();
|
|
61
|
-
|
|
62
|
-
if (!browserPath) {
|
|
63
|
-
return reject(new Error('No installed Chromium browser (Edge/Chrome) was found on your system. Please install Microsoft Edge or Google Chrome.'));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const fileUrl = 'file:///' + path.resolve(htmlPath).replace(/\\/g, '/');
|
|
67
|
-
const args = [
|
|
68
|
-
'--headless=new',
|
|
69
|
-
'--disable-gpu',
|
|
70
|
-
'--no-sandbox',
|
|
71
|
-
'--no-pdf-header-footer',
|
|
72
|
-
'--virtual-time-budget=8000',
|
|
73
|
-
`--print-to-pdf=${outputPath}`,
|
|
74
|
-
fileUrl
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
execFile(browserPath, args, { timeout: 30000 }, (error) => {
|
|
78
|
-
if (error) {
|
|
79
|
-
// Fallback for older chrome versions that don't support --headless=new
|
|
80
|
-
const fallbackArgs = [
|
|
81
|
-
'--headless',
|
|
82
|
-
'--disable-gpu',
|
|
83
|
-
'--no-sandbox',
|
|
84
|
-
'--no-pdf-header-footer',
|
|
85
|
-
'--virtual-time-budget=8000',
|
|
86
|
-
`--print-to-pdf=${outputPath}`,
|
|
87
|
-
fileUrl
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
execFile(browserPath, fallbackArgs, { timeout: 30000 }, (err2) => {
|
|
91
|
-
if (err2) {
|
|
92
|
-
return reject(new Error(`Failed to render PDF using browser: ${err2.message}`));
|
|
93
|
-
}
|
|
94
|
-
if (fs.existsSync(outputPath)) {
|
|
95
|
-
resolve(outputPath);
|
|
96
|
-
} else {
|
|
97
|
-
reject(new Error('Browser finished but output PDF file was not created.'));
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (fs.existsSync(outputPath)) {
|
|
104
|
-
resolve(outputPath);
|
|
105
|
-
} else {
|
|
106
|
-
reject(new Error('Browser finished but output PDF file was not created.'));
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Renders multiple HTML files to PDF using concurrent browser execution pool
|
|
114
|
-
* @param {Array<{ htmlPath: string, outputPath: string }>} itemPairs
|
|
115
|
-
* @param {Object} options
|
|
116
|
-
* @returns {Promise<Array<string>>}
|
|
117
|
-
*/
|
|
118
|
-
async function renderBatchHtmlToPdf(itemPairs, options = {}) {
|
|
119
|
-
const concurrency = Math.min(options.concurrency || 4, itemPairs.length);
|
|
120
|
-
const results = [];
|
|
121
|
-
const queue = [...itemPairs];
|
|
122
|
-
|
|
123
|
-
async function worker() {
|
|
124
|
-
while (queue.length > 0) {
|
|
125
|
-
const item = queue.shift();
|
|
126
|
-
if (!item) break;
|
|
127
|
-
const pdfPath = await renderHtmlToPdf(item.htmlPath, item.outputPath, options);
|
|
128
|
-
results.push(pdfPath);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const workers = Array.from({ length: concurrency }, () => worker());
|
|
133
|
-
await Promise.all(workers);
|
|
134
|
-
return results;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
module.exports = {
|
|
138
|
-
findBrowserExecutable,
|
|
139
|
-
renderHtmlToPdf,
|
|
140
|
-
renderBatchHtmlToPdf
|
|
141
|
-
};
|
|
142
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execFile } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Finds available Chrome or Edge binary path on system
|
|
8
|
+
* @returns {string|null}
|
|
9
|
+
*/
|
|
10
|
+
function findBrowserExecutable() {
|
|
11
|
+
const platform = os.platform();
|
|
12
|
+
const candidates = [];
|
|
13
|
+
|
|
14
|
+
if (platform === 'win32') {
|
|
15
|
+
const programFiles = process.env['PROGRAMFILES'] || 'C:\\Program Files';
|
|
16
|
+
const programFilesX86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)';
|
|
17
|
+
const localAppData = process.env['LOCALAPPDATA'] || path.join(os.homedir(), 'AppData', 'Local');
|
|
18
|
+
|
|
19
|
+
candidates.push(
|
|
20
|
+
path.join(programFilesX86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
21
|
+
path.join(programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
22
|
+
path.join(programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
23
|
+
path.join(programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
24
|
+
path.join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe')
|
|
25
|
+
);
|
|
26
|
+
} else if (platform === 'darwin') {
|
|
27
|
+
candidates.push(
|
|
28
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
29
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
30
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
|
31
|
+
);
|
|
32
|
+
} else {
|
|
33
|
+
// Linux / Unix
|
|
34
|
+
candidates.push(
|
|
35
|
+
'/usr/bin/google-chrome',
|
|
36
|
+
'/usr/bin/microsoft-edge',
|
|
37
|
+
'/usr/bin/chromium-browser',
|
|
38
|
+
'/usr/bin/chromium'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const binPath of candidates) {
|
|
43
|
+
if (fs.existsSync(binPath)) {
|
|
44
|
+
return binPath;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Renders HTML file to PDF using headless browser
|
|
53
|
+
* @param {string} htmlPath
|
|
54
|
+
* @param {string} outputPath
|
|
55
|
+
* @param {Object} options
|
|
56
|
+
* @returns {Promise<string>}
|
|
57
|
+
*/
|
|
58
|
+
function renderHtmlToPdf(htmlPath, outputPath, options = {}) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const browserPath = options.executablePath || findBrowserExecutable();
|
|
61
|
+
|
|
62
|
+
if (!browserPath) {
|
|
63
|
+
return reject(new Error('No installed Chromium browser (Edge/Chrome) was found on your system. Please install Microsoft Edge or Google Chrome.'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const fileUrl = 'file:///' + path.resolve(htmlPath).replace(/\\/g, '/');
|
|
67
|
+
const args = [
|
|
68
|
+
'--headless=new',
|
|
69
|
+
'--disable-gpu',
|
|
70
|
+
'--no-sandbox',
|
|
71
|
+
'--no-pdf-header-footer',
|
|
72
|
+
'--virtual-time-budget=8000',
|
|
73
|
+
`--print-to-pdf=${outputPath}`,
|
|
74
|
+
fileUrl
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
execFile(browserPath, args, { timeout: 30000 }, (error) => {
|
|
78
|
+
if (error) {
|
|
79
|
+
// Fallback for older chrome versions that don't support --headless=new
|
|
80
|
+
const fallbackArgs = [
|
|
81
|
+
'--headless',
|
|
82
|
+
'--disable-gpu',
|
|
83
|
+
'--no-sandbox',
|
|
84
|
+
'--no-pdf-header-footer',
|
|
85
|
+
'--virtual-time-budget=8000',
|
|
86
|
+
`--print-to-pdf=${outputPath}`,
|
|
87
|
+
fileUrl
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
execFile(browserPath, fallbackArgs, { timeout: 30000 }, (err2) => {
|
|
91
|
+
if (err2) {
|
|
92
|
+
return reject(new Error(`Failed to render PDF using browser: ${err2.message}`));
|
|
93
|
+
}
|
|
94
|
+
if (fs.existsSync(outputPath)) {
|
|
95
|
+
resolve(outputPath);
|
|
96
|
+
} else {
|
|
97
|
+
reject(new Error('Browser finished but output PDF file was not created.'));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (fs.existsSync(outputPath)) {
|
|
104
|
+
resolve(outputPath);
|
|
105
|
+
} else {
|
|
106
|
+
reject(new Error('Browser finished but output PDF file was not created.'));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Renders multiple HTML files to PDF using concurrent browser execution pool
|
|
114
|
+
* @param {Array<{ htmlPath: string, outputPath: string }>} itemPairs
|
|
115
|
+
* @param {Object} options
|
|
116
|
+
* @returns {Promise<Array<string>>}
|
|
117
|
+
*/
|
|
118
|
+
async function renderBatchHtmlToPdf(itemPairs, options = {}) {
|
|
119
|
+
const concurrency = Math.min(options.concurrency || 4, itemPairs.length);
|
|
120
|
+
const results = [];
|
|
121
|
+
const queue = [...itemPairs];
|
|
122
|
+
|
|
123
|
+
async function worker() {
|
|
124
|
+
while (queue.length > 0) {
|
|
125
|
+
const item = queue.shift();
|
|
126
|
+
if (!item) break;
|
|
127
|
+
const pdfPath = await renderHtmlToPdf(item.htmlPath, item.outputPath, options);
|
|
128
|
+
results.push(pdfPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const workers = Array.from({ length: concurrency }, () => worker());
|
|
133
|
+
await Promise.all(workers);
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
findBrowserExecutable,
|
|
139
|
+
renderHtmlToPdf,
|
|
140
|
+
renderBatchHtmlToPdf
|
|
141
|
+
};
|
|
142
|
+
|
package/src/core/cover.js
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cover Page Generator Utility
|
|
3
|
-
*/
|
|
4
|
-
const { escapeHtml } = require('./sanitizer');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Generates a Cover Page HTML block if requested in front matter or options
|
|
8
|
-
* @param {Object} metadata
|
|
9
|
-
* @returns {string} Cover page HTML string
|
|
10
|
-
*/
|
|
11
|
-
function generateCoverPage(metadata = {}) {
|
|
12
|
-
const { title, subtitle, author, date, cover } = metadata;
|
|
13
|
-
|
|
14
|
-
if (!cover && !subtitle && !author && !date) {
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const docTitle = title || 'Document Report';
|
|
19
|
-
const docSubtitle = subtitle || '';
|
|
20
|
-
const docAuthor = author || '';
|
|
21
|
-
const docDate = date || '';
|
|
22
|
-
|
|
23
|
-
return `<div class="cover-page">
|
|
24
|
-
<div class="cover-page-inner">
|
|
25
|
-
<div class="cover-badge">DOCUMENT REPORT</div>
|
|
26
|
-
<h1 class="cover-title">${escapeHtml(docTitle)}</h1>
|
|
27
|
-
${docSubtitle ? `<h2 class="cover-subtitle">${escapeHtml(docSubtitle)}</h2>` : ''}
|
|
28
|
-
<div class="cover-divider"></div>
|
|
29
|
-
<div class="cover-meta-container">
|
|
30
|
-
${docAuthor ? `<div class="cover-meta-item"><span class="cover-meta-label">Tác giả:</span> <span class="cover-meta-val">${escapeHtml(docAuthor)}</span></div>` : ''}
|
|
31
|
-
${docDate ? `<div class="cover-meta-item"><span class="cover-meta-label">Ngày lập:</span> <span class="cover-meta-val">${escapeHtml(docDate)}</span></div>` : ''}
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
<div class="page-break"></div>\n`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
module.exports = {
|
|
39
|
-
generateCoverPage
|
|
40
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Cover Page Generator Utility
|
|
3
|
+
*/
|
|
4
|
+
const { escapeHtml } = require('./sanitizer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates a Cover Page HTML block if requested in front matter or options
|
|
8
|
+
* @param {Object} metadata
|
|
9
|
+
* @returns {string} Cover page HTML string
|
|
10
|
+
*/
|
|
11
|
+
function generateCoverPage(metadata = {}) {
|
|
12
|
+
const { title, subtitle, author, date, cover } = metadata;
|
|
13
|
+
|
|
14
|
+
if (!cover && !subtitle && !author && !date) {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const docTitle = title || 'Document Report';
|
|
19
|
+
const docSubtitle = subtitle || '';
|
|
20
|
+
const docAuthor = author || '';
|
|
21
|
+
const docDate = date || '';
|
|
22
|
+
|
|
23
|
+
return `<div class="cover-page">
|
|
24
|
+
<div class="cover-page-inner">
|
|
25
|
+
<div class="cover-badge">DOCUMENT REPORT</div>
|
|
26
|
+
<h1 class="cover-title">${escapeHtml(docTitle)}</h1>
|
|
27
|
+
${docSubtitle ? `<h2 class="cover-subtitle">${escapeHtml(docSubtitle)}</h2>` : ''}
|
|
28
|
+
<div class="cover-divider"></div>
|
|
29
|
+
<div class="cover-meta-container">
|
|
30
|
+
${docAuthor ? `<div class="cover-meta-item"><span class="cover-meta-label">Tác giả:</span> <span class="cover-meta-val">${escapeHtml(docAuthor)}</span></div>` : ''}
|
|
31
|
+
${docDate ? `<div class="cover-meta-item"><span class="cover-meta-label">Ngày lập:</span> <span class="cover-meta-val">${escapeHtml(docDate)}</span></div>` : ''}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="page-break"></div>\n`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
generateCoverPage
|
|
40
|
+
};
|
package/src/core/frontmatter.js
CHANGED
|
@@ -1,54 +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
|
-
};
|
|
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
|
+
};
|
package/src/core/index.js
CHANGED
|
@@ -1,80 +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
|
-
};
|
|
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
|
+
};
|