juice-email-cli 2.1.8 → 2.1.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.md +15 -9
- package/bin/juice.js +12 -5
- package/edm/elabscience/elabscience-template.html +1 -1
- package/edm/elabscience/literature/snippet.html +1 -1
- package/package.json +3 -4
- package/src/context-menu.js +131 -72
- package/src/index.js +21 -8
- package/src/snippet.js +47 -13
package/README.md
CHANGED
|
@@ -105,8 +105,8 @@ juice
|
|
|
105
105
|
| `--snippet <path>` | `-s` | 片段 HTML 文件路径:插入到模板 `<tbody id="content">` |
|
|
106
106
|
| `--config <path>` | `-c` | 配置文件路径,不指定时自动查找 |
|
|
107
107
|
| `--name <name>` | `-n` | 片段模式输出文件名(不含扩展名) |
|
|
108
|
-
| `--install` | | 注册 Windows
|
|
109
|
-
| `--uninstall` | | 取消 Windows
|
|
108
|
+
| `--install` | | 注册 Windows 右键菜单(当前用户,无需管理员) |
|
|
109
|
+
| `--uninstall` | | 取消 Windows 右键菜单注册 |
|
|
110
110
|
| `--version` | `-v` | 查看版本号 |
|
|
111
111
|
| `--help` | `-h` | 查看帮助 |
|
|
112
112
|
|
|
@@ -312,22 +312,28 @@ edm/
|
|
|
312
312
|
|
|
313
313
|
## Windows 右键菜单
|
|
314
314
|
|
|
315
|
-
注册后,在 `.html` / `.htm`
|
|
315
|
+
注册后,在 `.html` / `.htm` / `.yaml` / `.yml` 文件上右键可看到统一子菜单(所有文件类型共享):
|
|
316
316
|
|
|
317
317
|
```
|
|
318
318
|
📧 用 juice 生成邮件 HTML
|
|
319
|
-
├──
|
|
320
|
-
├── 🧩
|
|
321
|
-
|
|
319
|
+
├── 📄 作为模板,生成邮件 HTML → juice -f %1(后台执行)
|
|
320
|
+
├── 🧩 作为片段,拼接邮件 HTML → juice -s %1(交互选择模板)
|
|
321
|
+
├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
|
|
322
|
+
└── 📂 打开 PowerShell ← 仅已安装 pwsh 时出现
|
|
322
323
|
```
|
|
323
324
|
|
|
325
|
+
| 子命令 | 说明 |
|
|
326
|
+
|--------|------|
|
|
327
|
+
| 📄 作为模板,生成邮件 HTML | 将文件当模板处理,CSS 内联 + 变量替换 + 压缩 |
|
|
328
|
+
| 🧩 作为片段,拼接邮件 HTML | 将文件当片段,打开终端选择模板后拼接输出 |
|
|
329
|
+
| ⚙️ 作为配置,拼接邮件 HTML | 将文件当配置,打开终端选择品牌/模板/片段 |
|
|
330
|
+
|
|
324
331
|
```bash
|
|
325
|
-
#
|
|
326
|
-
juice --install # 注册
|
|
332
|
+
juice --install # 注册(当前用户,无需管理员)
|
|
327
333
|
juice --uninstall # 卸载
|
|
328
334
|
```
|
|
329
335
|
|
|
330
|
-
>
|
|
336
|
+
> **注意**:注册到当前用户(HKCU),无需管理员权限。注册成功后如菜单未立即出现,重启文件资源管理器(`explorer.exe`)即可。
|
|
331
337
|
|
|
332
338
|
---
|
|
333
339
|
|
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} ║
|
|
@@ -63,9 +63,16 @@ program
|
|
|
63
63
|
右键菜单
|
|
64
64
|
════════════════════════════════════════════════════════════════
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
juice --install
|
|
68
|
-
juice --uninstall
|
|
66
|
+
注册后,在 .html / .htm / .yaml / .yml 文件上右键即可看到:
|
|
67
|
+
juice --install (当前用户,无需管理员)
|
|
68
|
+
juice --uninstall (卸载右键菜单)
|
|
69
|
+
|
|
70
|
+
菜单结构(所有文件类型统一):
|
|
71
|
+
📧 用 juice 生成邮件 HTML
|
|
72
|
+
├── 📄 作为模板,生成邮件 HTML → juice -f %1(后台执行)
|
|
73
|
+
├── 🧩 作为片段,拼接邮件 HTML → juice -s %1(交互选择模板)
|
|
74
|
+
├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
|
|
75
|
+
└── 📂 打开 PowerShell (仅已安装 pwsh 时出现)
|
|
69
76
|
|
|
70
77
|
更多信息:https://gitee.com/siriussupreme/juice-cli
|
|
71
78
|
`)
|
|
@@ -1524,7 +1524,7 @@
|
|
|
1524
1524
|
|
|
1525
1525
|
<tr>
|
|
1526
1526
|
<td>Quality Control</td>
|
|
1527
|
-
<td colspan="2">Sterilized by 0.1 μm filtration, Endotoxin Content
|
|
1527
|
+
<td colspan="2">Sterilized by 0.1 μm filtration, Endotoxin Content < 3 EU/mL.</td>
|
|
1528
1528
|
</tr>
|
|
1529
1529
|
</tbody>
|
|
1530
1530
|
</table>
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "juice-email-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.10",
|
|
4
4
|
"description": "CLI tool to generate email-client-compatible HTML with juice (CSS inlining) + Mustache templating + minification",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/juice.js",
|
|
11
|
-
"test": "node
|
|
11
|
+
"test": "node test/smoke.js",
|
|
12
12
|
"postinstall": "node bin/juice.js --install",
|
|
13
13
|
"preuninstall": "node bin/juice.js --uninstall",
|
|
14
14
|
"prepare": "husky install",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"ora": "^5.4.1"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">=16.0.0"
|
|
50
50
|
},
|
|
51
51
|
"files": [
|
|
52
52
|
"bin/",
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
"@commitlint/cli": "^21.0.1",
|
|
61
61
|
"@commitlint/config-conventional": "^21.0.1",
|
|
62
62
|
"husky": "^9.1.7",
|
|
63
|
-
"inquirer": "^13.4.3",
|
|
64
63
|
"standard-version": "^9.5.0"
|
|
65
64
|
}
|
|
66
65
|
}
|
package/src/context-menu.js
CHANGED
|
@@ -3,18 +3,15 @@
|
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* 菜单结构(所有文件类型统一):
|
|
9
|
+
* .html / .htm / .yaml / .yml:
|
|
10
|
+
* 📧 用 juice 生成邮件 HTML
|
|
11
|
+
* ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)
|
|
12
|
+
* ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)
|
|
13
|
+
* ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)
|
|
14
|
+
* └── 📂 打开 PowerShell (可选)
|
|
18
15
|
*/
|
|
19
16
|
|
|
20
17
|
const { execSync } = require('child_process');
|
|
@@ -47,108 +44,158 @@ function regAdd(key, valueName, type, data) {
|
|
|
47
44
|
const cmd = `reg add "${key}" ${vFlag} ${tFlag} ${dFlag} /f`.replace(/\s+/g, ' ').trim();
|
|
48
45
|
try {
|
|
49
46
|
execSync(cmd, { stdio: 'pipe' });
|
|
47
|
+
return true;
|
|
50
48
|
} catch (e) {
|
|
51
49
|
const msg = e.stderr ? e.stderr.toString().trim() : e.message;
|
|
52
|
-
console.warn(chalk.yellow(` ⚠ reg add
|
|
50
|
+
console.warn(chalk.yellow(` ⚠ reg add 失败\n 键:${key}\n 原因:${msg}`));
|
|
51
|
+
return false;
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
/**
|
|
57
|
-
* 执行 reg delete
|
|
56
|
+
* 执行 reg delete,键不存在时静默忽略,返回是否实际删除了
|
|
58
57
|
*/
|
|
59
58
|
function regDelete(key) {
|
|
60
59
|
try {
|
|
61
60
|
execSync(`reg delete "${key}" /f`, { stdio: 'pipe' });
|
|
62
|
-
|
|
61
|
+
return true;
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
66
|
-
*
|
|
68
|
+
* 转义注册表数据中的特殊字符
|
|
67
69
|
*/
|
|
68
70
|
function escapeRegData(str) {
|
|
69
|
-
|
|
70
|
-
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
71
|
+
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%');
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
// ─── 菜单结构常量 ─────────────────────────────────────────────────────────────
|
|
74
75
|
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// 使用 HKCU,无需管理员权限
|
|
77
|
+
const HKCU_SHELL = 'HKEY_CURRENT_USER\\Software\\Classes';
|
|
78
|
+
|
|
79
|
+
// HTML 文件右键
|
|
80
|
+
const HTML_ROOTS = [
|
|
81
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.html\\shell`,
|
|
82
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.htm\\shell`,
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// YAML 文件右键
|
|
86
|
+
const YAML_ROOTS = [
|
|
87
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.yaml\\shell`,
|
|
88
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.yml\\shell`,
|
|
79
89
|
];
|
|
80
90
|
|
|
81
|
-
//
|
|
91
|
+
// CommandStore 子命令注册空间
|
|
92
|
+
const SUBCMD_SPACE = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
|
|
93
|
+
|
|
94
|
+
// 子命令名称常量(register / unregister 共用,避免硬编码不一致)
|
|
95
|
+
const SUBCMDS = {
|
|
96
|
+
generate: 'JuiceEmail.Generate',
|
|
97
|
+
snippet: 'JuiceEmail.Snippet',
|
|
98
|
+
config: 'JuiceEmail.WithConfig',
|
|
99
|
+
pwsh: 'JuiceEmail.OpenPwsh',
|
|
100
|
+
};
|
|
101
|
+
|
|
82
102
|
const PARENT_KEY_NAME = 'JuiceEmail';
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 为交互式命令包一层 PowerShell:
|
|
106
|
+
* - 成功:窗口自动关闭
|
|
107
|
+
* - 失败:窗口保持开启,显示重新执行的命令
|
|
108
|
+
*/
|
|
109
|
+
function wrapInteractive(nodePath, scriptPath, cliArgs) {
|
|
110
|
+
const node = nodePath.replace(/'/g, "''");
|
|
111
|
+
const script = scriptPath.replace(/'/g, "''");
|
|
112
|
+
// %1 由 Windows 在 CreateProcess 前展开为实际文件路径
|
|
113
|
+
const ps = [
|
|
114
|
+
`& '${node}' '${script}' ${cliArgs}`,
|
|
115
|
+
`if ($LASTEXITCODE) {`,
|
|
116
|
+
` Write-Host ''`,
|
|
117
|
+
` Write-Host '[Failed] Exit code:' $LASTEXITCODE`,
|
|
118
|
+
` Write-Host '[Re-run] juice ${cliArgs}'`,
|
|
119
|
+
` Write-Host ''`,
|
|
120
|
+
` Read-Host 'Press Enter to close'`,
|
|
121
|
+
`}`,
|
|
122
|
+
].join('; ');
|
|
123
|
+
return `powershell.exe -Command "${ps}"`;
|
|
124
|
+
}
|
|
86
125
|
|
|
87
126
|
// ─── 注册 ─────────────────────────────────────────────────────────────────────
|
|
88
127
|
|
|
89
128
|
async function registerContextMenu() {
|
|
90
|
-
console.log(chalk.cyan('\n 注册 juice
|
|
129
|
+
console.log(chalk.cyan('\n 注册 juice 右键菜单(当前用户,无需管理员权限)...\n'));
|
|
91
130
|
|
|
92
131
|
const nodePath = getNodePath();
|
|
93
132
|
const scriptPath = getJuiceScript();
|
|
94
133
|
const iconPath = getIconPath();
|
|
95
134
|
|
|
96
|
-
|
|
97
|
-
const subCmd1 = `JuiceEmail.Generate`;
|
|
98
|
-
// 子命令 2:片段组装
|
|
99
|
-
const subCmd2 = `JuiceEmail.Snippet`;
|
|
100
|
-
// 子命令 3:在此打开 PowerShell(可选便利项)
|
|
101
|
-
const subCmd3 = `JuiceEmail.OpenPwsh`;
|
|
135
|
+
let ok = true;
|
|
102
136
|
|
|
103
|
-
// ──
|
|
137
|
+
// ── 子命令 1:普通模式 - juice 生成(非交互,不需终端)─────────────────
|
|
138
|
+
const generateCmd = `"${nodePath}" "${scriptPath}" -f %1`;
|
|
139
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, '', 'REG_SZ', '📄 作为模板,生成邮件 HTML') && ok;
|
|
140
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, 'Icon', 'REG_SZ', iconPath);
|
|
141
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}\\command`, '', 'REG_SZ', generateCmd);
|
|
104
142
|
|
|
105
|
-
// 子命令
|
|
106
|
-
const
|
|
107
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
108
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
109
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
143
|
+
// ── 子命令 2:片段模式 - 片段组装(交互,需要终端)────────────────────────
|
|
144
|
+
const snippetCmd = wrapInteractive(nodePath, scriptPath, '-s %1');
|
|
145
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, '', 'REG_SZ', '🧩 作为片段,拼接邮件 HTML') && ok;
|
|
146
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, 'Icon', 'REG_SZ', iconPath);
|
|
147
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}\\command`, '', 'REG_SZ', snippetCmd);
|
|
110
148
|
|
|
111
|
-
// 子命令
|
|
112
|
-
const
|
|
113
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
114
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
115
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
149
|
+
// ── 子命令 3:配置文件模式 - 交互式生成(交互,需要终端)──────────────────
|
|
150
|
+
const configCmd = wrapInteractive(nodePath, scriptPath, '-c %1');
|
|
151
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, '', 'REG_SZ', '⚙️ 作为配置,拼接邮件 HTML') && ok;
|
|
152
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, 'Icon', 'REG_SZ', iconPath);
|
|
153
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}\\command`, '', 'REG_SZ', configCmd);
|
|
116
154
|
|
|
117
|
-
// 子命令
|
|
155
|
+
// ── 子命令 4:在文件目录打开 pwsh(可选)──────────────────────────────────
|
|
118
156
|
const pwshPath = resolvePwsh();
|
|
119
157
|
if (pwshPath) {
|
|
120
|
-
const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%
|
|
121
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
122
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
123
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
158
|
+
const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`;
|
|
159
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, '', 'REG_SZ', '📂 打开 PowerShell') && ok;
|
|
160
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, 'Icon', 'REG_SZ', pwshPath);
|
|
161
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}\\command`, '', 'REG_SZ', pwshCmd);
|
|
124
162
|
}
|
|
125
163
|
|
|
126
|
-
// ──
|
|
164
|
+
// ── 所有文件类型共用同一套子命令 ────────────────────────────────────────
|
|
165
|
+
const allSubs = pwshPath
|
|
166
|
+
? `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.config};${SUBCMDS.pwsh}`
|
|
167
|
+
: `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.config}`;
|
|
127
168
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
for (const root of ROOTS) {
|
|
169
|
+
// .html / .htm
|
|
170
|
+
for (const root of HTML_ROOTS) {
|
|
132
171
|
const parentKey = `${root}\\${PARENT_KEY_NAME}`;
|
|
172
|
+
ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
|
|
173
|
+
regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
|
|
174
|
+
regAdd(parentKey, 'SubCommands', 'REG_SZ', allSubs);
|
|
175
|
+
}
|
|
133
176
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
177
|
+
// .yaml / .yml(与 .html 共享同一套子命令)
|
|
178
|
+
for (const root of YAML_ROOTS) {
|
|
179
|
+
const parentKey = `${root}\\${PARENT_KEY_NAME}`;
|
|
180
|
+
ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
|
|
137
181
|
regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
182
|
+
regAdd(parentKey, 'SubCommands', 'REG_SZ', allSubs);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── 输出 ──────────────────────────────────────────────────────────────────
|
|
186
|
+
if (!ok) {
|
|
187
|
+
console.log(chalk.yellow(' ⚠ 部分注册表项写入失败,右键菜单可能不完整。\n'));
|
|
142
188
|
}
|
|
143
189
|
|
|
144
190
|
console.log(
|
|
145
191
|
'\n' +
|
|
146
192
|
chalk.green(' ✔ 右键菜单注册完成!') + '\n\n' +
|
|
147
|
-
`
|
|
148
|
-
`
|
|
149
|
-
`
|
|
150
|
-
`
|
|
151
|
-
|
|
193
|
+
` ${chalk.bold('.html / .htm / .yaml / .yml')} 文件右键:\n` +
|
|
194
|
+
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
195
|
+
` ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)\n` +
|
|
196
|
+
` ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)\n` +
|
|
197
|
+
` ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)\n` +
|
|
198
|
+
(pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
|
|
152
199
|
'\n' +
|
|
153
200
|
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
154
201
|
);
|
|
@@ -159,15 +206,28 @@ async function registerContextMenu() {
|
|
|
159
206
|
async function unregisterContextMenu() {
|
|
160
207
|
console.log(chalk.cyan('\n 取消注册 juice 右键菜单...\n'));
|
|
161
208
|
|
|
162
|
-
|
|
163
|
-
|
|
209
|
+
let removed = 0;
|
|
210
|
+
|
|
211
|
+
// 清理 HTML 父菜单
|
|
212
|
+
for (const root of HTML_ROOTS) {
|
|
213
|
+
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
164
214
|
}
|
|
165
215
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
216
|
+
// 清理 YAML 父菜单
|
|
217
|
+
for (const root of YAML_ROOTS) {
|
|
218
|
+
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
219
|
+
}
|
|
169
220
|
|
|
170
|
-
|
|
221
|
+
// 清理子命令
|
|
222
|
+
for (const name of Object.values(SUBCMDS)) {
|
|
223
|
+
regDelete(`${SUBCMD_SPACE}\\${name}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (removed > 0) {
|
|
227
|
+
console.log(chalk.green(` ✔ 已移除 ${removed} 个右键菜单项。\n`));
|
|
228
|
+
} else {
|
|
229
|
+
console.log(chalk.gray(' ℹ 未找到已注册的右键菜单,无需卸载。\n'));
|
|
230
|
+
}
|
|
171
231
|
}
|
|
172
232
|
|
|
173
233
|
// ─── 工具:查找 PowerShell 7 ─────────────────────────────────────────────────
|
|
@@ -181,7 +241,6 @@ function resolvePwsh() {
|
|
|
181
241
|
for (const p of candidates) {
|
|
182
242
|
if (require('fs').existsSync(p)) return p;
|
|
183
243
|
}
|
|
184
|
-
// 尝试 where.exe
|
|
185
244
|
try {
|
|
186
245
|
const result = execSync('where pwsh', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
|
|
187
246
|
if (result && require('fs').existsSync(result)) return result;
|
package/src/index.js
CHANGED
|
@@ -33,6 +33,10 @@ const HOME_CANDIDATES = [
|
|
|
33
33
|
path.join(os.homedir(), 'juice.yml'),
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
+
function resolveHomeConfig() {
|
|
37
|
+
return HOME_CANDIDATES.find((c) => fs.existsSync(c)) || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
// ─── 配置文件查找 ─────────────────────────────────────────────────────────────
|
|
37
41
|
|
|
38
42
|
/**
|
|
@@ -73,7 +77,7 @@ function findConfigs(configPath, inputFile) {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
return { highPriorityPath, homePath:
|
|
80
|
+
return { highPriorityPath, homePath: resolveHomeConfig() };
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
// ─── 配置文件加载 ─────────────────────────────────────────────────────────────
|
|
@@ -121,14 +125,13 @@ function deepMerge(base, ...overrides) {
|
|
|
121
125
|
* CLI 内置默认值 < 用户目录 ~/juice.yaml < 优先配置(-c/输入目录)
|
|
122
126
|
*/
|
|
123
127
|
function buildConfig(highPriorityPath, homePath) {
|
|
124
|
-
const homeExists = fs.existsSync(homePath);
|
|
125
128
|
const layers = [];
|
|
126
129
|
|
|
127
130
|
// 1. CLI 内置默认值(最低优先级)
|
|
128
131
|
layers.push({ label: 'CLI 内置默认值', data: CODE_DEFAULTS });
|
|
129
132
|
|
|
130
133
|
// 2. 用户目录配置(如果有)
|
|
131
|
-
if (
|
|
134
|
+
if (homePath) {
|
|
132
135
|
layers.push({ label: `用户目录配置 (${homePath})`, data: loadYaml(homePath) });
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -148,11 +151,15 @@ function processTemplate(inputFile, config) {
|
|
|
148
151
|
|
|
149
152
|
// 1. Mustache 变量替换(根据 rawHtml 配置决定是否转义 HTML)
|
|
150
153
|
const originalEscape = Mustache.escape;
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
let htmlWithVars;
|
|
155
|
+
try {
|
|
156
|
+
if (config.rawHtml) {
|
|
157
|
+
Mustache.escape = (text) => text;
|
|
158
|
+
}
|
|
159
|
+
htmlWithVars = Mustache.render(htmlRaw, config.variables || {});
|
|
160
|
+
} finally {
|
|
161
|
+
Mustache.escape = originalEscape;
|
|
153
162
|
}
|
|
154
|
-
const htmlWithVars = Mustache.render(htmlRaw, config.variables || {});
|
|
155
|
-
Mustache.escape = originalEscape;
|
|
156
163
|
|
|
157
164
|
// 2. 收集 extraCssFiles
|
|
158
165
|
const basePath = path.dirname(path.resolve(inputFile));
|
|
@@ -228,10 +235,16 @@ async function run({ file, config: configPath }) {
|
|
|
228
235
|
|
|
229
236
|
// 5. 写出标准版
|
|
230
237
|
spinner.text = '写出 .output.html ...';
|
|
238
|
+
if (fs.existsSync(outPaths.normal)) {
|
|
239
|
+
spinner.warn(chalk.yellow(`目标文件已存在,将覆盖:${outPaths.normal}`));
|
|
240
|
+
}
|
|
231
241
|
fs.writeFileSync(outPaths.normal, resultHtml, encoding);
|
|
232
242
|
|
|
233
243
|
// 6. 压缩并写出
|
|
234
244
|
spinner.text = '写出 .minified.html ...';
|
|
245
|
+
if (fs.existsSync(outPaths.minified)) {
|
|
246
|
+
spinner.warn(chalk.yellow(`目标文件已存在,将覆盖:${outPaths.minified}`));
|
|
247
|
+
}
|
|
235
248
|
const minified = await minifyHtml(resultHtml, config.minify);
|
|
236
249
|
fs.writeFileSync(outPaths.minified, minified, encoding);
|
|
237
250
|
|
|
@@ -266,4 +279,4 @@ function savings(original, minified) {
|
|
|
266
279
|
return (((orig - mini) / orig) * 100).toFixed(1) + '%';
|
|
267
280
|
}
|
|
268
281
|
|
|
269
|
-
module.exports = { run, findConfigs, buildConfig, processTemplate, minifyHtml, deepMerge, collectExtraCss, loadYaml, fmtSize, savings };
|
|
282
|
+
module.exports = { run, findConfigs, buildConfig, processTemplate, minifyHtml, deepMerge, collectExtraCss, loadYaml, fmtSize, savings, DEFAULT_CONFIG_PATH, resolveHomeConfig };
|
package/src/snippet.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
minifyHtml,
|
|
14
14
|
fmtSize,
|
|
15
15
|
savings,
|
|
16
|
+
DEFAULT_CONFIG_PATH,
|
|
16
17
|
} = require('./index');
|
|
17
18
|
|
|
18
19
|
// ─── EDM 目录解析 ──────────────────────────────────────────────────────────────
|
|
@@ -95,8 +96,6 @@ function getBrand(filePath, edmDir) {
|
|
|
95
96
|
|
|
96
97
|
// ─── 配置合并 ─────────────────────────────────────────────────────────────────
|
|
97
98
|
|
|
98
|
-
const DEFAULT_CONFIG_PATH = path.resolve(__dirname, '..', 'defaults', 'juice.yaml');
|
|
99
|
-
|
|
100
99
|
/**
|
|
101
100
|
* 片段模式配置合并
|
|
102
101
|
*
|
|
@@ -114,8 +113,12 @@ function buildSnippetConfig({ priorityConfigPath, cliConfigPath }) {
|
|
|
114
113
|
layers.push({ label: 'CLI 内置默认值', data: defaults });
|
|
115
114
|
|
|
116
115
|
// 2. 用户主目录
|
|
117
|
-
const
|
|
118
|
-
|
|
116
|
+
const homeCandidates = [
|
|
117
|
+
path.join(os.homedir(), 'juice.yaml'),
|
|
118
|
+
path.join(os.homedir(), 'juice.yml'),
|
|
119
|
+
];
|
|
120
|
+
const homePath = homeCandidates.find((c) => fs.existsSync(c));
|
|
121
|
+
if (homePath) {
|
|
119
122
|
layers.push({ label: `用户目录配置 (${homePath})`, data: loadYaml(homePath) });
|
|
120
123
|
}
|
|
121
124
|
|
|
@@ -246,7 +249,7 @@ function resolveSnippetOutputPaths(outputBaseName, cwd) {
|
|
|
246
249
|
* 3. Juice CSS 内联 → .output.html
|
|
247
250
|
* 4. 压缩 → .minified.html
|
|
248
251
|
*/
|
|
249
|
-
async function assembleSnippet({ snippetPath, templatePath, config,
|
|
252
|
+
async function assembleSnippet({ snippetPath, templatePath, config, cwd, outputBaseName }) {
|
|
250
253
|
const templateHtml = fs.readFileSync(templatePath, 'utf8');
|
|
251
254
|
const snippetRaw = fs.readFileSync(snippetPath, 'utf8');
|
|
252
255
|
const outPaths = resolveSnippetOutputPaths(outputBaseName, cwd);
|
|
@@ -258,11 +261,15 @@ async function assembleSnippet({ snippetPath, templatePath, config, layers, cwd,
|
|
|
258
261
|
|
|
259
262
|
// 2. Mustache 渲染合并 HTML → .html(已渲染,无 juice 内联)
|
|
260
263
|
const originalEscape = Mustache.escape;
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
let renderedHtml;
|
|
265
|
+
try {
|
|
266
|
+
if (config.rawHtml) {
|
|
267
|
+
Mustache.escape = (text) => text;
|
|
268
|
+
}
|
|
269
|
+
renderedHtml = Mustache.render(rawMarkup, variables);
|
|
270
|
+
} finally {
|
|
271
|
+
Mustache.escape = originalEscape;
|
|
263
272
|
}
|
|
264
|
-
const renderedHtml = Mustache.render(rawMarkup, variables);
|
|
265
|
-
Mustache.escape = originalEscape;
|
|
266
273
|
fs.writeFileSync(outPaths.normal, renderedHtml, 'utf8');
|
|
267
274
|
|
|
268
275
|
// 3. 收集模板目录的额外 CSS + Juice CSS 内联 → .output.html
|
|
@@ -278,6 +285,7 @@ async function assembleSnippet({ snippetPath, templatePath, config, layers, cwd,
|
|
|
278
285
|
fs.writeFileSync(outPaths.minified, minified, 'utf8');
|
|
279
286
|
|
|
280
287
|
// 5. 报告
|
|
288
|
+
const layers = config._layers || [];
|
|
281
289
|
const layerLines = layers
|
|
282
290
|
.map((l) => ` ${chalk.gray('·')} ${l.label}`)
|
|
283
291
|
.join('\n');
|
|
@@ -479,6 +487,32 @@ async function promptOutputName(defaultBaseName, cwd) {
|
|
|
479
487
|
return baseName;
|
|
480
488
|
}
|
|
481
489
|
|
|
490
|
+
/**
|
|
491
|
+
* 无片段可用时,将模板拷贝到当前目录作为快捷副本
|
|
492
|
+
*/
|
|
493
|
+
async function copyTemplateToCwd(templatePath) {
|
|
494
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
495
|
+
|
|
496
|
+
const templateName = path.parse(templatePath).name;
|
|
497
|
+
console.log(chalk.cyan(`\n 可将模板「${path.basename(templatePath)}」拷贝到当前目录作为快捷副本。`));
|
|
498
|
+
|
|
499
|
+
const proceed = await confirm({
|
|
500
|
+
message: '是否拷贝模板到当前目录?',
|
|
501
|
+
default: true,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
if (!proceed) {
|
|
505
|
+
console.log(chalk.gray('已取消。'));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const baseName = await promptOutputName(templateName, process.cwd());
|
|
510
|
+
const destPath = path.join(process.cwd(), baseName + '.html');
|
|
511
|
+
|
|
512
|
+
fs.copyFileSync(templatePath, destPath);
|
|
513
|
+
console.log(chalk.green(`\n✔ 模板已拷贝到:${destPath}`));
|
|
514
|
+
}
|
|
515
|
+
|
|
482
516
|
async function promptConfirm(summary) {
|
|
483
517
|
const { confirm } = await import('@inquirer/prompts');
|
|
484
518
|
const outPaths = resolveSnippetOutputPaths(summary.outputBaseName, summary.outputDir);
|
|
@@ -559,6 +593,7 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
|
|
|
559
593
|
}
|
|
560
594
|
|
|
561
595
|
const { config, layers } = buildSnippetConfig({ priorityConfigPath, cliConfigPath });
|
|
596
|
+
config._layers = layers;
|
|
562
597
|
|
|
563
598
|
// 确定输出文件名
|
|
564
599
|
const defaultBaseName = outputName || path.parse(templatePath).name;
|
|
@@ -595,7 +630,6 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
|
|
|
595
630
|
snippetPath,
|
|
596
631
|
templatePath,
|
|
597
632
|
config,
|
|
598
|
-
layers,
|
|
599
633
|
cwd: process.cwd(),
|
|
600
634
|
outputBaseName,
|
|
601
635
|
});
|
|
@@ -621,8 +655,8 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
621
655
|
// 3. 选择片段文件夹
|
|
622
656
|
const snippetFolders = findSnippetFolders(brand.path);
|
|
623
657
|
if (snippetFolders.length === 0) {
|
|
624
|
-
console.log(chalk.yellow(`\n ⚠ 品牌「${brand.name}
|
|
625
|
-
|
|
658
|
+
console.log(chalk.yellow(`\n ⚠ 品牌「${brand.name}」下暂无片段系列。`));
|
|
659
|
+
await copyTemplateToCwd(templateChoice.path);
|
|
626
660
|
return;
|
|
627
661
|
}
|
|
628
662
|
const folder = await promptSnippetFolder(snippetFolders);
|
|
@@ -671,12 +705,12 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
671
705
|
|
|
672
706
|
// 8. 构建配置并执行
|
|
673
707
|
const { config, layers } = buildSnippetConfig({ priorityConfigPath, cliConfigPath });
|
|
708
|
+
config._layers = layers;
|
|
674
709
|
|
|
675
710
|
await assembleSnippet({
|
|
676
711
|
snippetPath: snippetFile.path,
|
|
677
712
|
templatePath: templateChoice.path,
|
|
678
713
|
config,
|
|
679
|
-
layers,
|
|
680
714
|
cwd: process.cwd(),
|
|
681
715
|
outputBaseName,
|
|
682
716
|
});
|