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,163 +1,164 @@
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
- };
1
+ const { marked } = require('marked');
2
+ const hljs = require('highlight.js/lib/common');
3
+ const {
4
+ parseFrontMatter,
5
+ generateCoverPage,
6
+ preprocessMarkdown,
7
+ generateToc,
8
+ readCustomCss,
9
+ sanitizeHtml,
10
+ escapeHtml
11
+ } = require('./utils');
12
+ const { getHtmlTemplate } = require('./templates');
13
+
14
+ /**
15
+ * Builds marked custom renderer for diagrams, UI mockups, and headings
16
+ * @param {Map<string, string>} headingsMap
17
+ * @param {boolean} highlight Colour fenced code blocks that name a known language
18
+ * @returns {marked.Renderer}
19
+ */
20
+ function buildMarkedRenderer(headingsMap, highlight = true) {
21
+ const renderer = new marked.Renderer();
22
+ const originalCodeRenderer = renderer.code.bind(renderer);
23
+
24
+ renderer.code = function(token) {
25
+ const text = token.text || '';
26
+ const lang = token.lang || '';
27
+
28
+ if (lang === 'mermaid') {
29
+ return `<div class="mermaid-container"><pre class="mermaid">${text}</pre></div>`;
30
+ }
31
+
32
+ if (text.includes('┌') && text.includes('└')) {
33
+ return `<div class="ui-mockup-container"><pre class="ui-mockup"><code>${escapeHtml(text)}</code></pre></div>`;
34
+ }
35
+
36
+ // Only fences that name a language are coloured: guessing one gets short snippets wrong.
37
+ const language = lang.trim().split(/\s+/)[0];
38
+ if (highlight && language && hljs.getLanguage(language)) {
39
+ const highlighted = hljs.highlight(text, { language, ignoreIllegals: true }).value;
40
+ return `<pre><code class="hljs language-${escapeHtml(language)}">${highlighted}</code></pre>\n`;
41
+ }
42
+
43
+ return originalCodeRenderer.call(this, token);
44
+ };
45
+
46
+ renderer.heading = function({ tokens, depth, text }) {
47
+ const slug = headingsMap.get(text.trim()) || `heading-${depth}`;
48
+ return `<h${depth} id="${slug}">${this.parser.parseInline(tokens)}</h${depth}>`;
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
+ highlight: options.highlight !== undefined ? options.highlight : (frontMatter.highlight !== false),
99
+ customCss: options.customCss || '',
100
+ cssFile: options.cssFile || frontMatter.cssFile || frontMatter.css
101
+ };
102
+
103
+ // 3. Preprocess Markdown (LaTeX, pagebreaks, alerts)
104
+ const processedMd = preprocessMarkdown(markdownContent);
105
+
106
+ // 4. Generate TOC & Cover Page
107
+ let tocHtml = '';
108
+ let headingsMap = new Map();
109
+ if (mergedOptions.toc) {
110
+ const tocResult = generateToc(processedMd);
111
+ tocHtml = tocResult.tocHtml;
112
+ headingsMap = tocResult.headingsMap;
113
+ }
114
+
115
+ const coverHtml = generateCoverPage({
116
+ title,
117
+ subtitle,
118
+ author,
119
+ date,
120
+ cover: mergedOptions.cover
121
+ });
122
+
123
+ // 5. Read custom CSS file if specified
124
+ let customCss = mergedOptions.customCss || '';
125
+ if (mergedOptions.cssFile) {
126
+ customCss += '\n' + readCustomCss(mergedOptions.cssFile);
127
+ }
128
+
129
+ // 6. Parse Markdown using custom renderer
130
+ marked.setOptions({ gfm: true, breaks: true });
131
+ const renderer = buildMarkedRenderer(headingsMap, mergedOptions.highlight);
132
+ marked.use({ renderer });
133
+
134
+ let bodyHtml = marked.parse(processedMd);
135
+ bodyHtml = sanitizeHtml(bodyHtml);
136
+
137
+ // 7. Inject TOC, Cover Page & Section Groups
138
+ if (tocHtml) {
139
+ bodyHtml = bodyHtml.includes('</h1>')
140
+ ? bodyHtml.replace('</h1>', '</h1>\n' + tocHtml)
141
+ : tocHtml + bodyHtml;
142
+ }
143
+
144
+ if (coverHtml) {
145
+ bodyHtml = coverHtml + bodyHtml;
146
+ }
147
+
148
+ bodyHtml = wrapSectionGroups(bodyHtml);
149
+
150
+ // 8. Generate and return full HTML template
151
+ return getHtmlTemplate({
152
+ title,
153
+ author,
154
+ bodyHtml,
155
+ options: {
156
+ ...mergedOptions,
157
+ customCss
158
+ }
159
+ });
160
+ }
161
+
162
+ module.exports = {
163
+ parseMarkdownToHtml
164
+ };
@@ -1,35 +1,57 @@
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
- };
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(OPENING_TAG, stripEventHandlers);
15
+ }
16
+
17
+ // An opening tag, with quoted attribute values allowed to contain ">". A quote that is never
18
+ // closed still ends at the next ">", as the tag would in a lenient parser.
19
+ const OPENING_TAG = /<([a-zA-Z][^\s/>]*)((?:"[^"]*"|'[^']*'|[^>])*)>/g;
20
+ // One attribute: its name, then an optional value. Consuming the value keeps it from being
21
+ // read as another attribute.
22
+ const ATTRIBUTE = /[\s/]*([^\s"'<>/=]+)(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?/g;
23
+
24
+ /**
25
+ * Drops on* attributes from one opening tag. Text between tags is never touched, so code
26
+ * such as `el.onclick = fn` survives.
27
+ * @param {string} tag
28
+ * @param {string} name
29
+ * @param {string} attributes
30
+ * @returns {string}
31
+ */
32
+ function stripEventHandlers(tag, name, attributes) {
33
+ const kept = attributes.replace(ATTRIBUTE, (attribute, attributeName) =>
34
+ /^on/i.test(attributeName) ? '' : attribute
35
+ );
36
+ return `<${name}${kept}>`;
37
+ }
38
+
39
+ /**
40
+ * Escapes special HTML characters
41
+ * @param {string} str
42
+ * @returns {string} Escaped string
43
+ */
44
+ function escapeHtml(str) {
45
+ if (!str) return '';
46
+ return str
47
+ .replace(/&/g, '&amp;')
48
+ .replace(/</g, '&lt;')
49
+ .replace(/>/g, '&gt;')
50
+ .replace(/"/g, '&quot;')
51
+ .replace(/'/g, '&#039;');
52
+ }
53
+
54
+ module.exports = {
55
+ sanitizeHtml,
56
+ escapeHtml
57
+ };