@kobekeye/mdf 1.0.0 → 1.1.2

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.
@@ -0,0 +1,29 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+
11
+ permissions:
12
+ contents: read
13
+ id-token: write
14
+
15
+ steps:
16
+ - name: Checkout repository
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Setup Node.js
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: '20.x'
23
+ registry-url: 'https://registry.npmjs.org'
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Publish package
29
+ run: npm publish --provenance --access public
package/README.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # mdf
2
+ <img width="370" height="527" alt="mdfintro1" src="https://github.com/user-attachments/assets/30436af4-2f05-4678-ad80-64daa47a5f7c" />
3
+ <img width="372" height="527" alt="mdfintro2" src="https://github.com/user-attachments/assets/79bab5be-2ea6-430c-8075-42870caa41bc" />
2
4
 
3
- Convert Markdown to beautiful PDFs — free, open-source, and zero-config.
4
5
 
5
- ## Features
6
6
 
7
+ Convert Markdown to beautiful PDFs — free, open-source, and zero-config.
8
+ Welcome to mdf! Please note that this project is currently in the MVP (Minimum Viable Product) stage. Any feedback, suggestions, or contributions are highly appreciated!
9
+ ## Features
7
10
  - **Syntax highlighting** — fenced code blocks with language detection (via highlight.js)
8
11
  - **Math formulas** — inline and block LaTeX via KaTeX (`$...$` and `$$...$$`)
9
12
  - **Table of contents** — insert `[TOC]` anywhere in your document
@@ -14,24 +17,47 @@ Convert Markdown to beautiful PDFs — free, open-source, and zero-config.
14
17
  - **CJK support** — uses Inter + Noto Sans TC via Google Fonts (requires internet on first run)
15
18
 
16
19
  ## Installation
17
-
20
+ ### Windows
21
+ If one hasn't install nodejs, click [here](https://nodejs.org) to install. After installing nodejs, type
18
22
  ```bash
19
- npm install -g mdf
23
+ npm install -g @kobekeye/mdf
20
24
  ```
