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/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 .pdf file
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
- const markdownContent = fs.readFileSync(absInputPath, 'utf8');
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 = path.join(path.parse(absOutputPath).dir, `_temp_${Date.now()}_${Math.random().toString(36).substring(7)}.html`);
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
  };
@@ -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 = '';
@@ -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(/^>\s*\[!NOTE\]\s*\r?\n/gm, '> **ℹ️ NOTE:** ')
22
- .replace(/^>\s*\[!TIP\]\s*\r?\n/gm, '> **💡 TIP:** ')
23
- .replace(/^>\s*\[!WARNING\]\s*\r?\n/gm, '> **⚠️ WARNING:** ')
24
- .replace(/^>\s*\[!IMPORTANT\]\s*\r?\n/gm, '> **❗ IMPORTANT:** ')
25
- .replace(/^>\s*\[!CAUTION\]\s*\r?\n/gm, '> **🚫 CAUTION:** ')
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>')
@@ -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
+ };