@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.
- package/.github/workflows/publish.yml +29 -0
- package/README.md +38 -12
- package/package.json +3 -3
- package/{index.js → src/cli.js} +56 -13
- package/{renderer.js → src/renderer.js} +54 -44
- package/themes/default.css +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
7
|
+
"mdf": "src/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"Error: no test specified\" && exit 1"
|
package/{index.js → src/cli.js}
RENAMED
|
@@ -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:
|
|
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
|
-
|
|
20
|
-
|
|
30
|
+
mdf README.md
|
|
31
|
+
mdf doc.md custom-name.pdf
|
|
21
32
|
`);
|
|
22
33
|
process.exit(0);
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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:
|
|
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
|
|
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
|
|
32
|
+
return wrap(md.utils.escapeHtml(str));
|
|
21
33
|
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
|
144
|
-
const
|
|
145
|
-
const
|
|
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
|