@wenyan-md/cli 1.0.8 → 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 +187 -91
- package/dist/cli.js +4 -0
- package/dist/commands/publish.js +12 -31
- package/dist/commands/render.js +60 -15
- package/dist/commands/theme.js +78 -0
- package/dist/index.js +39 -32
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/commands/publish.d.ts +2 -8
- package/dist/types/commands/render.d.ts +6 -7
- 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/utils.d.ts +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +27 -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,165 +7,259 @@
|
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* [嵌入版本](https://github.com/caol64/wenyan-core) - 将文颜的核心功能嵌入 Node 或者 Web 项目
|
|
17
|
+
- 微信公众号
|
|
18
|
+
- 知乎
|
|
19
|
+
- 今日头条
|
|
20
|
+
- 以及其它内容平台(持续扩展中)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
文颜的目标是:**让写作者专注内容,而不是排版和平台适配**。
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## 文颜的不同版本
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
* 支持图片自动上传
|
|
28
|
-
* 支持数学公式渲染
|
|
29
|
-
* 一键发布文章到微信公众号草稿箱
|
|
26
|
+
文颜目前提供多种形态,覆盖不同使用场景:
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
- [macOS App Store 版](https://github.com/caol64/wenyan) - MAC 桌面应用
|
|
29
|
+
- [跨平台桌面版](https://github.com/caol64/wenyan-pc) - Windows/Linux
|
|
30
|
+
- 👉 [CLI 版本](https://github.com/caol64/wenyan-cli) - 本项目
|
|
31
|
+
- [MCP 版本](https://github.com/caol64/wenyan-mcp) - AI 自动发文
|
|
32
|
+
- [核心库](https://github.com/caol64/wenyan-core) - 嵌入 Node / Web 项目
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
## 安装方式
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
### 方式一:npm(推荐)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
- [Pie](https://github.com/kevinzhao2233/typora-theme-pie)
|
|
41
|
-
- [Maize](https://github.com/BEATREE/typora-maize-theme)
|
|
42
|
-
- [Purple](https://github.com/hliu202/typora-purple-theme)
|
|
43
|
-
- [物理猫-薄荷](https://github.com/sumruler/typora-theme-phycat)
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g @wenyan-md/cli
|
|
40
|
+
```
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
安装完成后即可使用:
|
|
46
43
|
|
|
44
|
+
```bash
|
|
45
|
+
wenyan --help
|
|
47
46
|
```
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
### 方式二:Docker(无需 Node 环境)
|
|
49
|
+
|
|
50
|
+
如果你不想在本地安装 Node.js,也可以直接使用 Docker。
|
|
51
|
+
|
|
52
|
+
**拉取镜像**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
docker pull caol64/wenyan-cli
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**查看帮助**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
docker run --rm caol64/wenyan-cli
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**发布文章示例**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
docker run --rm \
|
|
68
|
+
--env-file .env.test \
|
|
69
|
+
-e HOST_FILE_PATH=$(pwd) \
|
|
70
|
+
-v $(pwd):/mnt/host-downloads \
|
|
71
|
+
caol64/wenyan-cli \
|
|
72
|
+
publish -f ./test/publish.md -t phycat
|
|
49
73
|
```
|
|
50
74
|
|
|
75
|
+
> 说明:
|
|
76
|
+
>
|
|
77
|
+
> - 使用 `-e` 传入环境变量
|
|
78
|
+
> - 使用 `-v` 挂载本地 Markdown 文件
|
|
79
|
+
> - 容器启动即执行 `wenyan` 命令
|
|
80
|
+
|
|
51
81
|
## 基本用法
|
|
52
82
|
|
|
53
|
-
|
|
83
|
+
CLI 主命令:
|
|
54
84
|
|
|
55
85
|
```bash
|
|
56
86
|
wenyan <command> [options]
|
|
57
87
|
```
|
|
58
88
|
|
|
59
|
-
|
|
89
|
+
目前支持的子命令有
|
|
90
|
+
- `publish` 排版并发布到公众号草稿箱
|
|
91
|
+
- `render` 仅排版,用做测试
|
|
92
|
+
- `theme` 主题管理
|
|
60
93
|
|
|
61
|
-
|
|
94
|
+
## 子命令
|
|
62
95
|
|
|
63
|
-
|
|
64
|
-
* `WECHAT_APP_SECRET`
|
|
96
|
+
### `publish`
|
|
65
97
|
|
|
66
|
-
|
|
98
|
+
将 Markdown 转换为适配微信公众号的富文本 HTML,并上传到草稿箱。
|
|
99
|
+
|
|
100
|
+
#### 参数
|
|
101
|
+
|
|
102
|
+
- `<input-content>`
|
|
103
|
+
|
|
104
|
+
Markdown 内容,可以:
|
|
67
105
|
|
|
68
|
-
|
|
106
|
+
- 直接作为参数传入
|
|
107
|
+
- 通过 stdin 管道输入
|
|
108
|
+
|
|
109
|
+
#### 常用选项
|
|
110
|
+
|
|
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`)
|
|
114
|
+
- atom-one-dark / atom-one-light / dracula / github-dark / github / monokai / solarized-dark / solarized-light / xcode
|
|
115
|
+
- `--no-mac-style`:关闭代码块 Mac 风格
|
|
116
|
+
- `--no-footnote`:关闭链接转脚注
|
|
117
|
+
- `-f <path>`:指定本地 Markdown 文件路径
|
|
118
|
+
- `-c <path>`:指定临时自定义主题路径,优先级大于`-t`
|
|
119
|
+
|
|
120
|
+
#### 使用示例
|
|
121
|
+
|
|
122
|
+
直接传入内容:
|
|
69
123
|
|
|
70
124
|
```bash
|
|
71
|
-
|
|
125
|
+
wenyan publish "# Hello, Wenyan" -t lapis -h solarized-light
|
|
72
126
|
```
|
|
73
127
|
|
|
74
|
-
|
|
128
|
+
从管道读取:
|
|
75
129
|
|
|
76
130
|
```bash
|
|
77
|
-
|
|
78
|
-
export WECHAT_APP_SECRET=yyy
|
|
131
|
+
cat example.md | wenyan publish -t lapis -h solarized-light --no-mac-style
|
|
79
132
|
```
|
|
80
133
|
|
|
81
|
-
|
|
134
|
+
从文件读取:
|
|
82
135
|
|
|
83
|
-
|
|
136
|
+
```bash
|
|
137
|
+
wenyan publish -f "./example.md" -t lapis -h solarized-light --no-mac-style
|
|
138
|
+
```
|
|
84
139
|
|
|
85
|
-
|
|
86
|
-
|
|
140
|
+
### `theme`
|
|
141
|
+
|
|
142
|
+
主题管理,浏览内置主题、添加/删除自定义主题。
|
|
143
|
+
|
|
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
|
|
87
162
|
```
|
|
88
163
|
|
|
89
|
-
|
|
164
|
+
安装自定义主题
|
|
90
165
|
|
|
91
|
-
|
|
166
|
+
```bash
|
|
167
|
+
wenyan theme --add --name new-theme --path https://wenyan.yuzhi.tech/manhua.css
|
|
168
|
+
```
|
|
92
169
|
|
|
93
|
-
|
|
170
|
+
删除自定义主题
|
|
94
171
|
|
|
95
|
-
|
|
172
|
+
```bash
|
|
173
|
+
wenyan theme --rm new-theme
|
|
174
|
+
```
|
|
96
175
|
|
|
97
|
-
|
|
176
|
+
## 使用自定义主题
|
|
98
177
|
|
|
99
|
-
|
|
178
|
+
你可以通过两种途径使用自定义主题:
|
|
100
179
|
|
|
101
|
-
-
|
|
180
|
+
- 不安装直接使用
|
|
102
181
|
|
|
103
|
-
|
|
182
|
+
```bash
|
|
183
|
+
wenyan publish -f "./example.md" -c "/path/to/theme" -h solarized-light --no-mac-style
|
|
184
|
+
```
|
|
104
185
|
|
|
105
|
-
-
|
|
106
|
-
- default
|
|
107
|
-
- orangeheart
|
|
108
|
-
- rainbow
|
|
109
|
-
- lapis
|
|
110
|
-
- pie
|
|
111
|
-
- maize
|
|
112
|
-
- purple
|
|
113
|
-
- phycat
|
|
114
|
-
- `-h`,代码高亮主题,默认`solarized-light`
|
|
115
|
-
- atom-one-dark
|
|
116
|
-
- atom-one-light
|
|
117
|
-
- dracula
|
|
118
|
-
- github-dark
|
|
119
|
-
- github
|
|
120
|
-
- monokai
|
|
121
|
-
- solarized-dark
|
|
122
|
-
- solarized-light
|
|
123
|
-
- xcode
|
|
124
|
-
- 代码块默认使用 Mac 风格,如要关闭:`--no-mac-style`
|
|
125
|
-
- 链接默认转脚注,如要关闭:`--no-footnote`
|
|
186
|
+
- 先安装再使用:
|
|
126
187
|
|
|
127
|
-
|
|
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
|
+
```
|
|
128
192
|
|
|
129
|
-
|
|
193
|
+
区别在于,安装后的主题永久有效。
|
|
194
|
+
|
|
195
|
+
## 关于图片自动上传
|
|
196
|
+
|
|
197
|
+
支持以下图片来源:
|
|
198
|
+
|
|
199
|
+
- 本地路径(如:`/Users/xxx/image.jpg`)
|
|
200
|
+
- 网络路径(如:`https://example.com/image.jpg`)
|
|
201
|
+
|
|
202
|
+
## 环境变量配置
|
|
203
|
+
|
|
204
|
+
部分功能(如发布微信公众号)需要配置以下环境变量:
|
|
205
|
+
|
|
206
|
+
- `WECHAT_APP_ID`
|
|
207
|
+
- `WECHAT_APP_SECRET`
|
|
208
|
+
|
|
209
|
+
### macOS / Linux
|
|
210
|
+
|
|
211
|
+
临时使用:
|
|
130
212
|
|
|
131
213
|
```bash
|
|
132
|
-
wenyan publish "
|
|
214
|
+
WECHAT_APP_ID=xxx WECHAT_APP_SECRET=yyy wenyan publish "your markdown"
|
|
133
215
|
```
|
|
134
216
|
|
|
135
|
-
|
|
217
|
+
永久配置(推荐):
|
|
136
218
|
|
|
137
219
|
```bash
|
|
138
|
-
|
|
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
|
|
139
232
|
```
|
|
140
233
|
|
|
234
|
+
永久设置(在环境变量里添加):
|
|
235
|
+
|
|
236
|
+
控制面板 → 系统和安全 → 系统 → 高级系统设置 → 环境变量 → 添加 `WECHAT_APP_ID` 和 `WECHAT_APP_SECRET`。
|
|
237
|
+
|
|
141
238
|
## 微信公众号 IP 白名单
|
|
142
239
|
|
|
143
|
-
|
|
144
|
-
|
|
240
|
+
> [!IMPORTANT]
|
|
241
|
+
>
|
|
242
|
+
> 请确保运行文颜的机器 IP 已加入微信公众号后台的 IP 白名单,否则上传接口将调用失败。
|
|
243
|
+
|
|
244
|
+
配置说明文档:[https://yuzhi.tech/docs/wenyan/upload](https://yuzhi.tech/docs/wenyan/upload)
|
|
145
245
|
|
|
146
|
-
##
|
|
246
|
+
## Markdown Frontmatter 说明(必读)
|
|
147
247
|
|
|
148
|
-
|
|
248
|
+
为了正确上传文章,每篇 Markdown 顶部需要包含 frontmatter:
|
|
149
249
|
|
|
150
250
|
```md
|
|
151
251
|
---
|
|
152
252
|
title: 在本地跑一个大语言模型(2) - 给模型提供外部知识库
|
|
153
|
-
cover: /Users/
|
|
253
|
+
cover: /Users/xxx/image.jpg
|
|
154
254
|
---
|
|
155
255
|
```
|
|
156
256
|
|
|
157
|
-
|
|
158
|
-
* `cover` 是文章封面,支持本地路径和网络图片:
|
|
257
|
+
字段说明:
|
|
159
258
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
* 支持图片路径:
|
|
166
|
-
|
|
167
|
-
* 本地路径(如:`/Users/lei/Downloads/result_image.jpg`)
|
|
168
|
-
* 网络路径(如:`https://example.com/image.jpg`)
|
|
259
|
+
- `title` 文章标题(必填)
|
|
260
|
+
- `cover` 文章封面
|
|
261
|
+
- 本地路径或网络图片
|
|
262
|
+
- 如果正文中已有图片,可省略
|
|
169
263
|
|
|
170
264
|
## 示例文章格式
|
|
171
265
|
|
|
@@ -186,7 +280,9 @@ cover: /Users/lei/Downloads/result_image.jpg
|
|
|
186
280
|
|
|
187
281
|
## 赞助
|
|
188
282
|
|
|
189
|
-
|
|
283
|
+
如果你觉得文颜对你有帮助,可以给我家猫咪买点罐头 ❤️
|
|
284
|
+
|
|
285
|
+
[https://yuzhi.tech/sponsor](https://yuzhi.tech/sponsor)
|
|
190
286
|
|
|
191
287
|
## License
|
|
192
288
|
|
package/dist/cli.js
ADDED
package/dist/commands/publish.js
CHANGED
|
@@ -1,40 +1,21 @@
|
|
|
1
|
-
import { getGzhContent } from "@wenyan-md/core/wrapper";
|
|
2
1
|
import { publishToDraft } from "@wenyan-md/core/publish";
|
|
3
|
-
import {
|
|
2
|
+
import { prepareRenderContext, runCommandWrapper } from "./render.js";
|
|
4
3
|
export async function publishCommand(inputContent, options) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!gzhContent.title) {
|
|
15
|
-
console.error("未能找到文章标题");
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
if (!gzhContent.cover) {
|
|
19
|
-
console.error("未能找到文章封面");
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
const data = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover);
|
|
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: 未能找到文章封面");
|
|
10
|
+
const data = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover, {
|
|
11
|
+
relativePath: absoluteDirPath,
|
|
12
|
+
});
|
|
23
13
|
if (data.media_id) {
|
|
24
14
|
console.log(`上传成功,media_id: ${data.media_id}`);
|
|
25
15
|
}
|
|
26
16
|
else {
|
|
27
17
|
console.error(`上传失败,\n${data}`);
|
|
18
|
+
process.exit(1);
|
|
28
19
|
}
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
if (error instanceof Error) {
|
|
32
|
-
console.error("An unexpected error occurred during publishing:");
|
|
33
|
-
console.error(error.message);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
console.error("An unexpected error occurred:", error);
|
|
37
|
-
}
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
20
|
+
});
|
|
40
21
|
}
|
package/dist/commands/render.js
CHANGED
|
@@ -1,22 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readStdin } from "../utils.js";
|
|
3
|
-
|
|
1
|
+
import { configStore, renderStyledContent } from "@wenyan-md/core/wrapper";
|
|
2
|
+
import { getNormalizeFilePath, readStdin } from "../utils.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
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) {
|
|
4
48
|
try {
|
|
5
|
-
|
|
6
|
-
if (process.stdin.isTTY) {
|
|
7
|
-
console.error("Error: missing input-content (no argument and no stdin).");
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
|
-
inputContent = await readStdin();
|
|
11
|
-
}
|
|
12
|
-
const gzhContent = await getGzhContent(inputContent, options["theme"], options["highlight"], options["macStyle"], options["footnote"]);
|
|
13
|
-
console.log(gzhContent.content);
|
|
14
|
-
// process.exit(0);
|
|
49
|
+
await action();
|
|
15
50
|
}
|
|
16
51
|
catch (error) {
|
|
17
52
|
if (error instanceof Error) {
|
|
18
|
-
|
|
19
|
-
|
|
53
|
+
if (error.message.startsWith("Error:")) {
|
|
54
|
+
console.error(error.message);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.error("An unexpected error occurred:", error.message);
|
|
58
|
+
}
|
|
20
59
|
}
|
|
21
60
|
else {
|
|
22
61
|
console.error("An unexpected error occurred:", error);
|
|
@@ -24,3 +63,9 @@ export async function renderCommand(inputContent, options) {
|
|
|
24
63
|
process.exit(1);
|
|
25
64
|
}
|
|
26
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,35 +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
|
-
|
|
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,8 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
theme: string;
|
|
4
|
-
highlight: string;
|
|
5
|
-
macStyle: boolean;
|
|
6
|
-
}
|
|
7
|
-
export declare function publishCommand(inputContent: string, options: RenderOptions): Promise<void>;
|
|
8
|
-
export {};
|
|
1
|
+
import { RenderOptions } from "../types.js";
|
|
2
|
+
export declare function publishCommand(inputContent: string | undefined, options: RenderOptions): Promise<void>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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>;
|
|
7
7
|
export declare function renderCommand(inputContent: string | undefined, options: RenderOptions): Promise<void>;
|
|
8
|
-
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/utils.d.ts
CHANGED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
export async function readStdin() {
|
|
2
3
|
return new Promise((resolve, reject) => {
|
|
3
4
|
let data = "";
|
|
@@ -7,3 +8,29 @@ export async function readStdin() {
|
|
|
7
8
|
process.stdin.on("error", reject);
|
|
8
9
|
});
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* 路径标准化工具函数
|
|
13
|
+
* 将 Windows 的反斜杠 \ 转换为正斜杠 /,并去除末尾斜杠
|
|
14
|
+
* 目的:在 Linux 容器内也能正确处理 Windows 路径字符串
|
|
15
|
+
*/
|
|
16
|
+
function normalizePath(p) {
|
|
17
|
+
return p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
18
|
+
}
|
|
19
|
+
export function getNormalizeFilePath(inputPath) {
|
|
20
|
+
const isContainer = !!process.env.CONTAINERIZED;
|
|
21
|
+
if (isContainer) {
|
|
22
|
+
const hostFilePath = normalizePath(process.env.HOST_FILE_PATH || "");
|
|
23
|
+
const containerFilePath = normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads");
|
|
24
|
+
let relativePart = normalizePath(inputPath);
|
|
25
|
+
if (relativePart.startsWith(hostFilePath)) {
|
|
26
|
+
relativePart = relativePart.slice(hostFilePath.length);
|
|
27
|
+
}
|
|
28
|
+
if (!relativePart.startsWith("/")) {
|
|
29
|
+
relativePart = "/" + relativePart;
|
|
30
|
+
}
|
|
31
|
+
return containerFilePath + relativePart;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return path.resolve(inputPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
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);
|