@jiyeqian/md2pdf 1.4.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dr. Qian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # md2pdf
2
+
3
+ 把 Markdown 排成**优雅的中文 A4 PDF**:报头大标题、元信息条、精心排过的表格/代码/引用/列表、页脚页码。
4
+ 不是 pandoc 的默认样式 —— 是可以直接拿去打印、发给别人看的版式。
5
+
6
+ ![themes](docs/themes.png)
7
+
8
+ 仓库:https://cnb.cool/jiyeqian/md2pdf
9
+
10
+ ## 两部分:命令 + 说明书
11
+
12
+ 这个工具是两层结构,各自独立存在、各自分发:
13
+
14
+ | 层 | 是什么 | 给谁用 | 落在哪 |
15
+ | --- | --- | --- | --- |
16
+ | **命令** `md2pdf` | 真正的程序(Node + 无头 Chrome 渲染) | 你、任何脚本 | npm 全局安装到 `node_modules/@jiyeqian/md2pdf/` |
17
+ | **技能说明书** `skill/SKILL.md` | 告诉 Agent「有 `md2pdf` 这个命令、怎么用」 | WorkBuddy 等 Agent 运行时 | `~/.workbuddy/skills/md-to-pdf/` |
18
+
19
+ npm 的 `postinstall` 一次装两样:环境里有 WorkBuddy(`~/.workbuddy` 存在)就顺带装说明书,
20
+ 没有就只装命令。只要命令用 `MD2PDF_SKILL=0` 跳过。
21
+
22
+ > 为什么说明书不在程序里?因为「怎么用」是给 Agent 看的,「能转换」是给系统跑的 ——
23
+ > 混在一起会让换机器时多一份要同步的实现。说明书只有一份,就在仓库 `skill/`。
24
+
25
+ ## 安装
26
+
27
+ **一条命令**(需要 Node ≥ 18,建议 ≥ 22):
28
+
29
+ ```bash
30
+ npm install -g @jiyeqian/md2pdf
31
+ ```
32
+
33
+ 它会装上 `md2pdf` 命令;`postinstall` 顺带把技能说明书装进 `~/.workbuddy`(存在时)。
34
+
35
+ ```bash
36
+ md2pdf 你的文档.md --open # 装完试一下
37
+ ```
38
+
39
+ **更新**:`npm update -g @jiyeqian/md2pdf` · **卸载**:`npm uninstall -g @jiyeqian/md2pdf`
40
+
41
+ | 变量 | 作用 |
42
+ | --- | --- |
43
+ | `MD2PDF_SKILL=0` | 安装时不装 Agent 技能说明书 |
44
+ | `MD2PDF_SKILL_DIR=<dir>` | 说明书落点,默认 `~/.workbuddy/skills/md-to-pdf`(`~/.workbuddy` 不存在时默认不装) |
45
+
46
+ ### 在仓库里开发
47
+
48
+ ```bash
49
+ git clone https://cnb.cool/jiyeqian/md2pdf.git
50
+ cd md2pdf
51
+ npm link # 把 bin/md2pdf 软链进 PATH,指向仓库本身,改代码立即生效
52
+ ```
53
+
54
+ `npm link` 之后命令就是仓库本身,改完立即生效,不需要重装。
55
+
56
+ ### 依赖
57
+
58
+ | 依赖 | 要求 | 说明 |
59
+ | --- | --- | --- |
60
+ | Node.js | ≥ 18(建议 ≥ 22) | < 22 时自动启用内置 WebSocket 实现;`MD2PDF_NODE` 可指定 |
61
+ | Chrome / Edge / Chromium | 任一 | 只用来渲染,不联网;`MD2PDF_CHROME` 可指定路径 |
62
+
63
+ 零 npm 依赖 —— Markdown 解析器(marked)已内置在 `vendor/`,装好即用。
64
+
65
+ ## 用法
66
+
67
+ ```bash
68
+ md2pdf 文件名.md # 同目录输出同名 .pdf
69
+ md2pdf 文件名.md --open # 转完直接打开
70
+ md2pdf a.md b.md -o 输出目录/ # 批量(共用一个浏览器实例,很快)
71
+ md2pdf 文件名.md --theme minimal --toc
72
+ ```
73
+
74
+ ### 选项
75
+
76
+ | 选项 | 作用 |
77
+ | --- | --- |
78
+ | `-o, --output <path>` | 输出路径;多文件或目标是目录时,作为输出目录 |
79
+ | `--theme <name>` | `elegant`(默认,墨蓝+古铜)| `minimal`(黑白公文风) |
80
+ | `--title <text>` | 覆盖标题(默认:正文首个 H1 → frontmatter.title → 文件名) |
81
+ | `--kicker <text>` | 报头小标题;`SKILL.md` 默认显示「技能文档」 |
82
+ | `--no-meta` | 不要 frontmatter 元信息条 |
83
+ | `--no-lead` | 首段不作为导语放大 |
84
+ | `-t, --toc` | 文首插入目录页(取自 H2,需 2 个以上),每项可点击跳转 |
85
+ | `--no-outline` | 不生成 PDF 书签(**默认生成**,见下) |
86
+ | `--link-urls` | 正文链接后附 URL(纸质可读) |
87
+ | `--landscape` | 横向页面 |
88
+ | `--font-size <pt>` | 正文字号,默认 10.5 |
89
+ | `--margin <mm>` | 页边距,默认 20;可写 `"20,18"`(上下,左右) |
90
+ | `--no-footer` | 不要页脚页码 |
91
+ | `--footer-left / --footer-right <text>` | 页脚左右文字 |
92
+ | `--colophon <text>` | 文末落款(默认:来源文件名) |
93
+ | `--keep-html` | 保留中间 HTML,方便调样式 |
94
+ | `--html-only` | 只生成 HTML,不启动浏览器(调样式 / CI 校验用) |
95
+ | `--open` | 完成后打开 PDF |
96
+
97
+ 布尔选项支持 `--flag=false`。环境变量:`MD2PDF_CHROME`、`MD2PDF_NODE`、`MD2PDF_WS=mini`。
98
+
99
+ ### 目录与书签是两件事
100
+
101
+ | | 是什么 | 在哪看 | 怎么开 |
102
+ | --- | --- | --- | --- |
103
+ | **目录页** | 排在文首的一张目录,条目是**可点击的内链** | 文档第 1 页 | `-t / --toc`(默认关) |
104
+ | **PDF 书签** | PDF 阅读器侧边栏里的**章节大纲树**(可折叠、点击跳转) | 阅读器侧栏 | **默认开**,`--no-outline` 关 |
105
+
106
+ 书签由 Chrome 按 HTML 的 `h1`–`h6` 结构生成(报头标题为根,H2/H3 逐层嵌套),
107
+ 所以只要文档用了标准标题层级,就有对应的大纲,不需要额外配置。
108
+
109
+ 需要看侧栏的阅读器操作:macOS 预览需手动展开侧栏(**⌘⌥3**,或右上角侧栏按钮);
110
+ Acrobat / 福昕 / Chrome 内置阅读器点侧栏图标即可。侧栏是否自动展开由阅读器自身决定,
111
+ 本工具不写 `/PageMode`(改这个字段要重写 PDF 目录对象,收益不值那份风险)。
112
+
113
+ 自己验一份 PDF 的书签与内链:
114
+
115
+ ```bash
116
+ node ci/inspect-pdf.mjs out.pdf
117
+ ```
118
+
119
+ ## 排版规则
120
+
121
+ - 首个 H1 提升为报头大标题,正文不再重复;其后的首段自动成为导语。
122
+ - YAML frontmatter 的 `name` / `description` 生成元信息条;description 里「适用于…」「不用于…」会自动拆成「适用 / 不适用」两栏。
123
+ - H2 自动分节并加色块标记;表格深色表头+隔行浅底;有序列表用圆形序号。
124
+ - 相对路径图片自动解析成绝对地址,能正常进入 PDF。
125
+
126
+ ## 改样式
127
+
128
+ ```
129
+ assets/base.css 骨架(占位符 {{PAGE_SIZE}} {{MARGIN_*}} {{FONT_SIZE}})
130
+ assets/theme-elegant.css 墨蓝 + 古铜(默认)
131
+ assets/theme-minimal.css 黑白公文
132
+ assets/shell.html 页面骨架
133
+ ```
134
+
135
+ 改完直接重跑命令,不用重启任何东西。
136
+
137
+ ## 它是怎么工作的
138
+
139
+ ```
140
+ Markdown ──(marked)──▶ HTML ──(模板+主题 CSS)──▶ 完整 HTML
141
+ ──▶ 无头 Chrome(CDP Page.printToPDF)──▶ PDF
142
+ ```
143
+
144
+ 选 CDP 而不是 `chrome --print-to-pdf` 的原因:命令行版不支持页眉页脚模板,出不了页码。
145
+ `preferCSSPageSize: true` 让页面尺寸/边距完全由 CSS `@page` 控制。
146
+
147
+ ## 校验与 CI
148
+
149
+ ```bash
150
+ bash ci/validate.sh # 本地跑,和 CI 完全同一套检查(约几秒)
151
+ ```
152
+
153
+ 校验分四层,全部不需要浏览器:
154
+
155
+ 1. **结构**:必需文件齐全、`bin/` 与安装脚本有可执行位、关键文件确实被 git 跟踪
156
+ 2. **语法**:`sh -n`、`node --check`
157
+ 3. **一致性**:版本号(package.json ↔ src);模板占位符 ↔ 替换逻辑双向闭合;
158
+ 主题 CSS 里 `var(--x)` 全部有定义;占位符替换必须是全量的
159
+ 4. **行为**:`--help`/`--version` 冒烟;`examples/demo.md` 端到端渲染到 HTML,
160
+ 断言表格、代码块、引用、嵌套列表、目录、链接 URL、分节都在,且无占位符残留
161
+ 与 `undefined` 泄漏;目录锚点与标题 `id` 一一对应
162
+ 5. **接线**:PDF 书签这类"只存在于 PDF 里"的特性,CI 没有浏览器验不了结果,
163
+ 就退一步断言参数真的传进了 `printToPDF`、开关真的从 `main` 接到了渲染 ——
164
+ 光有 `case '--no-outline'` 不等于接到了
165
+
166
+ 最后还有一步**守卫自测**:故意破坏一份副本(塞入未定义的占位符、改错主题变量名、
167
+ 改乱版本号、把目录项退回纯文本、关掉书签参数…),断言校验确实会失败 ——
168
+ 只会"全绿"的校验等于没有校验。
169
+
170
+ CNB 云原生构建在 push / PR 时跑同一脚本;打 tag 时发布到 npm
171
+ (见 `.cnb.yml`)。
172
+
173
+ ### 发版
174
+
175
+ 改完 `src/md2pdf.mjs` 的 `VERSION` 与 `package.json` 的 `version`(校验会检查两者一致),然后:
176
+
177
+ ```bash
178
+ git tag v1.4.0 && git push origin v1.4.0
179
+ ```
180
+
181
+ 流水线会自动:校验 → `npm publish --access public`(发布到 npm,需在 CNB 项目里配置 `NPM_TOKEN` secret)。
182
+ 也可以本地手动 `npm publish`。
183
+
184
+ 注意 CNB **不允许删除 tag**,打错了只能升版本号再发一版。
185
+
186
+ ## 目录结构
187
+
188
+ ```
189
+ bin/md2pdf 启动器(解析软链、挑选 node)
190
+ src/md2pdf.mjs 主程序
191
+ src/ws.mjs Node < 22 时的极简 WebSocket 客户端
192
+ src/install-skill.mjs npm postinstall:把技能说明书装进 WorkBuddy
193
+ assets/ 样式与页面骨架
194
+ vendor/marked.esm.js 内置 Markdown 解析器
195
+ examples/demo.md 示例文档(含表格/代码/引用/嵌套列表)
196
+ ci/validate.sh 校验入口(本地与 CI 同一套)
197
+ ci/checks.mjs 一致性 + 端到端渲染断言
198
+ ci/inspect-pdf.mjs 读出 PDF 的书签树与链接注解(本地验证 outline 用)
199
+ skill/SKILL.md Agent 技能说明书(postinstall 会装到技能目录)
200
+ ```
201
+
202
+
203
+
204
+ ## 常见问题
205
+
206
+ **装到哪了 / 怎么升级** → npm 全局包在 `npm root -g` 下的 `@jiyeqian/md2pdf`,命令软链进 npm 的 bin 目录。
207
+ 升级:`npm update -g @jiyeqian/md2pdf`。
208
+
209
+ **npm 装不上** → 确认 npm registry 可达;也可以 clone 仓库后 `npm link` 本地开发。
210
+
211
+ **找不到 Chrome** → 设 `export MD2PDF_CHROME=/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome`
212
+
213
+ **Node 版本老** → 升级到 22+;不想升也能用(自动走内置 WebSocket),只是没在老版本上充分测试。
214
+
215
+ **PDF 里目录不能点击** → Chrome 打印不保留内部锚点跳转,目录是纯文本。
216
+
217
+ **想改默认字号/边距** → 直接改命令行参数;要永久生效就改 `src/md2pdf.mjs` 里 `parseArgs` 的默认值。
218
+
219
+ ## License
220
+
221
+ MIT
222
+
223
+ 第三方组件:`vendor/marked.esm.js` 来自 [marked](https://github.com/markedjs/marked)(MIT License),随仓库分发以便零依赖安装。
@@ -0,0 +1,222 @@
1
+ /* md2pdf — 基础排版(与主题无关的骨架)
2
+ 占位符:{{MARGIN_TOP}} {{MARGIN_BOTTOM}} {{MARGIN_SIDE}} {{FONT_SIZE}} */
3
+
4
+ @page {
5
+ size: {{PAGE_SIZE}};
6
+ margin: {{MARGIN_TOP}} {{MARGIN_SIDE}} {{MARGIN_BOTTOM}};
7
+ }
8
+
9
+ :root {
10
+ --doc-font-size: {{FONT_SIZE}};
11
+ }
12
+
13
+ * { box-sizing: border-box; }
14
+
15
+ html {
16
+ -webkit-print-color-adjust: exact;
17
+ print-color-adjust: exact;
18
+ }
19
+
20
+ body {
21
+ margin: 0;
22
+ background: var(--paper);
23
+ color: var(--text);
24
+ font-family: var(--font-body);
25
+ font-size: var(--doc-font-size);
26
+ line-height: 1.9;
27
+ /* 中文正文不加字间距:否则 PDF 复制/检索时会被拆成单字 */
28
+ letter-spacing: 0;
29
+ }
30
+
31
+ /* ---------- 报头 ---------- */
32
+ .masthead {
33
+ padding-bottom: 14px;
34
+ border-bottom: var(--masthead-border);
35
+ }
36
+
37
+ .kicker {
38
+ font-size: 8pt;
39
+ letter-spacing: .28em;
40
+ text-transform: uppercase;
41
+ color: var(--accent);
42
+ font-weight: 600;
43
+ margin-bottom: 10px;
44
+ }
45
+ .kicker:empty { display: none; }
46
+
47
+ h1 {
48
+ font-family: var(--font-head);
49
+ font-size: 2.5em;
50
+ font-weight: 700;
51
+ color: var(--ink);
52
+ margin: 0 0 12px;
53
+ line-height: 1.25;
54
+ letter-spacing: 0;
55
+ }
56
+
57
+ .lead {
58
+ font-size: 1.05em;
59
+ color: var(--ink-soft);
60
+ line-height: 1.95;
61
+ margin: 0;
62
+ text-align: justify;
63
+ }
64
+
65
+ /* ---------- 元信息条 ---------- */
66
+ .meta {
67
+ display: flex;
68
+ gap: 0;
69
+ margin: 16px 0 30px;
70
+ padding: 12px 16px;
71
+ background: var(--tint);
72
+ border-left: 3px solid var(--accent);
73
+ }
74
+ .meta:empty { display: none; }
75
+ .meta-item { flex: 1; padding-right: 16px; }
76
+ .meta-item + .meta-item { border-left: 1px solid var(--rule); padding-left: 16px; }
77
+ .meta-label {
78
+ display: block;
79
+ font-size: 7.5pt;
80
+ letter-spacing: .18em;
81
+ color: var(--accent);
82
+ font-weight: 600;
83
+ margin-bottom: 4px;
84
+ }
85
+ .meta-value { font-size: 9pt; color: var(--text); line-height: 1.7; }
86
+
87
+ /* ---------- 目录 ---------- */
88
+ .toc {
89
+ margin: 0 0 28px;
90
+ padding: 14px 18px;
91
+ background: var(--tint);
92
+ border: 1px solid var(--rule);
93
+ }
94
+ .toc-title {
95
+ font-size: 7.5pt; letter-spacing: .2em; color: var(--accent);
96
+ font-weight: 600; margin-bottom: 8px;
97
+ }
98
+ .toc ol { margin: 0; padding-left: 18px; counter-reset: toc; list-style: none; }
99
+ .toc li { position: relative; margin: 3px 0; font-size: 9.5pt; }
100
+ .toc li::before {
101
+ counter-increment: toc; content: counter(toc,decimal-leading-zero);
102
+ color: var(--accent); font-family: var(--font-mono);
103
+ font-size: 8.5pt; margin-right: 8px;
104
+ }
105
+ .toc a { color: var(--ink); text-decoration: none; border: none; }
106
+
107
+ /* ---------- 章节 ---------- */
108
+ section { margin-bottom: 26px; }
109
+ section:last-of-type { margin-bottom: 8px; }
110
+
111
+ h2 {
112
+ font-family: var(--font-head);
113
+ font-size: 1.38em;
114
+ font-weight: 700;
115
+ color: var(--ink);
116
+ margin: 0 0 14px;
117
+ padding: 2px 0 8px 14px;
118
+ border-bottom: 1px solid var(--rule);
119
+ position: relative;
120
+ line-height: 1.5;
121
+ }
122
+ h2::before {
123
+ content: ""; position: absolute; left: 0; top: 6px; bottom: 10px;
124
+ width: 4px; background: var(--accent);
125
+ }
126
+
127
+ h3 {
128
+ font-family: var(--font-head);
129
+ font-size: 1.12em; font-weight: 700; color: var(--ink);
130
+ margin: 22px 0 10px; line-height: 1.5;
131
+ }
132
+ h4 {
133
+ font-size: 1em; font-weight: 700; color: var(--ink-soft);
134
+ margin: 16px 0 8px;
135
+ }
136
+
137
+ p { margin: 0 0 12px; text-align: justify; }
138
+ p:last-child { margin-bottom: 0; }
139
+
140
+ /* ---------- 表格 ---------- */
141
+ table {
142
+ width: 100%; border-collapse: collapse;
143
+ margin: 4px 0 16px; font-size: .92em;
144
+ }
145
+ thead th {
146
+ text-align: left; padding: 8px 10px;
147
+ font-weight: 600; letter-spacing: .05em; font-size: .95em;
148
+ }
149
+ tbody td {
150
+ padding: 9px 10px; border-bottom: 1px solid var(--rule);
151
+ vertical-align: top; line-height: 1.75; text-align: justify;
152
+ }
153
+ tbody td:first-child { font-weight: 600; color: var(--ink); }
154
+
155
+ /* ---------- 列表 ---------- */
156
+ ol, ul { margin: 0 0 14px; padding-left: 0; }
157
+ 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 {
161
+ counter-increment: step; content: counter(step);
162
+ position: absolute; left: 0; top: .28em;
163
+ width: 1.55em; height: 1.55em; line-height: 1.55em; text-align: center;
164
+ font-family: var(--font-mono); font-size: .82em; font-weight: 700;
165
+ }
166
+ 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 {
170
+ content: ""; position: absolute; left: 2px; top: .82em;
171
+ width: 5px; height: 5px; border-radius: 50%;
172
+ }
173
+ li > ul, li > ol { margin: 8px 0 0; }
174
+ li p { margin-bottom: 6px; }
175
+
176
+ /* ---------- 行内 ---------- */
177
+ strong { color: var(--ink); font-weight: 600; }
178
+ em { font-style: normal; color: var(--ink-soft); }
179
+ code {
180
+ font-family: var(--font-mono); font-size: .88em;
181
+ background: var(--tint); color: var(--ink);
182
+ padding: 1.5px 5px; border-radius: 3px; border: 1px solid var(--rule);
183
+ }
184
+ a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--accent-soft); }
185
+ a.ref { border-bottom: 1px dotted var(--accent); word-break: break-all; }
186
+ .link-url { font-family: var(--font-mono); font-size: .75em; color: var(--muted); }
187
+
188
+ /* ---------- 代码块 / 引用 / 分隔线 ---------- */
189
+ pre {
190
+ background: var(--code-bg); color: var(--code-fg);
191
+ border: 1px solid var(--rule); border-radius: 4px;
192
+ padding: 12px 14px; margin: 0 0 14px;
193
+ overflow: hidden; white-space: pre-wrap; word-break: break-word;
194
+ }
195
+ pre code {
196
+ background: transparent; border: none; padding: 0;
197
+ color: inherit; font-size: .85em; line-height: 1.7;
198
+ }
199
+ blockquote {
200
+ margin: 0 0 14px; padding: 10px 14px;
201
+ background: var(--tint); border-left: 3px solid var(--accent-soft);
202
+ color: var(--ink-soft); font-size: .95em; text-align: justify;
203
+ }
204
+ blockquote p:last-child { margin-bottom: 0; }
205
+ hr {
206
+ border: none; border-top: 1px solid var(--rule);
207
+ margin: 24px 0;
208
+ }
209
+ img { max-width: 100%; height: auto; display: block; margin: 8px auto 14px; }
210
+
211
+ /* ---------- 文末 ---------- */
212
+ .colophon {
213
+ margin-top: 32px; padding-top: 12px; border-top: 1px solid var(--rule);
214
+ display: flex; justify-content: space-between;
215
+ font-size: 8pt; color: var(--muted); letter-spacing: .08em;
216
+ }
217
+ .colophon:empty { display: none; }
218
+
219
+ /* ---------- 分页控制 ---------- */
220
+ h1, h2, h3, h4, thead { break-after: avoid; }
221
+ tr, li, pre, blockquote, img { break-inside: avoid; }
222
+ section { break-inside: auto; }
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>{{TITLE}}</title>
6
+ <style>
7
+ {{CSS}}
8
+ </style>
9
+ </head>
10
+ <body>
11
+
12
+ <header class="masthead">
13
+ <div class="kicker">{{KICKER}}</div>
14
+ <h1>{{TITLE}}</h1>
15
+ <p class="lead">{{LEAD}}</p>
16
+ </header>
17
+
18
+ {{META}}
19
+ {{TOC}}
20
+
21
+ <main>
22
+ {{BODY}}
23
+ </main>
24
+
25
+ <div class="colophon">
26
+ <span>{{COLOPHON_LEFT}}</span>
27
+ <span>{{COLOPHON_RIGHT}}</span>
28
+ </div>
29
+
30
+ </body>
31
+ </html>
@@ -0,0 +1,32 @@
1
+ /* 主题:elegant(默认)—— 墨蓝 + 古铜,带底色块 */
2
+ :root {
3
+ --ink: #1b2a41;
4
+ --ink-soft: #33465f;
5
+ --accent: #8a6d3b;
6
+ --accent-soft: #c9b48f;
7
+ --paper: #ffffff;
8
+ --tint: #f6f4ef;
9
+ --rule: #e4e0d7;
10
+ --text: #262626;
11
+ --muted: #8a8578;
12
+
13
+ --code-bg: #f8f7f3;
14
+ --code-fg: #26303d;
15
+
16
+ --font-body: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
17
+ --font-head: "Songti SC", "Source Han Serif SC", "STSong", serif;
18
+ --font-mono: "SF Mono", "Menlo", "Consolas", monospace;
19
+
20
+ --masthead-border: 2.5px solid var(--ink);
21
+ }
22
+
23
+ thead th { background: var(--ink); color: #fff; border: none; }
24
+ tbody tr:nth-child(even) { background: #fbfaf7; }
25
+
26
+ ol li::before {
27
+ background: var(--tint);
28
+ border: 1px solid var(--accent-soft);
29
+ border-radius: 50%;
30
+ color: var(--accent);
31
+ }
32
+ ul li::before { background: var(--accent-soft); }
@@ -0,0 +1,40 @@
1
+ /* 主题:minimal —— 纯黑白灰,无底色块,适合正式公文/报告 */
2
+ :root {
3
+ --ink: #141414;
4
+ --ink-soft: #3d3d3d;
5
+ --accent: #555555;
6
+ --accent-soft: #b8b8b8;
7
+ --paper: #ffffff;
8
+ --tint: #ffffff;
9
+ --rule: #dcdcdc;
10
+ --text: #1f1f1f;
11
+ --muted: #8c8c8c;
12
+
13
+ --code-bg: #f6f6f6;
14
+ --code-fg: #23272e;
15
+
16
+ --font-body: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
17
+ --font-head: "PingFang SC", "Hiragino Sans GB", sans-serif;
18
+ --font-mono: "SF Mono", "Menlo", "Consolas", monospace;
19
+
20
+ --masthead-border: 1px solid #141414;
21
+ }
22
+
23
+ .kicker { color: var(--muted); }
24
+ h2::before { width: 3px; background: #141414; }
25
+
26
+ .meta { border-left: 2px solid #141414; background: #fff; padding-left: 14px; }
27
+ .toc { background: #fff; }
28
+ blockquote { background: #fff; border-left: 2px solid #141414; }
29
+
30
+ thead th {
31
+ background: #fff; color: var(--ink);
32
+ border-top: 1px solid #141414; border-bottom: 1px solid #141414;
33
+ }
34
+ tbody tr:nth-child(even) { background: #fafafa; }
35
+
36
+ ol li::before {
37
+ background: #fff; border: 1px solid #b8b8b8; border-radius: 0;
38
+ color: var(--ink);
39
+ }
40
+ ul li::before { background: #8c8c8c; border-radius: 0; }
package/bin/md2pdf ADDED
@@ -0,0 +1,49 @@
1
+ #!/bin/sh
2
+ # md2pdf —— Markdown → 优雅 PDF
3
+ # 用法见:md2pdf --help
4
+ #
5
+ # 通过软链接调用也能正确定位项目根目录。
6
+ # 环境变量 MD2PDF_NODE 可强制指定 node 可执行文件。
7
+
8
+ set -e
9
+
10
+ # 解析软链接,找到真实脚本所在目录
11
+ SOURCE="$0"
12
+ while [ -L "$SOURCE" ]; do
13
+ DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
14
+ SOURCE="$(readlink "$SOURCE")"
15
+ case "$SOURCE" in
16
+ /*) ;;
17
+ *) SOURCE="$DIR/$SOURCE" ;;
18
+ esac
19
+ done
20
+ ROOT="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
21
+
22
+ # 选一个 node:优先 >= 22(有全局 WebSocket);找不到就用任意可用版本
23
+ node_major() {
24
+ "$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0
25
+ }
26
+
27
+ pick_node() {
28
+ if [ -n "$MD2PDF_NODE" ] && [ -x "$MD2PDF_NODE" ]; then echo "$MD2PDF_NODE"; return; fi
29
+ candidates="$HOME/.workbuddy/binaries/node/versions/22.22.2-3/bin/node
30
+ /usr/local/bin/node
31
+ /opt/homebrew/bin/node
32
+ /usr/bin/node
33
+ $(command -v node 2>/dev/null)"
34
+ for c in $candidates; do
35
+ [ -x "$c" ] || continue
36
+ if [ "$(node_major "$c")" -ge 22 ] 2>/dev/null; then echo "$c"; return; fi
37
+ done
38
+ for c in $candidates; do
39
+ [ -x "$c" ] && { echo "$c"; return; }
40
+ done
41
+ }
42
+
43
+ NODE="$(pick_node)"
44
+ if [ -z "$NODE" ]; then
45
+ echo "md2pdf: 未找到 Node.js(建议 >= 22)。可设置 MD2PDF_NODE=/path/to/node" >&2
46
+ exit 1
47
+ fi
48
+
49
+ exec "$NODE" "$ROOT/src/md2pdf.mjs" "$@"
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@jiyeqian/md2pdf",
3
+ "version": "1.4.0",
4
+ "description": "把 Markdown 排成优雅的中文 A4 PDF(无头 Chrome 渲染,带报头、表格/代码排版与页脚页码)",
5
+ "type": "module",
6
+ "bin": {
7
+ "md2pdf": "bin/md2pdf"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "assets",
13
+ "vendor",
14
+ "skill",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "scripts": {
22
+ "postinstall": "node src/install-skill.mjs",
23
+ "validate": "bash ci/validate.sh",
24
+ "test": "bash ci/validate.sh",
25
+ "demo:pdf": "node src/md2pdf.mjs examples/demo.md -o /tmp/md2pdf-demo.pdf"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "keywords": [
31
+ "markdown",
32
+ "pdf",
33
+ "cli",
34
+ "chinese",
35
+ "typography"
36
+ ],
37
+ "license": "MIT"
38
+ }