@chengzhao-dev/dsh-cpp-workflow 0.5.5 → 0.6.1
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.en.md +6 -2
- package/README.md +6 -2
- package/lib/client.js +757 -0
- package/lib/comment-policy.js +183 -0
- package/lib/index.js +35 -4
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +16 -6
- package/skills/developing-cpp/examples/comments-declaration.md +1 -1
- package/skills/developing-cpp/references/comments.md +4 -3
- package/skills/developing-cpp/scripts/checking-comments.mjs +25 -4
- package/skills/developing-cpp/steps/03-declaration.md +1 -1
package/README.en.md
CHANGED
|
@@ -19,17 +19,21 @@ The exception material follows the C++ Core Guidelines' Error handling section a
|
|
|
19
19
|
Pinned npm install:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.
|
|
22
|
+
dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.1
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The matching Git tag:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.
|
|
28
|
+
dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.1
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The desktop app owns the `desktop` profile. If the command-line `add` is rejected, add the plugin in the app settings and use the same pinned npm version. A Git tag install uses the prebuilt `lib/` committed in the repository and runs no build on the target machine; the package has no install script that needs approval.
|
|
32
32
|
|
|
33
|
+
### Settings
|
|
34
|
+
|
|
35
|
+
The plugin page and the "C++ Workflow" section under Settings share one page for file-header tags: `@file` is always the file name, `@brief` is written by default, and `@author`, `@date`, `@copyright`, `@version` stay off and are written only when they hold a non-empty value; `@param`, `@return`, `@throws` are required by default. The preview follows the switches, saving takes effect on the next skill read, and existing sources are never rewritten.
|
|
36
|
+
|
|
33
37
|
## Workspace gate
|
|
34
38
|
|
|
35
39
|
Only one condition must hold before either skill appears: walking up from the current directory to the Git root, the path contains `.agents/skills/cpp-workflow-project/SKILL.md`.
|
package/README.md
CHANGED
|
@@ -19,17 +19,21 @@
|
|
|
19
19
|
npm 安装钉死版本:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.
|
|
22
|
+
dsh plugin --profile web add npm:@chengzhao-dev/dsh-cpp-workflow@0.6.1
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
同版本的 Git 标签:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.
|
|
28
|
+
dsh plugin --profile web add github:chengzhao-dev/dsh-cpp-workflow#v0.6.1
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
桌面应用独占 `desktop` profile。命令行 `add` 被拒绝时,在应用设置里添加插件,地址填同一个 npm 版本。Git 标签安装使用仓库内已构建的 `lib/`,不在目标机器上重新构建;包内也不含需要授权执行的安装脚本。
|
|
32
32
|
|
|
33
|
+
### 设置
|
|
34
|
+
|
|
35
|
+
插件页与设置导航里的「C++ 工程流程」共用一页,只管文件头标签:`@file` 固定为文件名,`@brief` 默认写出,`@author`、`@date`、`@copyright`、`@version` 默认关闭且只在填了非空值时才写出;`@param`、`@return`、`@throws` 默认要求。预览随开关即时更新,保存后下一次读取技能即生效,已有源码不会被改写。
|
|
36
|
+
|
|
33
37
|
## 工作区门禁
|
|
34
38
|
|
|
35
39
|
一个会话里只需要满足一件事,技能就会出现:从当前目录向上、停在 git 根,沿途有 `.agents/skills/cpp-workflow-project/SKILL.md`。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
// 由 scripts/building-client.mjs 生成,请改 src/comment-policy.ts 与 src/client/app.cjs.js。
|
|
2
|
+
window.__ModuleLoader__.load({
|
|
3
|
+
id: "@chengzhao-dev/dsh-cpp-workflow",
|
|
4
|
+
factory: (require) => {
|
|
5
|
+
var module = { exports: {} }
|
|
6
|
+
var exports = module.exports
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 注释标签策略:设置页的预览、技能快照与检查器参数共用这一份。
|
|
10
|
+
*
|
|
11
|
+
* 配置只描述「写注释时默认带哪些标签」,不描述写法本身;写法在
|
|
12
|
+
* `skills/developing-cpp/references/comments.md`。设置页保存的值由宿主
|
|
13
|
+
* 写回插件 Config,服务端 `apply(ctx, config)` 用它生成本页快照与检查器参数。
|
|
14
|
+
*
|
|
15
|
+
* 这份文件被 `tsc` 编进 `lib/comment-policy.js`,打包客户端半侧时再内联进
|
|
16
|
+
* `lib/client.js`,所以只能依赖纯函数,不要引入 Node 或 React。
|
|
17
|
+
*/
|
|
18
|
+
/** 文件头标签。 */
|
|
19
|
+
const HEADER_TAGS = ['brief', 'author', 'date', 'copyright', 'version'];
|
|
20
|
+
/** 函数契约标签:开关决定是否要求写出。 */
|
|
21
|
+
const CONTRACT_TAGS = ['param', 'returns', 'throws'];
|
|
22
|
+
/** 折叠在「更多标签」里的可选标签:默认关闭。 */
|
|
23
|
+
const EXTRA_TAGS = ['note', 'see', 'pre', 'post', 'tparam', 'retval'];
|
|
24
|
+
/** 只有用户主动填了非空值才放行的隐私标签。 */
|
|
25
|
+
const PRIVACY_TAGS = ['author', 'date', 'copyright', 'version'];
|
|
26
|
+
/** 标签说明:设置页逐行显示,用作行的辅助文字。 */
|
|
27
|
+
const TAG_TEXT = {
|
|
28
|
+
brief: '一句话说明这个文件定义或实现什么',
|
|
29
|
+
author: '只在用户填了非空值时写出',
|
|
30
|
+
date: '只在用户填了非空值时写出',
|
|
31
|
+
copyright: '只在用户填了非空值时写出',
|
|
32
|
+
version: '只在用户填了非空值时写出',
|
|
33
|
+
param: '函数参数的含义、范围或所有权',
|
|
34
|
+
returns: '返回值的条件;返回类型自明时可以省略',
|
|
35
|
+
throws: '抛出的异常类型与条件;不抛异常的函数不写',
|
|
36
|
+
note: '调用方需要注意的补充说明',
|
|
37
|
+
see: '相关函数、类型或文档',
|
|
38
|
+
pre: '调用前必须成立的条件',
|
|
39
|
+
post: '调用后保证成立的条件',
|
|
40
|
+
tparam: '模板参数',
|
|
41
|
+
retval: '特定返回值的含义',
|
|
42
|
+
};
|
|
43
|
+
/** 开关型标签在设置页的标题。 */
|
|
44
|
+
const TAG_LABEL = {
|
|
45
|
+
brief: '@brief',
|
|
46
|
+
author: '@author',
|
|
47
|
+
date: '@date',
|
|
48
|
+
copyright: '@copyright',
|
|
49
|
+
version: '@version',
|
|
50
|
+
param: '@param',
|
|
51
|
+
returns: '@return',
|
|
52
|
+
throws: '@throws',
|
|
53
|
+
note: '@note',
|
|
54
|
+
see: '@see',
|
|
55
|
+
pre: '@pre',
|
|
56
|
+
post: '@post',
|
|
57
|
+
tparam: '@tparam',
|
|
58
|
+
retval: '@retval',
|
|
59
|
+
};
|
|
60
|
+
/** 预览里的示例摘要:用户没填 @brief 时用它,避免预览出现空标签。 */
|
|
61
|
+
const SAMPLE_BRIEF = '定义井字棋棋盘、格子坐标与格子类型。';
|
|
62
|
+
/** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
|
|
63
|
+
const DEFAULT_CONFIG = {
|
|
64
|
+
brief: { enabled: true, value: '' },
|
|
65
|
+
author: { enabled: false, value: '' },
|
|
66
|
+
date: { enabled: false, value: '' },
|
|
67
|
+
copyright: { enabled: false, value: '' },
|
|
68
|
+
version: { enabled: false, value: '' },
|
|
69
|
+
param: true,
|
|
70
|
+
returns: true,
|
|
71
|
+
throws: true,
|
|
72
|
+
note: false,
|
|
73
|
+
see: false,
|
|
74
|
+
pre: false,
|
|
75
|
+
post: false,
|
|
76
|
+
tparam: false,
|
|
77
|
+
retval: false,
|
|
78
|
+
};
|
|
79
|
+
function text(value) {
|
|
80
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
81
|
+
}
|
|
82
|
+
function flag(value, fallback) {
|
|
83
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 把宿主给的值补齐成完整配置;缺字段、旧版本或手工改坏的配置都回到出厂值。
|
|
87
|
+
*/
|
|
88
|
+
function normalizeConfig(raw) {
|
|
89
|
+
const source = raw && typeof raw === 'object' ? raw : {};
|
|
90
|
+
const config = {};
|
|
91
|
+
for (const tag of HEADER_TAGS) {
|
|
92
|
+
const entry = source[tag] && typeof source[tag] === 'object' ? source[tag] : {};
|
|
93
|
+
config[tag] = {
|
|
94
|
+
enabled: flag(entry.enabled, DEFAULT_CONFIG[tag].enabled),
|
|
95
|
+
value: text(entry.value),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
for (const tag of [...CONTRACT_TAGS, ...EXTRA_TAGS]) {
|
|
99
|
+
config[tag] = flag(source[tag], DEFAULT_CONFIG[tag]);
|
|
100
|
+
}
|
|
101
|
+
return config;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 按配置渲染文件头;`@file` 始终是传入的文件名。
|
|
105
|
+
*/
|
|
106
|
+
function renderFileHeader(raw, fileName) {
|
|
107
|
+
const config = normalizeConfig(raw);
|
|
108
|
+
const name = text(fileName) || 'board.h';
|
|
109
|
+
const lines = [`/// @file ${name}`];
|
|
110
|
+
if (config.brief.enabled) {
|
|
111
|
+
lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
|
|
112
|
+
}
|
|
113
|
+
for (const tag of ['author', 'date', 'copyright', 'version']) {
|
|
114
|
+
if (config[tag].enabled && config[tag].value)
|
|
115
|
+
lines.push(`/// @${tag} ${config[tag].value}`);
|
|
116
|
+
}
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 设置页的预览:文件头 + 一段函数注释,只显示当前配置会写出的标签。
|
|
121
|
+
*/
|
|
122
|
+
function previewParts(raw, sampleName) {
|
|
123
|
+
const config = normalizeConfig(raw);
|
|
124
|
+
const header = renderFileHeader(config, sampleName || 'board.h');
|
|
125
|
+
// 预览是示例:@brief 开着但用户没填句子时,用示例文件自己的摘要顶替。
|
|
126
|
+
const briefIndex = header.indexOf('/// @brief');
|
|
127
|
+
if (briefIndex !== -1)
|
|
128
|
+
header[briefIndex] = `/// @brief ${SAMPLE_BRIEF}`;
|
|
129
|
+
const contract = [];
|
|
130
|
+
if (config.param)
|
|
131
|
+
contract.push('/// @param position 目标行列坐标。');
|
|
132
|
+
if (config.returns)
|
|
133
|
+
contract.push('/// @return 放置成功时为 true。');
|
|
134
|
+
if (config.throws)
|
|
135
|
+
contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
|
|
136
|
+
const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
|
|
137
|
+
return { header, contract, declaration };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 设置页预览的纯文本形式;客户端半侧用同一份实现渲染 `<pre>`。
|
|
141
|
+
*/
|
|
142
|
+
function previewText(raw, sampleName) {
|
|
143
|
+
const { header, contract, declaration } = previewParts(raw, sampleName);
|
|
144
|
+
const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
|
|
145
|
+
if (contract.length > 0)
|
|
146
|
+
lines.push('///', ...contract);
|
|
147
|
+
lines.push(declaration);
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查。
|
|
152
|
+
*/
|
|
153
|
+
function checkerArgs(raw) {
|
|
154
|
+
const config = normalizeConfig(raw);
|
|
155
|
+
const args = [];
|
|
156
|
+
if (!config.brief.enabled)
|
|
157
|
+
args.push('--no-brief');
|
|
158
|
+
for (const tag of PRIVACY_TAGS) {
|
|
159
|
+
if (config[tag].enabled && config[tag].value)
|
|
160
|
+
args.push('--allow-tag', tag);
|
|
161
|
+
}
|
|
162
|
+
return args;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 技能快照:`get()` 把它接在技能正文末尾,让代理看到当前 profile 的标签开关。
|
|
166
|
+
*/
|
|
167
|
+
function snapshotText(raw) {
|
|
168
|
+
const config = normalizeConfig(raw);
|
|
169
|
+
const on = [];
|
|
170
|
+
const off = [];
|
|
171
|
+
for (const tag of HEADER_TAGS) {
|
|
172
|
+
const entry = config[tag];
|
|
173
|
+
const label = entry.value ? `${TAG_LABEL[tag]} ${entry.value}` : TAG_LABEL[tag];
|
|
174
|
+
if (entry.enabled)
|
|
175
|
+
on.push(label);
|
|
176
|
+
else
|
|
177
|
+
off.push(TAG_LABEL[tag]);
|
|
178
|
+
}
|
|
179
|
+
const contract = CONTRACT_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
|
|
180
|
+
const extra = EXTRA_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
|
|
181
|
+
const allowed = PRIVACY_TAGS.filter((tag) => config[tag].enabled && config[tag].value);
|
|
182
|
+
const head = `## 当前注释配置(profile)\n\n`;
|
|
183
|
+
const body = [
|
|
184
|
+
`- 文件头:@file 固定为文件名;开启 ${on.join('、') || '无'};关闭 ${off.join('、') || '无'}。`,
|
|
185
|
+
`- 函数契约:${contract.length > 0 ? contract.join('、') : '不要求'}。`,
|
|
186
|
+
`- 更多标签:${extra.length > 0 ? `可选 ${extra.join('、')}` : '未启用'}。`,
|
|
187
|
+
`- 检查器放行:${allowed.length > 0 ? allowed.join('、') : '无'}(隐私标签只有填了非空值才放行)。`,
|
|
188
|
+
];
|
|
189
|
+
return head + body.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
// 客户端半侧:设置导航里的「C++ 工程流程」页与插件页的配置卡片共用同一个组件。
|
|
194
|
+
// 标签策略在本文件上方内联(由 scripts/building-client.mjs 从 src/comment-policy.js
|
|
195
|
+
// 拼进来),所以预览、默认值与服务端快照永远同源;这里只做表单与排版。
|
|
196
|
+
const React = require('react')
|
|
197
|
+
const h = React.createElement
|
|
198
|
+
|
|
199
|
+
const PACKAGE_NAME = '@chengzhao-dev/dsh-cpp-workflow'
|
|
200
|
+
const SAMPLE_FILE = 'board.h'
|
|
201
|
+
|
|
202
|
+
const STRINGS = {
|
|
203
|
+
zh: {
|
|
204
|
+
nav: 'C++ 工程流程',
|
|
205
|
+
title: '文档注释',
|
|
206
|
+
intro: '文件头与函数契约默认写出哪些 Doxygen 标签。配置只影响代理写注释的默认值,不改已有源码。',
|
|
207
|
+
header: '文件头',
|
|
208
|
+
headerHint: '每个手写源文件开头的一段 ///;带值的标签填写后写出,@brief 留空由文件自述。',
|
|
209
|
+
contract: '函数契约',
|
|
210
|
+
contractHint: '声明注释里的参数与失败方式;关闭后不再要求写出这些标签。',
|
|
211
|
+
extra: '更多标签',
|
|
212
|
+
extraHint: '默认关闭;需要时在单个函数上补写。',
|
|
213
|
+
file: '@file',
|
|
214
|
+
fixed: '自动',
|
|
215
|
+
fileHint: '固定为当前文件名,不参与配置。',
|
|
216
|
+
briefHint: '一句话说明本文件定义或实现什么;留空则由每个文件自己写。',
|
|
217
|
+
authorHint: '作者署名;留空不写。',
|
|
218
|
+
dateHint: '创建或修改日期;留空不写。',
|
|
219
|
+
copyrightHint: '版权声明;留空不写。',
|
|
220
|
+
versionHint: '文件版本号;留空不写。',
|
|
221
|
+
emptyValue: '留空不写',
|
|
222
|
+
briefEmpty: '留空则由文件自述',
|
|
223
|
+
preview: '预览',
|
|
224
|
+
previewHint: '示例文件名 board.h;预览随开关即时更新。',
|
|
225
|
+
save: '保存',
|
|
226
|
+
reset: '重置',
|
|
227
|
+
saved: '已保存,下一次读取技能时生效。',
|
|
228
|
+
rejected: '宿主拒绝了这次保存:配置可能已在别处修改,请重置后再试。',
|
|
229
|
+
unsaved: '有未保存的改动。',
|
|
230
|
+
loading: '正在读取配置…',
|
|
231
|
+
unreadable: '当前账户不能修改这份配置。',
|
|
232
|
+
readOnly: '此处只显示出厂值。要修改配置,请到「插件」页打开 dsh-cpp-workflow 的配置卡片。',
|
|
233
|
+
openSettings: '打开插件配置',
|
|
234
|
+
noForm: '这个入口没有可写表单,配置在插件页修改。',
|
|
235
|
+
},
|
|
236
|
+
en: {
|
|
237
|
+
nav: 'C++ Workflow',
|
|
238
|
+
title: 'Documentation comments',
|
|
239
|
+
intro: 'Which Doxygen tags the agent writes by default. This changes defaults only; existing sources are untouched.',
|
|
240
|
+
header: 'File header',
|
|
241
|
+
headerHint: 'The /// block at the top of every hand-written source; value tags are written only when filled.',
|
|
242
|
+
contract: 'Function contract',
|
|
243
|
+
contractHint: 'Parameter and failure tags in declaration comments; turning one off stops requiring it.',
|
|
244
|
+
extra: 'More tags',
|
|
245
|
+
extraHint: 'Off by default; add them per function when needed.',
|
|
246
|
+
file: '@file',
|
|
247
|
+
fixed: 'Auto',
|
|
248
|
+
fileHint: 'Always the current file name; not configurable.',
|
|
249
|
+
briefHint: 'One sentence about what this file defines; leave empty and each file writes its own.',
|
|
250
|
+
authorHint: 'Author credit; left empty, omitted.',
|
|
251
|
+
dateHint: 'Creation or change date; left empty, omitted.',
|
|
252
|
+
copyrightHint: 'Copyright notice; left empty, omitted.',
|
|
253
|
+
versionHint: 'File version; left empty, omitted.',
|
|
254
|
+
emptyValue: 'Empty means omit',
|
|
255
|
+
briefEmpty: 'Empty: the file writes its own',
|
|
256
|
+
preview: 'Preview',
|
|
257
|
+
previewHint: 'Sample name board.h; the preview follows the switches.',
|
|
258
|
+
save: 'Save',
|
|
259
|
+
reset: 'Reset',
|
|
260
|
+
saved: 'Saved; the next skill read uses it.',
|
|
261
|
+
rejected: 'The host rejected the save: the config changed elsewhere. Reset and try again.',
|
|
262
|
+
unsaved: 'Unsaved changes.',
|
|
263
|
+
loading: 'Reading configuration…',
|
|
264
|
+
unreadable: 'This account cannot edit the configuration.',
|
|
265
|
+
readOnly: 'Showing factory defaults. Edit the dsh-cpp-workflow card on the Plugins page.',
|
|
266
|
+
openSettings: 'Open plugin settings',
|
|
267
|
+
noForm: 'This entry has no writable form; edit it on the Plugins page.',
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function language() {
|
|
272
|
+
const value = typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'zh'
|
|
273
|
+
return String(value).toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function t() {
|
|
277
|
+
return STRINGS[language()]
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const palette = {
|
|
281
|
+
label: 'var(--dsw-alias-label-primary, inherit)',
|
|
282
|
+
muted: 'var(--dsw-alias-label-secondary, #6b7280)',
|
|
283
|
+
faint: 'var(--dsw-alias-label-tertiary, #8b93a1)',
|
|
284
|
+
line: 'var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.24))',
|
|
285
|
+
surface: 'var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, 0.06))',
|
|
286
|
+
field: 'var(--dsw-alias-bg-layer-1, rgba(127, 127, 127, 0.04))',
|
|
287
|
+
brand: 'var(--dsw-alias-brand-primary, #4f6bed)',
|
|
288
|
+
mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const hintStyle = () => ({ margin: '2px 0 0', fontSize: 12, lineHeight: 1.55, color: palette.muted })
|
|
292
|
+
const labelCellStyle = () => ({
|
|
293
|
+
padding: '10px 0',
|
|
294
|
+
fontFamily: palette.mono,
|
|
295
|
+
fontSize: 12.5,
|
|
296
|
+
lineHeight: '20px',
|
|
297
|
+
color: palette.label,
|
|
298
|
+
whiteSpace: 'nowrap',
|
|
299
|
+
borderBottom: `1px solid ${palette.line}`,
|
|
300
|
+
})
|
|
301
|
+
const controlCellStyle = () => ({
|
|
302
|
+
padding: '10px 0',
|
|
303
|
+
minWidth: 0,
|
|
304
|
+
borderBottom: `1px solid ${palette.line}`,
|
|
305
|
+
})
|
|
306
|
+
const gridStyle = () => ({ display: 'grid', gridTemplateColumns: '92px minmax(0, 1fr)', columnGap: 14 })
|
|
307
|
+
|
|
308
|
+
function Toggle(props) {
|
|
309
|
+
return h(
|
|
310
|
+
'button',
|
|
311
|
+
{
|
|
312
|
+
type: 'button',
|
|
313
|
+
role: 'switch',
|
|
314
|
+
'aria-checked': Boolean(props.checked),
|
|
315
|
+
'aria-label': props.label,
|
|
316
|
+
disabled: Boolean(props.disabled),
|
|
317
|
+
onClick: () => props.onChange(!props.checked),
|
|
318
|
+
style: {
|
|
319
|
+
flex: 'none',
|
|
320
|
+
width: 34,
|
|
321
|
+
height: 20,
|
|
322
|
+
padding: 0,
|
|
323
|
+
border: 0,
|
|
324
|
+
borderRadius: 999,
|
|
325
|
+
position: 'relative',
|
|
326
|
+
cursor: props.disabled ? 'default' : 'pointer',
|
|
327
|
+
opacity: props.disabled ? 0.5 : 1,
|
|
328
|
+
background: props.checked ? palette.brand : 'var(--dsw-alias-bg-layer-4, rgba(127, 127, 127, 0.35))',
|
|
329
|
+
transition: 'background-color 0.15s ease',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
h('span', {
|
|
333
|
+
'aria-hidden': 'true',
|
|
334
|
+
style: {
|
|
335
|
+
position: 'absolute',
|
|
336
|
+
top: 2,
|
|
337
|
+
left: props.checked ? 16 : 2,
|
|
338
|
+
width: 16,
|
|
339
|
+
height: 16,
|
|
340
|
+
borderRadius: '50%',
|
|
341
|
+
background: '#fff',
|
|
342
|
+
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.25)',
|
|
343
|
+
transition: 'left 0.15s ease',
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function TagRow(props) {
|
|
350
|
+
const s = props.strings
|
|
351
|
+
return [
|
|
352
|
+
h('div', { key: `${props.tag}-label`, style: labelCellStyle() }, props.label),
|
|
353
|
+
h(
|
|
354
|
+
'div',
|
|
355
|
+
{ key: `${props.tag}-control`, style: controlCellStyle() },
|
|
356
|
+
h(
|
|
357
|
+
'div',
|
|
358
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 10, minHeight: 22, flexWrap: 'wrap' } },
|
|
359
|
+
props.control,
|
|
360
|
+
),
|
|
361
|
+
props.hint ? h('p', { style: hintStyle() }, props.hint) : null,
|
|
362
|
+
),
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function ValueRow(props) {
|
|
367
|
+
const s = props.strings
|
|
368
|
+
return h(TagRow, {
|
|
369
|
+
tag: props.tag,
|
|
370
|
+
label: TAG_LABEL[props.tag],
|
|
371
|
+
strings: s,
|
|
372
|
+
hint: props.hint,
|
|
373
|
+
control: [
|
|
374
|
+
h(Toggle, {
|
|
375
|
+
key: 'toggle',
|
|
376
|
+
label: props.label || TAG_LABEL[props.tag],
|
|
377
|
+
checked: props.entry.enabled,
|
|
378
|
+
disabled: props.disabled,
|
|
379
|
+
onChange: (enabled) => !props.disabled && props.onChange({ ...props.entry, enabled }),
|
|
380
|
+
}),
|
|
381
|
+
h('input', {
|
|
382
|
+
key: 'input',
|
|
383
|
+
type: 'text',
|
|
384
|
+
value: props.entry.value,
|
|
385
|
+
placeholder: props.tag === 'brief' ? s.briefEmpty : s.emptyValue,
|
|
386
|
+
disabled: props.disabled || !props.entry.enabled,
|
|
387
|
+
'aria-label': TAG_LABEL[props.tag],
|
|
388
|
+
onChange: (event) => props.onChange({ ...props.entry, value: event.target.value }),
|
|
389
|
+
style: {
|
|
390
|
+
flex: '1 1 180px',
|
|
391
|
+
minWidth: 0,
|
|
392
|
+
maxWidth: 320,
|
|
393
|
+
fontSize: 13,
|
|
394
|
+
padding: '4px 8px',
|
|
395
|
+
borderRadius: 6,
|
|
396
|
+
border: `1px solid ${palette.line}`,
|
|
397
|
+
background: props.entry.enabled && !props.disabled ? palette.field : 'transparent',
|
|
398
|
+
color: props.entry.enabled ? 'inherit' : palette.muted,
|
|
399
|
+
fontFamily: palette.mono,
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
],
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function FlagRow(props) {
|
|
407
|
+
return h(TagRow, {
|
|
408
|
+
tag: props.tag,
|
|
409
|
+
label: TAG_LABEL[props.tag],
|
|
410
|
+
strings: props.strings,
|
|
411
|
+
hint: props.hint,
|
|
412
|
+
control: h(Toggle, {
|
|
413
|
+
label: TAG_LABEL[props.tag],
|
|
414
|
+
checked: Boolean(props.checked),
|
|
415
|
+
disabled: props.disabled,
|
|
416
|
+
onChange: (value) => !props.disabled && props.onChange(value),
|
|
417
|
+
}),
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function Preview(props) {
|
|
422
|
+
const s = props.strings
|
|
423
|
+
return h(
|
|
424
|
+
'section',
|
|
425
|
+
{ 'aria-label': s.preview, style: { marginTop: 20 } },
|
|
426
|
+
h('h4', { style: { margin: '0 0 4px', fontSize: 13, fontWeight: 600, color: palette.label } }, s.preview),
|
|
427
|
+
h('p', { style: hintStyle() }, s.previewHint),
|
|
428
|
+
h(
|
|
429
|
+
'pre',
|
|
430
|
+
{
|
|
431
|
+
style: {
|
|
432
|
+
margin: '8px 0 0',
|
|
433
|
+
padding: '10px 12px',
|
|
434
|
+
borderRadius: 8,
|
|
435
|
+
background: palette.surface,
|
|
436
|
+
border: `1px solid ${palette.line}`,
|
|
437
|
+
overflowX: 'auto',
|
|
438
|
+
fontSize: 12.5,
|
|
439
|
+
lineHeight: 1.65,
|
|
440
|
+
fontFamily: palette.mono,
|
|
441
|
+
whiteSpace: 'pre',
|
|
442
|
+
color: palette.label,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
previewText(props.config, SAMPLE_FILE),
|
|
446
|
+
),
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function diffOperations(draft, base) {
|
|
451
|
+
const operations = []
|
|
452
|
+
for (const key of Object.keys(DEFAULT_CONFIG)) {
|
|
453
|
+
const next = JSON.stringify(draft[key])
|
|
454
|
+
const previous = JSON.stringify(base[key])
|
|
455
|
+
if (next !== previous) operations.push({ op: 'set', path: [key], value: draft[key] })
|
|
456
|
+
}
|
|
457
|
+
return operations
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function ConfigPage(props) {
|
|
461
|
+
const s = t()
|
|
462
|
+
const form = props.form
|
|
463
|
+
const initial = React.useMemo(() => normalizeConfig(form && form.state ? form.state.value : undefined), [])
|
|
464
|
+
const [draft, setDraft] = React.useState(initial)
|
|
465
|
+
const [base, setBase] = React.useState(initial)
|
|
466
|
+
const [dirty, setDirty] = React.useState(false)
|
|
467
|
+
const [busy, setBusy] = React.useState(false)
|
|
468
|
+
const [message, setMessage] = React.useState('')
|
|
469
|
+
const revision = form && form.state ? form.state.revision : undefined
|
|
470
|
+
const ready = Boolean(form && form.state && form.state.status === 'ready' && form.state.writable && form.state.mode === 'host')
|
|
471
|
+
const status = form && form.state ? form.state.status : 'missing'
|
|
472
|
+
|
|
473
|
+
React.useEffect(() => {
|
|
474
|
+
if (dirty || !form || !form.state || form.state.status !== 'ready') return
|
|
475
|
+
const next = normalizeConfig(form.state.value)
|
|
476
|
+
setBase(next)
|
|
477
|
+
setDraft(next)
|
|
478
|
+
}, [revision, status, dirty, form])
|
|
479
|
+
|
|
480
|
+
const update = (patch) => {
|
|
481
|
+
setDraft((current) => ({ ...current, ...patch }))
|
|
482
|
+
setDirty(true)
|
|
483
|
+
setMessage('')
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const reset = () => {
|
|
487
|
+
setDraft(base)
|
|
488
|
+
setDirty(false)
|
|
489
|
+
setMessage('')
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const save = async () => {
|
|
493
|
+
if (!ready || busy) return
|
|
494
|
+
const operations = diffOperations(draft, base)
|
|
495
|
+
if (operations.length === 0) return
|
|
496
|
+
setBusy(true)
|
|
497
|
+
try {
|
|
498
|
+
const accepted = await form.mutate(operations, revision)
|
|
499
|
+
if (accepted) {
|
|
500
|
+
setBase(draft)
|
|
501
|
+
setDirty(false)
|
|
502
|
+
setMessage(s.saved)
|
|
503
|
+
} else {
|
|
504
|
+
setMessage(s.rejected)
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
setMessage(error instanceof Error ? error.message : String(error))
|
|
508
|
+
} finally {
|
|
509
|
+
setBusy(false)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const editable = Boolean(form)
|
|
514
|
+
const editableReady = editable && ready
|
|
515
|
+
const header = [
|
|
516
|
+
{ tag: 'brief', hint: s.briefHint },
|
|
517
|
+
{ tag: 'author', hint: s.authorHint },
|
|
518
|
+
{ tag: 'date', hint: s.dateHint },
|
|
519
|
+
{ tag: 'copyright', hint: s.copyrightHint },
|
|
520
|
+
{ tag: 'version', hint: s.versionHint },
|
|
521
|
+
]
|
|
522
|
+
const contractHints = { param: TAG_TEXT.param, returns: TAG_TEXT.returns, throws: TAG_TEXT.throws }
|
|
523
|
+
const extraHints = { note: TAG_TEXT.note, see: TAG_TEXT.see, pre: TAG_TEXT.pre, post: TAG_TEXT.post, tparam: TAG_TEXT.tparam, retval: TAG_TEXT.retval }
|
|
524
|
+
|
|
525
|
+
return h(
|
|
526
|
+
'div',
|
|
527
|
+
{ style: { display: 'flex', flexDirection: 'column', padding: '4px 0 12px', maxWidth: 720, color: palette.label } },
|
|
528
|
+
h('h3', { style: { margin: '0 0 4px', fontSize: 15, fontWeight: 600 } }, s.title),
|
|
529
|
+
h('p', { style: hintStyle() }, s.intro),
|
|
530
|
+
!editable
|
|
531
|
+
? h(
|
|
532
|
+
'div',
|
|
533
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginTop: 8 } },
|
|
534
|
+
h('p', { role: 'status', style: { ...hintStyle(), margin: 0 } }, s.readOnly),
|
|
535
|
+
props.openPluginPage
|
|
536
|
+
? h('button', { type: 'button', onClick: props.openPluginPage, style: secondaryButton(true) }, s.openSettings)
|
|
537
|
+
: null,
|
|
538
|
+
)
|
|
539
|
+
: null,
|
|
540
|
+
editable && !ready && status !== 'missing'
|
|
541
|
+
? h('p', { role: 'status', style: hintStyle() }, status === 'loading' ? s.loading : s.unreadable)
|
|
542
|
+
: null,
|
|
543
|
+
h(
|
|
544
|
+
'section',
|
|
545
|
+
{ style: { marginTop: 18 } },
|
|
546
|
+
h('h4', { style: { margin: '0 0 4px', fontSize: 13, fontWeight: 600 } }, s.header),
|
|
547
|
+
h('p', { style: hintStyle() }, s.headerHint),
|
|
548
|
+
h(
|
|
549
|
+
'div',
|
|
550
|
+
{ role: 'group', 'aria-label': s.header, style: { ...gridStyle(), marginTop: 8 } },
|
|
551
|
+
h(
|
|
552
|
+
'div',
|
|
553
|
+
{ key: 'file-label', style: labelCellStyle() },
|
|
554
|
+
s.file,
|
|
555
|
+
),
|
|
556
|
+
h(
|
|
557
|
+
'div',
|
|
558
|
+
{ key: 'file-control', style: controlCellStyle() },
|
|
559
|
+
h(
|
|
560
|
+
'div',
|
|
561
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 10, minHeight: 22 } },
|
|
562
|
+
h(
|
|
563
|
+
'span',
|
|
564
|
+
{
|
|
565
|
+
style: {
|
|
566
|
+
fontSize: 12,
|
|
567
|
+
lineHeight: '20px',
|
|
568
|
+
padding: '0 8px',
|
|
569
|
+
borderRadius: 999,
|
|
570
|
+
border: `1px solid ${palette.line}`,
|
|
571
|
+
color: palette.muted,
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
s.fixed,
|
|
575
|
+
),
|
|
576
|
+
h('code', { style: { fontSize: 12.5, fontFamily: palette.mono, color: palette.muted } }, SAMPLE_FILE),
|
|
577
|
+
),
|
|
578
|
+
h('p', { style: hintStyle() }, s.fileHint),
|
|
579
|
+
),
|
|
580
|
+
...header.flatMap(({ tag, hint }) =>
|
|
581
|
+
h(ValueRow, {
|
|
582
|
+
key: tag,
|
|
583
|
+
tag,
|
|
584
|
+
hint,
|
|
585
|
+
strings: s,
|
|
586
|
+
disabled: !editable,
|
|
587
|
+
entry: draft[tag],
|
|
588
|
+
onChange: (entry) => update({ [tag]: entry }),
|
|
589
|
+
}),
|
|
590
|
+
),
|
|
591
|
+
),
|
|
592
|
+
),
|
|
593
|
+
h(
|
|
594
|
+
'section',
|
|
595
|
+
{ style: { marginTop: 20 } },
|
|
596
|
+
h('h4', { style: { margin: '0 0 4px', fontSize: 13, fontWeight: 600 } }, s.contract),
|
|
597
|
+
h('p', { style: hintStyle() }, s.contractHint),
|
|
598
|
+
h(
|
|
599
|
+
'div',
|
|
600
|
+
{ role: 'group', 'aria-label': s.contract, style: { ...gridStyle(), marginTop: 8 } },
|
|
601
|
+
...CONTRACT_TAGS.flatMap((tag) =>
|
|
602
|
+
h(FlagRow, {
|
|
603
|
+
key: tag,
|
|
604
|
+
tag,
|
|
605
|
+
hint: contractHints[tag],
|
|
606
|
+
strings: s,
|
|
607
|
+
disabled: !editable,
|
|
608
|
+
checked: draft[tag],
|
|
609
|
+
onChange: (value) => update({ [tag]: value }),
|
|
610
|
+
}),
|
|
611
|
+
),
|
|
612
|
+
),
|
|
613
|
+
),
|
|
614
|
+
h(
|
|
615
|
+
'section',
|
|
616
|
+
{ style: { marginTop: 20, paddingTop: 12, borderTop: `1px solid ${palette.line}` } },
|
|
617
|
+
h(
|
|
618
|
+
'details',
|
|
619
|
+
{},
|
|
620
|
+
h(
|
|
621
|
+
'summary',
|
|
622
|
+
{ style: { cursor: 'pointer', fontSize: 13, fontWeight: 600, listStylePosition: 'inside' } },
|
|
623
|
+
s.extra,
|
|
624
|
+
),
|
|
625
|
+
h('p', { style: hintStyle() }, s.extraHint),
|
|
626
|
+
h(
|
|
627
|
+
'div',
|
|
628
|
+
{ role: 'group', 'aria-label': s.extra, style: { ...gridStyle(), marginTop: 8 } },
|
|
629
|
+
...EXTRA_TAGS.flatMap((tag) =>
|
|
630
|
+
h(FlagRow, {
|
|
631
|
+
key: tag,
|
|
632
|
+
tag,
|
|
633
|
+
hint: extraHints[tag],
|
|
634
|
+
strings: s,
|
|
635
|
+
disabled: !editable,
|
|
636
|
+
checked: draft[tag],
|
|
637
|
+
onChange: (value) => update({ [tag]: value }),
|
|
638
|
+
}),
|
|
639
|
+
),
|
|
640
|
+
),
|
|
641
|
+
),
|
|
642
|
+
),
|
|
643
|
+
h(Preview, { config: draft, strings: s }),
|
|
644
|
+
editable
|
|
645
|
+
? h(
|
|
646
|
+
'div',
|
|
647
|
+
{
|
|
648
|
+
style: {
|
|
649
|
+
display: 'flex',
|
|
650
|
+
alignItems: 'center',
|
|
651
|
+
gap: 10,
|
|
652
|
+
marginTop: 18,
|
|
653
|
+
paddingTop: 12,
|
|
654
|
+
borderTop: `1px solid ${palette.line}`,
|
|
655
|
+
flexWrap: 'wrap',
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
h(
|
|
659
|
+
'button',
|
|
660
|
+
{
|
|
661
|
+
type: 'button',
|
|
662
|
+
onClick: save,
|
|
663
|
+
disabled: busy || !editableReady || !dirty,
|
|
664
|
+
style: primaryButton(dirty && editableReady && !busy),
|
|
665
|
+
},
|
|
666
|
+
s.save,
|
|
667
|
+
),
|
|
668
|
+
h(
|
|
669
|
+
'button',
|
|
670
|
+
{
|
|
671
|
+
type: 'button',
|
|
672
|
+
onClick: reset,
|
|
673
|
+
disabled: busy || !dirty,
|
|
674
|
+
style: secondaryButton(dirty && !busy),
|
|
675
|
+
},
|
|
676
|
+
s.reset,
|
|
677
|
+
),
|
|
678
|
+
h('p', { role: 'status', style: { ...hintStyle(), margin: 0, flex: '1 1 220px' } }, message || (dirty ? s.unsaved : '')),
|
|
679
|
+
)
|
|
680
|
+
: null,
|
|
681
|
+
)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function primaryButton(enabled) {
|
|
685
|
+
return {
|
|
686
|
+
fontSize: 13,
|
|
687
|
+
lineHeight: '20px',
|
|
688
|
+
padding: '5px 14px',
|
|
689
|
+
borderRadius: 6,
|
|
690
|
+
border: '1px solid transparent',
|
|
691
|
+
background: enabled ? palette.brand : 'var(--dsw-alias-bg-layer-3, rgba(127, 127, 127, 0.2))',
|
|
692
|
+
color: enabled ? '#fff' : palette.muted,
|
|
693
|
+
cursor: enabled ? 'pointer' : 'default',
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function secondaryButton(enabled) {
|
|
698
|
+
return {
|
|
699
|
+
fontSize: 13,
|
|
700
|
+
lineHeight: '20px',
|
|
701
|
+
padding: '5px 14px',
|
|
702
|
+
borderRadius: 6,
|
|
703
|
+
border: `1px solid ${palette.line}`,
|
|
704
|
+
background: 'transparent',
|
|
705
|
+
color: enabled ? palette.label : palette.muted,
|
|
706
|
+
cursor: enabled ? 'pointer' : 'default',
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function page(ownerProps, openPluginPage) {
|
|
711
|
+
const props = ownerProps || {}
|
|
712
|
+
return h(ConfigPage, { ...props, openPluginPage })
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function apply(ctx) {
|
|
716
|
+
let openPluginPage
|
|
717
|
+
try {
|
|
718
|
+
const navigation = typeof ctx.get === 'function' ? ctx.get('pluginNavigation') : undefined
|
|
719
|
+
if (navigation && typeof navigation.openBundle === 'function') {
|
|
720
|
+
openPluginPage = () => navigation.openBundle(PACKAGE_NAME)
|
|
721
|
+
}
|
|
722
|
+
} catch {}
|
|
723
|
+
ctx.slots.inject('settings.section', () =>
|
|
724
|
+
ctx.slots.register(
|
|
725
|
+
{
|
|
726
|
+
name: 'settings.section',
|
|
727
|
+
id: 'cpp-workflow',
|
|
728
|
+
order: 60,
|
|
729
|
+
label: () => t().nav,
|
|
730
|
+
},
|
|
731
|
+
(ownerProps) => page(ownerProps, openPluginPage),
|
|
732
|
+
),
|
|
733
|
+
)
|
|
734
|
+
ctx.slots.inject('plugins.bundle.config', () =>
|
|
735
|
+
ctx.slots.register(
|
|
736
|
+
{
|
|
737
|
+
name: 'plugins.bundle.config',
|
|
738
|
+
key: PACKAGE_NAME,
|
|
739
|
+
label: () => t().nav,
|
|
740
|
+
},
|
|
741
|
+
(ownerProps) => (ownerProps && ownerProps.view === 'summary' ? null : page(ownerProps, undefined)),
|
|
742
|
+
),
|
|
743
|
+
)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
module.exports = {
|
|
747
|
+
name: PACKAGE_NAME,
|
|
748
|
+
inject: ['slots'],
|
|
749
|
+
apply,
|
|
750
|
+
ConfigPage,
|
|
751
|
+
diffOperations,
|
|
752
|
+
previewText,
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return module.exports
|
|
756
|
+
},
|
|
757
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 注释标签策略:设置页的预览、技能快照与检查器参数共用这一份。
|
|
3
|
+
*
|
|
4
|
+
* 配置只描述「写注释时默认带哪些标签」,不描述写法本身;写法在
|
|
5
|
+
* `skills/developing-cpp/references/comments.md`。设置页保存的值由宿主
|
|
6
|
+
* 写回插件 Config,服务端 `apply(ctx, config)` 用它生成本页快照与检查器参数。
|
|
7
|
+
*
|
|
8
|
+
* 这份文件被 `tsc` 编进 `lib/comment-policy.js`,打包客户端半侧时再内联进
|
|
9
|
+
* `lib/client.js`,所以只能依赖纯函数,不要引入 Node 或 React。
|
|
10
|
+
*/
|
|
11
|
+
/** 文件头标签。 */
|
|
12
|
+
export const HEADER_TAGS = ['brief', 'author', 'date', 'copyright', 'version'];
|
|
13
|
+
/** 函数契约标签:开关决定是否要求写出。 */
|
|
14
|
+
export const CONTRACT_TAGS = ['param', 'returns', 'throws'];
|
|
15
|
+
/** 折叠在「更多标签」里的可选标签:默认关闭。 */
|
|
16
|
+
export const EXTRA_TAGS = ['note', 'see', 'pre', 'post', 'tparam', 'retval'];
|
|
17
|
+
/** 只有用户主动填了非空值才放行的隐私标签。 */
|
|
18
|
+
export const PRIVACY_TAGS = ['author', 'date', 'copyright', 'version'];
|
|
19
|
+
/** 标签说明:设置页逐行显示,用作行的辅助文字。 */
|
|
20
|
+
export const TAG_TEXT = {
|
|
21
|
+
brief: '一句话说明这个文件定义或实现什么',
|
|
22
|
+
author: '只在用户填了非空值时写出',
|
|
23
|
+
date: '只在用户填了非空值时写出',
|
|
24
|
+
copyright: '只在用户填了非空值时写出',
|
|
25
|
+
version: '只在用户填了非空值时写出',
|
|
26
|
+
param: '函数参数的含义、范围或所有权',
|
|
27
|
+
returns: '返回值的条件;返回类型自明时可以省略',
|
|
28
|
+
throws: '抛出的异常类型与条件;不抛异常的函数不写',
|
|
29
|
+
note: '调用方需要注意的补充说明',
|
|
30
|
+
see: '相关函数、类型或文档',
|
|
31
|
+
pre: '调用前必须成立的条件',
|
|
32
|
+
post: '调用后保证成立的条件',
|
|
33
|
+
tparam: '模板参数',
|
|
34
|
+
retval: '特定返回值的含义',
|
|
35
|
+
};
|
|
36
|
+
/** 开关型标签在设置页的标题。 */
|
|
37
|
+
export const TAG_LABEL = {
|
|
38
|
+
brief: '@brief',
|
|
39
|
+
author: '@author',
|
|
40
|
+
date: '@date',
|
|
41
|
+
copyright: '@copyright',
|
|
42
|
+
version: '@version',
|
|
43
|
+
param: '@param',
|
|
44
|
+
returns: '@return',
|
|
45
|
+
throws: '@throws',
|
|
46
|
+
note: '@note',
|
|
47
|
+
see: '@see',
|
|
48
|
+
pre: '@pre',
|
|
49
|
+
post: '@post',
|
|
50
|
+
tparam: '@tparam',
|
|
51
|
+
retval: '@retval',
|
|
52
|
+
};
|
|
53
|
+
/** 预览里的示例摘要:用户没填 @brief 时用它,避免预览出现空标签。 */
|
|
54
|
+
export const SAMPLE_BRIEF = '定义井字棋棋盘、格子坐标与格子类型。';
|
|
55
|
+
/** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
|
|
56
|
+
export const DEFAULT_CONFIG = {
|
|
57
|
+
brief: { enabled: true, value: '' },
|
|
58
|
+
author: { enabled: false, value: '' },
|
|
59
|
+
date: { enabled: false, value: '' },
|
|
60
|
+
copyright: { enabled: false, value: '' },
|
|
61
|
+
version: { enabled: false, value: '' },
|
|
62
|
+
param: true,
|
|
63
|
+
returns: true,
|
|
64
|
+
throws: true,
|
|
65
|
+
note: false,
|
|
66
|
+
see: false,
|
|
67
|
+
pre: false,
|
|
68
|
+
post: false,
|
|
69
|
+
tparam: false,
|
|
70
|
+
retval: false,
|
|
71
|
+
};
|
|
72
|
+
function text(value) {
|
|
73
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
74
|
+
}
|
|
75
|
+
function flag(value, fallback) {
|
|
76
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 把宿主给的值补齐成完整配置;缺字段、旧版本或手工改坏的配置都回到出厂值。
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeConfig(raw) {
|
|
82
|
+
const source = raw && typeof raw === 'object' ? raw : {};
|
|
83
|
+
const config = {};
|
|
84
|
+
for (const tag of HEADER_TAGS) {
|
|
85
|
+
const entry = source[tag] && typeof source[tag] === 'object' ? source[tag] : {};
|
|
86
|
+
config[tag] = {
|
|
87
|
+
enabled: flag(entry.enabled, DEFAULT_CONFIG[tag].enabled),
|
|
88
|
+
value: text(entry.value),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
for (const tag of [...CONTRACT_TAGS, ...EXTRA_TAGS]) {
|
|
92
|
+
config[tag] = flag(source[tag], DEFAULT_CONFIG[tag]);
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 按配置渲染文件头;`@file` 始终是传入的文件名。
|
|
98
|
+
*/
|
|
99
|
+
export function renderFileHeader(raw, fileName) {
|
|
100
|
+
const config = normalizeConfig(raw);
|
|
101
|
+
const name = text(fileName) || 'board.h';
|
|
102
|
+
const lines = [`/// @file ${name}`];
|
|
103
|
+
if (config.brief.enabled) {
|
|
104
|
+
lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
|
|
105
|
+
}
|
|
106
|
+
for (const tag of ['author', 'date', 'copyright', 'version']) {
|
|
107
|
+
if (config[tag].enabled && config[tag].value)
|
|
108
|
+
lines.push(`/// @${tag} ${config[tag].value}`);
|
|
109
|
+
}
|
|
110
|
+
return lines;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 设置页的预览:文件头 + 一段函数注释,只显示当前配置会写出的标签。
|
|
114
|
+
*/
|
|
115
|
+
export function previewParts(raw, sampleName) {
|
|
116
|
+
const config = normalizeConfig(raw);
|
|
117
|
+
const header = renderFileHeader(config, sampleName || 'board.h');
|
|
118
|
+
// 预览是示例:@brief 开着但用户没填句子时,用示例文件自己的摘要顶替。
|
|
119
|
+
const briefIndex = header.indexOf('/// @brief');
|
|
120
|
+
if (briefIndex !== -1)
|
|
121
|
+
header[briefIndex] = `/// @brief ${SAMPLE_BRIEF}`;
|
|
122
|
+
const contract = [];
|
|
123
|
+
if (config.param)
|
|
124
|
+
contract.push('/// @param position 目标行列坐标。');
|
|
125
|
+
if (config.returns)
|
|
126
|
+
contract.push('/// @return 放置成功时为 true。');
|
|
127
|
+
if (config.throws)
|
|
128
|
+
contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
|
|
129
|
+
const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
|
|
130
|
+
return { header, contract, declaration };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 设置页预览的纯文本形式;客户端半侧用同一份实现渲染 `<pre>`。
|
|
134
|
+
*/
|
|
135
|
+
export function previewText(raw, sampleName) {
|
|
136
|
+
const { header, contract, declaration } = previewParts(raw, sampleName);
|
|
137
|
+
const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
|
|
138
|
+
if (contract.length > 0)
|
|
139
|
+
lines.push('///', ...contract);
|
|
140
|
+
lines.push(declaration);
|
|
141
|
+
return lines.join('\n');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查。
|
|
145
|
+
*/
|
|
146
|
+
export function checkerArgs(raw) {
|
|
147
|
+
const config = normalizeConfig(raw);
|
|
148
|
+
const args = [];
|
|
149
|
+
if (!config.brief.enabled)
|
|
150
|
+
args.push('--no-brief');
|
|
151
|
+
for (const tag of PRIVACY_TAGS) {
|
|
152
|
+
if (config[tag].enabled && config[tag].value)
|
|
153
|
+
args.push('--allow-tag', tag);
|
|
154
|
+
}
|
|
155
|
+
return args;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 技能快照:`get()` 把它接在技能正文末尾,让代理看到当前 profile 的标签开关。
|
|
159
|
+
*/
|
|
160
|
+
export function snapshotText(raw) {
|
|
161
|
+
const config = normalizeConfig(raw);
|
|
162
|
+
const on = [];
|
|
163
|
+
const off = [];
|
|
164
|
+
for (const tag of HEADER_TAGS) {
|
|
165
|
+
const entry = config[tag];
|
|
166
|
+
const label = entry.value ? `${TAG_LABEL[tag]} ${entry.value}` : TAG_LABEL[tag];
|
|
167
|
+
if (entry.enabled)
|
|
168
|
+
on.push(label);
|
|
169
|
+
else
|
|
170
|
+
off.push(TAG_LABEL[tag]);
|
|
171
|
+
}
|
|
172
|
+
const contract = CONTRACT_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
|
|
173
|
+
const extra = EXTRA_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
|
|
174
|
+
const allowed = PRIVACY_TAGS.filter((tag) => config[tag].enabled && config[tag].value);
|
|
175
|
+
const head = `## 当前注释配置(profile)\n\n`;
|
|
176
|
+
const body = [
|
|
177
|
+
`- 文件头:@file 固定为文件名;开启 ${on.join('、') || '无'};关闭 ${off.join('、') || '无'}。`,
|
|
178
|
+
`- 函数契约:${contract.length > 0 ? contract.join('、') : '不要求'}。`,
|
|
179
|
+
`- 更多标签:${extra.length > 0 ? `可选 ${extra.join('、')}` : '未启用'}。`,
|
|
180
|
+
`- 检查器放行:${allowed.length > 0 ? allowed.join('、') : '无'}(隐私标签只有填了非空值才放行)。`,
|
|
181
|
+
];
|
|
182
|
+
return head + body.join('\n');
|
|
183
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -21,9 +21,35 @@
|
|
|
21
21
|
import { readdir, readFile } from 'node:fs/promises';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
24
25
|
import { isCppWorkflowWorkspace } from './workspace.js';
|
|
26
|
+
import { normalizeConfig, snapshotText } from './comment-policy.js';
|
|
25
27
|
export const name = 'dsh-cpp-workflow';
|
|
26
28
|
export const inject = ['skills'];
|
|
29
|
+
// 设置页保存的标签开关;字段即时生效,改配置不必重装插件。
|
|
30
|
+
const HeaderTag = Schema.object({
|
|
31
|
+
enabled: Schema.boolean().default(false).volatile(),
|
|
32
|
+
value: Schema.string().default('').volatile(),
|
|
33
|
+
});
|
|
34
|
+
export const Config = Schema.object({
|
|
35
|
+
brief: Schema.object({
|
|
36
|
+
enabled: Schema.boolean().default(true).volatile(),
|
|
37
|
+
value: Schema.string().default('').volatile(),
|
|
38
|
+
}),
|
|
39
|
+
author: HeaderTag,
|
|
40
|
+
date: HeaderTag,
|
|
41
|
+
copyright: HeaderTag,
|
|
42
|
+
version: HeaderTag,
|
|
43
|
+
param: Schema.boolean().default(true).volatile(),
|
|
44
|
+
returns: Schema.boolean().default(true).volatile(),
|
|
45
|
+
throws: Schema.boolean().default(true).volatile(),
|
|
46
|
+
note: Schema.boolean().default(false).volatile(),
|
|
47
|
+
see: Schema.boolean().default(false).volatile(),
|
|
48
|
+
pre: Schema.boolean().default(false).volatile(),
|
|
49
|
+
post: Schema.boolean().default(false).volatile(),
|
|
50
|
+
tparam: Schema.boolean().default(false).volatile(),
|
|
51
|
+
retval: Schema.boolean().default(false).volatile(),
|
|
52
|
+
});
|
|
27
53
|
const PROVIDER = 'dsh-cpp-workflow';
|
|
28
54
|
const RANK = 550;
|
|
29
55
|
const KNOWN_SKILLS = new Set(['building-cpp', 'configuring-cmake', 'developing-cpp']);
|
|
@@ -150,7 +176,7 @@ async function listSkills(options = {}) {
|
|
|
150
176
|
candidates.sort((a, b) => a.name.localeCompare(b.name));
|
|
151
177
|
return candidates;
|
|
152
178
|
}
|
|
153
|
-
async function getSkill(candidate, options = {}) {
|
|
179
|
+
async function getSkill(candidate, options = {}, commentConfig = undefined) {
|
|
154
180
|
if (options.signal?.aborted)
|
|
155
181
|
return undefined;
|
|
156
182
|
if (!candidate || candidate.provider !== PROVIDER)
|
|
@@ -190,13 +216,18 @@ async function getSkill(candidate, options = {}) {
|
|
|
190
216
|
resourceBase: { kind: 'directory', path: resolved },
|
|
191
217
|
path: path.join(resolved, 'SKILL.md'),
|
|
192
218
|
...(candidate.metadata ? { metadata: candidate.metadata } : {}),
|
|
193
|
-
|
|
219
|
+
// 标签开关只跟注释技能有关,另两个技能的正文保持原样。
|
|
220
|
+
content: candidate.name === 'developing-cpp'
|
|
221
|
+
? `${body.trim()}\n\n${snapshotText(commentConfig)}`
|
|
222
|
+
: body.trim(),
|
|
194
223
|
};
|
|
195
224
|
}
|
|
196
|
-
export function apply(ctx) {
|
|
225
|
+
export function apply(ctx, config = undefined) {
|
|
226
|
+
// 配置决定注释标签的默认集合;快照随每次 get() 读出,改配置立即生效。
|
|
227
|
+
const resolved = normalizeConfig(config);
|
|
197
228
|
ctx.skills.registerProvider(() => ({
|
|
198
229
|
name: PROVIDER,
|
|
199
230
|
list: listSkills,
|
|
200
|
-
get: getSkill,
|
|
231
|
+
get: (candidate, options) => getSkill(candidate, options, resolved),
|
|
201
232
|
}));
|
|
202
233
|
}
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "C++ Workflow",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Write C++ sources, configure CMake, and build with Clang; /// Doxygen comments, with file-header tags you can set in Settings."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chengzhao-dev/dsh-cpp-workflow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "C++
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "写 C++ 源码、配 CMake、用 Clang 构建;文档注释统一为 /// Doxygen,文件头标签可在设置里改。 · Write C++ sources, configure CMake, and build with Clang; /// Doxygen comments, with file-header tags you can change in Settings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
9
10
|
"./package.json": "./package.json",
|
|
10
11
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
11
12
|
},
|
|
@@ -28,11 +29,17 @@
|
|
|
28
29
|
"bundle": {
|
|
29
30
|
"patch": "./cordis.patch.yml"
|
|
30
31
|
},
|
|
32
|
+
"client": {
|
|
33
|
+
"platform": "web",
|
|
34
|
+
"inject": [
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
31
38
|
"catalog": {
|
|
32
39
|
"category": "skill",
|
|
33
40
|
"summary": {
|
|
34
|
-
"zh": "
|
|
35
|
-
"en": "
|
|
41
|
+
"zh": "写 C++ 源码、配 CMake、用 Clang 构建;文档注释统一为 /// Doxygen,文件头标签可在设置里改。",
|
|
42
|
+
"en": "Write C++ sources, configure CMake, and build with Clang; /// Doxygen comments, with file-header tags you can set in Settings."
|
|
36
43
|
},
|
|
37
44
|
"capabilities": [
|
|
38
45
|
"skills"
|
|
@@ -41,8 +48,8 @@
|
|
|
41
48
|
},
|
|
42
49
|
"icon": "./icon.svg",
|
|
43
50
|
"scripts": {
|
|
44
|
-
"build": "tsc -p tsconfig.json",
|
|
45
|
-
"test": "npm run build && node scripts/checking-skill-links.mjs && node skills/building-cpp/scripts/checking-encoding.mjs --quiet && node scripts/checking-github-workflows.mjs && node --test tests/*.test.mjs",
|
|
51
|
+
"build": "tsc -p tsconfig.json && node scripts/building-client.mjs",
|
|
52
|
+
"test": "npm run build && node scripts/building-client.mjs --check && node scripts/checking-skill-links.mjs && node skills/building-cpp/scripts/checking-encoding.mjs --quiet && node scripts/checking-github-workflows.mjs && node --test tests/*.test.mjs",
|
|
46
53
|
"ci": "npm ci && npm test && npm run pack:check",
|
|
47
54
|
"ci:full": "npm run ci && npm run test:matrix",
|
|
48
55
|
"check:encoding": "node skills/building-cpp/scripts/checking-encoding.mjs",
|
|
@@ -79,6 +86,9 @@
|
|
|
79
86
|
"peerDependencies": {
|
|
80
87
|
"@deepseek-ai/dsh-skill": ">=0.2.0-rc.1 <0.3.0-0"
|
|
81
88
|
},
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"@deepseek-ai/schemastery": "^3.18.5"
|
|
91
|
+
},
|
|
82
92
|
"devDependencies": {
|
|
83
93
|
"@deepseek-ai/dsh-skill": ">=0.2.0-rc.1 <0.3.0-0",
|
|
84
94
|
"@types/node": "^22.20.5",
|
|
@@ -82,7 +82,7 @@ GameResult result_ = GameResult::kInProgress;
|
|
|
82
82
|
|
|
83
83
|
### 实现:阶段边界与为什么
|
|
84
84
|
|
|
85
|
-
`.cpp` / `.cc` 里,重要函数按空行分段,每一段上方写一句
|
|
85
|
+
`.cpp` / `.cc` 里,重要函数按空行分段,每一段上方写一句 `///`。句子写边界、失败后果或为什么停在这里,不把下一行条件翻译一遍;`IsWin` 这种单行函数不配注释。只写删掉会让下一次修改得到错误结果、而代码本身看不出的内容。
|
|
86
86
|
|
|
87
87
|
大型工程的共识是解释意图而不是逐步翻译:[Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html#Implementation_Comments) 要求注释解释代码在做什么,不描述代码本身;[LLVM Coding Standards](https://www.llvm.org/docs/CodingStandards.html#commenting) 要求写意图与理由。阶段注释写的就是这两类信息。
|
|
88
88
|
|
|
@@ -119,13 +119,13 @@ GameResult result_ = GameResult::kInProgress;
|
|
|
119
119
|
|
|
120
120
|
### 文件头
|
|
121
121
|
|
|
122
|
-
每个手写源文件开头写一段 doxygen 文件头:第一行 `/// @file <文件名>`,第二行 `/// @brief` 一句话说明这个文件定义或实现什么,用一行 `///`
|
|
122
|
+
每个手写源文件开头写一段 doxygen 文件头:第一行 `/// @file <文件名>`,第二行 `/// @brief` 一句话说明这个文件定义或实现什么,用一行 `///` 隔开后,只写本文件自己的组成部分或边界。`@file` 的参数必须与文件名逐字相同,检查器会核对。`@brief` 默认写出;`@author`、`@date`、`@copyright`、`@version` 默认不写,只有插件配置里打开并填了非空值才写出,作者、年份和变更史不写进源码。删掉把读者指去另一个头文件、另一个目录或另一层的句子,也删掉「这一层不负责什么」。不写作者、版权年份、变更日志和外部资料路径。
|
|
123
123
|
|
|
124
124
|
```cpp
|
|
125
125
|
/// @file board.h
|
|
126
126
|
/// @brief 定义井字棋棋盘、格子坐标与格子类型。
|
|
127
127
|
///
|
|
128
|
-
///
|
|
128
|
+
/// 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
- C++ 源文件用以上格式;CMake 用 `#`,YAML/JSON 用 `#`。
|
|
@@ -142,6 +142,7 @@ GameResult result_ = GameResult::kInProgress;
|
|
|
142
142
|
- `.clang-format`、`.clang-tidy`、`.clangd`、CMake 的配置项旁边写「为什么这样设」,不写「这一项是什么」;配置注释贴在被解释的键上方。
|
|
143
143
|
- 禁止:大段表格、装饰图案、表情;外部资料路径、跨仓库引用、团队流程;注释掉的代码;注释元叙述(「其余内容见某处」「两处不重复」)。
|
|
144
144
|
- `///`、`@param`、横幅不进入 `.qmd`、YAML、CSS。
|
|
145
|
+
- C++ 源文件的说明性注释一律 `///`,实现区不与 `//` 混用;`//` 只留给文件头横幅边框与被引用的配置行。
|
|
145
146
|
|
|
146
147
|
## 润色:删复述,统一句式
|
|
147
148
|
|
|
@@ -25,7 +25,9 @@ const SKIP_DIR = new Set([
|
|
|
25
25
|
'.tmp', 'temp', 'third_party', 'third-party', 'external', 'vendor', 'deps', 'cmake-build-debug',
|
|
26
26
|
'cmake-build-release', '.vs', '.idea',
|
|
27
27
|
])
|
|
28
|
-
const
|
|
28
|
+
const STATIC_FORBIDDEN_TAGS = ['@returns', '@details', '@since', '@todo']
|
|
29
|
+
const PRIVACY_TAGS = ['@author', '@date', '@copyright', '@version']
|
|
30
|
+
const FORBIDDEN_TAGS = [...STATIC_FORBIDDEN_TAGS, ...PRIVACY_TAGS]
|
|
29
31
|
const BANNER = /^\/\/=+-+=+\/\/$/
|
|
30
32
|
const DOXY_FILE = /^\/\/\/\s*@file\s+\S/
|
|
31
33
|
const DOXY_BRIEF = /^\/\/\/\s*@brief\s+\S/
|
|
@@ -40,12 +42,12 @@ const DECL_KEYWORDS = /^(class|struct|union|enum|namespace|using|typedef|templat
|
|
|
40
42
|
|
|
41
43
|
function usage(message) {
|
|
42
44
|
if (message) process.stderr.write(`错误:${message}\n`)
|
|
43
|
-
process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--banner-width N] [--no-header] [--json] [--quiet]\n')
|
|
45
|
+
process.stderr.write('用法:node checking-comments.mjs [--root DIR] [--width N] [--banner-width N] [--no-header] [--no-brief] [--allow-tag TAG] [--json] [--quiet]\n')
|
|
44
46
|
return EXIT.usage
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function parseArgs(argv) {
|
|
48
|
-
const options = { root: undefined, width: 80, bannerWidth: 80, widthExplicit: false, bannerWidthExplicit: false, requireHeader: true, json: false, quiet: false }
|
|
50
|
+
const options = { root: undefined, width: 80, bannerWidth: 80, widthExplicit: false, bannerWidthExplicit: false, requireHeader: true, requireBrief: true, allowTags: new Set(), json: false, quiet: false }
|
|
49
51
|
for (let index = 0; index < argv.length; index += 1) {
|
|
50
52
|
const arg = argv[index]
|
|
51
53
|
if (arg === '--root') {
|
|
@@ -61,6 +63,13 @@ function parseArgs(argv) {
|
|
|
61
63
|
index += 1
|
|
62
64
|
} else if (arg === '--no-header') {
|
|
63
65
|
options.requireHeader = false
|
|
66
|
+
} else if (arg === '--no-brief') {
|
|
67
|
+
options.requireBrief = false
|
|
68
|
+
} else if (arg === '--allow-tag') {
|
|
69
|
+
const value = argv[index + 1]
|
|
70
|
+
if (!value) throw new Error('--allow-tag 需要标签名,例如 author')
|
|
71
|
+
options.allowTags.add(value.replace(/^@/, ''))
|
|
72
|
+
index += 1
|
|
64
73
|
} else if (arg === '--json') {
|
|
65
74
|
options.json = true
|
|
66
75
|
} else if (arg === '--quiet') {
|
|
@@ -220,7 +229,13 @@ export function checkText(text, options = {}) {
|
|
|
220
229
|
if (firstContent !== -1 && (BANNER.test(firstTrimmed) || hasDoxyHeader)) {
|
|
221
230
|
const own = options.fileName ? options.fileName.split(/[\\/]/).pop() : ''
|
|
222
231
|
const bannerHeader = BANNER.test(firstTrimmed)
|
|
223
|
-
if (hasDoxyHeader &&
|
|
232
|
+
if (hasDoxyHeader && own) {
|
|
233
|
+
const declared = /@file\s+(\S+)/.exec(lines[firstContent].trim())
|
|
234
|
+
if (declared && declared[1] !== own) {
|
|
235
|
+
add(firstContent + 1, 'header-file-name', `文件头写的是 ${declared[1]},本文件是 ${own}:@file 必须等于文件名`)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (hasDoxyHeader && options.requireBrief !== false && !DOXY_BRIEF.test((lines[firstContent + 1] ?? '').trim())) {
|
|
224
239
|
add(firstContent + 2, 'header-brief', '文件头第二行缺少 /// @brief 一句话说明')
|
|
225
240
|
}
|
|
226
241
|
for (let index = firstContent + 1; index < lines.length; index += 1) {
|
|
@@ -247,6 +262,7 @@ export function checkText(text, options = {}) {
|
|
|
247
262
|
if (trimmed.startsWith('///') || (trimmed.startsWith('//') && !trimmed.startsWith('//!'))) {
|
|
248
263
|
if (displayWidth(trimmed) > width) add(index + 1, 'comment-width', `注释行 ${displayWidth(trimmed)} 列,超过 ${width} 列`)
|
|
249
264
|
for (const tag of FORBIDDEN_TAGS) {
|
|
265
|
+
if (options.allowTags?.has(tag.slice(1))) continue
|
|
250
266
|
if (trimmed.includes(tag)) add(index + 1, 'forbidden-tag', `禁止使用 ${tag},按 references/comments.md 的标签表改写`)
|
|
251
267
|
}
|
|
252
268
|
}
|
|
@@ -429,6 +445,11 @@ function main(argv) {
|
|
|
429
445
|
}
|
|
430
446
|
if (options.help) return usage()
|
|
431
447
|
const root = options.root ? path.resolve(options.root) : process.cwd()
|
|
448
|
+
try {
|
|
449
|
+
statSync(root)
|
|
450
|
+
} catch {
|
|
451
|
+
return usage(`根目录不存在或无法读取:${root}`)
|
|
452
|
+
}
|
|
432
453
|
const columnLimit = readColumnLimit(root)
|
|
433
454
|
if (columnLimit !== undefined) {
|
|
434
455
|
if (!options.widthExplicit) options.width = columnLimit
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
**异常声明**:构造函数、`Get` 类查询、解析函数最容易抛;在声明注释里写「当……时抛出 `std::out_of_range`」这样的具体类型,不要只写「可能抛异常」,也不要给每个参数错误都新建类型。类型选择见 `references/exceptions.md`。
|
|
26
26
|
|
|
27
|
-
**注释**:声明注释用 `///` 写中文散文,只写调用方需要知道的;实现细节留给 `.cpp`。`@param`、`@return`、`@throws`
|
|
27
|
+
**注释**:声明注释用 `///` 写中文散文,只写调用方需要知道的;实现细节留给 `.cpp`。`@param`、`@return`、`@throws` 要么全写要么全不写,标识符和标签保持英文。文件头默认写 `@file` 与 `@brief` 两行,`@file` 必须与文件名相同;作者、日期、版权、版本默认不写。标签开关在插件设置页,技能正文末尾附带当前配置的快照。写完运行 `node "<skill-dir>/scripts/checking-comments.mjs" --root <项目根>`;关闭 `@brief` 时加 `--no-brief`,配置里打开了隐私标签并填了值时加 `--allow-tag <标签>`。横幅、标签、参数完整性、成员清单与设计论证由脚本兜底。文件头、章节横幅和 TODO 的格式见 `references/comments.md`。
|
|
28
28
|
|
|
29
29
|
## 正反例指针
|
|
30
30
|
|