@kobekeye/mdf 1.0.0 → 1.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
@@ -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,23 +17,34 @@ 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
18
21
  ```bash
19
- npm install -g mdf
22
+ npm install -g @kobekeye/mdf
20
23
  ```
21
-
22
- Or run without installing:
23
-
24
+ ### Linux / macOS
25
+ 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.
26
+ #### 1. Install nvm
24
27
  ```bash
25
- npx mdf input.md
28
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
29
+ ```
30
+ #### 2. Install nodejs
31
+ ```bash
32
+ nvm install --lts
33
+ ```
34
+ #### 3. Install mdf
35
+ ```bash
36
+ npm install -g @kobekeye/mdf
26
37
  ```
27
-
28
38
  ## Usage
29
-
30
39
  ```bash
31
40
  mdf input.md # outputs input.pdf
32
41
  mdf input.md output.pdf # custom output name
33
42
  ```
43
+ Or, if you want to try without installation,
44
+ ```bash
45
+ npx @kobekeye/mdf input.md # outputs input.pdf
46
+ npx @kobekeye/mdf input.md output.pdf # custom output name
47
+ ```
34
48
 
35
49
  ## Syntax Guide
36
50
 
@@ -71,7 +85,7 @@ Operation completed.
71
85
  :::
72
86
  ```
73
87
 
74
- ### Manual Page Break
88
+ ### Manual Page Break!
75
89
 
76
90
  ```markdown
77
91
  ==page==
package/index.js CHANGED
@@ -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();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@kobekeye/mdf",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Convert Markdown to beautiful PDFs — free, open-source, and zero-config.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "mdf": "./index.js"
7
+ "mdf": "index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "test": "echo \"Error: no test specified\" && exit 1"
package/renderer.js CHANGED
@@ -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
@@ -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 */