@jiyeqian/md2pdf 1.6.0 → 1.7.4
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 +36 -287
- package/assets/base.css +8 -6
- package/package.json +1 -1
- package/skill/SKILL.md +2 -1
- package/src/md2pdf.mjs +54 -15
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# md2pdf
|
|
2
2
|
|
|
3
|
-
把 Markdown 排成**优雅的中文 A4 PDF
|
|
4
|
-
不是 pandoc 的默认样式 —— 是可以直接拿去打印、发给别人看的版式。
|
|
3
|
+
把 Markdown 排成**优雅的中文 A4 PDF**:报头大标题、元信息条、精心排过的表格/代码/引用/列表、页脚页码,还支持数学公式与参考文献。不是 pandoc 的默认样式——是可以直接拿去打印、发给别人看的版式。
|
|
5
4
|
|
|
6
5
|
**elegant 主题**(默认,墨蓝 + 古铜):
|
|
7
6
|
|
|
@@ -13,30 +12,13 @@
|
|
|
13
12
|
|
|
14
13
|
仓库:https://cnb.cool/jiyeqian/md2pdf
|
|
15
14
|
|
|
16
|
-
## 两部分:命令 + 说明书
|
|
17
|
-
|
|
18
|
-
这个工具是两层结构,各自独立存在、各自分发:
|
|
19
|
-
|
|
20
|
-
| 层 | 是什么 | 给谁用 | 落在哪 |
|
|
21
|
-
| --- | --- | --- | --- |
|
|
22
|
-
| **命令** `md2pdf` | 真正的程序(Node + 无头 Chrome 渲染) | 你、任何脚本 | npm 全局安装到 `node_modules/@jiyeqian/md2pdf/` |
|
|
23
|
-
| **技能说明书** `skill/SKILL.md` | 告诉 Agent「有 `md2pdf` 这个命令、怎么用」 | WorkBuddy 等 Agent 运行时 | `~/.workbuddy/skills/md-to-pdf/` |
|
|
24
|
-
|
|
25
|
-
npm 的 `postinstall` 一次装两样:环境里有 WorkBuddy(`~/.workbuddy` 存在)就顺带装说明书,
|
|
26
|
-
没有就只装命令。只要命令用 `MD2PDF_SKILL=0` 跳过。
|
|
27
|
-
|
|
28
|
-
> 为什么说明书不在程序里?因为「怎么用」是给 Agent 看的,「能转换」是给系统跑的 ——
|
|
29
|
-
> 混在一起会让换机器时多一份要同步的实现。说明书只有一份,就在仓库 `skill/`。
|
|
30
|
-
|
|
31
15
|
## 安装
|
|
32
16
|
|
|
33
|
-
**一条命令**(需要 Node ≥ 18,建议 ≥ 22):
|
|
34
|
-
|
|
35
17
|
```bash
|
|
36
|
-
npm install -g @jiyeqian/md2pdf
|
|
18
|
+
npm install -g @jiyeqian/md2pdf # 需要 Node ≥ 18(建议 ≥ 22)
|
|
37
19
|
```
|
|
38
20
|
|
|
39
|
-
|
|
21
|
+
装上即可用 `md2pdf` 命令;npm 的 `postinstall` 会把技能说明书装进 `~/.workbuddy`(存在时,供 Agent 使用)。
|
|
40
22
|
|
|
41
23
|
```bash
|
|
42
24
|
md2pdf 你的文档.md --open # 装完试一下
|
|
@@ -47,26 +29,9 @@ md2pdf 你的文档.md --open # 装完试一下
|
|
|
47
29
|
| 变量 | 作用 |
|
|
48
30
|
| --- | --- |
|
|
49
31
|
| `MD2PDF_SKILL=0` | 安装时不装 Agent 技能说明书 |
|
|
50
|
-
| `MD2PDF_SKILL_DIR=<dir>` | 说明书落点,默认 `~/.workbuddy/skills/md-to-pdf
|
|
51
|
-
|
|
52
|
-
### 在仓库里开发
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
git clone https://cnb.cool/jiyeqian/md2pdf.git
|
|
56
|
-
cd md2pdf
|
|
57
|
-
npm link # 把 bin/md2pdf 软链进 PATH,指向仓库本身,改代码立即生效
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`npm link` 之后命令就是仓库本身,改完立即生效,不需要重装。
|
|
61
|
-
|
|
62
|
-
### 依赖
|
|
63
|
-
|
|
64
|
-
| 依赖 | 要求 | 说明 |
|
|
65
|
-
| --- | --- | --- |
|
|
66
|
-
| Node.js | ≥ 18(建议 ≥ 22) | < 22 时自动启用内置 WebSocket 实现;`MD2PDF_NODE` 可指定 |
|
|
67
|
-
| Chrome / Edge / Chromium | 任一 | 只用来渲染,不联网;`MD2PDF_CHROME` 可指定路径 |
|
|
32
|
+
| `MD2PDF_SKILL_DIR=<dir>` | 说明书落点,默认 `~/.workbuddy/skills/md-to-pdf` |
|
|
68
33
|
|
|
69
|
-
|
|
34
|
+
**依赖**:Node.js ≥ 18(建议 ≥ 22),以及 Chrome / Edge / Chromium 任一(只渲染、不联网)。零 npm 运行时依赖——`marked` 与 MathJax 已内置在 `vendor/`。
|
|
70
35
|
|
|
71
36
|
## 用法
|
|
72
37
|
|
|
@@ -81,24 +46,20 @@ md2pdf 文件名.md --theme minimal --toc
|
|
|
81
46
|
|
|
82
47
|
| 选项 | 作用 |
|
|
83
48
|
| --- | --- |
|
|
84
|
-
| `-o, --output <path>` |
|
|
85
|
-
| `--theme <name>` | `elegant
|
|
86
|
-
| `--title <text>`
|
|
87
|
-
| `--
|
|
88
|
-
|
|
|
89
|
-
| `--no-
|
|
90
|
-
|
|
|
91
|
-
| `--no-
|
|
92
|
-
| `--
|
|
49
|
+
| `-o, --output <path>` | 输出路径;多文件或目标是目录时作为输出目录 |
|
|
50
|
+
| `--theme <name>` | `elegant`(默认)| `minimal` |
|
|
51
|
+
| `--title <text>` / `--kicker <text>` | 覆盖标题 / 报头小标题 |
|
|
52
|
+
| `--no-meta` / `--no-lead` | 不要元信息条 / 首段不作为导语 |
|
|
53
|
+
| `-t, --toc` | 文首插入目录页(取自 H2),条目可点击跳转 |
|
|
54
|
+
| `--no-outline` | 不生成 PDF 书签(默认生成) |
|
|
55
|
+
| `--bibliography [footnote\|bib]` | 参考文献模式,**默认启用**(`footnote`,不加参数也生效);`bib` 为未来支持 |
|
|
56
|
+
| `--no-bibliography` | 关闭参考文献模式(脚注作为普通脚注) |
|
|
57
|
+
| `--numbering <mode>` | 章节编号:`auto`(默认,识别到已有编号则不动)| `force`(强制)| `none`(不加) |
|
|
93
58
|
| `--link-urls` | 正文链接后附 URL(纸质可读) |
|
|
94
|
-
| `--landscape` |
|
|
95
|
-
| `--
|
|
96
|
-
| `--
|
|
97
|
-
| `--
|
|
98
|
-
| `--footer-left / --footer-right <text>` | 页脚左右文字 |
|
|
99
|
-
| `--colophon <text>` | 文末落款(默认:来源文件名) |
|
|
100
|
-
| `--keep-html` | 保留中间 HTML,方便调样式 |
|
|
101
|
-
| `--html-only` | 只生成 HTML,不启动浏览器(调样式 / CI 校验用) |
|
|
59
|
+
| `--landscape` / `--font-size <pt>` / `--margin <mm>` | 横向 / 字号(默认 10.5)/ 页边距(默认 20) |
|
|
60
|
+
| `--no-footer` / `--footer-left` / `--footer-right` | 页脚控制 |
|
|
61
|
+
| `--colophon <text>` | 文末落款 |
|
|
62
|
+
| `--keep-html` / `--html-only` | 留中间 HTML 调样式 / 只出 HTML(CI 校验用) |
|
|
102
63
|
| `--open` | 完成后打开 PDF |
|
|
103
64
|
|
|
104
65
|
布尔选项支持 `--flag=false`。环境变量:`MD2PDF_CHROME`、`MD2PDF_NODE`、`MD2PDF_WS=mini`。
|
|
@@ -107,17 +68,10 @@ md2pdf 文件名.md --theme minimal --toc
|
|
|
107
68
|
|
|
108
69
|
| | 是什么 | 在哪看 | 怎么开 |
|
|
109
70
|
| --- | --- | --- | --- |
|
|
110
|
-
| **目录页** |
|
|
111
|
-
| **PDF 书签** |
|
|
71
|
+
| **目录页** | 文首一张目录,条目是**可点击内链** | 文档第 1 页 | `-t / --toc`(默认关) |
|
|
72
|
+
| **PDF 书签** | 阅读器侧栏的**章节大纲树** | 阅读器侧栏 | **默认开**,`--no-outline` 关 |
|
|
112
73
|
|
|
113
|
-
书签由 Chrome 按
|
|
114
|
-
所以只要文档用了标准标题层级,就有对应的大纲,不需要额外配置。
|
|
115
|
-
|
|
116
|
-
需要看侧栏的阅读器操作:macOS 预览需手动展开侧栏(**⌘⌥3**,或右上角侧栏按钮);
|
|
117
|
-
Acrobat / 福昕 / Chrome 内置阅读器点侧栏图标即可。侧栏是否自动展开由阅读器自身决定,
|
|
118
|
-
本工具不写 `/PageMode`(改这个字段要重写 PDF 目录对象,收益不值那份风险)。
|
|
119
|
-
|
|
120
|
-
自己验一份 PDF 的书签与内链:
|
|
74
|
+
书签由 Chrome 按 `h1`–`h6` 结构生成,无需额外配置。验一份 PDF 的书签与内链:
|
|
121
75
|
|
|
122
76
|
```bash
|
|
123
77
|
node ci/inspect-pdf.mjs out.pdf
|
|
@@ -125,239 +79,34 @@ node ci/inspect-pdf.mjs out.pdf
|
|
|
125
79
|
|
|
126
80
|
## 排版规则
|
|
127
81
|
|
|
128
|
-
- 首个 H1
|
|
129
|
-
- YAML frontmatter 的 `name` / `description`
|
|
130
|
-
- H2
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
不是 pandoc 的默认样式 —— 是可以直接拿去打印、发给别人看的版式。
|
|
135
|
-
|
|
136
|
-
**elegant 主题**(默认,墨蓝 + 古铜):
|
|
137
|
-
|
|
138
|
-

|
|
82
|
+
- 首个 H1 提升为报头大标题;其后的首段自动成为导语。
|
|
83
|
+
- YAML frontmatter 的 `name` / `description` 生成元信息条;「适用于…/不用于…」自动拆两栏。
|
|
84
|
+
- H2 自动分节加色块;表格深色表头+隔行浅底;有序列表圆形序号。
|
|
85
|
+
- 数学公式:`$...$`(行内)与 `$$...$$`(独立成行)由内置 MathJax 渲染。
|
|
86
|
+
- 脚注:`[^id]` 引用 + `[^id]: 内容` 定义;BibTeX 脚注(`@article{...}` 等)按 GB/T 7714-2025 著录,默认收集为「参考文献」章节(`--no-bibliography` 关闭)。
|
|
87
|
+
- 相对路径图片自动解析进 PDF。
|
|
139
88
|
|
|
140
|
-
|
|
89
|
+
改样式:`assets/base.css`(骨架)与 `assets/theme-*.css`(配色),改完重跑命令即生效。
|
|
141
90
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
仓库:https://cnb.cool/jiyeqian/md2pdf
|
|
145
|
-
|
|
146
|
-
## 两部分:命令 + 说明书
|
|
147
|
-
|
|
148
|
-
这个工具是两层结构,各自独立存在、各自分发:
|
|
149
|
-
|
|
150
|
-
| 层 | 是什么 | 给谁用 | 落在哪 |
|
|
151
|
-
| --- | --- | --- | --- |
|
|
152
|
-
| **命令** `md2pdf` | 真正的程序(Node + 无头 Chrome 渲染) | 你、任何脚本 | npm 全局安装到 `node_modules/@jiyeqian/md2pdf/` |
|
|
153
|
-
| **技能说明书** `skill/SKILL.md` | 告诉 Agent「有 `md2pdf` 这个命令、怎么用」 | WorkBuddy 等 Agent 运行时 | `~/.workbuddy/skills/md-to-pdf/` |
|
|
154
|
-
|
|
155
|
-
npm 的 `postinstall` 一次装两样:环境里有 WorkBuddy(`~/.workbuddy` 存在)就顺带装说明书,
|
|
156
|
-
没有就只装命令。只要命令用 `MD2PDF_SKILL=0` 跳过。
|
|
157
|
-
|
|
158
|
-
> 为什么说明书不在程序里?因为「怎么用」是给 Agent 看的,「能转换」是给系统跑的 ——
|
|
159
|
-
> 混在一起会让换机器时多一份要同步的实现。说明书只有一份,就在仓库 `skill/`。
|
|
160
|
-
|
|
161
|
-
## 安装
|
|
162
|
-
|
|
163
|
-
**一条命令**(需要 Node ≥ 18,建议 ≥ 22):
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
npm install -g @jiyeqian/md2pdf
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
它会装上 `md2pdf` 命令;`postinstall` 顺带把技能说明书装进 `~/.workbuddy`(存在时)。
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
md2pdf 你的文档.md --open # 装完试一下
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
**更新**:`npm update -g @jiyeqian/md2pdf` · **卸载**:`npm uninstall -g @jiyeqian/md2pdf`
|
|
176
|
-
|
|
177
|
-
| 变量 | 作用 |
|
|
178
|
-
| --- | --- |
|
|
179
|
-
| `MD2PDF_SKILL=0` | 安装时不装 Agent 技能说明书 |
|
|
180
|
-
| `MD2PDF_SKILL_DIR=<dir>` | 说明书落点,默认 `~/.workbuddy/skills/md-to-pdf`(`~/.workbuddy` 不存在时默认不装) |
|
|
181
|
-
|
|
182
|
-
### 在仓库里开发
|
|
91
|
+
## 开发
|
|
183
92
|
|
|
184
93
|
```bash
|
|
185
94
|
git clone https://cnb.cool/jiyeqian/md2pdf.git
|
|
186
95
|
cd md2pdf
|
|
187
|
-
npm link
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`npm link` 之后命令就是仓库本身,改完立即生效,不需要重装。
|
|
191
|
-
|
|
192
|
-
### 依赖
|
|
193
|
-
|
|
194
|
-
| 依赖 | 要求 | 说明 |
|
|
195
|
-
| --- | --- | --- |
|
|
196
|
-
| Node.js | ≥ 18(建议 ≥ 22) | < 22 时自动启用内置 WebSocket 实现;`MD2PDF_NODE` 可指定 |
|
|
197
|
-
| Chrome / Edge / Chromium | 任一 | 只用来渲染,不联网;`MD2PDF_CHROME` 可指定路径 |
|
|
198
|
-
|
|
199
|
-
零 npm 依赖 —— Markdown 解析器(marked)已内置在 `vendor/`,装好即用。
|
|
200
|
-
|
|
201
|
-
## 用法
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
md2pdf 文件名.md # 同目录输出同名 .pdf
|
|
205
|
-
md2pdf 文件名.md --open # 转完直接打开
|
|
206
|
-
md2pdf a.md b.md -o 输出目录/ # 批量(共用一个浏览器实例,很快)
|
|
207
|
-
md2pdf 文件名.md --theme minimal --toc
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### 选项
|
|
211
|
-
|
|
212
|
-
| 选项 | 作用 |
|
|
213
|
-
| --- | --- |
|
|
214
|
-
| `-o, --output <path>` | 输出路径;多文件或目标是目录时,作为输出目录 |
|
|
215
|
-
| `--theme <name>` | `elegant`(默认,墨蓝+古铜)| `minimal`(黑白公文风) |
|
|
216
|
-
| `--title <text>` | 覆盖标题(默认:正文首个 H1 → frontmatter.title → 文件名) |
|
|
217
|
-
| `--kicker <text>` | 报头小标题;`SKILL.md` 默认显示「技能文档」 |
|
|
218
|
-
| `--no-meta` | 不要 frontmatter 元信息条 |
|
|
219
|
-
| `--no-lead` | 首段不作为导语放大 |
|
|
220
|
-
| `-t, --toc` | 文首插入目录页(取自 H2,需 2 个以上),每项可点击跳转 |
|
|
221
|
-
| `--no-outline` | 不生成 PDF 书签(**默认生成**,见下) |
|
|
222
|
-
| `--bibliography` | 将脚注收集为文末「参考文献」章节(BibTeX 脚注按 GB/T 7714 渲染) |
|
|
223
|
-
| `--link-urls` | 正文链接后附 URL(纸质可读) |
|
|
224
|
-
| `--landscape` | 横向页面 |
|
|
225
|
-
| `--font-size <pt>` | 正文字号,默认 10.5 |
|
|
226
|
-
| `--margin <mm>` | 页边距,默认 20;可写 `"20,18"`(上下,左右) |
|
|
227
|
-
| `--no-footer` | 不要页脚页码 |
|
|
228
|
-
| `--footer-left / --footer-right <text>` | 页脚左右文字 |
|
|
229
|
-
| `--colophon <text>` | 文末落款(默认:来源文件名) |
|
|
230
|
-
| `--keep-html` | 保留中间 HTML,方便调样式 |
|
|
231
|
-
| `--html-only` | 只生成 HTML,不启动浏览器(调样式 / CI 校验用) |
|
|
232
|
-
| `--open` | 完成后打开 PDF |
|
|
233
|
-
|
|
234
|
-
布尔选项支持 `--flag=false`。环境变量:`MD2PDF_CHROME`、`MD2PDF_NODE`、`MD2PDF_WS=mini`。
|
|
235
|
-
|
|
236
|
-
### 目录与书签是两件事
|
|
237
|
-
|
|
238
|
-
| | 是什么 | 在哪看 | 怎么开 |
|
|
239
|
-
| --- | --- | --- | --- |
|
|
240
|
-
| **目录页** | 排在文首的一张目录,条目是**可点击的内链** | 文档第 1 页 | `-t / --toc`(默认关) |
|
|
241
|
-
| **PDF 书签** | PDF 阅读器侧边栏里的**章节大纲树**(可折叠、点击跳转) | 阅读器侧栏 | **默认开**,`--no-outline` 关 |
|
|
242
|
-
|
|
243
|
-
书签由 Chrome 按 HTML 的 `h1`–`h6` 结构生成(报头标题为根,H2/H3 逐层嵌套),
|
|
244
|
-
所以只要文档用了标准标题层级,就有对应的大纲,不需要额外配置。
|
|
245
|
-
|
|
246
|
-
需要看侧栏的阅读器操作:macOS 预览需手动展开侧栏(**⌘⌥3**,或右上角侧栏按钮);
|
|
247
|
-
Acrobat / 福昕 / Chrome 内置阅读器点侧栏图标即可。侧栏是否自动展开由阅读器自身决定,
|
|
248
|
-
本工具不写 `/PageMode`(改这个字段要重写 PDF 目录对象,收益不值那份风险)。
|
|
249
|
-
|
|
250
|
-
自己验一份 PDF 的书签与内链:
|
|
251
|
-
|
|
252
|
-
```bash
|
|
253
|
-
node ci/inspect-pdf.mjs out.pdf
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
## 排版规则
|
|
257
|
-
|
|
258
|
-
- 首个 H1 提升为报头大标题,正文不再重复;其后的首段自动成为导语。
|
|
259
|
-
- YAML frontmatter 的 `name` / `description` 生成元信息条;description 里「适用于…」「不用于…」会自动拆成「适用 / 不适用」两栏。
|
|
260
|
-
- H2 自动分节并加色块标记;表格深色表头+隔行浅底;有序列表用圆形序号。
|
|
261
|
-
(行内)与 `$...$`(独立成行)由内置 MathJax 渲染(SVG 输出,零字体依赖)。
|
|
262
|
-
- 脚注:正文 `[^id]` 引用 + 文末 `[^id]: 内容` 定义;`--bibliography` 时收集为「参考文献」章节。
|
|
263
|
-
- BibTeX 脚注(`@article{...}`、`@book{...}` 等)自动按 GB/T 7714-2025 著录格式渲染。
|
|
264
|
-
- 相对路径图片自动解析成绝对地址,能正常进入 PDF。
|
|
265
|
-
|
|
266
|
-
## 改样式
|
|
267
|
-
|
|
268
|
-
```
|
|
269
|
-
assets/base.css 骨架(占位符 {{PAGE_SIZE}} {{MARGIN_*}} {{FONT_SIZE}})
|
|
270
|
-
assets/theme-elegant.css 墨蓝 + 古铜(默认)
|
|
271
|
-
assets/theme-minimal.css 黑白公文
|
|
272
|
-
assets/shell.html 页面骨架
|
|
96
|
+
npm link # 命令指向仓库,改代码立即生效
|
|
97
|
+
bash ci/validate.sh # 本地与 CI 同一套校验(无需浏览器)
|
|
273
98
|
```
|
|
274
99
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
## 它是怎么工作的
|
|
278
|
-
|
|
279
|
-
```
|
|
280
|
-
Markdown ──(marked)──▶ HTML ──(模板+主题 CSS)──▶ 完整 HTML
|
|
281
|
-
──▶ 无头 Chrome(CDP Page.printToPDF)──▶ PDF
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
选 CDP 而不是 `chrome --print-to-pdf` 的原因:命令行版不支持页眉页脚模板,出不了页码。
|
|
285
|
-
`preferCSSPageSize: true` 让页面尺寸/边距完全由 CSS `@page` 控制。
|
|
286
|
-
|
|
287
|
-
## 校验与 CI
|
|
288
|
-
|
|
289
|
-
```bash
|
|
290
|
-
bash ci/validate.sh # 本地跑,和 CI 完全同一套检查(约几秒)
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
校验分四层,全部不需要浏览器:
|
|
294
|
-
|
|
295
|
-
1. **结构**:必需文件齐全、`bin/` 与安装脚本有可执行位、关键文件确实被 git 跟踪
|
|
296
|
-
2. **语法**:`sh -n`、`node --check`
|
|
297
|
-
3. **一致性**:版本号(package.json ↔ src);模板占位符 ↔ 替换逻辑双向闭合;
|
|
298
|
-
主题 CSS 里 `var(--x)` 全部有定义;占位符替换必须是全量的
|
|
299
|
-
4. **行为**:`--help`/`--version` 冒烟;`examples/demo.md` 端到端渲染到 HTML,
|
|
300
|
-
断言表格、代码块、引用、嵌套列表、目录、链接 URL、分节都在,且无占位符残留
|
|
301
|
-
与 `undefined` 泄漏;目录锚点与标题 `id` 一一对应
|
|
302
|
-
5. **接线**:PDF 书签这类"只存在于 PDF 里"的特性,CI 没有浏览器验不了结果,
|
|
303
|
-
就退一步断言参数真的传进了 `printToPDF`、开关真的从 `main` 接到了渲染 ——
|
|
304
|
-
光有 `case '--no-outline'` 不等于接到了
|
|
305
|
-
|
|
306
|
-
最后还有一步**守卫自测**:故意破坏一份副本(塞入未定义的占位符、改错主题变量名、
|
|
307
|
-
改乱版本号、把目录项退回纯文本、关掉书签参数…),断言校验确实会失败 ——
|
|
308
|
-
只会"全绿"的校验等于没有校验。
|
|
309
|
-
|
|
310
|
-
CNB 云原生构建在 push / PR 时跑同一脚本;打 tag 时发布到 npm
|
|
311
|
-
(见 `.cnb.yml`)。
|
|
312
|
-
|
|
313
|
-
### 发版
|
|
314
|
-
|
|
315
|
-
改完 `src/md2pdf.mjs` 的 `VERSION` 与 `package.json` 的 `version`(校验会检查两者一致),然后:
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
git tag v1.4.0 && git push origin v1.4.0
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
流水线会自动:校验 → `npm publish --access public`(发布到 npm,需在 CNB 项目里配置 `NPM_TOKEN` secret)。
|
|
322
|
-
也可以本地手动 `npm publish`。
|
|
323
|
-
|
|
324
|
-
注意 CNB **不允许删除 tag**,打错了只能升版本号再发一版。
|
|
325
|
-
|
|
326
|
-
## 目录结构
|
|
327
|
-
|
|
328
|
-
```
|
|
329
|
-
bin/md2pdf 启动器(解析软链、挑选 node)
|
|
330
|
-
src/md2pdf.mjs 主程序
|
|
331
|
-
src/ws.mjs Node < 22 时的极简 WebSocket 客户端
|
|
332
|
-
src/install-skill.mjs npm postinstall:把技能说明书装进 WorkBuddy
|
|
333
|
-
assets/ 样式与页面骨架
|
|
334
|
-
vendor/marked.esm.js 内置 Markdown 解析器
|
|
335
|
-
examples/demo.md 示例文档(含表格/代码/引用/嵌套列表)
|
|
336
|
-
ci/validate.sh 校验入口(本地与 CI 同一套)
|
|
337
|
-
ci/checks.mjs 一致性 + 端到端渲染断言
|
|
338
|
-
ci/inspect-pdf.mjs 读出 PDF 的书签树与链接注解(本地验证 outline 用)
|
|
339
|
-
skill/SKILL.md Agent 技能说明书(postinstall 会装到技能目录)
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
|
|
100
|
+
发版:同步 `src/md2pdf.mjs` 的 `VERSION` 与 `package.json` 的 `version`,然后 `git tag v1.x.x && git push origin v1.x.x`,CNB 流水线会自动 `npm publish`(需配置 `NPM_TOKEN`)。
|
|
343
101
|
|
|
344
102
|
## 常见问题
|
|
345
103
|
|
|
346
|
-
|
|
347
|
-
升级:`npm update -g @jiyeqian/md2pdf`。
|
|
348
|
-
|
|
349
|
-
**npm 装不上** → 确认 npm registry 可达;也可以 clone 仓库后 `npm link` 本地开发。
|
|
104
|
+
**找不到 Chrome** → `export MD2PDF_CHROME=/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome`
|
|
350
105
|
|
|
351
|
-
|
|
106
|
+
**Node 版本老** → 升级到 22+;不升也能用(自动走内置 WebSocket)。
|
|
352
107
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
**PDF 里目录不能点击** → Chrome 打印不保留内部锚点跳转,目录是纯文本。
|
|
356
|
-
|
|
357
|
-
**想改默认字号/边距** → 直接改命令行参数;要永久生效就改 `src/md2pdf.mjs` 里 `parseArgs` 的默认值。
|
|
108
|
+
**想改默认字号/边距** → 改命令行参数;永久生效改 `src/md2pdf.mjs` 里 `parseArgs` 的默认值。
|
|
358
109
|
|
|
359
110
|
## License
|
|
360
111
|
|
|
361
|
-
MIT
|
|
362
|
-
|
|
363
|
-
第三方组件:`vendor/marked.esm.js` 来自 [marked](https://github.com/markedjs/marked)(MIT License);`vendor/mathjax/tex-svg.js` 来自 [MathJax](https://github.com/mathjax/MathJax)(Apache-2.0 License)。均随仓库分发以便零依赖安装。
|
|
112
|
+
MIT。第三方组件:`vendor/marked.esm.js`(marked,MIT)、`vendor/mathjax/tex-svg.js`(MathJax,Apache-2.0),均随仓库分发以便零依赖安装。
|
package/assets/base.css
CHANGED
|
@@ -52,6 +52,7 @@ h1 {
|
|
|
52
52
|
margin: 0 0 12px;
|
|
53
53
|
line-height: 1.25;
|
|
54
54
|
letter-spacing: 0;
|
|
55
|
+
text-align: center;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
.lead {
|
|
@@ -155,18 +156,18 @@ tbody td:first-child { font-weight: 600; color: var(--ink); }
|
|
|
155
156
|
/* ---------- 列表 ---------- */
|
|
156
157
|
ol, ul { margin: 0 0 14px; padding-left: 0; }
|
|
157
158
|
ol { list-style: none; counter-reset: step; }
|
|
158
|
-
ol li { position: relative; padding-left: 30px; margin-bottom: 11px; text-align: justify; }
|
|
159
|
-
ol li:last-child { margin-bottom: 0; }
|
|
160
|
-
ol li::before {
|
|
159
|
+
ol > li { position: relative; padding-left: 30px; margin-bottom: 11px; text-align: justify; }
|
|
160
|
+
ol > li:last-child { margin-bottom: 0; }
|
|
161
|
+
ol > li::before {
|
|
161
162
|
counter-increment: step; content: counter(step);
|
|
162
163
|
position: absolute; left: 0; top: .28em;
|
|
163
164
|
width: 1.55em; height: 1.55em; line-height: 1.55em; text-align: center;
|
|
164
165
|
font-family: var(--font-mono); font-size: .82em; font-weight: 700;
|
|
165
166
|
}
|
|
166
167
|
ul { list-style: none; }
|
|
167
|
-
ul li { position: relative; padding-left: 18px; margin-bottom: 11px; text-align: justify; }
|
|
168
|
-
ul li:last-child { margin-bottom: 0; }
|
|
169
|
-
ul li::before {
|
|
168
|
+
ul > li { position: relative; padding-left: 18px; margin-bottom: 11px; text-align: justify; }
|
|
169
|
+
ul > li:last-child { margin-bottom: 0; }
|
|
170
|
+
ul > li::before {
|
|
170
171
|
content: ""; position: absolute; left: 2px; top: .82em;
|
|
171
172
|
width: 5px; height: 5px; border-radius: 50%;
|
|
172
173
|
}
|
|
@@ -211,6 +212,7 @@ img { max-width: 100%; height: auto; display: block; margin: 8px auto 14px; }
|
|
|
211
212
|
/* ---------- 脚注与参考文献 ---------- */
|
|
212
213
|
sup.fnref { font-size: .7em; line-height: 0; }
|
|
213
214
|
sup.fnref a { border-bottom: none; color: var(--accent); }
|
|
215
|
+
a.fnref-back { border-bottom: none; color: var(--accent); font-weight: 600; }
|
|
214
216
|
.references, .footnotes {
|
|
215
217
|
list-style: none; counter-reset: none;
|
|
216
218
|
margin: 4px 0 0; padding-left: 0;
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -47,7 +47,8 @@ npm install -g @jiyeqian/md2pdf
|
|
|
47
47
|
| `--no-meta` / `--no-lead` | 去掉元信息条 / 首段不作为导语 |
|
|
48
48
|
| `-t, --toc` | 文首插入目录页(取自 H2),条目可点击跳转 |
|
|
49
49
|
| `--no-outline` | 不生成 PDF 书签(默认生成) |
|
|
50
|
-
| `--bibliography` |
|
|
50
|
+
| `--bibliography [footnote\|bib]` | 参考文献模式,默认启用(footnote);bib 为未来支持。`--no-bibliography` 关闭 |
|
|
51
|
+
| `--numbering <mode>` | 章节编号:auto(默认)| force(强制)| none(不加) |
|
|
51
52
|
| `--link-urls` | 链接后附 URL |
|
|
52
53
|
| `--landscape` / `--font-size` / `--margin` | 横向 / 字号(默认 10.5pt)/ 页边距(默认 20mm) |
|
|
53
54
|
| `--no-footer` / `--footer-left` / `--footer-right` | 页脚控制 |
|
package/src/md2pdf.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
40
40
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
41
41
|
const ASSETS = path.join(ROOT, 'assets');
|
|
42
42
|
|
|
43
|
-
const VERSION = '1.
|
|
43
|
+
const VERSION = '1.7.4';
|
|
44
44
|
|
|
45
45
|
// Node ≥ 22 有全局 WebSocket;更老的版本退回到内置的极简实现
|
|
46
46
|
let _WS;
|
|
@@ -68,7 +68,8 @@ md2pdf ${VERSION} —— Markdown → 优雅 PDF
|
|
|
68
68
|
--no-lead 首段不作为导语
|
|
69
69
|
-t, --toc 在文首插入目录页(取自二级标题,可点击跳转)
|
|
70
70
|
--no-outline 不生成 PDF 书签(默认生成,阅读器侧边栏按标题成树)
|
|
71
|
-
--bibliography
|
|
71
|
+
--bibliography [footnote|bib] 将脚注收集为「参考文献」章节(默认 footnote;bib 为未来支持)
|
|
72
|
+
--numbering <mode> 章节编号:auto(默认,识别到已有编号则不动)| force(强制)| none(不加)
|
|
72
73
|
--link-urls 正文链接后附 URL
|
|
73
74
|
--landscape 横向
|
|
74
75
|
--font-size <pt> 正文字号(默认 10.5)
|
|
@@ -108,7 +109,8 @@ function parseArgs(argv) {
|
|
|
108
109
|
inputs: [], theme: 'elegant', fontSize: 10.5,
|
|
109
110
|
marginTop: 20, marginSide: 18, marginBottom: 18,
|
|
110
111
|
footer: true, footerLeft: '', footerRight: '',
|
|
111
|
-
meta: true, lead: true, toc: false, linkUrls: false, outline: true, bibliography:
|
|
112
|
+
meta: true, lead: true, toc: false, linkUrls: false, outline: true, bibliography: 'footnote',
|
|
113
|
+
numbering: 'auto',
|
|
112
114
|
landscape: false, keepHtml: false, htmlOnly: false, open: false, help: false,
|
|
113
115
|
};
|
|
114
116
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -143,7 +145,13 @@ function parseArgs(argv) {
|
|
|
143
145
|
case '--no-toc': o.toc = false; break;
|
|
144
146
|
case '--outline': o.outline = true; break;
|
|
145
147
|
case '--no-outline': o.outline = false; break;
|
|
146
|
-
case '--bibliography':
|
|
148
|
+
case '--bibliography': {
|
|
149
|
+
const n = argv[i + 1];
|
|
150
|
+
if (n && ['footnote', 'bib'].includes(n)) { o.bibliography = n; i++; }
|
|
151
|
+
else o.bibliography = 'footnote';
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case '--numbering': o.numbering = next(); break;
|
|
147
155
|
case '--no-bibliography': o.bibliography = false; break;
|
|
148
156
|
case '--no-landscape': o.landscape = false; break;
|
|
149
157
|
case '--no-link-urls': o.linkUrls = false; break;
|
|
@@ -211,6 +219,28 @@ function sectionize(html) {
|
|
|
211
219
|
.join('\n');
|
|
212
220
|
}
|
|
213
221
|
|
|
222
|
+
/* ---------------- 章节编号 ---------------- */
|
|
223
|
+
|
|
224
|
+
// 标题编号前缀识别(阿拉伯/中文/罗马数字、第X章、括号编号等)
|
|
225
|
+
const HEADING_NUM_RE = /^\s*(?:\d+(?:\.\d+)*[、..)]\s?|[一二三四五六七八九十百]+[、..]|第[一二三四五六七八九十百\d]+[章节篇]|\([一二三四五六七八九十\d]+\)|[IVX]+[.、])\s*/;
|
|
226
|
+
|
|
227
|
+
// 章节编号:mode 为 auto(默认,识别到已有编号则不动)/ force(强制编号)/ none(不动)
|
|
228
|
+
function numberHeadings(html, mode) {
|
|
229
|
+
if (mode === 'none') return html;
|
|
230
|
+
const re = /<h([2-6])([^>]*)>([\s\S]*?)<\/h\1>/g;
|
|
231
|
+
const all = [...html.matchAll(re)];
|
|
232
|
+
if (!all.length) return html;
|
|
233
|
+
if (mode !== 'force' && all.some(m => HEADING_NUM_RE.test(stripTags(m[3])))) return html;
|
|
234
|
+
const counters = [0, 0, 0, 0, 0];
|
|
235
|
+
return html.replace(re, (m, level, attrs, inner) => {
|
|
236
|
+
const lvl = parseInt(level, 10) - 2;
|
|
237
|
+
counters[lvl]++;
|
|
238
|
+
for (let k = lvl + 1; k < 5; k++) counters[k] = 0;
|
|
239
|
+
const num = counters.slice(0, lvl + 1).join('.');
|
|
240
|
+
return '<h' + level + attrs + '>' + num + ' ' + inner.replace(HEADING_NUM_RE, '') + '</h' + level + '>';
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
/* ---------------- 脚注与参考文献 ---------------- */
|
|
215
245
|
|
|
216
246
|
// 解析 markdown 脚注:[^id]: 定义(后续缩进行为内容)与正文 [^id] 引用
|
|
@@ -355,7 +385,7 @@ function formatGB7714(entry) {
|
|
|
355
385
|
}
|
|
356
386
|
|
|
357
387
|
// 渲染脚注列表;bibliography=true 时作为「参考文献」章节
|
|
358
|
-
function renderFootnotes(footnotes, bibliography) {
|
|
388
|
+
function renderFootnotes(footnotes, bibliography, marked) {
|
|
359
389
|
if (!footnotes.length) return '';
|
|
360
390
|
const items = footnotes.map((fn, i) => {
|
|
361
391
|
const num = i + 1;
|
|
@@ -364,14 +394,13 @@ function renderFootnotes(footnotes, bibliography) {
|
|
|
364
394
|
const entry = parseBibTeX(fn.content);
|
|
365
395
|
content = entry ? esc(formatGB7714(entry)) : esc(fn.content);
|
|
366
396
|
} else {
|
|
367
|
-
content =
|
|
397
|
+
content = marked.parseInline(fn.content, { gfm: true });
|
|
368
398
|
}
|
|
369
|
-
return '<li id="fn-' + num + '">[' + num + '] ' + content + '</li>';
|
|
399
|
+
return '<li id="fn-' + num + '"><a href="#fnref-' + num + '" class="fnref-back">[' + num + ']</a> ' + content + '</li>';
|
|
370
400
|
}).join('\n');
|
|
371
401
|
const cls = bibliography ? 'references' : 'footnotes';
|
|
372
402
|
const list = '<ol class="' + cls + '">\n' + items + '\n</ol>';
|
|
373
|
-
|
|
374
|
-
return '<section><h2 id="sec-footnotes">脚注</h2>\n' + list + '</section>';
|
|
403
|
+
return '<h2>' + (bibliography ? '参考文献' : '脚注') + '</h2>\n' + list;
|
|
375
404
|
}
|
|
376
405
|
|
|
377
406
|
/* ---------------- Chrome ---------------- */
|
|
@@ -514,13 +543,24 @@ async function renderOne(mdPath, opts, chrome, marked, tmpRoot) {
|
|
|
514
543
|
const { body, footnotes } = parseFootnotes(rawBody);
|
|
515
544
|
const fnIndex = new Map();
|
|
516
545
|
footnotes.forEach((fn, i) => fnIndex.set(fn.id, i + 1));
|
|
546
|
+
const fnRefCount = new Map();
|
|
517
547
|
const bodyWithRefs = body.replace(/\[\^([^\]]+)\]/g, (m, id) => {
|
|
518
548
|
const n = fnIndex.get(id);
|
|
519
|
-
|
|
549
|
+
if (n === undefined) return m;
|
|
550
|
+
const c = (fnRefCount.get(id) || 0) + 1;
|
|
551
|
+
fnRefCount.set(id, c);
|
|
552
|
+
const refId = c === 1 ? 'fnref-' + n : 'fnref-' + n + '-' + c;
|
|
553
|
+
return '<sup class="fnref" id="' + refId + '"><a href="#fn-' + n + '">[' + n + ']</a></sup>';
|
|
520
554
|
});
|
|
521
555
|
|
|
522
556
|
let html = marked.parse(bodyWithRefs, { gfm: true, breaks: false, async: false });
|
|
523
557
|
|
|
558
|
+
// 脚注/参考文献:先追加到正文末尾,再编号,使参考文献章节纳入编号体系
|
|
559
|
+
html += renderFootnotes(footnotes, opts.bibliography, marked);
|
|
560
|
+
|
|
561
|
+
// 章节编号:H2 起编号,H1 作为文档标题不动
|
|
562
|
+
html = numberHeadings(html, opts.numbering);
|
|
563
|
+
|
|
524
564
|
// 标题:正文首个 H1 → 报头
|
|
525
565
|
let title = opts.title || fm.title || '';
|
|
526
566
|
const h1 = /<h1(?:\s[^>]*)?>([\s\S]*?)<\/h1>/.exec(html);
|
|
@@ -542,8 +582,10 @@ async function renderOne(mdPath, opts, chrome, marked, tmpRoot) {
|
|
|
542
582
|
}
|
|
543
583
|
|
|
544
584
|
// 链接 / 图片
|
|
545
|
-
html = html.replace(/<a\s+href="([^"]*)"([^>]*)>/g, (m, href, rest) =>
|
|
546
|
-
|
|
585
|
+
html = html.replace(/<a\s+href="([^"]*)"([^>]*)>/g, (m, href, rest) => {
|
|
586
|
+
const cls = /\sclass=/.test(rest) ? '' : ' class="ref"';
|
|
587
|
+
return `<a href="${href}"${cls}${rest}>`;
|
|
588
|
+
});
|
|
547
589
|
if (opts.linkUrls) {
|
|
548
590
|
html = html.replace(/<a\s+href="(https?:\/\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/g,
|
|
549
591
|
(m, href, text) => `${m} <span class="link-url">(${esc(href)})</span>`);
|
|
@@ -577,9 +619,6 @@ async function renderOne(mdPath, opts, chrome, marked, tmpRoot) {
|
|
|
577
619
|
|
|
578
620
|
html = sectionize(html);
|
|
579
621
|
|
|
580
|
-
// 脚注/参考文献:追加到正文末尾
|
|
581
|
-
html += renderFootnotes(footnotes, opts.bibliography);
|
|
582
|
-
|
|
583
622
|
// CSS
|
|
584
623
|
const themeFile = path.join(ASSETS, `theme-${opts.theme}.css`);
|
|
585
624
|
if (!existsSync(themeFile)) throw new Error(`未知主题:${opts.theme}(可用:elegant, minimal)`);
|