md-pdf-studio 2.1.0 → 2.3.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 +35 -2
- package/bin/cli.js +35 -14
- package/package.json +5 -3
- package/src/core/cover.js +1 -2
- package/src/core/docmodel.js +323 -0
- package/src/core/docx.js +492 -0
- package/src/core/epub.js +337 -0
- package/src/core/footnotes.js +122 -0
- package/src/core/images.js +69 -0
- package/src/core/include.js +104 -0
- package/src/core/index.js +63 -9
- package/src/core/parser.js +3 -2
- package/src/core/templates.js +6 -0
- package/src/core/utils.js +5 -5
- package/src/core/zip.js +77 -0
package/src/core/index.js
CHANGED
|
@@ -3,13 +3,40 @@ const path = require('path');
|
|
|
3
3
|
const { parseMarkdownToHtml } = require('./parser');
|
|
4
4
|
const { renderHtmlToPdf } = require('../browser/renderer');
|
|
5
5
|
const { resolvePath } = require('./utils');
|
|
6
|
+
const { expandIncludes } = require('./include');
|
|
7
|
+
const { markdownToDocx } = require('./docx');
|
|
8
|
+
const { markdownToEpub } = require('./epub');
|
|
9
|
+
|
|
10
|
+
const FORMATS = ['pdf', 'html', 'docx', 'epub'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where the page the browser prints is written: beside the Markdown file, so
|
|
14
|
+
* that its relative pictures and links resolve as they do in the source, also
|
|
15
|
+
* when the PDF goes to another folder. Falls back to the PDF's folder when the
|
|
16
|
+
* source's folder cannot be written to.
|
|
17
|
+
* @param {string} absInputPath
|
|
18
|
+
* @param {string} absOutputPath
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function temporaryHtmlPath(absInputPath, absOutputPath) {
|
|
22
|
+
const name = `_temp_${Date.now()}_${Math.random().toString(36).substring(7)}.html`;
|
|
23
|
+
const sourceDir = path.dirname(absInputPath);
|
|
24
|
+
try {
|
|
25
|
+
fs.accessSync(sourceDir, fs.constants.W_OK);
|
|
26
|
+
return path.join(sourceDir, name);
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return path.join(path.dirname(absOutputPath), name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
6
31
|
|
|
7
32
|
/**
|
|
8
|
-
* Main function to convert Markdown file to PDF
|
|
33
|
+
* Main function to convert a Markdown file: to PDF by default, or to the
|
|
34
|
+
* format named in `options.format` (`html`, `docx`, `epub`)
|
|
9
35
|
* @param {string} inputPath Path to source .md file
|
|
10
|
-
* @param {string} [outputPath] Path to target
|
|
36
|
+
* @param {string} [outputPath] Path to the target file
|
|
11
37
|
* @param {Object} [options] Conversion options
|
|
12
|
-
* @returns {Promise<{ htmlPath: string, pdfPath: string }>}
|
|
38
|
+
* @returns {Promise<{ outputPath: string, htmlPath: string, pdfPath: string, includedFiles: string[] }>}
|
|
39
|
+
* `outputPath` is the file that was asked for, whatever its format
|
|
13
40
|
*/
|
|
14
41
|
async function convertMarkdownToPdf(inputPath, outputPath, options = {}) {
|
|
15
42
|
const absInputPath = resolvePath(inputPath);
|
|
@@ -19,6 +46,9 @@ async function convertMarkdownToPdf(inputPath, outputPath, options = {}) {
|
|
|
19
46
|
}
|
|
20
47
|
|
|
21
48
|
const format = options.format ? options.format.toLowerCase() : 'pdf';
|
|
49
|
+
if (!FORMATS.includes(format)) {
|
|
50
|
+
throw new Error(`Unknown output format "${options.format}". Use one of: ${FORMATS.join(', ')}`);
|
|
51
|
+
}
|
|
22
52
|
|
|
23
53
|
// Derive output path if omitted
|
|
24
54
|
let absOutputPath;
|
|
@@ -29,8 +59,24 @@ async function convertMarkdownToPdf(inputPath, outputPath, options = {}) {
|
|
|
29
59
|
absOutputPath = path.join(parsed.dir, `${parsed.name}.${format}`);
|
|
30
60
|
}
|
|
31
61
|
|
|
32
|
-
// Read Markdown
|
|
33
|
-
|
|
62
|
+
// Read Markdown, pulling in the files it imports with `@import "path"`
|
|
63
|
+
let markdownContent = fs.readFileSync(absInputPath, 'utf8');
|
|
64
|
+
let includedFiles = [];
|
|
65
|
+
if (options.include !== false) {
|
|
66
|
+
({ content: markdownContent, files: includedFiles } = expandIncludes(markdownContent, absInputPath));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Word and EPUB are written from the Markdown itself, without a browser
|
|
70
|
+
if (format === 'docx' || format === 'epub') {
|
|
71
|
+
const build = format === 'docx' ? markdownToDocx : markdownToEpub;
|
|
72
|
+
fs.writeFileSync(absOutputPath, build(markdownContent, { ...options, baseDir: path.dirname(absInputPath) }));
|
|
73
|
+
return {
|
|
74
|
+
outputPath: absOutputPath,
|
|
75
|
+
htmlPath: null,
|
|
76
|
+
pdfPath: null,
|
|
77
|
+
includedFiles
|
|
78
|
+
};
|
|
79
|
+
}
|
|
34
80
|
|
|
35
81
|
// Parse to HTML
|
|
36
82
|
const htmlContent = parseMarkdownToHtml(markdownContent, options);
|
|
@@ -38,13 +84,15 @@ async function convertMarkdownToPdf(inputPath, outputPath, options = {}) {
|
|
|
38
84
|
if (format === 'html') {
|
|
39
85
|
fs.writeFileSync(absOutputPath, htmlContent, 'utf8');
|
|
40
86
|
return {
|
|
87
|
+
outputPath: absOutputPath,
|
|
41
88
|
htmlPath: absOutputPath,
|
|
42
|
-
pdfPath: null
|
|
89
|
+
pdfPath: null,
|
|
90
|
+
includedFiles
|
|
43
91
|
};
|
|
44
92
|
}
|
|
45
93
|
|
|
46
94
|
// Temporary HTML path
|
|
47
|
-
const tempHtmlPath =
|
|
95
|
+
const tempHtmlPath = temporaryHtmlPath(absInputPath, absOutputPath);
|
|
48
96
|
|
|
49
97
|
try {
|
|
50
98
|
// Save temporary HTML file
|
|
@@ -61,8 +109,10 @@ async function convertMarkdownToPdf(inputPath, outputPath, options = {}) {
|
|
|
61
109
|
}
|
|
62
110
|
|
|
63
111
|
return {
|
|
112
|
+
outputPath: absOutputPath,
|
|
64
113
|
htmlPath: tempHtmlPath,
|
|
65
|
-
pdfPath: absOutputPath
|
|
114
|
+
pdfPath: absOutputPath,
|
|
115
|
+
includedFiles
|
|
66
116
|
};
|
|
67
117
|
} catch (error) {
|
|
68
118
|
// Clean up on failure
|
|
@@ -75,6 +125,10 @@ async function convertMarkdownToPdf(inputPath, outputPath, options = {}) {
|
|
|
75
125
|
|
|
76
126
|
module.exports = {
|
|
77
127
|
convertMarkdownToPdf,
|
|
128
|
+
expandIncludes,
|
|
129
|
+
markdownToDocx,
|
|
130
|
+
markdownToEpub,
|
|
78
131
|
parseMarkdownToHtml,
|
|
79
|
-
renderHtmlToPdf
|
|
132
|
+
renderHtmlToPdf,
|
|
133
|
+
temporaryHtmlPath
|
|
80
134
|
};
|
package/src/core/parser.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
escapeHtml
|
|
11
11
|
} = require('./utils');
|
|
12
12
|
const { getHtmlTemplate } = require('./templates');
|
|
13
|
+
const { applyFootnotes } = require('./footnotes');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Builds marked custom renderer for diagrams, UI mockups, and headings
|
|
@@ -100,8 +101,8 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
|
|
|
100
101
|
cssFile: options.cssFile || frontMatter.cssFile || frontMatter.css
|
|
101
102
|
};
|
|
102
103
|
|
|
103
|
-
// 3. Preprocess Markdown (LaTeX, pagebreaks, alerts)
|
|
104
|
-
const processedMd = preprocessMarkdown(markdownContent);
|
|
104
|
+
// 3. Preprocess Markdown (footnotes, LaTeX, pagebreaks, alerts)
|
|
105
|
+
const processedMd = preprocessMarkdown(applyFootnotes(markdownContent));
|
|
105
106
|
|
|
106
107
|
// 4. Generate TOC & Cover Page
|
|
107
108
|
let tocHtml = '';
|
package/src/core/templates.js
CHANGED
|
@@ -99,6 +99,12 @@ function getHtmlTemplate({ title, author = '', bodyHtml, options = {} }) {
|
|
|
99
99
|
page-break-after: always;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
.footnote-ref { font-size: 0.75em; line-height: 0; }
|
|
103
|
+
.footnote-ref a, .footnote-backref { text-decoration: none; }
|
|
104
|
+
.footnotes { margin-top: 28px; padding-top: 10px; border-top: 1px solid currentColor; font-size: 0.85em; break-inside: avoid-page; }
|
|
105
|
+
.footnotes ol { margin: 0; padding-left: 1.6em; }
|
|
106
|
+
.footnotes li { margin: 3px 0; }
|
|
107
|
+
|
|
102
108
|
img {
|
|
103
109
|
max-width: 100% !important;
|
|
104
110
|
height: auto !important;
|
package/src/core/utils.js
CHANGED
|
@@ -18,11 +18,11 @@ function preprocessMarkdown(mdContent = '') {
|
|
|
18
18
|
|
|
19
19
|
return mdContent
|
|
20
20
|
// GitHub Callouts / Alerts
|
|
21
|
-
.replace(
|
|
22
|
-
.replace(
|
|
23
|
-
.replace(
|
|
24
|
-
.replace(
|
|
25
|
-
.replace(
|
|
21
|
+
.replace(/^>[ \t]*\[!NOTE\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **ℹ️ NOTE:** ')
|
|
22
|
+
.replace(/^>[ \t]*\[!TIP\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **💡 TIP:** ')
|
|
23
|
+
.replace(/^>[ \t]*\[!WARNING\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **⚠️ WARNING:** ')
|
|
24
|
+
.replace(/^>[ \t]*\[!IMPORTANT\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **❗ IMPORTANT:** ')
|
|
25
|
+
.replace(/^>[ \t]*\[!CAUTION\][ \t]*(?:\r?\n>[ \t]?)?/gm, '> **🚫 CAUTION:** ')
|
|
26
26
|
// Pagebreaks
|
|
27
27
|
.replace(/<!--\s*page-?break\s*-->/gi, '<div class="page-break"></div>')
|
|
28
28
|
.replace(/\\pagebreak/gi, '<div class="page-break"></div>')
|
package/src/core/zip.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const zlib = require('zlib');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A ZIP archive writer, enough for the two container formats built on ZIP
|
|
5
|
+
* that this tool exports: DOCX and EPUB.
|
|
6
|
+
*
|
|
7
|
+
* Entries are written in the order given, which EPUB relies on: its
|
|
8
|
+
* `mimetype` entry has to come first and uncompressed. Timestamps are fixed,
|
|
9
|
+
* so the same input gives the same bytes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// 1980-01-01 00:00:00, the earliest date the format can express.
|
|
13
|
+
const DOS_TIME = 0;
|
|
14
|
+
const DOS_DATE = (0 << 9) | (1 << 5) | 1;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ name: string, data: Buffer | string, store?: boolean }[]} entries
|
|
18
|
+
* @returns {Buffer}
|
|
19
|
+
*/
|
|
20
|
+
function createZip(entries) {
|
|
21
|
+
const localParts = [];
|
|
22
|
+
const centralParts = [];
|
|
23
|
+
let offset = 0;
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const name = Buffer.from(entry.name, 'utf8');
|
|
27
|
+
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data, 'utf8');
|
|
28
|
+
const compressed = entry.store ? data : zlib.deflateRawSync(data, { level: 9 });
|
|
29
|
+
const method = entry.store ? 0 : 8;
|
|
30
|
+
const crc = zlib.crc32(data);
|
|
31
|
+
|
|
32
|
+
const local = Buffer.alloc(30);
|
|
33
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
34
|
+
local.writeUInt16LE(20, 4); // version needed
|
|
35
|
+
local.writeUInt16LE(0x0800, 6); // names are UTF-8
|
|
36
|
+
local.writeUInt16LE(method, 8);
|
|
37
|
+
local.writeUInt16LE(DOS_TIME, 10);
|
|
38
|
+
local.writeUInt16LE(DOS_DATE, 12);
|
|
39
|
+
local.writeUInt32LE(crc, 14);
|
|
40
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
41
|
+
local.writeUInt32LE(data.length, 22);
|
|
42
|
+
local.writeUInt16LE(name.length, 26);
|
|
43
|
+
local.writeUInt16LE(0, 28);
|
|
44
|
+
|
|
45
|
+
const central = Buffer.alloc(46);
|
|
46
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
47
|
+
central.writeUInt16LE(20, 4); // version made by
|
|
48
|
+
central.writeUInt16LE(20, 6); // version needed
|
|
49
|
+
central.writeUInt16LE(0x0800, 8);
|
|
50
|
+
central.writeUInt16LE(method, 10);
|
|
51
|
+
central.writeUInt16LE(DOS_TIME, 12);
|
|
52
|
+
central.writeUInt16LE(DOS_DATE, 14);
|
|
53
|
+
central.writeUInt32LE(crc, 16);
|
|
54
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
55
|
+
central.writeUInt32LE(data.length, 24);
|
|
56
|
+
central.writeUInt16LE(name.length, 28);
|
|
57
|
+
central.writeUInt32LE(offset, 42);
|
|
58
|
+
|
|
59
|
+
localParts.push(local, name, compressed);
|
|
60
|
+
centralParts.push(central, name);
|
|
61
|
+
offset += local.length + name.length + compressed.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const centralSize = centralParts.reduce((size, part) => size + part.length, 0);
|
|
65
|
+
const end = Buffer.alloc(22);
|
|
66
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
67
|
+
end.writeUInt16LE(entries.length, 8);
|
|
68
|
+
end.writeUInt16LE(entries.length, 10);
|
|
69
|
+
end.writeUInt32LE(centralSize, 12);
|
|
70
|
+
end.writeUInt32LE(offset, 16);
|
|
71
|
+
|
|
72
|
+
return Buffer.concat([...localParts, ...centralParts, end]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
createZip
|
|
77
|
+
};
|