juice-email-cli 2.1.7 → 2.1.9
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 +161 -112
- package/bin/juice.js +4 -4
- package/edm/elabscience/literature/snippet.html +1 -1
- package/package.json +1 -1
- package/src/context-menu.js +118 -70
- package/src/snippet.js +28 -2
package/README.md
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# juice-email-cli
|
|
2
2
|
|
|
3
|
-
> 一个用于生成符合各大邮件平台标准的 HTML 邮件命令行工具,基于 [juice](https://github.com/Automattic/juice) 实现 CSS 内联,支持 Mustache
|
|
3
|
+
> 一个用于生成符合各大邮件平台标准的 HTML 邮件命令行工具,基于 [juice](https://github.com/Automattic/juice) 实现 CSS 内联,支持 Mustache 模板变量替换,同时输出标准版与压缩版。
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 功能特性
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
9
|
+
- **CSS 内联** —— 将 `<style>` 中的样式全部内联为 `style=""` 属性,兼容 Gmail / Outlook / Apple Mail 等
|
|
10
|
+
- **Mustache 模板变量** —— 支持 `{{变量名}}` 语法,通过配置文件批量替换
|
|
11
|
+
- **Mustache 列表循环** —— 支持 `{{#items}}...{{/items}}` 遍历数组,支持嵌套循环
|
|
12
|
+
- **片段组装** —— 将片段 HTML 插入模板的 `<tbody id="content">`,自动调整缩进,输出 4 个阶段文件
|
|
13
|
+
- **三层配置合并** —— CLI 默认 < 用户目录 < 优先配置,层层合并
|
|
14
|
+
- **HTML 标签渲染** —— 变量值中的 `<sup>`、`<sub>` 等 HTML 标签直接渲染(可通过 `rawHtml` 关闭)
|
|
15
|
+
- **双文件输出** —— 普通模式生成 `.output.html` + `.minified.html`
|
|
16
|
+
- **四文件输出** —— 片段模式生成 `.raw.html` + `.html` + `.output.html` + `.minified.html`
|
|
17
|
+
- **交互模式** —— 无参数运行,逐步选择品牌、模板、片段、配置
|
|
18
|
+
- **Windows 右键菜单** —— 子菜单层级,一键注册/卸载
|
|
16
19
|
|
|
17
20
|
---
|
|
18
21
|
|
|
@@ -42,32 +45,79 @@ npm link # 链接到全局
|
|
|
42
45
|
|
|
43
46
|
## 使用方法
|
|
44
47
|
|
|
48
|
+
### 普通模式(CSS 内联 + 压缩,输出 2 个文件)
|
|
49
|
+
|
|
45
50
|
```bash
|
|
46
|
-
#
|
|
51
|
+
# 最简用法
|
|
47
52
|
juice -f my-email.html
|
|
48
53
|
|
|
49
54
|
# 指定配置文件
|
|
50
55
|
juice -c project.yaml -f my-email.html
|
|
56
|
+
|
|
57
|
+
# 使用 EDM 模板库中的模板
|
|
58
|
+
juice -f edm/elabscience/elabscience-template.html
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
生成文件(与输入文件同目录):
|
|
62
|
+
|
|
63
|
+
| 文件 | 说明 |
|
|
64
|
+
|------|------|
|
|
65
|
+
| `<name>.output.html` | CSS 内联 + 变量替换后的标准版 |
|
|
66
|
+
| `<name>.minified.html` | 压缩版 |
|
|
67
|
+
|
|
68
|
+
### 片段模式(片段 + 模板拼接,输出 4 个文件)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 完整指定片段和模板
|
|
72
|
+
juice -s edm/elabscience/literature/snippet.html -f edm/elabscience/elabscience-template.html
|
|
73
|
+
|
|
74
|
+
# 只指定片段,交互式选择模板
|
|
75
|
+
juice -s edm/elabscience/literature/snippet.html
|
|
76
|
+
|
|
77
|
+
# 自定义输出文件名
|
|
78
|
+
juice -s snippet.html -f template.html -n my-output
|
|
51
79
|
```
|
|
52
80
|
|
|
53
|
-
|
|
81
|
+
生成文件(当前工作目录):
|
|
54
82
|
|
|
55
83
|
| 文件 | 说明 |
|
|
56
84
|
|------|------|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
85
|
+
| `<name>.raw.html` | 原始组装(Mustache 未渲染,无 CSS 内联) |
|
|
86
|
+
| `<name>.html` | 已渲染(Mustache 变量已替换,无 CSS 内联) |
|
|
87
|
+
| `<name>.output.html` | Juice CSS 内联后 |
|
|
88
|
+
| `<name>.minified.html` | 压缩版 |
|
|
89
|
+
|
|
90
|
+
输出文件名默认为模板文件名(不含扩展名)。如果文件冲突,交互模式下可选择覆盖、自动版本号(`-v1`、`-v2`...)或重新输入。
|
|
91
|
+
|
|
92
|
+
### 交互模式(逐步选择)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
juice
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
流程:选择品牌 → 模板 → 片段文件夹 → 片段 HTML → 配置文件 → 输出文件名 → 确认执行。
|
|
59
99
|
|
|
60
100
|
### 参数说明
|
|
61
101
|
|
|
62
102
|
| 参数 | 简写 | 说明 |
|
|
63
103
|
|------|------|------|
|
|
64
|
-
| `--file <path>` | `-f` | 输入 HTML
|
|
65
|
-
| `--
|
|
104
|
+
| `--file <path>` | `-f` | 输入 HTML 模板文件路径 |
|
|
105
|
+
| `--snippet <path>` | `-s` | 片段 HTML 文件路径:插入到模板 `<tbody id="content">` |
|
|
106
|
+
| `--config <path>` | `-c` | 配置文件路径,不指定时自动查找 |
|
|
107
|
+
| `--name <name>` | `-n` | 片段模式输出文件名(不含扩展名) |
|
|
66
108
|
| `--install` | | 注册 Windows 右键菜单(需管理员权限) |
|
|
67
109
|
| `--uninstall` | | 取消 Windows 右键菜单注册(需管理员权限) |
|
|
68
110
|
| `--version` | `-v` | 查看版本号 |
|
|
69
111
|
| `--help` | `-h` | 查看帮助 |
|
|
70
112
|
|
|
113
|
+
### CLI 执行模式
|
|
114
|
+
|
|
115
|
+
| `-s` | `-f` | `-c` | 执行模式 |
|
|
116
|
+
|------|------|------|----------|
|
|
117
|
+
| ✓ | * | * | 片段模式(-s 指定片段,-f 可选指定模板) |
|
|
118
|
+
| ✗ | ✓ | * | 普通模式(生成 .output.html + .minified.html) |
|
|
119
|
+
| ✗ | ✗ | * | 交互式片段模式(逐步选择) |
|
|
120
|
+
|
|
71
121
|
---
|
|
72
122
|
|
|
73
123
|
## 配置文件优先级
|
|
@@ -97,11 +147,17 @@ juice -c project.yaml -f my-email.html
|
|
|
97
147
|
juice -c project.yaml -f email.html
|
|
98
148
|
|
|
99
149
|
# 使用输入文件同级目录配置(与用户目录合并)
|
|
100
|
-
# → email.html 同目录下有 juice.yaml
|
|
101
150
|
juice -f email.html
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 片段模式配置
|
|
154
|
+
|
|
155
|
+
片段模式下,会自动检测片段目录下的 `juice.yaml` / `juice.yml` 作为项目配置参与合并:
|
|
102
156
|
|
|
103
|
-
|
|
104
|
-
|
|
157
|
+
```
|
|
158
|
+
优先级 低 ──────────────────────────────────────────────────────── 高
|
|
159
|
+
|
|
160
|
+
内置默认 < ~/juice.yaml < 片段目录 juice.yaml < -c 指定
|
|
105
161
|
```
|
|
106
162
|
|
|
107
163
|
---
|
|
@@ -111,6 +167,9 @@ juice -f email.html
|
|
|
111
167
|
用户配置文件(`~/juice.yaml` 或项目目录 `juice.yaml`)只需填写需要覆盖的字段,其余自动继承:
|
|
112
168
|
|
|
113
169
|
```yaml
|
|
170
|
+
# 设为 false 时,变量值中的 HTML 标签会被转义
|
|
171
|
+
rawHtml: true
|
|
172
|
+
|
|
114
173
|
variables:
|
|
115
174
|
brandName: "我的品牌"
|
|
116
175
|
brandColor: "#ff6600"
|
|
@@ -127,28 +186,17 @@ variables:
|
|
|
127
186
|
tag: "热销"
|
|
128
187
|
- name: "产品 B"
|
|
129
188
|
price: "¥199.00"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
items:
|
|
136
|
-
- "优势 A"
|
|
137
|
-
- "优势 B"
|
|
138
|
-
- title: "特性二"
|
|
139
|
-
items:
|
|
140
|
-
- "优势 C"
|
|
141
|
-
- "优势 D"
|
|
189
|
+
|
|
190
|
+
# 变量值支持 HTML 标签(rawHtml: true 时)
|
|
191
|
+
overview:
|
|
192
|
+
title: "CD38-NAD<sup>+</sup> Axis Study"
|
|
193
|
+
keywords: "Immune Thrombocytopenia, NMN, NAD<sup>+</sup>"
|
|
142
194
|
|
|
143
195
|
# juice 选项(全部可选,均有内置默认值)
|
|
144
196
|
# juice:
|
|
145
197
|
# removeStyleTags: true
|
|
146
198
|
# preserveMediaQueries: true
|
|
147
|
-
# preservePseudos: true
|
|
148
|
-
# preservedSelectors: # 保留的选择器(不会被内联)
|
|
149
|
-
# - "a:hover"
|
|
150
|
-
# extraCssFiles:
|
|
151
|
-
# - email-reset.css
|
|
199
|
+
# preservePseudos: true
|
|
152
200
|
|
|
153
201
|
# 输出后缀(可选)
|
|
154
202
|
# output:
|
|
@@ -169,12 +217,26 @@ variables:
|
|
|
169
217
|
<a href="{{ctaUrl}}" style="background-color: {{brandColor}};">{{ctaText}}</a>
|
|
170
218
|
```
|
|
171
219
|
|
|
220
|
+
### HTML 标签在变量中
|
|
221
|
+
|
|
222
|
+
当 `rawHtml: true`(默认)时,变量值中的 HTML 标签直接渲染:
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
variables:
|
|
226
|
+
overview:
|
|
227
|
+
keywords: "CD38-NAD<sup>+</sup> Axis, NO<sub>3</sub><sup>-</sup>"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<p>{{overview.keywords}}</p>
|
|
232
|
+
<!-- 渲染为:CD38-NAD<sup>+</sup> Axis, NO<sub>3</sub><sup>-</sup> -->
|
|
233
|
+
```
|
|
234
|
+
|
|
172
235
|
### Mustache 列表循环
|
|
173
236
|
|
|
174
237
|
支持 `{{#items}}...{{/items}}` 语法遍历数组数据:
|
|
175
238
|
|
|
176
239
|
```yaml
|
|
177
|
-
# juice.yaml - 配置列表数据
|
|
178
240
|
variables:
|
|
179
241
|
products:
|
|
180
242
|
- name: "产品 A"
|
|
@@ -182,12 +244,9 @@ variables:
|
|
|
182
244
|
tag: "热销"
|
|
183
245
|
- name: "产品 B"
|
|
184
246
|
price: "¥199.00"
|
|
185
|
-
- name: "产品 C"
|
|
186
|
-
price: "¥299.00"
|
|
187
247
|
```
|
|
188
248
|
|
|
189
249
|
```html
|
|
190
|
-
<!-- HTML 模板 -->
|
|
191
250
|
{{#products}}
|
|
192
251
|
<div class="product-item">
|
|
193
252
|
<h3>{{name}}</h3>
|
|
@@ -196,55 +255,78 @@ variables:
|
|
|
196
255
|
</div>
|
|
197
256
|
{{/products}}
|
|
198
257
|
|
|
199
|
-
<!-- 空列表备选内容 -->
|
|
200
258
|
{{^products}}
|
|
201
259
|
<p>暂无商品</p>
|
|
202
260
|
{{/products}}
|
|
203
|
-
|
|
204
|
-
<!-- 嵌套循环示例 -->
|
|
205
|
-
{{#features}}
|
|
206
|
-
<div class="feature-column">
|
|
207
|
-
<h4>{{title}}</h4>
|
|
208
|
-
<ul>
|
|
209
|
-
{{#items}}
|
|
210
|
-
<li>{{.}}</li>
|
|
211
|
-
{{/items}}
|
|
212
|
-
</ul>
|
|
213
|
-
</div>
|
|
214
|
-
{{/features}}
|
|
215
261
|
```
|
|
216
262
|
|
|
217
263
|
| 语法 | 说明 | 示例 |
|
|
218
264
|
|------|------|------|
|
|
219
265
|
| `{{#list}}{{/list}}` | 循环遍历 | `{{#products}}{{name}}{{/products}}` |
|
|
220
266
|
| `{{^list}}{{/list}}` | 反向(空列表时显示) | `{{^products}}暂无{{/products}}` |
|
|
221
|
-
| `{{.}}` | 当前元素 | `{{
|
|
267
|
+
| `{{.}}` | 当前元素 | `{{#items}}{{.}}{{/items}}` |
|
|
222
268
|
| `{{#var}}{{/var}}` | 条件渲染(仅当有值时显示) | `{{#tag}}{{tag}}{{/tag}}` |
|
|
223
269
|
|
|
224
270
|
---
|
|
225
271
|
|
|
226
|
-
##
|
|
272
|
+
## 片段组装
|
|
227
273
|
|
|
228
|
-
|
|
274
|
+
### EDM 目录结构
|
|
229
275
|
|
|
230
276
|
```
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
277
|
+
edm/
|
|
278
|
+
├── <brand>/ # 品牌目录
|
|
279
|
+
│ ├── <brand>-template.html # 品牌模板(含 <tbody id="content">)
|
|
280
|
+
│ └── <series>/ # 片段系列目录
|
|
281
|
+
│ ├── snippet.html # 片段 HTML(Mustache 模板片段)
|
|
282
|
+
│ └── juice.yaml # 片段配置(variables)
|
|
234
283
|
```
|
|
235
284
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
285
|
+
### 片段 HTML 格式
|
|
286
|
+
|
|
287
|
+
片段是模板中 `<tbody id="content">` 内的内容片段,会被自动插入并调整缩进:
|
|
239
288
|
|
|
240
|
-
|
|
241
|
-
|
|
289
|
+
```html
|
|
290
|
+
<!-- edm/elabscience/literature/snippet.html -->
|
|
291
|
+
<tr>
|
|
292
|
+
<td></td>
|
|
293
|
+
<td colspan="2">
|
|
294
|
+
<table>
|
|
295
|
+
<tbody>
|
|
296
|
+
<tr>
|
|
297
|
+
<td><img src="{{overview.image}}" /></td>
|
|
298
|
+
<td><strong>{{overview.title}}</strong></td>
|
|
299
|
+
</tr>
|
|
300
|
+
</tbody>
|
|
301
|
+
</table>
|
|
302
|
+
</td>
|
|
303
|
+
<td></td>
|
|
304
|
+
</tr>
|
|
242
305
|
```
|
|
243
306
|
|
|
244
|
-
|
|
245
|
-
> 注册成功后如菜单未立即出现,重启文件资源管理器(`explorer.exe`)即可。
|
|
307
|
+
### 跨品牌检查
|
|
246
308
|
|
|
247
|
-
|
|
309
|
+
片段和模板来自不同品牌时,会输出警告但仍继续执行,避免样式错乱。
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Windows 右键菜单
|
|
314
|
+
|
|
315
|
+
注册后,在 `.html` / `.htm` 文件上右键可看到级联子菜单:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
📧 用 juice 生成邮件 HTML
|
|
319
|
+
├── ⚡ 生成邮件 HTML(标准 + 压缩) → juice -f
|
|
320
|
+
├── 🧩 邮件片段组装(交互选择模板) → juice -s
|
|
321
|
+
└── 📂 在此目录打开 PowerShell 7 ← 仅在已安装 pwsh 时出现
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
juice --install # 注册(当前用户,无需管理员)
|
|
326
|
+
juice --uninstall # 卸载
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
> **注意**:注册到当前用户(HKCU),无需管理员权限。注册成功后如菜单未立即出现,重启文件资源管理器(`explorer.exe`)即可。
|
|
248
330
|
|
|
249
331
|
---
|
|
250
332
|
|
|
@@ -256,26 +338,24 @@ juice-cli/
|
|
|
256
338
|
│ └── juice.js # CLI 入口
|
|
257
339
|
├── src/
|
|
258
340
|
│ ├── index.js # 核心逻辑(配置合并、模板处理、双输出)
|
|
259
|
-
│
|
|
341
|
+
│ ├── snippet.js # 片段组装模式 + 交互式提示
|
|
342
|
+
│ └── context-menu.js # Windows 右键菜单注册
|
|
260
343
|
├── defaults/
|
|
261
|
-
│ └── juice.yaml # CLI
|
|
262
|
-
├──
|
|
263
|
-
│ ├──
|
|
264
|
-
│ ├── template.html
|
|
265
|
-
│ └──
|
|
344
|
+
│ └── juice.yaml # CLI 内置默认配置
|
|
345
|
+
├── edm/ # EDM 模板库(npm 发布时包含)
|
|
346
|
+
│ ├── elabscience/
|
|
347
|
+
│ │ ├── elabscience-template.html
|
|
348
|
+
│ │ └── literature/
|
|
349
|
+
│ │ ├── snippet.html
|
|
350
|
+
│ │ └── juice.yaml
|
|
351
|
+
│ └── procell/
|
|
352
|
+
│ └── procell-template.html
|
|
266
353
|
├── icons/
|
|
267
|
-
│ └── juice-icon.ico #
|
|
354
|
+
│ └── juice-icon.ico # 右键菜单图标
|
|
268
355
|
├── scripts/
|
|
269
|
-
│ ├── generate-icon.js # 图标生成脚本
|
|
270
356
|
│ └── release.mjs # 发布脚本
|
|
271
|
-
├── .husky/
|
|
272
|
-
│ └── commit-msg # Git hooks - 提交信息校验
|
|
273
|
-
├── .commitlintrc.json # Commitlint 配置
|
|
274
|
-
├── .versionrc # Standard-version 配置
|
|
275
|
-
├── .npmrc # npm 发布配置
|
|
276
357
|
├── CHANGELOG.md # 变更日志(自动生成)
|
|
277
358
|
├── LICENSE # MIT
|
|
278
|
-
├── README.md
|
|
279
359
|
└── package.json
|
|
280
360
|
```
|
|
281
361
|
|
|
@@ -283,55 +363,24 @@ juice-cli/
|
|
|
283
363
|
|
|
284
364
|
## 发布说明
|
|
285
365
|
|
|
286
|
-
本项目使用自动化发布流程,包含以下功能:
|
|
287
|
-
|
|
288
366
|
### 提交信息规范
|
|
289
367
|
|
|
290
368
|
使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
|
|
291
369
|
|
|
292
370
|
```
|
|
293
|
-
<type>(<scope>): <subject>
|
|
294
|
-
|
|
295
371
|
feat: 添加新功能
|
|
296
372
|
fix: 修复 bug
|
|
297
373
|
docs: 更新文档
|
|
298
|
-
style: 代码格式调整
|
|
299
|
-
refactor: 重构代码
|
|
300
|
-
perf: 性能优化
|
|
301
|
-
test: 添加测试
|
|
302
|
-
build: 构建系统变更
|
|
303
|
-
ci: CI/CD 配置变更
|
|
304
374
|
chore: 其他变更
|
|
305
|
-
revert: 回退提交
|
|
306
375
|
```
|
|
307
376
|
|
|
308
377
|
### 自动化发布
|
|
309
378
|
|
|
310
379
|
```bash
|
|
311
|
-
#
|
|
312
|
-
npm run release
|
|
313
|
-
|
|
314
|
-
# 预览发布(不实际执行)
|
|
315
|
-
npm run release:dry
|
|
380
|
+
npm run release # 一键发布
|
|
381
|
+
npm run release:dry # 预览
|
|
316
382
|
```
|
|
317
383
|
|
|
318
|
-
**发布流程:**
|
|
319
|
-
1. 检测未提交的文件,询问是否提交
|
|
320
|
-
2. 选择版本更新类型(Major / Minor / Patch)
|
|
321
|
-
3. 确认版本号
|
|
322
|
-
4. 自动更新版本号、生成 CHANGELOG、打 tag
|
|
323
|
-
5. 推送到 origin 和 github 远程仓库
|
|
324
|
-
6. GitHub Actions 自动发布到 npm
|
|
325
|
-
|
|
326
|
-
### 配置文件
|
|
327
|
-
|
|
328
|
-
| 文件 | 说明 |
|
|
329
|
-
|------|------|
|
|
330
|
-
| `.commitlintrc.json` | 提交信息校验规则 |
|
|
331
|
-
| `.versionrc` | 版本更新和 CHANGELOG 生成规则 |
|
|
332
|
-
| `.npmrc` | npm 发布配置 |
|
|
333
|
-
| `.husky/commit-msg` | Git hooks 配置 |
|
|
334
|
-
|
|
335
384
|
---
|
|
336
385
|
|
|
337
386
|
## License
|
package/bin/juice.js
CHANGED
|
@@ -14,8 +14,8 @@ program
|
|
|
14
14
|
.option('-s, --snippet <path>', '片段 HTML 文件路径:将片段内容插入模板 <tbody id="content"> 中')
|
|
15
15
|
.option('-c, --config <path>', '配置文件路径')
|
|
16
16
|
.option('-n, --name <name>', '片段模式输出文件名(不含扩展名)')
|
|
17
|
-
.option('--install', '注册 Windows
|
|
18
|
-
.option('--uninstall', '取消 Windows
|
|
17
|
+
.option('--install', '注册 Windows 右键菜单(当前用户,无需管理员)')
|
|
18
|
+
.option('--uninstall', '取消 Windows 右键菜单注册')
|
|
19
19
|
.addHelpText('before', `
|
|
20
20
|
╔════════════════════════════════════════════════════════════════╗
|
|
21
21
|
║ juice-email-cli v${pkg.version} ║
|
|
@@ -64,8 +64,8 @@ program
|
|
|
64
64
|
════════════════════════════════════════════════════════════════
|
|
65
65
|
|
|
66
66
|
在 .html/.htm 文件上右键即可使用:
|
|
67
|
-
juice --install
|
|
68
|
-
juice --uninstall
|
|
67
|
+
juice --install (当前用户,注册右键菜单)
|
|
68
|
+
juice --uninstall (卸载右键菜单)
|
|
69
69
|
|
|
70
70
|
更多信息:https://gitee.com/siriussupreme/juice-cli
|
|
71
71
|
`)
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
<td rowspan="2" style="padding-right: 12px;" class="image-box align-middle">
|
|
66
66
|
<img src="{{information.image}}" />
|
|
67
67
|
</td>
|
|
68
|
-
<td
|
|
68
|
+
<td class="h-0 {{#information.link}}pb-24{{/information.link}}">
|
|
69
69
|
<p>{{information.content}}</p>
|
|
70
70
|
</td>
|
|
71
71
|
</tr>
|
package/package.json
CHANGED
package/src/context-menu.js
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Windows 右键菜单注册 / 取消注册
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* - 使用 MUIVerb(菜单显示文字)+ Icon 键
|
|
8
|
-
* - 通过 ExtendedSubCommandsKey 实现子菜单("用 juice 处理" → 子项)
|
|
9
|
-
* - 注册到 SystemFileAssociations,兼容任意关联方式打开的 .html/.htm
|
|
6
|
+
* 使用 HKEY_CURRENT_USER,无需管理员权限,仅对当前用户生效。
|
|
10
7
|
*
|
|
11
8
|
* 菜单结构:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* .html / .htm:
|
|
10
|
+
* 📧 用 juice 生成邮件 HTML
|
|
11
|
+
* ├── ⚡ 生成邮件 HTML(标准 + 压缩) → juice -f
|
|
12
|
+
* ├── 🧩 邮件片段组装(交互选择模板) → juice -s
|
|
13
|
+
* └── 📂 在此目录打开 PowerShell 7 (可选)
|
|
16
14
|
*
|
|
17
|
-
*
|
|
15
|
+
* .yaml / .yml:
|
|
16
|
+
* 📧 用 juice 生成邮件 HTML
|
|
17
|
+
* └── 📋 交互式生成邮件(使用此配置) → juice -c
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
const { execSync } = require('child_process');
|
|
@@ -47,108 +47,143 @@ function regAdd(key, valueName, type, data) {
|
|
|
47
47
|
const cmd = `reg add "${key}" ${vFlag} ${tFlag} ${dFlag} /f`.replace(/\s+/g, ' ').trim();
|
|
48
48
|
try {
|
|
49
49
|
execSync(cmd, { stdio: 'pipe' });
|
|
50
|
+
return true;
|
|
50
51
|
} catch (e) {
|
|
51
52
|
const msg = e.stderr ? e.stderr.toString().trim() : e.message;
|
|
52
|
-
console.warn(chalk.yellow(` ⚠ reg add
|
|
53
|
+
console.warn(chalk.yellow(` ⚠ reg add 失败\n 键:${key}\n 原因:${msg}`));
|
|
54
|
+
return false;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
|
-
* 执行 reg delete
|
|
59
|
+
* 执行 reg delete,键不存在时静默忽略,返回是否实际删除了
|
|
58
60
|
*/
|
|
59
61
|
function regDelete(key) {
|
|
60
62
|
try {
|
|
61
63
|
execSync(`reg delete "${key}" /f`, { stdio: 'pipe' });
|
|
62
|
-
|
|
64
|
+
return true;
|
|
65
|
+
} catch (_) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
|
-
*
|
|
71
|
+
* 转义注册表数据中的特殊字符
|
|
67
72
|
*/
|
|
68
73
|
function escapeRegData(str) {
|
|
69
|
-
|
|
70
|
-
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
74
|
+
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%');
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
// ─── 菜单结构常量 ─────────────────────────────────────────────────────────────
|
|
74
78
|
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// 使用 HKCU,无需管理员权限
|
|
80
|
+
const HKCU_SHELL = 'HKEY_CURRENT_USER\\Software\\Classes';
|
|
81
|
+
|
|
82
|
+
// HTML 文件右键
|
|
83
|
+
const HTML_ROOTS = [
|
|
84
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.html\\shell`,
|
|
85
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.htm\\shell`,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// YAML 文件右键
|
|
89
|
+
const YAML_ROOTS = [
|
|
90
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.yaml\\shell`,
|
|
91
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.yml\\shell`,
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// CommandStore 子命令注册空间
|
|
95
|
+
const SUBCMD_SPACE = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
|
|
96
|
+
|
|
97
|
+
// 子命令名称常量(register / unregister 共用,避免硬编码不一致)
|
|
98
|
+
const SUBCMDS = {
|
|
99
|
+
generate: 'JuiceEmail.Generate',
|
|
100
|
+
snippet: 'JuiceEmail.Snippet',
|
|
101
|
+
config: 'JuiceEmail.WithConfig',
|
|
102
|
+
pwsh: 'JuiceEmail.OpenPwsh',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// 旧版本可能残留的子命令名(升级时清理)
|
|
106
|
+
const LEGACY_SUBCMDS = [
|
|
107
|
+
'JuiceEmail.Generate',
|
|
108
|
+
'JuiceEmail.Snippet',
|
|
109
|
+
'JuiceEmail.OpenPwsh',
|
|
79
110
|
];
|
|
80
111
|
|
|
81
|
-
// 父菜单键名(与 PowerShell 7 风格保持一致:不含特殊字符的英文键名)
|
|
82
112
|
const PARENT_KEY_NAME = 'JuiceEmail';
|
|
83
|
-
// 子命令注册表空间(SubCommands 引用的命令存放在 shell\JuiceEmail.SubCommands 下)
|
|
84
|
-
// PowerShell 7 的做法:在 shell 同级放一个专用 SubCommands 键
|
|
85
|
-
const SUBCMD_SPACE = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
|
|
86
113
|
|
|
87
114
|
// ─── 注册 ─────────────────────────────────────────────────────────────────────
|
|
88
115
|
|
|
89
116
|
async function registerContextMenu() {
|
|
90
|
-
console.log(chalk.cyan('\n 注册 juice
|
|
117
|
+
console.log(chalk.cyan('\n 注册 juice 右键菜单(当前用户,无需管理员权限)...\n'));
|
|
91
118
|
|
|
92
119
|
const nodePath = getNodePath();
|
|
93
120
|
const scriptPath = getJuiceScript();
|
|
94
121
|
const iconPath = getIconPath();
|
|
95
122
|
|
|
96
|
-
|
|
97
|
-
const subCmd1 = `JuiceEmail.Generate`;
|
|
98
|
-
// 子命令 2:片段组装
|
|
99
|
-
const subCmd2 = `JuiceEmail.Snippet`;
|
|
100
|
-
// 子命令 3:在此打开 PowerShell(可选便利项)
|
|
101
|
-
const subCmd3 = `JuiceEmail.OpenPwsh`;
|
|
123
|
+
let ok = true;
|
|
102
124
|
|
|
103
|
-
// ──
|
|
125
|
+
// ── 子命令 1:普通模式 - juice 生成 ──────────────────────────────────────
|
|
126
|
+
const generateCmd = `"${nodePath}" "${scriptPath}" -f %1`;
|
|
127
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, '', 'REG_SZ', '⚡ 生成邮件 HTML(标准 + 压缩)') && ok;
|
|
128
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, 'Icon', 'REG_SZ', iconPath);
|
|
129
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}\\command`, '', 'REG_SZ', generateCmd);
|
|
104
130
|
|
|
105
|
-
// 子命令
|
|
106
|
-
const
|
|
107
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
108
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
109
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
131
|
+
// ── 子命令 2:片段模式 - 片段组装 ────────────────────────────────────────
|
|
132
|
+
const snippetCmd = `"${nodePath}" "${scriptPath}" -s %1`;
|
|
133
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, '', 'REG_SZ', '🧩 邮件片段组装(交互选择模板)') && ok;
|
|
134
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, 'Icon', 'REG_SZ', iconPath);
|
|
135
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}\\command`, '', 'REG_SZ', snippetCmd);
|
|
110
136
|
|
|
111
|
-
// 子命令
|
|
112
|
-
const
|
|
113
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
114
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
115
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
137
|
+
// ── 子命令 3:配置文件模式 - 交互式生成 ──────────────────────────────────
|
|
138
|
+
const configCmd = `"${nodePath}" "${scriptPath}" -c %1`;
|
|
139
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, '', 'REG_SZ', '📋 交互式生成邮件(使用此配置)') && ok;
|
|
140
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, 'Icon', 'REG_SZ', iconPath);
|
|
141
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}\\command`, '', 'REG_SZ', configCmd);
|
|
116
142
|
|
|
117
|
-
// 子命令
|
|
143
|
+
// ── 子命令 4:在文件目录打开 pwsh(可选)──────────────────────────────────
|
|
118
144
|
const pwshPath = resolvePwsh();
|
|
119
145
|
if (pwshPath) {
|
|
120
|
-
const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%
|
|
121
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
122
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
123
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
146
|
+
const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`;
|
|
147
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, '', 'REG_SZ', '📂 在此目录打开 PowerShell 7') && ok;
|
|
148
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, 'Icon', 'REG_SZ', pwshPath);
|
|
149
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}\\command`, '', 'REG_SZ', pwshCmd);
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
// ──
|
|
152
|
+
// ── 注册 HTML 父菜单(二级子菜单)────────────────────────────────────────
|
|
153
|
+
const htmlSubs = pwshPath
|
|
154
|
+
? `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.pwsh}`
|
|
155
|
+
: `${SUBCMDS.generate};${SUBCMDS.snippet}`;
|
|
127
156
|
|
|
128
|
-
|
|
129
|
-
if (pwshPath) subCommands += `;${subCmd3}`;
|
|
130
|
-
|
|
131
|
-
for (const root of ROOTS) {
|
|
157
|
+
for (const root of HTML_ROOTS) {
|
|
132
158
|
const parentKey = `${root}\\${PARENT_KEY_NAME}`;
|
|
159
|
+
ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
|
|
160
|
+
regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
|
|
161
|
+
regAdd(parentKey, 'SubCommands', 'REG_SZ', htmlSubs);
|
|
162
|
+
}
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
// ── 注册 YAML 父菜单(二级子菜单)────────────────────────────────────────
|
|
165
|
+
for (const root of YAML_ROOTS) {
|
|
166
|
+
const parentKey = `${root}\\${PARENT_KEY_NAME}`;
|
|
167
|
+
ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
|
|
137
168
|
regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
regAdd(parentKey, 'SubCommands', 'REG_SZ', SUBCMDS.config);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── 输出 ──────────────────────────────────────────────────────────────────
|
|
173
|
+
if (!ok) {
|
|
174
|
+
console.log(chalk.yellow(' ⚠ 部分注册表项写入失败,右键菜单可能不完整。\n'));
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
console.log(
|
|
145
178
|
'\n' +
|
|
146
179
|
chalk.green(' ✔ 右键菜单注册完成!') + '\n\n' +
|
|
147
|
-
`
|
|
148
|
-
`
|
|
149
|
-
`
|
|
150
|
-
`
|
|
151
|
-
(pwshPath ? `
|
|
180
|
+
` ${chalk.bold('.html / .htm')} 文件右键:\n` +
|
|
181
|
+
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
182
|
+
` ├── ⚡ 生成邮件 HTML(标准 + 压缩) → juice -f\n` +
|
|
183
|
+
` ├── 🧩 邮件片段组装(交互选择模板) → juice -s\n` +
|
|
184
|
+
(pwshPath ? ` └── 📂 在此目录打开 PowerShell 7\n` : '') +
|
|
185
|
+
`\n ${chalk.bold('.yaml / .yml')} 文件右键:\n` +
|
|
186
|
+
` └── 📧 交互式生成邮件(使用此配置) → juice -c\n` +
|
|
152
187
|
'\n' +
|
|
153
188
|
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
154
189
|
);
|
|
@@ -159,15 +194,29 @@ async function registerContextMenu() {
|
|
|
159
194
|
async function unregisterContextMenu() {
|
|
160
195
|
console.log(chalk.cyan('\n 取消注册 juice 右键菜单...\n'));
|
|
161
196
|
|
|
162
|
-
|
|
163
|
-
|
|
197
|
+
let removed = 0;
|
|
198
|
+
|
|
199
|
+
// 清理 HTML 父菜单
|
|
200
|
+
for (const root of HTML_ROOTS) {
|
|
201
|
+
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
164
202
|
}
|
|
165
203
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
204
|
+
// 清理 YAML 父菜单
|
|
205
|
+
for (const root of YAML_ROOTS) {
|
|
206
|
+
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 清理子命令(当前版本 + 旧版本残留)
|
|
210
|
+
const allSubcmds = [...new Set([...Object.values(SUBCMDS), ...LEGACY_SUBCMDS])];
|
|
211
|
+
for (const name of allSubcmds) {
|
|
212
|
+
regDelete(`${SUBCMD_SPACE}\\${name}`);
|
|
213
|
+
}
|
|
169
214
|
|
|
170
|
-
|
|
215
|
+
if (removed > 0) {
|
|
216
|
+
console.log(chalk.green(` ✔ 已移除 ${removed} 个右键菜单项。\n`));
|
|
217
|
+
} else {
|
|
218
|
+
console.log(chalk.gray(' ℹ 未找到已注册的右键菜单,无需卸载。\n'));
|
|
219
|
+
}
|
|
171
220
|
}
|
|
172
221
|
|
|
173
222
|
// ─── 工具:查找 PowerShell 7 ─────────────────────────────────────────────────
|
|
@@ -181,7 +230,6 @@ function resolvePwsh() {
|
|
|
181
230
|
for (const p of candidates) {
|
|
182
231
|
if (require('fs').existsSync(p)) return p;
|
|
183
232
|
}
|
|
184
|
-
// 尝试 where.exe
|
|
185
233
|
try {
|
|
186
234
|
const result = execSync('where pwsh', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
|
|
187
235
|
if (result && require('fs').existsSync(result)) return result;
|
package/src/snippet.js
CHANGED
|
@@ -479,6 +479,32 @@ async function promptOutputName(defaultBaseName, cwd) {
|
|
|
479
479
|
return baseName;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
/**
|
|
483
|
+
* 无片段可用时,将模板拷贝到当前目录作为快捷副本
|
|
484
|
+
*/
|
|
485
|
+
async function copyTemplateToCwd(templatePath) {
|
|
486
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
487
|
+
|
|
488
|
+
const templateName = path.parse(templatePath).name;
|
|
489
|
+
console.log(chalk.cyan(`\n 可将模板「${path.basename(templatePath)}」拷贝到当前目录作为快捷副本。`));
|
|
490
|
+
|
|
491
|
+
const proceed = await confirm({
|
|
492
|
+
message: '是否拷贝模板到当前目录?',
|
|
493
|
+
default: true,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
if (!proceed) {
|
|
497
|
+
console.log(chalk.gray('已取消。'));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const baseName = await promptOutputName(templateName, process.cwd());
|
|
502
|
+
const destPath = path.join(process.cwd(), baseName + '.html');
|
|
503
|
+
|
|
504
|
+
fs.copyFileSync(templatePath, destPath);
|
|
505
|
+
console.log(chalk.green(`\n✔ 模板已拷贝到:${destPath}`));
|
|
506
|
+
}
|
|
507
|
+
|
|
482
508
|
async function promptConfirm(summary) {
|
|
483
509
|
const { confirm } = await import('@inquirer/prompts');
|
|
484
510
|
const outPaths = resolveSnippetOutputPaths(summary.outputBaseName, summary.outputDir);
|
|
@@ -621,8 +647,8 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
621
647
|
// 3. 选择片段文件夹
|
|
622
648
|
const snippetFolders = findSnippetFolders(brand.path);
|
|
623
649
|
if (snippetFolders.length === 0) {
|
|
624
|
-
console.log(chalk.yellow(`\n ⚠ 品牌「${brand.name}
|
|
625
|
-
|
|
650
|
+
console.log(chalk.yellow(`\n ⚠ 品牌「${brand.name}」下暂无片段系列。`));
|
|
651
|
+
await copyTemplateToCwd(templateChoice.path);
|
|
626
652
|
return;
|
|
627
653
|
}
|
|
628
654
|
const folder = await promptSnippetFolder(snippetFolders);
|