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 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. Merging code into `main` automatically triggers a GitHub Release.
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 code is merged from `develop` into `main` (or a version tag `v*` is pushed), GitHub Actions ([`.github/workflows/release.yml`](.github/workflows/release.yml)) automatically:
142
- 1. Runs full unit test suite (`npm test`).
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.0.0",
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": {
@@ -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);
@@ -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(/\s*on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/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}>`;
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 };
@@ -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 themeCss = THEMES[theme] || THEMES.modern;
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 = {