21
-
22
- Or run without installing:
23
-
25
+ ### Linux / macOS
26
+ It is highly recommended to use [nvm](https://github.com/nvm-sh/nvm) to manage permissions. Using sudo with global npm installs is not recommended.
27
+ #### 1. Install nvm
24
28
  ```bash
25
- npx mdf input.md
29
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
30
+ ```
31
+ #### 2. Install nodejs
32
+ ```bash
33
+ nvm install --lts
34
+ ```
35
+ #### 3. Install mdf
36
+ ```bash
37
+ npm install -g @kobekeye/mdf
26
38
  ```
27
-
28
39
  ## Usage
29
-
30
40
  ```bash
31
41
  mdf input.md # outputs input.pdf
32
42
  mdf input.md output.pdf # custom output name
33
43
  ```
34
-
44
+ To watch the output pdf file, use
45
+ ```bash
46
+ mdf input.md -w # outputs input.pdf and watches for changes
47
+ mdf input.md output.pdf -w # custom output name and watches for changes
48
+ ```
49
+ Or, if you want to try without installation,
50
+ ```bash
51
+ npx @kobekeye/mdf input.md # outputs input.pdf
52
+ npx @kobekeye/mdf input.md output.pdf # custom output name
53
+ npx @kobekeye/mdf input.md -w # outputs input.pdf and watches for changes
54
+ npx @kobekeye/mdf input.md output.pdf -w # custom output name and watches for changes
55
+ ```
56
+ ## Update
57
+ ```bash
58
+ npm update -g @kobekeye/mdf
59
+ ```
60
+ If you only use `npx` to run mdf, you don't need to update.
35
61
  ## Syntax Guide
36
62
 
37
63
  ### Table of Contents
@@ -71,7 +97,7 @@ Operation completed.
71
97
  :::
72
98
  ```
73
99
 
74
- ### Manual Page Break
100
+ ### Manual Page Break!
75
101
 
76
102
  ```markdown
77
103
  ==page==
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@kobekeye/mdf",
3
- "version": "1.0.0",
3
+ "version": "1.1.2",
4
4
  "description": "Convert Markdown to beautiful PDFs — free, open-source, and zero-config.",
5
- "main": "index.js",
5
+ "main": "src/index.js",
6
6
  "bin": {
7
- "mdf": "./index.js"
7
+ "mdf": "src/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -4,26 +4,39 @@ const path = require('path');
4
4
  const puppeteer = require('puppeteer');
5
5
  const { renderToHtml } = require('./renderer');
6
6
 
7
+ let tempFiles = [];
8
+
9
+ // gracefully handle Ctrl+C: clean up temp files before exit
10
+ // must be registered before puppeteer.launch(), which installs its own SIGINT wrapper
11
+ process.on('SIGINT', () => {
12
+ for (const tempFile of tempFiles) {
13
+ try { fs.unlinkSync(tempFile); } catch (_) { }
14
+ }
15
+ process.exit(0);
16
+ });
17
+
7
18
  // parse command line arguments
8
19
  const args = process.argv.slice(2);
9
20
 
10
21
  if (args.length === 0) {
11
22
  console.log(`
12
- usage: node index.js <input.md> [output.pdf]
23
+ usage: mdf <input.md> [output.pdf]
13
24
 
14
25
  input.md required, the Markdown file to convert
15
26
  output.pdf optional, the output PDF file name
16
27
  (if omitted, it will automatically use the same name, e.g. input.pdf)
17
28
 
18
29
  examples:
19
- node index.js README.md
20
- node index.js doc.md custom-name.pdf
30
+ mdf README.md
31
+ mdf doc.md custom-name.pdf
21
32
  `);
22
33
  process.exit(0);
23
34
  }
24
35
 
25
- const inputFile = args[0];
26
- const outputFile = args[1] || inputFile.replace(/\.md$/i, '.pdf');
36
+ const watchMode = args.includes('--watch') || args.includes('-w');
37
+ const filteredArgs = args.filter(a => a !== '--watch' && a !== '-w');
38
+ const inputFile = filteredArgs[0];
39
+ const outputFile = filteredArgs[1] || inputFile.replace(/\.md$/i, '.pdf');
27
40
 
28
41
  // check if input file exists
29
42
  if (!fs.existsSync(inputFile)) {
@@ -31,7 +44,7 @@ if (!fs.existsSync(inputFile)) {
31
44
  process.exit(1);
32
45
  }
33
46
 
34
- async function convertToPdf() {
47
+ async function convertToPdf(browser) {
35
48
  console.log(`converting: ${inputFile} → ${outputFile}`);
36
49
 
37
50
  const fullHtml = renderToHtml(inputFile);
@@ -41,10 +54,9 @@ async function convertToPdf() {
41
54
  const tempHtmlPath = path.join(inputDir, `.mdf-temp-${Date.now()}.html`);
42
55
  fs.writeFileSync(tempHtmlPath, fullHtml, 'utf-8');
43
56
 
57
+ tempFiles.push(tempHtmlPath);
44
58
  try {
45
- const browser = await puppeteer.launch();
46
59
  const page = await browser.newPage();
47
-
48
60
  // use file:// URL to load, let relative path of images parse correctly
49
61
  await page.goto(`file://${tempHtmlPath}`, { waitUntil: 'networkidle0' });
50
62
  // wait for all fonts (including Google Fonts) to load completely, avoid CJK characters use synthetic bold
@@ -64,7 +76,7 @@ async function convertToPdf() {
64
76
  </div>`,
65
77
  });
66
78
 
67
- await browser.close();
79
+ await page.close();
68
80
  console.log(`\x1b[32mDone! Output to: ${path.resolve(outputFile)}\x1b[0m`);
69
81
  } finally {
70
82
  // clean up temp HTML file
@@ -72,7 +84,38 @@ async function convertToPdf() {
72
84
  }
73
85
  }
74
86
 
75
- convertToPdf().catch((err) => {
76
- console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
77
- process.exit(1);
78
- });
87
+ async function main() {
88
+ const browser = await puppeteer.launch();
89
+
90
+ try {
91
+ await convertToPdf(browser);
92
+
93
+ if (watchMode) {
94
+ console.log(`\x1b[36mWatching for changes: ${inputFile}...\x1b[0m`);
95
+
96
+ let debounceTimer;
97
+ fs.watch(inputFile, (eventType) => {
98
+ if (eventType === 'change') {
99
+ clearTimeout(debounceTimer);
100
+ debounceTimer = setTimeout(async () => {
101
+ console.log(`\x1b[33mFile changed, re-converting...\x1b[0m`);
102
+ try {
103
+ await convertToPdf(browser);
104
+ } catch (err) {
105
+ console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
106
+ }
107
+ }, 300);
108
+ }
109
+ });
110
+
111
+ } else {
112
+ await browser.close();
113
+ }
114
+ } catch (err) {
115
+ await browser.close();
116
+ console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ main();
@@ -7,67 +7,76 @@ const hljs = require('highlight.js');
7
7
  const anchor = require('markdown-it-anchor');
8
8
  const taskLists = require('markdown-it-task-lists');
9
9
  const container = require('markdown-it-container');
10
+ // const md = new MarkdownIt({
11
+ // html: true,
12
+ // highlight: function (str, lang) {
13
+ // if (lang && hljs.getLanguage(lang)) {
14
+ // try {
15
+ // return '<pre class="hljs"><code>' +
16
+ // hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
17
+ // '</code></pre>';
18
+ // } catch (_) { }
19
+ // }
20
+ // return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
21
+ // }
22
+ // });
10
23
  const md = new MarkdownIt({
11
24
  html: true,
12
- highlight: function (str, lang) {
25
+ highlight: (str, lang) => {
26
+ const wrap = (content) => `<pre class="hljs"><code>${content}</code></pre>`;
13
27
  if (lang && hljs.getLanguage(lang)) {
14
28
  try {
15
- return '<pre class="hljs"><code>' +
16
- hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
17
- '</code></pre>';
29
+ return wrap(hljs.highlight(str, { language: lang, ignoreIllegals: true }).value);
18
30
  } catch (_) { }
19
31
  }
20
- return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
32
+ return wrap(md.utils.escapeHtml(str));
21
33
  }
22
- });
23
- md.use(texmath, {
24
- engine: katex,
25
- delimiters: 'dollars',
26
- });
27
- md.use(anchor, {
28
- permalink: false,
29
- slugify: (s) => encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-')),
30
- });
31
- md.use(taskLists, { enabled: true });
32
- // --- Callout container blocks: :::info, :::warning, :::danger, :::success ---
34
+ })
35
+ .use(texmath, {
36
+ engine: katex,
37
+ delimiters: 'dollars',
38
+ // maybe can add some katexoptions later on.
39
+ })
40
+ .use(anchor, {
41
+ permalink: false,
42
+ // e.g. hello world 你好 -> hello-world-%E4%BD%A0%E5%A5%BD(你好)
43
+ slugify: (s) => encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-')),
44
+ })
45
+ .use(taskLists, { enabled: true });
46
+ // --- Container blocks: :::info, :::warning, :::danger, :::success, :::spoiler ---
33
47
  // Color aliases: :::blue = :::info, :::orange = :::warning, etc.
34
- const CALLOUT_TYPES = [
35
- { names: ['info', 'blue'], cssClass: 'info' },
36
- { names: ['warning', 'orange'], cssClass: 'warning' },
37
- { names: ['danger', 'red'], cssClass: 'danger' },
38
- { names: ['success', 'green'], cssClass: 'success' },
48
+ const CONTAINERS = [
49
+ { names: ['info', 'blue'], type: 'callout', cssClass: 'info' },
50
+ { names: ['warning', 'orange'], type: 'callout', cssClass: 'warning' },
51
+ { names: ['danger', 'red'], type: 'callout', cssClass: 'danger' },
52
+ { names: ['success', 'green'], type: 'callout', cssClass: 'success' },
53
+ { names: ['spoiler'], type: 'spoiler' } // different type
39
54
  ];
40
- for (const { names, cssClass } of CALLOUT_TYPES) {
55
+
56
+ for (const { names, type, cssClass } of CONTAINERS) {
41
57
  for (const name of names) {
42
58
  md.use(container, name, {
43
- render(tokens, idx) {
59
+ render: function (tokens, idx) {
44
60
  const token = tokens[idx];
45
61
  if (token.nesting === 1) {
46
- // opening tag – extract optional title after :::type
47
62
  const titleMatch = token.info.trim().match(new RegExp(`^${name}\\s+(.*)$`));
48
63
  const title = titleMatch ? titleMatch[1].trim() : '';
49
- const titleHtml = title
50
- ? `<p class="callout-title">${md.utils.escapeHtml(title)}</p>\n`
51
- : '';
52
- return `<div class="callout callout-${cssClass}">\n${titleHtml}`;
64
+
65
+ // decide the HTML structure based on type
66
+ if (type === 'spoiler') {
67
+ const summary = title || 'Spoiler';
68
+ return `<details class="callout-spoiler">\n<summary>${md.utils.escapeHtml(summary)}</summary>\n`;
69
+ } else { // 'callout'
70
+ const titleHtml = title ? `<p class="callout-title">${md.utils.escapeHtml(title)}</p>\n` : '';
71
+ return `<div class="callout callout-${cssClass}">\n${titleHtml}`;
72
+ }
53
73
  }
54
- return '</div>\n';
74
+ // ending tag
75
+ return type === 'spoiler' ? '</details>\n' : '</div>\n';
55
76
  },
56
77
  });
57
78
  }
58
79
  }
59
- // --- Spoiler (collapsible) container: :::spoiler ---
60
- md.use(container, 'spoiler', {
61
- render(tokens, idx) {
62
- const token = tokens[idx];
63
- if (token.nesting === 1) {
64
- const titleMatch = token.info.trim().match(/^spoiler\s+(.*)$/);
65
- const summary = titleMatch ? titleMatch[1].trim() : 'Spoiler';
66
- return `<details class="callout-spoiler">\n<summary>${md.utils.escapeHtml(summary)}</summary>\n`;
67
- }
68
- return '</details>\n';
69
- },
70
- });
71
80
  // Unique HTML comment used as TOC marker (survives markdown-it rendering)
72
81
  const TOC_MARKER = '<!--TOC_PLACEHOLDER-->';
73
82
  // Regex to match [TOC] on its own line in raw Markdown
@@ -140,9 +149,10 @@ function processTOC(html) {
140
149
  return html.replace(TOC_MARKER, toc);
141
150
  }
142
151
  // CSS file paths
143
- const themePath = path.join(__dirname, 'themes', 'default.css');
144
- const hljsCssPath = path.join(__dirname, 'node_modules', 'highlight.js', 'styles', 'github-dark.css');
145
- const texmathCssPath = path.join(__dirname, 'node_modules', 'markdown-it-texmath', 'css', 'texmath.css');
152
+ const root = path.join(__dirname, '..');
153
+ const themePath = path.join(root, 'themes', 'default.css');
154
+ const hljsCssPath = path.join(root, 'node_modules', 'highlight.js', 'styles', 'github-dark.css');
155
+ const texmathCssPath = path.join(root, 'node_modules', 'markdown-it-texmath', 'css', 'texmath.css');
146
156
  // KaTeX CSS is referenced as a local file:// URL so its bundled fonts resolve correctly
147
157
  const katexCssUrl = `file://${path.join(__dirname, 'node_modules', 'katex', 'dist', 'katex.min.css')}`;
148
158
  // CSS cache: read once, avoid I/O on every render
@@ -80,7 +80,7 @@ strong {
80
80
 
81
81
  em {
82
82
  font-style: italic;
83
- color: #333;
83
+ color: inherit;
84
84
  }
85
85
 
86
86
  /* code */