md-pdf-studio 2.0.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/README.md +9 -3
- package/bin/cli.js +2 -0
- package/package.json +2 -1
- package/src/core/parser.js +12 -2
- package/src/core/sanitizer.js +23 -1
- package/src/core/themes/highlight.js +38 -0
- package/src/core/themes/index.js +8 -2
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
- 🚀 **CLI & Batch Conversion**: Convert single files or batch process directories with wildcard patterns.
|
|
28
28
|
- 🎨 **Custom CSS Injection**: Inject custom branding CSS stylesheets (`-c, --css custom.css`).
|
|
29
29
|
- 📊 **Mermaid Diagrams**: Sequence diagrams (with auto note wrapping), flowcharts, class diagrams, and gantt charts.
|
|
30
|
+
- 🌈 **Syntax Highlighting**: Fenced code blocks that name a language (` ```js `, ` ```python `, ` ```diff `, …) are coloured at build time with highlight.js — no CDN, so it also works offline and in static HTML export. Colours follow the theme's code block background. Works in the CLI and in the extension preview.
|
|
30
31
|
- 📐 **LaTeX Math Support**: Mathematical equations rendered with KaTeX.
|
|
31
32
|
- 💡 **GitHub Callout Box Alerts**: Support for `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!IMPORTANT]`, and `> [!CAUTION]`.
|
|
32
33
|
- 💻 **ASCII UI Mockups**: Styled dark-theme boxes for terminal output & ASCII wireframe mockups.
|
|
@@ -103,6 +104,7 @@ npx md-pdf-studio ./docs/ -o ./dist/
|
|
|
103
104
|
| `-k, --keep-html` | | Keep temporary HTML file after conversion | `false` |
|
|
104
105
|
| `--no-mermaid` | | Disable Mermaid diagram rendering | `false` |
|
|
105
106
|
| `--no-katex` | | Disable KaTeX math formula rendering | `false` |
|
|
107
|
+
| `--no-highlight` | | Disable syntax highlighting of code blocks | `false` |
|
|
106
108
|
|
|
107
109
|
---
|
|
108
110
|
|
|
@@ -122,6 +124,7 @@ theme: github
|
|
|
122
124
|
font: Inter
|
|
123
125
|
pageSize: A4
|
|
124
126
|
landscape: false
|
|
127
|
+
highlight: true
|
|
125
128
|
---
|
|
126
129
|
# Document Executive Summary
|
|
127
130
|
...
|
|
@@ -134,15 +137,18 @@ landscape: false
|
|
|
134
137
|
This project follows standard **Git Flow** branching and automated release publishing:
|
|
135
138
|
|
|
136
139
|
### 🔀 Branching Strategy
|
|
137
|
-
- **`main`**: Production-ready branch. Stores stable releases.
|
|
140
|
+
- **`main`**: Production-ready branch. Stores stable releases. Pushing a version tag `v*` from `main` triggers a release.
|
|
138
141
|
- **`develop`**: Active development branch. Daily commits, feature additions, and refactoring take place here.
|
|
139
142
|
|
|
140
143
|
### 🚀 Automated Release Pipeline
|
|
141
|
-
Whenever
|
|
142
|
-
1.
|
|
144
|
+
Whenever a version tag `v*` is pushed, GitHub Actions ([`.github/workflows/release.yml`](.github/workflows/release.yml)) builds from that tag and automatically:
|
|
145
|
+
1. Checks that the tag matches the version in `package.json` and `extension/manifest.json`, then runs the full unit test suite (`npm test`).
|
|
143
146
|
2. Bundles cross-platform extension zip artifacts (`dist/extension-vX.Y.Z.zip`).
|
|
144
147
|
3. Auto-generates release notes from commit history.
|
|
145
148
|
4. Publishes a new official **[GitHub Release](https://github.com/vannt-dev/convert-md-to-pdf/releases)** with downloadable ZIP binaries attached!
|
|
149
|
+
5. Publishes [`md-pdf-studio`](https://www.npmjs.com/package/md-pdf-studio) to npm with provenance, through npm trusted publishing (no token secret). A version already on npm is skipped.
|
|
150
|
+
|
|
151
|
+
To release an existing tag again (for example after a failed run), run the workflow manually with `tag_name`.
|
|
146
152
|
|
|
147
153
|
### 💡 1-Click CLI Release Command
|
|
148
154
|
You can also bump version and trigger a release with a single CLI command:
|
package/bin/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ program
|
|
|
28
28
|
.option('-k, --keep-html', 'Keep temporary HTML file after conversion', false)
|
|
29
29
|
.option('--no-mermaid', 'Disable Mermaid diagram rendering')
|
|
30
30
|
.option('--no-katex', 'Disable KaTeX math formula rendering')
|
|
31
|
+
.option('--no-highlight', 'Disable syntax highlighting of code blocks')
|
|
31
32
|
.option('-b, --browser <path>', 'Custom Chrome/Edge executable path')
|
|
32
33
|
.action(async (inputArgs, options) => {
|
|
33
34
|
try {
|
|
@@ -89,6 +90,7 @@ program
|
|
|
89
90
|
keepHtml: options.keepHtml,
|
|
90
91
|
mermaid: options.mermaid,
|
|
91
92
|
katex: options.katex,
|
|
93
|
+
highlight: options.highlight,
|
|
92
94
|
executablePath: options.browser
|
|
93
95
|
});
|
|
94
96
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-pdf-studio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A powerful CLI tool and Manifest V3 Browser Extension to convert Markdown files, text, or web pages into beautifully styled PDFs with Mermaid diagrams, LaTeX math, TOC, and themes.",
|
|
5
5
|
"main": "src/core/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"commander": "^15.0.0",
|
|
50
|
+
"highlight.js": "^11.12.0",
|
|
50
51
|
"marked": "^18.0.14"
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
package/src/core/parser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { marked } = require('marked');
|
|
2
|
+
const hljs = require('highlight.js/lib/common');
|
|
2
3
|
const {
|
|
3
4
|
parseFrontMatter,
|
|
4
5
|
generateCoverPage,
|
|
@@ -13,9 +14,10 @@ const { getHtmlTemplate } = require('./templates');
|
|
|
13
14
|
/**
|
|
14
15
|
* Builds marked custom renderer for diagrams, UI mockups, and headings
|
|
15
16
|
* @param {Map<string, string>} headingsMap
|
|
17
|
+
* @param {boolean} highlight Colour fenced code blocks that name a known language
|
|
16
18
|
* @returns {marked.Renderer}
|
|
17
19
|
*/
|
|
18
|
-
function buildMarkedRenderer(headingsMap) {
|
|
20
|
+
function buildMarkedRenderer(headingsMap, highlight = true) {
|
|
19
21
|
const renderer = new marked.Renderer();
|
|
20
22
|
const originalCodeRenderer = renderer.code.bind(renderer);
|
|
21
23
|
|
|
@@ -31,6 +33,13 @@ function buildMarkedRenderer(headingsMap) {
|
|
|
31
33
|
return `<div class="ui-mockup-container"><pre class="ui-mockup"><code>${escapeHtml(text)}</code></pre></div>`;
|
|
32
34
|
}
|
|
33
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
|
+
|
|
34
43
|
return originalCodeRenderer.call(this, token);
|
|
35
44
|
};
|
|
36
45
|
|
|
@@ -86,6 +95,7 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
|
|
|
86
95
|
footer: options.footer !== undefined ? options.footer : (frontMatter.footer || true),
|
|
87
96
|
mermaid: options.mermaid !== undefined ? options.mermaid : (frontMatter.mermaid !== false),
|
|
88
97
|
katex: options.katex !== undefined ? options.katex : (frontMatter.katex !== false),
|
|
98
|
+
highlight: options.highlight !== undefined ? options.highlight : (frontMatter.highlight !== false),
|
|
89
99
|
customCss: options.customCss || '',
|
|
90
100
|
cssFile: options.cssFile || frontMatter.cssFile || frontMatter.css
|
|
91
101
|
};
|
|
@@ -118,7 +128,7 @@ function parseMarkdownToHtml(rawMarkdownContent, options = {}) {
|
|
|
118
128
|
|
|
119
129
|
// 6. Parse Markdown using custom renderer
|
|
120
130
|
marked.setOptions({ gfm: true, breaks: true });
|
|
121
|
-
const renderer = buildMarkedRenderer(headingsMap);
|
|
131
|
+
const renderer = buildMarkedRenderer(headingsMap, mergedOptions.highlight);
|
|
122
132
|
marked.use({ renderer });
|
|
123
133
|
|
|
124
134
|
let bodyHtml = marked.parse(processedMd);
|
package/src/core/sanitizer.js
CHANGED
|
@@ -11,7 +11,29 @@ function sanitizeHtml(html) {
|
|
|
11
11
|
if (!html) return '';
|
|
12
12
|
return html
|
|
13
13
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
14
|
-
.replace(
|
|
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}>`;
|
|
15
37
|
}
|
|
16
38
|
|
|
17
39
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax Highlighting Token Colors
|
|
3
|
+
* Two palettes, picked by the background each theme gives its code blocks.
|
|
4
|
+
*/
|
|
5
|
+
// Inline-code chrome must not repeat on every line of a block.
|
|
6
|
+
const base = `
|
|
7
|
+
pre code { background: none; padding: 0; border: none; }
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
const light = `
|
|
11
|
+
.hljs-comment, .hljs-quote, .hljs-meta { color: #6e7781; font-style: italic; }
|
|
12
|
+
.hljs-keyword, .hljs-selector-tag, .hljs-doctag, .hljs-template-tag { color: #cf222e; }
|
|
13
|
+
.hljs-string, .hljs-regexp, .hljs-char.escape_ { color: #0a3069; }
|
|
14
|
+
.hljs-number, .hljs-literal, .hljs-attr, .hljs-attribute, .hljs-variable, .hljs-template-variable, .hljs-symbol, .hljs-bullet { color: #0550ae; }
|
|
15
|
+
.hljs-title, .hljs-section, .hljs-selector-id, .hljs-selector-class { color: #8250df; }
|
|
16
|
+
.hljs-type, .hljs-built_in, .hljs-params { color: #953800; }
|
|
17
|
+
.hljs-name, .hljs-tag { color: #116329; }
|
|
18
|
+
.hljs-addition { color: #116329; background-color: #dafbe1; }
|
|
19
|
+
.hljs-deletion { color: #82071e; background-color: #ffebe9; }
|
|
20
|
+
.hljs-emphasis { font-style: italic; }
|
|
21
|
+
.hljs-strong { font-weight: 700; }
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const dark = `
|
|
25
|
+
.hljs-comment, .hljs-quote, .hljs-meta { color: #8b949e; font-style: italic; }
|
|
26
|
+
.hljs-keyword, .hljs-selector-tag, .hljs-doctag, .hljs-template-tag { color: #ff7b72; }
|
|
27
|
+
.hljs-string, .hljs-regexp, .hljs-char.escape_ { color: #a5d6ff; }
|
|
28
|
+
.hljs-number, .hljs-literal, .hljs-attr, .hljs-attribute, .hljs-variable, .hljs-template-variable, .hljs-symbol, .hljs-bullet { color: #79c0ff; }
|
|
29
|
+
.hljs-title, .hljs-section, .hljs-selector-id, .hljs-selector-class { color: #d2a8ff; }
|
|
30
|
+
.hljs-type, .hljs-built_in, .hljs-params { color: #ffa657; }
|
|
31
|
+
.hljs-name, .hljs-tag { color: #7ee787; }
|
|
32
|
+
.hljs-addition { color: #aff5b4; background-color: #033a16; }
|
|
33
|
+
.hljs-deletion { color: #ffdcd7; background-color: #67060c; }
|
|
34
|
+
.hljs-emphasis { font-style: italic; }
|
|
35
|
+
.hljs-strong { font-weight: 700; }
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
module.exports = { light: base + light, dark: base + dark };
|
package/src/core/themes/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const githubTheme = require('./github');
|
|
|
6
6
|
const ebookTheme = require('./ebook');
|
|
7
7
|
const cyberpunkTheme = require('./cyberpunk');
|
|
8
8
|
const minimalTheme = require('./minimal');
|
|
9
|
+
const highlightCss = require('./highlight');
|
|
9
10
|
|
|
10
11
|
const THEMES = {
|
|
11
12
|
modern: modernTheme,
|
|
@@ -17,6 +18,9 @@ const THEMES = {
|
|
|
17
18
|
minimal: minimalTheme
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
// Themes whose code blocks sit on a dark background; the others use the light palette.
|
|
22
|
+
const DARK_CODE_THEMES = new Set(['modern', 'dark', 'ebook', 'cyberpunk']);
|
|
23
|
+
|
|
20
24
|
/**
|
|
21
25
|
* Builds font override CSS string
|
|
22
26
|
* @param {string} font
|
|
@@ -40,10 +44,12 @@ function getFontOverrideCss(font) {
|
|
|
40
44
|
* @returns {string} Consolidated CSS string
|
|
41
45
|
*/
|
|
42
46
|
function getThemeStyles(theme, font = '') {
|
|
43
|
-
const
|
|
47
|
+
const themeName = THEMES[theme] ? theme : 'modern';
|
|
48
|
+
const themeCss = THEMES[themeName];
|
|
49
|
+
const tokenCss = DARK_CODE_THEMES.has(themeName) ? highlightCss.dark : highlightCss.light;
|
|
44
50
|
const fontOverrideCss = getFontOverrideCss(font);
|
|
45
51
|
|
|
46
|
-
return `${coverCss}\n${fontOverrideCss}${themeCss}`;
|
|
52
|
+
return `${coverCss}\n${fontOverrideCss}${themeCss}${tokenCss}`;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
module.exports = {
|