@wenyan-md/cli 1.0.9 → 1.0.10
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.docker.md +147 -0
- package/README.md +119 -88
- package/dist/cli.js +4 -0
- package/dist/commands/publish.js +9 -41
- package/dist/commands/render.js +58 -21
- package/dist/commands/theme.js +78 -0
- package/dist/index.js +39 -34
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/commands/publish.d.ts +1 -8
- package/dist/types/commands/render.d.ts +6 -8
- package/dist/types/commands/theme.d.ts +2 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/types.d.ts +15 -0
- package/dist/types.js +1 -0
- package/package.json +17 -11
- package/bin/cli.js +0 -9
package/README.docker.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img alt="logo" src="https://raw.githubusercontent.com/caol64/wenyan/main/Data/256-mac.png" width="120" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
# wenyan-cli (Docker)
|
|
6
|
+
|
|
7
|
+
**Render Markdown to beautifully styled articles and publish to content platforms — powered by Docker.**
|
|
8
|
+
|
|
9
|
+
> This image bundles **wenyan CLI** and all required runtime dependencies.
|
|
10
|
+
> No local Node.js or npm environment required.
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
### Pull image
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
docker pull caol64/wenyan-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Show help
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
docker run --rm caol64/wenyan-cli --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Basic Usage
|
|
27
|
+
|
|
28
|
+
Render and publish a Markdown file to WeChat Official Account draft box:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
docker run --rm \
|
|
32
|
+
--env-file .env \
|
|
33
|
+
-e HOST_FILE_PATH=$(pwd) \
|
|
34
|
+
-v $(pwd):/mnt/host-downloads \
|
|
35
|
+
caol64/wenyan-cli \
|
|
36
|
+
publish -f ./article.md
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Render Markdown content directly:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
docker run --rm caol64/wenyan-cli \
|
|
43
|
+
render "# Hello Wenyan"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Working with Local Files (Recommended)
|
|
47
|
+
|
|
48
|
+
When using local Markdown or image files, mount the current directory:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
docker run --rm \
|
|
52
|
+
-e HOST_FILE_PATH=$(pwd) \
|
|
53
|
+
-v $(pwd):/mnt/host-downloads \
|
|
54
|
+
caol64/wenyan-cli \
|
|
55
|
+
publish -f ./example.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**How it works:**
|
|
59
|
+
|
|
60
|
+
| Path | Description |
|
|
61
|
+
| --------------------- | ----------------------------- |
|
|
62
|
+
| `HOST_FILE_PATH` | Absolute path on host machine |
|
|
63
|
+
| `/mnt/host-downloads` | Mounted path inside container |
|
|
64
|
+
|
|
65
|
+
All file paths in Markdown (cover / images) should reference host paths.
|
|
66
|
+
|
|
67
|
+
## Input Methods
|
|
68
|
+
|
|
69
|
+
`publish` supports **exactly one** input source:
|
|
70
|
+
|
|
71
|
+
- `-f <file>` — read Markdown from local file
|
|
72
|
+
- `<input-content>` — inline Markdown string
|
|
73
|
+
- `stdin` — pipe from another command
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cat article.md | docker run --rm -i caol64/wenyan-cli render
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
docker run --rm caol64/wenyan-cli render "# Title"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Options
|
|
86
|
+
|
|
87
|
+
Commonly used options:
|
|
88
|
+
|
|
89
|
+
- `-t, --theme` — theme ID (default: `default`)
|
|
90
|
+
- `-h, --highlight` — code highlight theme
|
|
91
|
+
- `--no-mac-style` — disable macOS-style code blocks
|
|
92
|
+
- `--no-footnote` — disable link-to-footnote conversion
|
|
93
|
+
|
|
94
|
+
## Markdown Frontmatter (Required)
|
|
95
|
+
|
|
96
|
+
Each Markdown file must include frontmatter:
|
|
97
|
+
|
|
98
|
+
```md
|
|
99
|
+
---
|
|
100
|
+
title: My Article Title
|
|
101
|
+
cover: /absolute/path/to/cover.jpg
|
|
102
|
+
---
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `title` — article title (required)
|
|
106
|
+
- `cover` — optional cover image (local or remote)
|
|
107
|
+
|
|
108
|
+
## Environment Variables
|
|
109
|
+
|
|
110
|
+
Publishing to WeChat requires:
|
|
111
|
+
|
|
112
|
+
- `WECHAT_APP_ID`
|
|
113
|
+
- `WECHAT_APP_SECRET`
|
|
114
|
+
|
|
115
|
+
Recommended usage with `.env` file:
|
|
116
|
+
|
|
117
|
+
```env
|
|
118
|
+
WECHAT_APP_ID=xxx
|
|
119
|
+
WECHAT_APP_SECRET=yyy
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Image Details
|
|
123
|
+
|
|
124
|
+
- Entrypoint: `wenyan`
|
|
125
|
+
- Runtime: Node.js (bundled)
|
|
126
|
+
- Architecture: `linux/amd64`, `linux/arm64`
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
Apache License Version 2.0
|
|
131
|
+
|
|
132
|
+
### Tip
|
|
133
|
+
|
|
134
|
+
For frequent usage, create an alias:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
alias wenyan='docker run --rm \
|
|
138
|
+
-e HOST_FILE_PATH=$(pwd) \
|
|
139
|
+
-v $(pwd):/mnt/host-downloads \
|
|
140
|
+
caol64/wenyan-cli'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then use it like a native CLI:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
wenyan publish -f article.md
|
|
147
|
+
```
|
package/README.md
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/@wenyan-md/cli)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|

|
|
10
|
+
[](https://hub.docker.com/r/caol64/wenyan-cli)
|
|
10
11
|
[](https://github.com/caol64/wenyan-cli)
|
|
11
12
|
|
|
12
13
|
## 简介
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
**[文颜(Wenyan)](https://wenyan.yuzhi.tech)** 是一款多平台 Markdown 排版与发布工具,支持将 Markdown 一键转换并发布至:
|
|
15
16
|
|
|
16
17
|
- 微信公众号
|
|
17
18
|
- 知乎
|
|
@@ -20,13 +21,6 @@
|
|
|
20
21
|
|
|
21
22
|
文颜的目标是:**让写作者专注内容,而不是排版和平台适配**。
|
|
22
23
|
|
|
23
|
-
本仓库是 **文颜的 CLI 版本**,适合以下场景:
|
|
24
|
-
|
|
25
|
-
- 命令行使用
|
|
26
|
-
- CI / CD 自动化发布
|
|
27
|
-
- 脚本或工具链集成
|
|
28
|
-
- 与 AI / MCP 系统联动自动发文
|
|
29
|
-
|
|
30
24
|
## 文颜的不同版本
|
|
31
25
|
|
|
32
26
|
文颜目前提供多种形态,覆盖不同使用场景:
|
|
@@ -37,28 +31,6 @@
|
|
|
37
31
|
- [MCP 版本](https://github.com/caol64/wenyan-mcp) - AI 自动发文
|
|
38
32
|
- [核心库](https://github.com/caol64/wenyan-core) - 嵌入 Node / Web 项目
|
|
39
33
|
|
|
40
|
-
## 功能特性
|
|
41
|
-
|
|
42
|
-
- 使用内置主题对 Markdown 内容排版
|
|
43
|
-
- 自动处理并上传图片(本地 / 网络)
|
|
44
|
-
- 支持数学公式(MathJax)
|
|
45
|
-
- 一键发布文章到微信公众号草稿箱
|
|
46
|
-
- 支持 CI / 自动化流程调用
|
|
47
|
-
|
|
48
|
-
## 主题效果预览
|
|
49
|
-
|
|
50
|
-
👉 [内置主题预览](https://yuzhi.tech/docs/wenyan/theme)
|
|
51
|
-
|
|
52
|
-
文颜内置并适配了多个优秀的 Typora 主题,在此感谢原作者:
|
|
53
|
-
|
|
54
|
-
- [Orange Heart](https://github.com/evgo2017/typora-theme-orange-heart)
|
|
55
|
-
- [Rainbow](https://github.com/thezbm/typora-theme-rainbow)
|
|
56
|
-
- [Lapis](https://github.com/YiNNx/typora-theme-lapis)
|
|
57
|
-
- [Pie](https://github.com/kevinzhao2233/typora-theme-pie)
|
|
58
|
-
- [Maize](https://github.com/BEATREE/typora-maize-theme)
|
|
59
|
-
- [Purple](https://github.com/hliu202/typora-purple-theme)
|
|
60
|
-
- [物理猫-薄荷](https://github.com/sumruler/typora-theme-phycat)
|
|
61
|
-
|
|
62
34
|
## 安装方式
|
|
63
35
|
|
|
64
36
|
### 方式一:npm(推荐)
|
|
@@ -114,51 +86,18 @@ CLI 主命令:
|
|
|
114
86
|
wenyan <command> [options]
|
|
115
87
|
```
|
|
116
88
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
部分功能(如发布微信公众号)需要配置以下环境变量:
|
|
122
|
-
|
|
123
|
-
- `WECHAT_APP_ID`
|
|
124
|
-
- `WECHAT_APP_SECRET`
|
|
125
|
-
|
|
126
|
-
### macOS / Linux
|
|
127
|
-
|
|
128
|
-
临时使用:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
WECHAT_APP_ID=xxx WECHAT_APP_SECRET=yyy wenyan publish "your markdown"
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
永久配置(推荐):
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
export WECHAT_APP_ID=xxx
|
|
138
|
-
export WECHAT_APP_SECRET=yyy
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Windows (PowerShell)
|
|
142
|
-
|
|
143
|
-
临时使用:
|
|
144
|
-
|
|
145
|
-
```powershell
|
|
146
|
-
$env:WECHAT_APP_ID="xxx"
|
|
147
|
-
$env:WECHAT_APP_SECRET="yyy"
|
|
148
|
-
wenyan publish example.md
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
永久设置(在环境变量里添加):
|
|
152
|
-
|
|
153
|
-
控制面板 → 系统和安全 → 系统 → 高级系统设置 → 环境变量 → 添加 `WECHAT_APP_ID` 和 `WECHAT_APP_SECRET`。
|
|
89
|
+
目前支持的子命令有
|
|
90
|
+
- `publish` 排版并发布到公众号草稿箱
|
|
91
|
+
- `render` 仅排版,用做测试
|
|
92
|
+
- `theme` 主题管理
|
|
154
93
|
|
|
155
94
|
## 子命令
|
|
156
95
|
|
|
157
|
-
`publish`
|
|
96
|
+
### `publish`
|
|
158
97
|
|
|
159
98
|
将 Markdown 转换为适配微信公众号的富文本 HTML,并上传到草稿箱。
|
|
160
99
|
|
|
161
|
-
|
|
100
|
+
#### 参数
|
|
162
101
|
|
|
163
102
|
- `<input-content>`
|
|
164
103
|
|
|
@@ -167,17 +106,18 @@ wenyan publish example.md
|
|
|
167
106
|
- 直接作为参数传入
|
|
168
107
|
- 通过 stdin 管道输入
|
|
169
108
|
|
|
170
|
-
|
|
109
|
+
#### 常用选项
|
|
171
110
|
|
|
172
|
-
- `-t
|
|
173
|
-
-
|
|
174
|
-
- `-h
|
|
111
|
+
- `-t <theme-id>`:主题id(默认 `default`),可以是内置主题,也可以是通过`theme --add`添加的自定义主题
|
|
112
|
+
- [内置主题](https://github.com/caol64/wenyan-core/tree/main/src/assets/themes)
|
|
113
|
+
- `-h <highlight-theme-id>`:代码高亮主题(默认 `solarized-light`)
|
|
175
114
|
- atom-one-dark / atom-one-light / dracula / github-dark / github / monokai / solarized-dark / solarized-light / xcode
|
|
176
115
|
- `--no-mac-style`:关闭代码块 Mac 风格
|
|
177
116
|
- `--no-footnote`:关闭链接转脚注
|
|
178
|
-
- `-f
|
|
117
|
+
- `-f <path>`:指定本地 Markdown 文件路径
|
|
118
|
+
- `-c <path>`:指定临时自定义主题路径,优先级大于`-t`
|
|
179
119
|
|
|
180
|
-
|
|
120
|
+
#### 使用示例
|
|
181
121
|
|
|
182
122
|
直接传入内容:
|
|
183
123
|
|
|
@@ -197,23 +137,60 @@ cat example.md | wenyan publish -t lapis -h solarized-light --no-mac-style
|
|
|
197
137
|
wenyan publish -f "./example.md" -t lapis -h solarized-light --no-mac-style
|
|
198
138
|
```
|
|
199
139
|
|
|
200
|
-
|
|
140
|
+
### `theme`
|
|
201
141
|
|
|
202
|
-
|
|
142
|
+
主题管理,浏览内置主题、添加/删除自定义主题。
|
|
203
143
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
144
|
+
#### 参数
|
|
145
|
+
|
|
146
|
+
无。
|
|
147
|
+
|
|
148
|
+
#### 常用选项
|
|
149
|
+
|
|
150
|
+
- `-l`:列出所有可用主题
|
|
151
|
+
- `--add`:添加自定义主题(永久)
|
|
152
|
+
- `--name <name>`:主题名称
|
|
153
|
+
- `--path <path>`:主题路径(本地或网络)
|
|
154
|
+
- `--rm <name>`:删除自定义主题
|
|
155
|
+
|
|
156
|
+
#### 使用示例
|
|
157
|
+
|
|
158
|
+
列出可用主题:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
wenyan theme -l
|
|
209
162
|
```
|
|
210
163
|
|
|
211
|
-
|
|
164
|
+
安装自定义主题
|
|
212
165
|
|
|
213
|
-
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
|
|
166
|
+
```bash
|
|
167
|
+
wenyan theme --add --name new-theme --path https://wenyan.yuzhi.tech/manhua.css
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
删除自定义主题
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
wenyan theme --rm new-theme
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 使用自定义主题
|
|
177
|
+
|
|
178
|
+
你可以通过两种途径使用自定义主题:
|
|
179
|
+
|
|
180
|
+
- 不安装直接使用
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
wenyan publish -f "./example.md" -c "/path/to/theme" -h solarized-light --no-mac-style
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- 先安装再使用:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
wenyan theme --add --name new-theme --path https://wenyan.yuzhi.tech/manhua.css
|
|
190
|
+
wenyan publish -f "./example.md" -t new-theme -h solarized-light --no-mac-style
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
区别在于,安装后的主题永久有效。
|
|
217
194
|
|
|
218
195
|
## 关于图片自动上传
|
|
219
196
|
|
|
@@ -222,14 +199,68 @@ cover: /Users/xxx/image.jpg
|
|
|
222
199
|
- 本地路径(如:`/Users/xxx/image.jpg`)
|
|
223
200
|
- 网络路径(如:`https://example.com/image.jpg`)
|
|
224
201
|
|
|
202
|
+
## 环境变量配置
|
|
203
|
+
|
|
204
|
+
部分功能(如发布微信公众号)需要配置以下环境变量:
|
|
205
|
+
|
|
206
|
+
- `WECHAT_APP_ID`
|
|
207
|
+
- `WECHAT_APP_SECRET`
|
|
208
|
+
|
|
209
|
+
### macOS / Linux
|
|
210
|
+
|
|
211
|
+
临时使用:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
WECHAT_APP_ID=xxx WECHAT_APP_SECRET=yyy wenyan publish "your markdown"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
永久配置(推荐):
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
export WECHAT_APP_ID=xxx
|
|
221
|
+
export WECHAT_APP_SECRET=yyy
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Windows (PowerShell)
|
|
225
|
+
|
|
226
|
+
临时使用:
|
|
227
|
+
|
|
228
|
+
```powershell
|
|
229
|
+
$env:WECHAT_APP_ID="xxx"
|
|
230
|
+
$env:WECHAT_APP_SECRET="yyy"
|
|
231
|
+
wenyan publish example.md
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
永久设置(在环境变量里添加):
|
|
235
|
+
|
|
236
|
+
控制面板 → 系统和安全 → 系统 → 高级系统设置 → 环境变量 → 添加 `WECHAT_APP_ID` 和 `WECHAT_APP_SECRET`。
|
|
237
|
+
|
|
225
238
|
## 微信公众号 IP 白名单
|
|
226
239
|
|
|
227
|
-
>
|
|
240
|
+
> [!IMPORTANT]
|
|
228
241
|
>
|
|
229
242
|
> 请确保运行文颜的机器 IP 已加入微信公众号后台的 IP 白名单,否则上传接口将调用失败。
|
|
230
243
|
|
|
231
244
|
配置说明文档:[https://yuzhi.tech/docs/wenyan/upload](https://yuzhi.tech/docs/wenyan/upload)
|
|
232
245
|
|
|
246
|
+
## Markdown Frontmatter 说明(必读)
|
|
247
|
+
|
|
248
|
+
为了正确上传文章,每篇 Markdown 顶部需要包含 frontmatter:
|
|
249
|
+
|
|
250
|
+
```md
|
|
251
|
+
---
|
|
252
|
+
title: 在本地跑一个大语言模型(2) - 给模型提供外部知识库
|
|
253
|
+
cover: /Users/xxx/image.jpg
|
|
254
|
+
---
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
字段说明:
|
|
258
|
+
|
|
259
|
+
- `title` 文章标题(必填)
|
|
260
|
+
- `cover` 文章封面
|
|
261
|
+
- 本地路径或网络图片
|
|
262
|
+
- 如果正文中已有图片,可省略
|
|
263
|
+
|
|
233
264
|
## 示例文章格式
|
|
234
265
|
|
|
235
266
|
```md
|
package/dist/cli.js
ADDED
package/dist/commands/publish.js
CHANGED
|
@@ -1,35 +1,12 @@
|
|
|
1
|
-
import { getGzhContent } from "@wenyan-md/core/wrapper";
|
|
2
1
|
import { publishToDraft } from "@wenyan-md/core/publish";
|
|
3
|
-
import {
|
|
4
|
-
import fs from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
2
|
+
import { prepareRenderContext, runCommandWrapper } from "./render.js";
|
|
6
3
|
export async function publishCommand(inputContent, options) {
|
|
7
|
-
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
if (!inputContent && file) {
|
|
16
|
-
const normalizePath = getNormalizeFilePath(file);
|
|
17
|
-
inputContent = await fs.readFile(normalizePath, "utf-8");
|
|
18
|
-
absoluteDirPath = path.dirname(normalizePath);
|
|
19
|
-
}
|
|
20
|
-
if (!inputContent) {
|
|
21
|
-
console.error("Error: missing input-content (no argument, no stdin, and no file).");
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
const gzhContent = await getGzhContent(inputContent, options["theme"], options["highlight"], options["macStyle"], options["footnote"]);
|
|
25
|
-
if (!gzhContent.title) {
|
|
26
|
-
console.error("未能找到文章标题");
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
if (!gzhContent.cover) {
|
|
30
|
-
console.error("未能找到文章封面");
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
4
|
+
await runCommandWrapper(async () => {
|
|
5
|
+
const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options);
|
|
6
|
+
if (!gzhContent.title)
|
|
7
|
+
throw new Error("Error: 未能找到文章标题");
|
|
8
|
+
if (!gzhContent.cover)
|
|
9
|
+
throw new Error("Error: 未能找到文章封面");
|
|
33
10
|
const data = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover, {
|
|
34
11
|
relativePath: absoluteDirPath,
|
|
35
12
|
});
|
|
@@ -38,16 +15,7 @@ export async function publishCommand(inputContent, options) {
|
|
|
38
15
|
}
|
|
39
16
|
else {
|
|
40
17
|
console.error(`上传失败,\n${data}`);
|
|
18
|
+
process.exit(1);
|
|
41
19
|
}
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
if (error instanceof Error) {
|
|
45
|
-
console.error("An unexpected error occurred during publishing:");
|
|
46
|
-
console.error(error.message);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
console.error("An unexpected error occurred:", error);
|
|
50
|
-
}
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
20
|
+
});
|
|
53
21
|
}
|
package/dist/commands/render.js
CHANGED
|
@@ -1,30 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { configStore, renderStyledContent } from "@wenyan-md/core/wrapper";
|
|
2
2
|
import { getNormalizeFilePath, readStdin } from "../utils.js";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
// --- 处理输入源、文件路径和主题 ---
|
|
6
|
+
export async function prepareRenderContext(inputContent, options) {
|
|
7
|
+
const { file, theme, customTheme, highlight, macStyle, footnote } = options;
|
|
8
|
+
let absoluteDirPath = undefined;
|
|
9
|
+
// 1. 尝试从 Stdin 读取
|
|
10
|
+
if (!inputContent && !process.stdin.isTTY) {
|
|
11
|
+
inputContent = await readStdin();
|
|
12
|
+
}
|
|
13
|
+
// 2. 尝试从文件读取
|
|
14
|
+
if (!inputContent && file) {
|
|
15
|
+
const normalizePath = getNormalizeFilePath(file);
|
|
16
|
+
inputContent = await fs.readFile(normalizePath, "utf-8");
|
|
17
|
+
absoluteDirPath = path.dirname(normalizePath);
|
|
18
|
+
}
|
|
19
|
+
// 3. 校验输入
|
|
20
|
+
if (!inputContent) {
|
|
21
|
+
throw new Error("Error: missing input-content (no argument, no stdin, and no file).");
|
|
22
|
+
}
|
|
23
|
+
let handledCustomTheme = customTheme;
|
|
24
|
+
// 4. 当用户传入自定义主题路径时,优先级最高
|
|
25
|
+
if (customTheme) {
|
|
26
|
+
const normalizePath = getNormalizeFilePath(customTheme);
|
|
27
|
+
handledCustomTheme = await fs.readFile(normalizePath, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
else if (theme) {
|
|
30
|
+
// 否则尝试读取配置中的自定义主题
|
|
31
|
+
handledCustomTheme = configStore.getThemeById(theme);
|
|
32
|
+
}
|
|
33
|
+
if (!handledCustomTheme && !theme) {
|
|
34
|
+
throw new Error(`Error: theme "${theme}" not found.`);
|
|
35
|
+
}
|
|
36
|
+
// 5. 执行核心渲染
|
|
37
|
+
const gzhContent = await renderStyledContent(inputContent, {
|
|
38
|
+
themeId: theme,
|
|
39
|
+
hlThemeId: highlight,
|
|
40
|
+
isMacStyle: macStyle,
|
|
41
|
+
isAddFootnote: footnote,
|
|
42
|
+
themeCss: handledCustomTheme,
|
|
43
|
+
});
|
|
44
|
+
return { gzhContent, absoluteDirPath };
|
|
45
|
+
}
|
|
46
|
+
// --- 统一的错误处理包装器 ---
|
|
47
|
+
export async function runCommandWrapper(action) {
|
|
5
48
|
try {
|
|
6
|
-
|
|
7
|
-
if (!inputContent) {
|
|
8
|
-
if (!process.stdin.isTTY) {
|
|
9
|
-
inputContent = await readStdin();
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
if (!inputContent && file) {
|
|
13
|
-
const normalizePath = getNormalizeFilePath(file);
|
|
14
|
-
inputContent = await fs.readFile(normalizePath, "utf-8");
|
|
15
|
-
}
|
|
16
|
-
if (!inputContent) {
|
|
17
|
-
console.error("Error: missing input-content (no argument, no stdin, and no file).");
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
const gzhContent = await getGzhContent(inputContent, options["theme"], options["highlight"], options["macStyle"], options["footnote"]);
|
|
21
|
-
console.log(gzhContent.content);
|
|
22
|
-
// process.exit(0);
|
|
49
|
+
await action();
|
|
23
50
|
}
|
|
24
51
|
catch (error) {
|
|
25
52
|
if (error instanceof Error) {
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
if (error.message.startsWith("Error:")) {
|
|
54
|
+
console.error(error.message);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.error("An unexpected error occurred:", error.message);
|
|
58
|
+
}
|
|
28
59
|
}
|
|
29
60
|
else {
|
|
30
61
|
console.error("An unexpected error occurred:", error);
|
|
@@ -32,3 +63,9 @@ export async function renderCommand(inputContent, options) {
|
|
|
32
63
|
process.exit(1);
|
|
33
64
|
}
|
|
34
65
|
}
|
|
66
|
+
export async function renderCommand(inputContent, options) {
|
|
67
|
+
await runCommandWrapper(async () => {
|
|
68
|
+
const { gzhContent } = await prepareRenderContext(inputContent, options);
|
|
69
|
+
console.log(gzhContent.content);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getAllGzhThemes } from "@wenyan-md/core";
|
|
2
|
+
import { getNormalizeFilePath } from "../utils.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { configStore } from "@wenyan-md/core/wrapper";
|
|
5
|
+
export async function themeCommand(options) {
|
|
6
|
+
const { list, add, name, path, rm } = options;
|
|
7
|
+
if (list) {
|
|
8
|
+
listThemes();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (add) {
|
|
12
|
+
await addTheme(name, path);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (rm) {
|
|
16
|
+
await removeTheme(rm);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function listThemes() {
|
|
21
|
+
const themes = getAllGzhThemes();
|
|
22
|
+
console.log("\n内置主题:");
|
|
23
|
+
themes.forEach((theme) => console.log(`- ${theme.meta.id}: ${theme.meta.description}`));
|
|
24
|
+
const customThemes = configStore.getThemes();
|
|
25
|
+
if (customThemes.length > 0) {
|
|
26
|
+
console.log("\n自定义主题:");
|
|
27
|
+
customThemes.forEach((theme) => {
|
|
28
|
+
console.log(`- ${theme.id}: ${theme.description ?? ""}`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
console.log("");
|
|
32
|
+
}
|
|
33
|
+
async function addTheme(name, path) {
|
|
34
|
+
if (!name || !path) {
|
|
35
|
+
console.log("❌ 添加主题时必须提供名称(name)和路径(path)\n");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (checkThemeExists(name) || checkCustomThemeExists(name)) {
|
|
39
|
+
console.log(`❌ 主题 "${name}" 已存在\n`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (path.startsWith("http")) {
|
|
43
|
+
console.log(`⏳ 正在从远程获取主题: ${path} ...`);
|
|
44
|
+
const response = await fetch(path);
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
console.log(`❌ 无法从远程获取主题: ${response.statusText}\n`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const content = await response.text();
|
|
50
|
+
configStore.addThemeToConfig(name, content);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const normalizePath = getNormalizeFilePath(path);
|
|
54
|
+
const content = await fs.readFile(normalizePath, "utf-8");
|
|
55
|
+
configStore.addThemeToConfig(name, content);
|
|
56
|
+
}
|
|
57
|
+
console.log(`✅ 主题 "${name}" 已添加\n`);
|
|
58
|
+
}
|
|
59
|
+
async function removeTheme(name) {
|
|
60
|
+
if (checkThemeExists(name)) {
|
|
61
|
+
console.log(`❌ 默认主题 "${name}" 不能删除\n`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!checkCustomThemeExists(name)) {
|
|
65
|
+
console.log(`❌ 自定义主题 "${name}" 不存在\n`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
configStore.deleteThemeFromConfig(name);
|
|
69
|
+
console.log(`✅ 主题 "${name}" 已删除\n`);
|
|
70
|
+
}
|
|
71
|
+
function checkThemeExists(themeId) {
|
|
72
|
+
const themes = getAllGzhThemes();
|
|
73
|
+
return themes.some((theme) => theme.meta.id === themeId);
|
|
74
|
+
}
|
|
75
|
+
function checkCustomThemeExists(themeId) {
|
|
76
|
+
const customThemes = configStore.getThemes();
|
|
77
|
+
return customThemes.some((theme) => theme.id === themeId);
|
|
78
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,37 +2,42 @@ import { Command } from "commander";
|
|
|
2
2
|
import { publishCommand } from "./commands/publish.js";
|
|
3
3
|
import { renderCommand } from "./commands/render.js";
|
|
4
4
|
import pkg from "../package.json" with { type: "json" };
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
5
|
+
import { themeCommand } from "./commands/theme.js";
|
|
6
|
+
export function createProgram(version = pkg.version) {
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("wenyan")
|
|
10
|
+
.description("A CLI for WenYan Markdown Render.")
|
|
11
|
+
.version(version, "-v, --version", "output the current version")
|
|
12
|
+
.action(() => {
|
|
13
|
+
program.outputHelp();
|
|
14
|
+
});
|
|
15
|
+
const addCommonOptions = (cmd) => {
|
|
16
|
+
return cmd
|
|
17
|
+
.argument("[input-content]", "markdown content (string input)")
|
|
18
|
+
.option("-f, --file <path>", "read markdown content from local file")
|
|
19
|
+
.option("-t, --theme <theme-id>", "ID of the theme to use", "default")
|
|
20
|
+
.option("-h, --highlight <highlight-theme-id>", "ID of the code highlight theme to use", "solarized-light")
|
|
21
|
+
.option("-c, --custom-theme <path>", "path to custom theme CSS file")
|
|
22
|
+
.option("--mac-style", "display codeblock with mac style", true)
|
|
23
|
+
.option("--no-mac-style", "disable mac style")
|
|
24
|
+
.option("--footnote", "convert link to footnote", true)
|
|
25
|
+
.option("--no-footnote", "disable footnote");
|
|
26
|
+
};
|
|
27
|
+
const pubCmd = program
|
|
28
|
+
.command("publish")
|
|
29
|
+
.description("Render a markdown file to styled HTML and publish to wechat GZH");
|
|
30
|
+
addCommonOptions(pubCmd).action(publishCommand);
|
|
31
|
+
const renderCmd = program.command("render").description("Render a markdown file to styled HTML");
|
|
32
|
+
addCommonOptions(renderCmd).action(renderCommand);
|
|
33
|
+
program
|
|
34
|
+
.command("theme")
|
|
35
|
+
.description("Manage themes")
|
|
36
|
+
.option("-l, --list", "List all available themes")
|
|
37
|
+
.option("--add", "Add a new custom theme")
|
|
38
|
+
.option("--name <name>", "Name of the new custom theme")
|
|
39
|
+
.option("--path <path>", "Path to the new custom theme CSS file")
|
|
40
|
+
.option("--rm <name>", "Name of the custom theme to remove")
|
|
41
|
+
.action(themeCommand);
|
|
42
|
+
return program;
|
|
43
|
+
}
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
file?: string;
|
|
3
|
-
theme: string;
|
|
4
|
-
highlight: string;
|
|
5
|
-
macStyle: boolean;
|
|
6
|
-
footnote: boolean;
|
|
7
|
-
}
|
|
1
|
+
import { RenderOptions } from "../types.js";
|
|
8
2
|
export declare function publishCommand(inputContent: string | undefined, options: RenderOptions): Promise<void>;
|
|
9
|
-
export {};
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
1
|
+
import { RenderOptions } from "../types.js";
|
|
2
|
+
export declare function prepareRenderContext(inputContent: string | undefined, options: RenderOptions): Promise<{
|
|
3
|
+
gzhContent: import("@wenyan-md/core/wrapper").StyledContent;
|
|
4
|
+
absoluteDirPath: string | undefined;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function runCommandWrapper(action: () => Promise<void>): Promise<void>;
|
|
8
7
|
export declare function renderCommand(inputContent: string | undefined, options: RenderOptions): Promise<void>;
|
|
9
|
-
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export declare function createProgram(version?: string): Command;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RenderOptions {
|
|
2
|
+
file?: string;
|
|
3
|
+
theme?: string;
|
|
4
|
+
customTheme?: string;
|
|
5
|
+
highlight: string;
|
|
6
|
+
macStyle: boolean;
|
|
7
|
+
footnote: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ThemeOptions {
|
|
10
|
+
list?: boolean;
|
|
11
|
+
add?: boolean;
|
|
12
|
+
name?: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
rm?: string;
|
|
15
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenyan-md/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "A CLI tool for Wenyan markdown rendering & publishing",
|
|
5
5
|
"author": "Lei <caol64@gmail.com> (https://github.com/caol64)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -15,13 +15,12 @@
|
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
17
|
"bin": {
|
|
18
|
-
"wenyan": "./
|
|
18
|
+
"wenyan": "./dist/cli.js"
|
|
19
19
|
},
|
|
20
20
|
"main": "./dist/index.js",
|
|
21
21
|
"types": "./dist/types/index.d.ts",
|
|
22
22
|
"files": [
|
|
23
|
-
"dist"
|
|
24
|
-
"bin"
|
|
23
|
+
"dist"
|
|
25
24
|
],
|
|
26
25
|
"homepage": "https://github.com/caol64/wenyan-cli#readme",
|
|
27
26
|
"repository": {
|
|
@@ -32,20 +31,27 @@
|
|
|
32
31
|
"url": "https://github.com/caol64/wenyan-cli/issues"
|
|
33
32
|
},
|
|
34
33
|
"dependencies": {
|
|
35
|
-
"@wenyan-md/core": "^
|
|
36
|
-
"commander": "^14.0.0"
|
|
34
|
+
"@wenyan-md/core": "^2.0.2",
|
|
35
|
+
"commander": "^14.0.0",
|
|
36
|
+
"form-data-encoder": "^4.1.0",
|
|
37
|
+
"formdata-node": "^6.0.3",
|
|
38
|
+
"jsdom": "^27.4.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@types/node": "^24.3.0",
|
|
40
42
|
"dotenv-cli": "^10.0.0",
|
|
41
|
-
"tsx": "^4.
|
|
42
|
-
"typescript": "^5.9.2"
|
|
43
|
+
"tsx": "^4.21.0",
|
|
44
|
+
"typescript": "^5.9.2",
|
|
45
|
+
"vite": "^7.3.1",
|
|
46
|
+
"vitest": "^4.0.18"
|
|
43
47
|
},
|
|
44
48
|
"scripts": {
|
|
45
49
|
"build": "tsc",
|
|
46
|
-
"dev": "tsx src/index.ts",
|
|
47
50
|
"upgrade:core": "pnpm update @wenyan-md/core",
|
|
48
|
-
"test:
|
|
49
|
-
"test:
|
|
51
|
+
"test:bin": "pnpm build && node ./dist/cli.js render -f tests/publish.md -c tests/manhua.css --no-mac-style",
|
|
52
|
+
"test:cli": "vitest run tests/cli.test.ts",
|
|
53
|
+
"test:render": "vitest run tests/render.test.ts",
|
|
54
|
+
"test:publish": "vitest run tests/publish.test.ts",
|
|
55
|
+
"test:realPublish": "pnpm build && pnpm dotenv -e .env.test -- node ./dist/cli.js publish -f tests/publish.md -c tests/manhua.css --no-mac-style"
|
|
50
56
|
}
|
|
51
57
|
}
|
package/bin/cli.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'url';
|
|
4
|
-
import { dirname, join } from 'path';
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const mainModulePath = join(__dirname, "..", "dist", 'index.js');
|
|
8
|
-
|
|
9
|
-
import(pathToFileURL(mainModulePath).href);
|