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.
@@ -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
+ };
@@ -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
+ };