@chengzhao-dev/dsh-cpp-workflow 0.5.4 → 0.6.0
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 +9 -3
- package/README.md +9 -3
- package/lib/client.js +570 -0
- package/lib/comment-policy.js +177 -0
- package/lib/index.js +35 -4
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +18 -6
- package/skills/developing-cpp/examples/comments-declaration.md +8 -10
- package/skills/developing-cpp/references/comments.md +14 -14
- package/skills/developing-cpp/references/file-template.md +6 -8
- package/skills/developing-cpp/scripts/checking-comments.mjs +48 -8
- 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.0
|
|
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.0
|
|
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`.
|
|
@@ -103,9 +107,11 @@ npm ci
|
|
|
103
107
|
npm test # build, three-skill structural check, gate/environment/schema/comment tests
|
|
104
108
|
npm run test:matrix # real build matrix: 4 templates x 5 standards
|
|
105
109
|
npm run pack:check # inspect the npm package contents
|
|
110
|
+
npm run ci # replay the Ubuntu CI job: install, test, package check
|
|
111
|
+
npm run ci:full # ci plus the standard matrix; needs a POSIX host
|
|
106
112
|
```
|
|
107
113
|
|
|
108
|
-
Source lives in `src/`; `npm run build` writes `lib/`. The structural check is `scripts/checking-skill-links.mjs`. Tests do not invoke a local compiler; the environment checks use fixed version strings. The standard matrix needs `cmake`, `ninja`, `perl`, and `clang++` (or the compiler named by `CXX`) on a POSIX host such as WSL, Linux, or macOS.
|
|
114
|
+
Source lives in `src/`; `npm run build` writes `lib/`. The structural check is `scripts/checking-skill-links.mjs`. Tests do not invoke a local compiler; the environment checks use fixed version strings. The standard matrix needs `cmake`, `ninja`, `perl`, and `clang++` (or the compiler named by `CXX`) on a POSIX host such as WSL, Linux, or macOS. On Windows, run `npm run ci` first, then run `npm run ci:full` from WSL inside the checkout.
|
|
109
115
|
|
|
110
116
|
## License
|
|
111
117
|
|
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.0
|
|
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.0
|
|
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`。
|
|
@@ -103,9 +107,11 @@ npm ci
|
|
|
103
107
|
npm test # 构建、三技能结构检查、门禁/环境/schema/注释测试
|
|
104
108
|
npm run test:matrix # 四套模板 × 五档标准的真实构建矩阵
|
|
105
109
|
npm run pack:check # 核对 npm 包内容
|
|
110
|
+
npm run ci # 复现 Ubuntu CI:安装、测试、打包检查
|
|
111
|
+
npm run ci:full # 在 ci 之上再跑标准矩阵,需 POSIX 宿主
|
|
106
112
|
```
|
|
107
113
|
|
|
108
|
-
源码在 `src/`,`npm run build` 生成 `lib/`。结构检查在 `scripts/checking-skill-links.mjs`;测试不调用本机编译器,环境检查用固定版本字符串。标准矩阵需要 POSIX 宿主上的 `cmake`、`ninja`、`perl` 和 `clang++`(或 `CXX` 指定的编译器),在 WSL/Linux/macOS 上运行。
|
|
114
|
+
源码在 `src/`,`npm run build` 生成 `lib/`。结构检查在 `scripts/checking-skill-links.mjs`;测试不调用本机编译器,环境检查用固定版本字符串。标准矩阵需要 POSIX 宿主上的 `cmake`、`ninja`、`perl` 和 `clang++`(或 `CXX` 指定的编译器),在 WSL/Linux/macOS 上运行。Windows 上先跑 `npm run ci`,再从 WSL 进入仓库跑 `npm run ci:full`。
|
|
109
115
|
|
|
110
116
|
## 许可
|
|
111
117
|
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
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
|
+
/** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
|
|
61
|
+
const DEFAULT_CONFIG = {
|
|
62
|
+
brief: { enabled: true, value: '' },
|
|
63
|
+
author: { enabled: false, value: '' },
|
|
64
|
+
date: { enabled: false, value: '' },
|
|
65
|
+
copyright: { enabled: false, value: '' },
|
|
66
|
+
version: { enabled: false, value: '' },
|
|
67
|
+
param: true,
|
|
68
|
+
returns: true,
|
|
69
|
+
throws: true,
|
|
70
|
+
note: false,
|
|
71
|
+
see: false,
|
|
72
|
+
pre: false,
|
|
73
|
+
post: false,
|
|
74
|
+
tparam: false,
|
|
75
|
+
retval: false,
|
|
76
|
+
};
|
|
77
|
+
function text(value) {
|
|
78
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
79
|
+
}
|
|
80
|
+
function flag(value, fallback) {
|
|
81
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 把宿主给的值补齐成完整配置;缺字段、旧版本或手工改坏的配置都回到出厂值。
|
|
85
|
+
*/
|
|
86
|
+
function normalizeConfig(raw) {
|
|
87
|
+
const source = raw && typeof raw === 'object' ? raw : {};
|
|
88
|
+
const config = {};
|
|
89
|
+
for (const tag of HEADER_TAGS) {
|
|
90
|
+
const entry = source[tag] && typeof source[tag] === 'object' ? source[tag] : {};
|
|
91
|
+
config[tag] = {
|
|
92
|
+
enabled: flag(entry.enabled, DEFAULT_CONFIG[tag].enabled),
|
|
93
|
+
value: text(entry.value),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
for (const tag of [...CONTRACT_TAGS, ...EXTRA_TAGS]) {
|
|
97
|
+
config[tag] = flag(source[tag], DEFAULT_CONFIG[tag]);
|
|
98
|
+
}
|
|
99
|
+
return config;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 按配置渲染文件头;`@file` 始终是传入的文件名。
|
|
103
|
+
*/
|
|
104
|
+
function renderFileHeader(raw, fileName) {
|
|
105
|
+
const config = normalizeConfig(raw);
|
|
106
|
+
const name = text(fileName) || 'board.h';
|
|
107
|
+
const lines = [`/// @file ${name}`];
|
|
108
|
+
if (config.brief.enabled) {
|
|
109
|
+
lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
|
|
110
|
+
}
|
|
111
|
+
for (const tag of ['author', 'date', 'copyright', 'version']) {
|
|
112
|
+
if (config[tag].enabled && config[tag].value)
|
|
113
|
+
lines.push(`/// @${tag} ${config[tag].value}`);
|
|
114
|
+
}
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 设置页的预览:文件头 + 一段函数注释,只显示当前配置会写出的标签。
|
|
119
|
+
*/
|
|
120
|
+
function previewParts(raw, sampleName) {
|
|
121
|
+
const config = normalizeConfig(raw);
|
|
122
|
+
const header = renderFileHeader(config, sampleName || 'board.h');
|
|
123
|
+
const contract = [];
|
|
124
|
+
if (config.param)
|
|
125
|
+
contract.push('/// @param position 目标行列坐标。');
|
|
126
|
+
if (config.returns)
|
|
127
|
+
contract.push('/// @return 放置成功时为 true。');
|
|
128
|
+
if (config.throws)
|
|
129
|
+
contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
|
|
130
|
+
const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
|
|
131
|
+
return { header, contract, declaration };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 设置页预览的纯文本形式;客户端半侧用同一份实现渲染 `<pre>`。
|
|
135
|
+
*/
|
|
136
|
+
function previewText(raw, sampleName) {
|
|
137
|
+
const { header, contract, declaration } = previewParts(raw, sampleName);
|
|
138
|
+
const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
|
|
139
|
+
if (contract.length > 0)
|
|
140
|
+
lines.push('///', ...contract);
|
|
141
|
+
lines.push(declaration);
|
|
142
|
+
return lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查。
|
|
146
|
+
*/
|
|
147
|
+
function checkerArgs(raw) {
|
|
148
|
+
const config = normalizeConfig(raw);
|
|
149
|
+
const args = [];
|
|
150
|
+
if (!config.brief.enabled)
|
|
151
|
+
args.push('--no-brief');
|
|
152
|
+
for (const tag of PRIVACY_TAGS) {
|
|
153
|
+
if (config[tag].enabled && config[tag].value)
|
|
154
|
+
args.push('--allow-tag', tag);
|
|
155
|
+
}
|
|
156
|
+
return args;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 技能快照:`get()` 把它接在技能正文末尾,让代理看到当前 profile 的标签开关。
|
|
160
|
+
*/
|
|
161
|
+
function snapshotText(raw) {
|
|
162
|
+
const config = normalizeConfig(raw);
|
|
163
|
+
const on = [];
|
|
164
|
+
const off = [];
|
|
165
|
+
for (const tag of HEADER_TAGS) {
|
|
166
|
+
const entry = config[tag];
|
|
167
|
+
const label = entry.value ? `${TAG_LABEL[tag]} ${entry.value}` : TAG_LABEL[tag];
|
|
168
|
+
if (entry.enabled)
|
|
169
|
+
on.push(label);
|
|
170
|
+
else
|
|
171
|
+
off.push(TAG_LABEL[tag]);
|
|
172
|
+
}
|
|
173
|
+
const contract = CONTRACT_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
|
|
174
|
+
const extra = EXTRA_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
|
|
175
|
+
const allowed = PRIVACY_TAGS.filter((tag) => config[tag].enabled && config[tag].value);
|
|
176
|
+
const head = `## 当前注释配置(profile)\n\n`;
|
|
177
|
+
const body = [
|
|
178
|
+
`- 文件头:@file 固定为文件名;开启 ${on.join('、') || '无'};关闭 ${off.join('、') || '无'}。`,
|
|
179
|
+
`- 函数契约:${contract.length > 0 ? contract.join('、') : '不要求'}。`,
|
|
180
|
+
`- 更多标签:${extra.length > 0 ? `可选 ${extra.join('、')}` : '未启用'}。`,
|
|
181
|
+
`- 检查器放行:${allowed.length > 0 ? allowed.join('、') : '无'}(隐私标签只有填了非空值才放行)。`,
|
|
182
|
+
];
|
|
183
|
+
return head + body.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
// 客户端半侧:设置导航里的「C++ 工程流程」页与插件页的配置卡片共用同一个组件。
|
|
188
|
+
// 标签策略在本文件上方内联(由 scripts/building-client.mjs 从 src/comment-policy.js
|
|
189
|
+
// 拼进来),所以预览、默认值与服务端快照永远同源;这里只做表单与排版。
|
|
190
|
+
const React = require('react')
|
|
191
|
+
const h = React.createElement
|
|
192
|
+
|
|
193
|
+
const PACKAGE_NAME = '@chengzhao-dev/dsh-cpp-workflow'
|
|
194
|
+
const SAMPLE_FILE = 'board.h'
|
|
195
|
+
|
|
196
|
+
const STRINGS = {
|
|
197
|
+
zh: {
|
|
198
|
+
nav: 'C++ 工程流程',
|
|
199
|
+
title: '文档注释',
|
|
200
|
+
intro: '文件头与函数契约默认写出哪些 Doxygen 标签。配置只影响代理写注释的默认值,不改已有源码。',
|
|
201
|
+
header: '文件头',
|
|
202
|
+
contract: '函数契约',
|
|
203
|
+
extra: '更多标签',
|
|
204
|
+
extraHint: '默认关闭;需要时在单个函数上补写。',
|
|
205
|
+
file: '@file',
|
|
206
|
+
fileHint: '固定为当前文件名,不参与配置。',
|
|
207
|
+
emptyValue: '留空则由每个文件自己写',
|
|
208
|
+
on: '写出',
|
|
209
|
+
off: '不写',
|
|
210
|
+
preview: '预览',
|
|
211
|
+
previewHint: '示例文件名 board.h;预览随开关即时更新。',
|
|
212
|
+
save: '保存',
|
|
213
|
+
reset: '重置',
|
|
214
|
+
saved: '已保存,下一次读取技能时生效。',
|
|
215
|
+
rejected: '宿主拒绝了这次保存:配置可能已在别处修改,请重置后再试。',
|
|
216
|
+
unsaved: '有未保存的改动。',
|
|
217
|
+
loading: '正在读取配置…',
|
|
218
|
+
unreadable: '当前账户不能修改这份配置。',
|
|
219
|
+
readOnly: '此处只显示出厂值。要修改配置,请到「插件」页打开 dsh-cpp-workflow 的配置卡片。',
|
|
220
|
+
noForm: '这个入口没有可写表单,配置在插件页修改。',
|
|
221
|
+
},
|
|
222
|
+
en: {
|
|
223
|
+
nav: 'C++ Workflow',
|
|
224
|
+
title: 'Documentation comments',
|
|
225
|
+
intro: 'Which Doxygen tags the agent writes by default. This changes defaults only; existing sources are untouched.',
|
|
226
|
+
header: 'File header',
|
|
227
|
+
contract: 'Function contract',
|
|
228
|
+
extra: 'More tags',
|
|
229
|
+
extraHint: 'Off by default; add them per function when needed.',
|
|
230
|
+
file: '@file',
|
|
231
|
+
fileHint: 'Always the current file name; not configurable.',
|
|
232
|
+
emptyValue: 'Leave empty and each file writes its own',
|
|
233
|
+
on: 'Write',
|
|
234
|
+
off: 'Omit',
|
|
235
|
+
preview: 'Preview',
|
|
236
|
+
previewHint: 'Sample name board.h; the preview follows the switches.',
|
|
237
|
+
save: 'Save',
|
|
238
|
+
reset: 'Reset',
|
|
239
|
+
saved: 'Saved; the next skill read uses it.',
|
|
240
|
+
rejected: 'The host rejected the save: the config changed elsewhere. Reset and try again.',
|
|
241
|
+
unsaved: 'Unsaved changes.',
|
|
242
|
+
loading: 'Reading configuration…',
|
|
243
|
+
unreadable: 'This account cannot edit the configuration.',
|
|
244
|
+
readOnly: 'Showing factory defaults. Edit the dsh-cpp-workflow card on the Plugins page.',
|
|
245
|
+
noForm: 'This entry has no writable form; edit it on the Plugins page.',
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function language() {
|
|
250
|
+
const value = typeof navigator !== 'undefined' && navigator.language ? navigator.language : 'zh'
|
|
251
|
+
return String(value).toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function t() {
|
|
255
|
+
return STRINGS[language()]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const palette = {
|
|
259
|
+
muted: 'var(--dsw-text-tertiary, #6b7280)',
|
|
260
|
+
line: 'var(--dsw-border-subtle, rgba(127, 127, 127, 0.24))',
|
|
261
|
+
surface: 'var(--dsw-surface-raised, rgba(127, 127, 127, 0.06))',
|
|
262
|
+
mono: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function rowStyle(extra) {
|
|
266
|
+
return {
|
|
267
|
+
display: 'flex',
|
|
268
|
+
alignItems: 'center',
|
|
269
|
+
gap: 10,
|
|
270
|
+
padding: '7px 0',
|
|
271
|
+
borderBottom: `1px solid ${palette.line}`,
|
|
272
|
+
...extra,
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function hintStyle() {
|
|
277
|
+
return { margin: '2px 0 0', fontSize: 12, lineHeight: 1.5, color: palette.muted }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function Switch(props) {
|
|
281
|
+
return h(
|
|
282
|
+
'label',
|
|
283
|
+
{ style: { display: 'inline-flex', alignItems: 'center', gap: 8, cursor: props.disabled ? 'default' : 'pointer', minWidth: 132 } },
|
|
284
|
+
h('input', {
|
|
285
|
+
type: 'checkbox',
|
|
286
|
+
checked: Boolean(props.checked),
|
|
287
|
+
disabled: Boolean(props.disabled),
|
|
288
|
+
onChange: (event) => props.onChange(event.target.checked),
|
|
289
|
+
style: { margin: 0 },
|
|
290
|
+
}),
|
|
291
|
+
h('span', { style: { fontSize: 13, fontFamily: palette.mono } }, props.label),
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function HeaderTagRow(props) {
|
|
296
|
+
const s = props.strings
|
|
297
|
+
const entry = props.entry
|
|
298
|
+
return h(
|
|
299
|
+
'div',
|
|
300
|
+
{ style: rowStyle({ flexWrap: 'wrap' }) },
|
|
301
|
+
h('div', { style: { flex: '1 1 280px', minWidth: 0 } },
|
|
302
|
+
h(Switch, {
|
|
303
|
+
label: TAG_LABEL[props.tag],
|
|
304
|
+
checked: entry.enabled,
|
|
305
|
+
onChange: (enabled) => props.onChange({ ...entry, enabled }),
|
|
306
|
+
}),
|
|
307
|
+
h('p', { style: hintStyle() }, TAG_TEXT[props.tag] || ''),
|
|
308
|
+
),
|
|
309
|
+
h('input', {
|
|
310
|
+
type: 'text',
|
|
311
|
+
value: entry.value,
|
|
312
|
+
placeholder: s.emptyValue,
|
|
313
|
+
disabled: !entry.enabled,
|
|
314
|
+
'aria-label': `${TAG_LABEL[props.tag]} value`,
|
|
315
|
+
onChange: (event) => props.onChange({ ...entry, value: event.target.value }),
|
|
316
|
+
style: {
|
|
317
|
+
flex: '1 1 220px',
|
|
318
|
+
minWidth: 0,
|
|
319
|
+
fontSize: 13,
|
|
320
|
+
padding: '5px 8px',
|
|
321
|
+
borderRadius: 6,
|
|
322
|
+
border: `1px solid ${palette.line}`,
|
|
323
|
+
background: entry.enabled ? 'transparent' : palette.surface,
|
|
324
|
+
color: 'inherit',
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function Preview(props) {
|
|
331
|
+
const s = props.strings
|
|
332
|
+
const text = previewText(props.config, SAMPLE_FILE)
|
|
333
|
+
return h(
|
|
334
|
+
'section',
|
|
335
|
+
{ 'aria-label': s.preview, style: { marginTop: 18 } },
|
|
336
|
+
h('h4', { style: { margin: '0 0 4px', fontSize: 13, fontWeight: 600 } }, s.preview),
|
|
337
|
+
h('p', { style: hintStyle() }, s.previewHint),
|
|
338
|
+
h(
|
|
339
|
+
'pre',
|
|
340
|
+
{
|
|
341
|
+
style: {
|
|
342
|
+
margin: '8px 0 0',
|
|
343
|
+
padding: '10px 12px',
|
|
344
|
+
borderRadius: 8,
|
|
345
|
+
background: palette.surface,
|
|
346
|
+
border: `1px solid ${palette.line}`,
|
|
347
|
+
overflowX: 'auto',
|
|
348
|
+
fontSize: 12.5,
|
|
349
|
+
lineHeight: 1.65,
|
|
350
|
+
fontFamily: palette.mono,
|
|
351
|
+
whiteSpace: 'pre',
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
text,
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function diffOperations(draft, base) {
|
|
360
|
+
const operations = []
|
|
361
|
+
for (const key of Object.keys(DEFAULT_CONFIG)) {
|
|
362
|
+
const next = JSON.stringify(draft[key])
|
|
363
|
+
const previous = JSON.stringify(base[key])
|
|
364
|
+
if (next !== previous) operations.push({ op: 'set', path: [key], value: draft[key] })
|
|
365
|
+
}
|
|
366
|
+
return operations
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function ConfigPage(props) {
|
|
370
|
+
const s = t()
|
|
371
|
+
const form = props.form
|
|
372
|
+
const initial = React.useMemo(() => normalizeConfig(form && form.state ? form.state.value : undefined), [])
|
|
373
|
+
const [draft, setDraft] = React.useState(initial)
|
|
374
|
+
const [base, setBase] = React.useState(initial)
|
|
375
|
+
const [dirty, setDirty] = React.useState(false)
|
|
376
|
+
const [busy, setBusy] = React.useState(false)
|
|
377
|
+
const [message, setMessage] = React.useState('')
|
|
378
|
+
const revision = form && form.state ? form.state.revision : undefined
|
|
379
|
+
const ready = Boolean(form && form.state && form.state.status === 'ready' && form.state.writable && form.state.mode === 'host')
|
|
380
|
+
const status = form && form.state ? form.state.status : 'missing'
|
|
381
|
+
|
|
382
|
+
React.useEffect(() => {
|
|
383
|
+
if (dirty || !form || !form.state || form.state.status !== 'ready') return
|
|
384
|
+
const next = normalizeConfig(form.state.value)
|
|
385
|
+
setBase(next)
|
|
386
|
+
setDraft(next)
|
|
387
|
+
}, [revision, status, dirty, form])
|
|
388
|
+
|
|
389
|
+
const update = (patch) => {
|
|
390
|
+
setDraft((current) => ({ ...current, ...patch }))
|
|
391
|
+
setDirty(true)
|
|
392
|
+
setMessage('')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const reset = () => {
|
|
396
|
+
setDraft(base)
|
|
397
|
+
setDirty(false)
|
|
398
|
+
setMessage('')
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const save = async () => {
|
|
402
|
+
if (!ready || busy) return
|
|
403
|
+
const operations = diffOperations(draft, base)
|
|
404
|
+
if (operations.length === 0) return
|
|
405
|
+
setBusy(true)
|
|
406
|
+
try {
|
|
407
|
+
const accepted = await form.mutate(operations, revision)
|
|
408
|
+
if (accepted) {
|
|
409
|
+
setBase(draft)
|
|
410
|
+
setDirty(false)
|
|
411
|
+
setMessage(s.saved)
|
|
412
|
+
} else {
|
|
413
|
+
setMessage(s.rejected)
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
setMessage(error instanceof Error ? error.message : String(error))
|
|
417
|
+
} finally {
|
|
418
|
+
setBusy(false)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const editable = Boolean(form)
|
|
423
|
+
const editableReady = editable && ready
|
|
424
|
+
const headerTags = ['brief', 'author', 'date', 'copyright', 'version']
|
|
425
|
+
|
|
426
|
+
return h(
|
|
427
|
+
'div',
|
|
428
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 4, padding: '4px 0 12px', maxWidth: 720 } },
|
|
429
|
+
h('h3', { style: { margin: '0 0 2px', fontSize: 15, fontWeight: 600 } }, s.title),
|
|
430
|
+
h('p', { style: hintStyle() }, s.intro),
|
|
431
|
+
!editable
|
|
432
|
+
? h('p', { role: 'status', style: { ...hintStyle(), color: 'inherit', marginTop: 8 } }, s.readOnly)
|
|
433
|
+
: null,
|
|
434
|
+
editable && !ready && status !== 'missing'
|
|
435
|
+
? h('p', { role: 'status', style: hintStyle() }, status === 'loading' ? s.loading : s.unreadable)
|
|
436
|
+
: null,
|
|
437
|
+
h(
|
|
438
|
+
'fieldset',
|
|
439
|
+
{ style: { margin: '14px 0 0', padding: 0, border: 0 } },
|
|
440
|
+
h('legend', { style: { padding: 0, fontSize: 13, fontWeight: 600 } }, s.header),
|
|
441
|
+
h(
|
|
442
|
+
'div',
|
|
443
|
+
{ style: rowStyle() },
|
|
444
|
+
h(Switch, { label: s.file, checked: true, disabled: true, onChange: () => {} }),
|
|
445
|
+
h('p', { style: { ...hintStyle(), margin: 0 } }, s.fileHint),
|
|
446
|
+
),
|
|
447
|
+
...headerTags.map((tag) =>
|
|
448
|
+
h(HeaderTagRow, {
|
|
449
|
+
key: tag,
|
|
450
|
+
tag,
|
|
451
|
+
strings: s,
|
|
452
|
+
entry: draft[tag],
|
|
453
|
+
onChange: (entry) => update({ [tag]: entry }),
|
|
454
|
+
}),
|
|
455
|
+
),
|
|
456
|
+
),
|
|
457
|
+
h(
|
|
458
|
+
'fieldset',
|
|
459
|
+
{ style: { margin: '18px 0 0', padding: 0, border: 0 } },
|
|
460
|
+
h('legend', { style: { padding: 0, fontSize: 13, fontWeight: 600 } }, s.contract),
|
|
461
|
+
h(
|
|
462
|
+
'div',
|
|
463
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: '6px 18px', padding: '8px 0 2px' } },
|
|
464
|
+
...CONTRACT_TAGS.map((tag) =>
|
|
465
|
+
h(
|
|
466
|
+
'div',
|
|
467
|
+
{ key: tag, style: { flex: '1 1 180px', minWidth: 180 } },
|
|
468
|
+
h(Switch, {
|
|
469
|
+
label: TAG_LABEL[tag],
|
|
470
|
+
checked: draft[tag],
|
|
471
|
+
onChange: (value) => update({ [tag]: value }),
|
|
472
|
+
}),
|
|
473
|
+
h('p', { style: hintStyle() }, TAG_TEXT[tag]),
|
|
474
|
+
),
|
|
475
|
+
),
|
|
476
|
+
),
|
|
477
|
+
),
|
|
478
|
+
h(
|
|
479
|
+
'details',
|
|
480
|
+
{ style: { marginTop: 16, paddingTop: 10, borderTop: `1px solid ${palette.line}` } },
|
|
481
|
+
h('summary', { style: { cursor: 'pointer', fontSize: 13, fontWeight: 600 } }, s.extra),
|
|
482
|
+
h('p', { style: hintStyle() }, s.extraHint),
|
|
483
|
+
h(
|
|
484
|
+
'div',
|
|
485
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: '6px 18px', paddingTop: 4 } },
|
|
486
|
+
...EXTRA_TAGS.map((tag) =>
|
|
487
|
+
h(
|
|
488
|
+
'div',
|
|
489
|
+
{ key: tag, style: { flex: '1 1 180px', minWidth: 180 } },
|
|
490
|
+
h(Switch, {
|
|
491
|
+
label: TAG_LABEL[tag],
|
|
492
|
+
checked: draft[tag],
|
|
493
|
+
onChange: (value) => update({ [tag]: value }),
|
|
494
|
+
}),
|
|
495
|
+
),
|
|
496
|
+
),
|
|
497
|
+
),
|
|
498
|
+
),
|
|
499
|
+
h(Preview, { config: draft, strings: s }),
|
|
500
|
+
editable
|
|
501
|
+
? h(
|
|
502
|
+
'div',
|
|
503
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 16, flexWrap: 'wrap' } },
|
|
504
|
+
h(
|
|
505
|
+
'button',
|
|
506
|
+
{
|
|
507
|
+
type: 'button',
|
|
508
|
+
onClick: save,
|
|
509
|
+
disabled: busy || !editableReady || !dirty,
|
|
510
|
+
style: { fontSize: 13, padding: '6px 14px', borderRadius: 6, cursor: 'pointer' },
|
|
511
|
+
},
|
|
512
|
+
s.save,
|
|
513
|
+
),
|
|
514
|
+
h(
|
|
515
|
+
'button',
|
|
516
|
+
{
|
|
517
|
+
type: 'button',
|
|
518
|
+
onClick: reset,
|
|
519
|
+
disabled: busy || !dirty,
|
|
520
|
+
style: { fontSize: 13, padding: '6px 14px', borderRadius: 6, cursor: 'pointer' },
|
|
521
|
+
},
|
|
522
|
+
s.reset,
|
|
523
|
+
),
|
|
524
|
+
h('p', { role: 'status', style: { ...hintStyle(), margin: 0, flex: '1 1 220px' } }, message || (dirty ? s.unsaved : '')),
|
|
525
|
+
)
|
|
526
|
+
: null,
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function page(ownerProps, titled) {
|
|
531
|
+
const props = ownerProps || {}
|
|
532
|
+
return h(ConfigPage, { ...props, titled })
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function apply(ctx) {
|
|
536
|
+
ctx.slots.inject('settings.section', () =>
|
|
537
|
+
ctx.slots.register(
|
|
538
|
+
{
|
|
539
|
+
name: 'settings.section',
|
|
540
|
+
id: 'cpp-workflow',
|
|
541
|
+
order: 60,
|
|
542
|
+
label: () => t().nav,
|
|
543
|
+
},
|
|
544
|
+
(ownerProps) => page(ownerProps, true),
|
|
545
|
+
),
|
|
546
|
+
)
|
|
547
|
+
ctx.slots.inject('plugins.bundle.config', () =>
|
|
548
|
+
ctx.slots.register(
|
|
549
|
+
{
|
|
550
|
+
name: 'plugins.bundle.config',
|
|
551
|
+
key: PACKAGE_NAME,
|
|
552
|
+
label: () => t().nav,
|
|
553
|
+
},
|
|
554
|
+
(ownerProps) => (ownerProps && ownerProps.view === 'summary' ? null : page(ownerProps, false)),
|
|
555
|
+
),
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
module.exports = {
|
|
560
|
+
name: PACKAGE_NAME,
|
|
561
|
+
inject: ['slots'],
|
|
562
|
+
apply,
|
|
563
|
+
ConfigPage,
|
|
564
|
+
diffOperations,
|
|
565
|
+
previewText,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return module.exports
|
|
569
|
+
},
|
|
570
|
+
})
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
/** 出厂配置:文件头只留 @file 与 @brief,契约标签全开,隐私标签全关。 */
|
|
54
|
+
export const DEFAULT_CONFIG = {
|
|
55
|
+
brief: { enabled: true, value: '' },
|
|
56
|
+
author: { enabled: false, value: '' },
|
|
57
|
+
date: { enabled: false, value: '' },
|
|
58
|
+
copyright: { enabled: false, value: '' },
|
|
59
|
+
version: { enabled: false, value: '' },
|
|
60
|
+
param: true,
|
|
61
|
+
returns: true,
|
|
62
|
+
throws: true,
|
|
63
|
+
note: false,
|
|
64
|
+
see: false,
|
|
65
|
+
pre: false,
|
|
66
|
+
post: false,
|
|
67
|
+
tparam: false,
|
|
68
|
+
retval: false,
|
|
69
|
+
};
|
|
70
|
+
function text(value) {
|
|
71
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
72
|
+
}
|
|
73
|
+
function flag(value, fallback) {
|
|
74
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 把宿主给的值补齐成完整配置;缺字段、旧版本或手工改坏的配置都回到出厂值。
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeConfig(raw) {
|
|
80
|
+
const source = raw && typeof raw === 'object' ? raw : {};
|
|
81
|
+
const config = {};
|
|
82
|
+
for (const tag of HEADER_TAGS) {
|
|
83
|
+
const entry = source[tag] && typeof source[tag] === 'object' ? source[tag] : {};
|
|
84
|
+
config[tag] = {
|
|
85
|
+
enabled: flag(entry.enabled, DEFAULT_CONFIG[tag].enabled),
|
|
86
|
+
value: text(entry.value),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
for (const tag of [...CONTRACT_TAGS, ...EXTRA_TAGS]) {
|
|
90
|
+
config[tag] = flag(source[tag], DEFAULT_CONFIG[tag]);
|
|
91
|
+
}
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 按配置渲染文件头;`@file` 始终是传入的文件名。
|
|
96
|
+
*/
|
|
97
|
+
export function renderFileHeader(raw, fileName) {
|
|
98
|
+
const config = normalizeConfig(raw);
|
|
99
|
+
const name = text(fileName) || 'board.h';
|
|
100
|
+
const lines = [`/// @file ${name}`];
|
|
101
|
+
if (config.brief.enabled) {
|
|
102
|
+
lines.push(config.brief.value ? `/// @brief ${config.brief.value}` : '/// @brief');
|
|
103
|
+
}
|
|
104
|
+
for (const tag of ['author', 'date', 'copyright', 'version']) {
|
|
105
|
+
if (config[tag].enabled && config[tag].value)
|
|
106
|
+
lines.push(`/// @${tag} ${config[tag].value}`);
|
|
107
|
+
}
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 设置页的预览:文件头 + 一段函数注释,只显示当前配置会写出的标签。
|
|
112
|
+
*/
|
|
113
|
+
export function previewParts(raw, sampleName) {
|
|
114
|
+
const config = normalizeConfig(raw);
|
|
115
|
+
const header = renderFileHeader(config, sampleName || 'board.h');
|
|
116
|
+
const contract = [];
|
|
117
|
+
if (config.param)
|
|
118
|
+
contract.push('/// @param position 目标行列坐标。');
|
|
119
|
+
if (config.returns)
|
|
120
|
+
contract.push('/// @return 放置成功时为 true。');
|
|
121
|
+
if (config.throws)
|
|
122
|
+
contract.push('/// @throws std::out_of_range 坐标越界时抛出。');
|
|
123
|
+
const declaration = '[[nodiscard]] bool PlaceCell(CellPosition position, CellType cell_type);';
|
|
124
|
+
return { header, contract, declaration };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 设置页预览的纯文本形式;客户端半侧用同一份实现渲染 `<pre>`。
|
|
128
|
+
*/
|
|
129
|
+
export function previewText(raw, sampleName) {
|
|
130
|
+
const { header, contract, declaration } = previewParts(raw, sampleName);
|
|
131
|
+
const lines = [...header, '///', '/// 放置成功返回 true;坐标越界或目标已占时返回 false。'];
|
|
132
|
+
if (contract.length > 0)
|
|
133
|
+
lines.push('///', ...contract);
|
|
134
|
+
lines.push(declaration);
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查。
|
|
139
|
+
*/
|
|
140
|
+
export function checkerArgs(raw) {
|
|
141
|
+
const config = normalizeConfig(raw);
|
|
142
|
+
const args = [];
|
|
143
|
+
if (!config.brief.enabled)
|
|
144
|
+
args.push('--no-brief');
|
|
145
|
+
for (const tag of PRIVACY_TAGS) {
|
|
146
|
+
if (config[tag].enabled && config[tag].value)
|
|
147
|
+
args.push('--allow-tag', tag);
|
|
148
|
+
}
|
|
149
|
+
return args;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 技能快照:`get()` 把它接在技能正文末尾,让代理看到当前 profile 的标签开关。
|
|
153
|
+
*/
|
|
154
|
+
export function snapshotText(raw) {
|
|
155
|
+
const config = normalizeConfig(raw);
|
|
156
|
+
const on = [];
|
|
157
|
+
const off = [];
|
|
158
|
+
for (const tag of HEADER_TAGS) {
|
|
159
|
+
const entry = config[tag];
|
|
160
|
+
const label = entry.value ? `${TAG_LABEL[tag]} ${entry.value}` : TAG_LABEL[tag];
|
|
161
|
+
if (entry.enabled)
|
|
162
|
+
on.push(label);
|
|
163
|
+
else
|
|
164
|
+
off.push(TAG_LABEL[tag]);
|
|
165
|
+
}
|
|
166
|
+
const contract = CONTRACT_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
|
|
167
|
+
const extra = EXTRA_TAGS.filter((tag) => config[tag]).map((tag) => TAG_LABEL[tag]);
|
|
168
|
+
const allowed = PRIVACY_TAGS.filter((tag) => config[tag].enabled && config[tag].value);
|
|
169
|
+
const head = `## 当前注释配置(profile)\n\n`;
|
|
170
|
+
const body = [
|
|
171
|
+
`- 文件头:@file 固定为文件名;开启 ${on.join('、') || '无'};关闭 ${off.join('、') || '无'}。`,
|
|
172
|
+
`- 函数契约:${contract.length > 0 ? contract.join('、') : '不要求'}。`,
|
|
173
|
+
`- 更多标签:${extra.length > 0 ? `可选 ${extra.join('、')}` : '未启用'}。`,
|
|
174
|
+
`- 检查器放行:${allowed.length > 0 ? allowed.join('、') : '无'}(隐私标签只有填了非空值才放行)。`,
|
|
175
|
+
];
|
|
176
|
+
return head + body.join('\n');
|
|
177
|
+
}
|
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.0",
|
|
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,10 @@
|
|
|
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",
|
|
53
|
+
"ci": "npm ci && npm test && npm run pack:check",
|
|
54
|
+
"ci:full": "npm run ci && npm run test:matrix",
|
|
46
55
|
"check:encoding": "node skills/building-cpp/scripts/checking-encoding.mjs",
|
|
47
56
|
"check:workflows": "node scripts/checking-github-workflows.mjs",
|
|
48
57
|
"test:matrix": "bash scripts/checking-standard-matrix.sh",
|
|
@@ -77,6 +86,9 @@
|
|
|
77
86
|
"peerDependencies": {
|
|
78
87
|
"@deepseek-ai/dsh-skill": ">=0.2.0-rc.1 <0.3.0-0"
|
|
79
88
|
},
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"@deepseek-ai/schemastery": "^3.18.5"
|
|
91
|
+
},
|
|
80
92
|
"devDependencies": {
|
|
81
93
|
"@deepseek-ai/dsh-skill": ">=0.2.0-rc.1 <0.3.0-0",
|
|
82
94
|
"@types/node": "^22.20.5",
|
|
@@ -14,11 +14,10 @@
|
|
|
14
14
|
**好**:
|
|
15
15
|
|
|
16
16
|
```cpp
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//===----------------------------------------------------------------------===//
|
|
17
|
+
/// @file board.h
|
|
18
|
+
/// @brief 定义井字棋棋盘、格子坐标与格子类型。
|
|
19
|
+
///
|
|
20
|
+
/// 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
|
|
22
21
|
```
|
|
23
22
|
|
|
24
23
|
文件头只介绍这个文件里有什么,不写作者和变更史,也不把读者指去另一个文件。
|
|
@@ -310,11 +309,10 @@ Standard: Latest
|
|
|
310
309
|
**好**:
|
|
311
310
|
|
|
312
311
|
```cpp
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
//===----------------------------------------------------------------------===//
|
|
312
|
+
/// @file board.h
|
|
313
|
+
/// @brief 棋盘状态与规则判断。
|
|
314
|
+
///
|
|
315
|
+
/// 定义 Board 的查询、落子与清空棋盘。
|
|
318
316
|
```
|
|
319
317
|
|
|
320
318
|
变更史进提交说明,外部资料进文档,团队流程进规范文件;注释只写这个文件或这个类型自己的内容,不把读者指去别的文件。
|
|
@@ -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
|
|
|
@@ -100,7 +100,7 @@ GameResult result_ = GameResult::kInProgress;
|
|
|
100
100
|
|
|
101
101
|
| 层 | 行数 | 第一句 | 写什么 |
|
|
102
102
|
| --- | --- | --- | --- |
|
|
103
|
-
| 文件头 | 3–
|
|
103
|
+
| 文件头 | 3–7 | `@file` 文件名 + `@brief` 一句话 | 本文件由哪几部分组成 |
|
|
104
104
|
| 类型注释 | 2–14 | 名词短语:这个类型或取值是什么 | 只有类的不变量 |
|
|
105
105
|
| 特殊成员 | 1–3 | 标签(默认/拷贝/移动构造函数、析构函数) | 后置条件或职责 |
|
|
106
106
|
| 函数注释 | 3–10 | 正常返回什么、失败时返回或抛什么、检查什么 | 参数与失败方式 |
|
|
@@ -109,29 +109,28 @@ GameResult result_ = GameResult::kInProgress;
|
|
|
109
109
|
|
|
110
110
|
## 布局:放在哪一层、哪一行
|
|
111
111
|
|
|
112
|
-
- `///`
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
112
|
+
- `///` 给声明与文件头:头文件里的类型、函数、枚举值、公开与私有数据成员,以及文件头的 `@file`/`@brief` 块。数据成员的主注释写在上一行;行尾 `///<` 只用于枚举项和 `struct` 的短字段。调用方和 Doxygen 从这里取契约。
|
|
113
|
+
- `///` 也给实现:函数体内的阶段与边界说明、局部变量,一律用 `///`,不混 `//`。
|
|
114
|
+
- `//` 只留给工具指令与别种文件:`// NOLINTNEXTLINE(...)`、`#endif // MACRO` 这类要被工具读取的标记,以及 CMake、YAML 等非 C++ 文件。
|
|
115
|
+
- 判断看位置:声明写契约,实现写理由,两者都用 `///`;`//` 不再表示「实现注释」。
|
|
116
116
|
- 位置固定:文件头在最上方;类型注释紧贴类型声明;特殊成员(构造、析构、拷贝与移动)排在其他函数之前;函数注释紧贴函数声明;数据成员的主注释紧贴声明上一行,不写行尾补充。
|
|
117
117
|
- 类数据成员(含私有)的主注释用 `///`;行尾 `///<` 只给枚举项和 `struct` 的短字段。
|
|
118
118
|
- 注释和它说明的声明之间不留空行;同一个声明块内,空行只用来分组。
|
|
119
119
|
|
|
120
120
|
### 文件头
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
每个手写源文件开头写一段 doxygen 文件头:第一行 `/// @file <文件名>`,第二行 `/// @brief` 一句话说明这个文件定义或实现什么,用一行 `///` 隔开后,只写本文件自己的组成部分或边界。`@file` 的参数必须与文件名逐字相同,检查器会核对。`@brief` 默认写出;`@author`、`@date`、`@copyright`、`@version` 默认不写,只有插件配置里打开并填了非空值才写出,作者、年份和变更史不写进源码。删掉把读者指去另一个头文件、另一个目录或另一层的句子,也删掉「这一层不负责什么」。不写作者、版权年份、变更日志和外部资料路径。
|
|
123
123
|
|
|
124
124
|
```cpp
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//===----------------------------------------------------------------------===//
|
|
125
|
+
/// @file board.h
|
|
126
|
+
/// @brief 定义井字棋棋盘、格子坐标与格子类型。
|
|
127
|
+
///
|
|
128
|
+
/// 棋盘保存九个棋盘格子的状态,提供查询、落子与清空棋盘。
|
|
130
129
|
```
|
|
131
130
|
|
|
132
|
-
- C++
|
|
131
|
+
- C++ 源文件用以上格式;CMake 用 `#`,YAML/JSON 用 `#`。
|
|
133
132
|
- 超过约 200 行的文件用章节横幅分组,组名是读者要跳转的锚点;一个文件不超过 8 个横幅。
|
|
134
|
-
- 文件头 3–
|
|
133
|
+
- 文件头 3–7 行;`@file` 与 `@brief` 各占一行,后面只写本文件的范围与组成,不写实现步骤、作者和变更史。
|
|
135
134
|
|
|
136
135
|
## 排版:符号、宽度、横幅与标签
|
|
137
136
|
|
|
@@ -143,6 +142,7 @@ GameResult result_ = GameResult::kInProgress;
|
|
|
143
142
|
- `.clang-format`、`.clang-tidy`、`.clangd`、CMake 的配置项旁边写「为什么这样设」,不写「这一项是什么」;配置注释贴在被解释的键上方。
|
|
144
143
|
- 禁止:大段表格、装饰图案、表情;外部资料路径、跨仓库引用、团队流程;注释掉的代码;注释元叙述(「其余内容见某处」「两处不重复」)。
|
|
145
144
|
- `///`、`@param`、横幅不进入 `.qmd`、YAML、CSS。
|
|
145
|
+
- C++ 源文件的说明性注释一律 `///`,实现区不与 `//` 混用;`//` 只留给文件头横幅边框与被引用的配置行。
|
|
146
146
|
|
|
147
147
|
## 润色:删复述,统一句式
|
|
148
148
|
|
|
@@ -46,11 +46,10 @@
|
|
|
46
46
|
5. `#endif // MACRO`
|
|
47
47
|
|
|
48
48
|
```cpp
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//===----------------------------------------------------------------------===//
|
|
49
|
+
/// @file thing_table.h
|
|
50
|
+
/// @brief 定义 ThingTable 与它用到的状态。
|
|
51
|
+
///
|
|
52
|
+
/// 头文件放常量、枚举、结构体与类声明。
|
|
54
53
|
|
|
55
54
|
#ifndef PROJECT_PATH_FILE_H_
|
|
56
55
|
#define PROJECT_PATH_FILE_H_
|
|
@@ -99,9 +98,8 @@ class ThingTable {
|
|
|
99
98
|
4. 命名空间与定义
|
|
100
99
|
|
|
101
100
|
```cpp
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//===----------------------------------------------------------------------===//
|
|
101
|
+
/// @file thing_table.cpp
|
|
102
|
+
/// @brief 实现 ThingTable 的添加与查询。
|
|
105
103
|
|
|
106
104
|
#include "project/path/thing.h"
|
|
107
105
|
|
|
@@ -25,8 +25,12 @@ 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 = /^\/\/=+-+=+\/\/$/
|
|
32
|
+
const DOXY_FILE = /^\/\/\/\s*@file\s+\S/
|
|
33
|
+
const DOXY_BRIEF = /^\/\/\/\s*@brief\s+\S/
|
|
30
34
|
const MEMBER_LIST = /提供[^。]{0,40}、[^。]{0,40}(的功能|的能力|的接口)|功能[::]/
|
|
31
35
|
const RATIONALE = /而不是|而非|代价.{0,4}忽略|只留一份|与其|比.{0,12}更|更(?:安全|快|清楚|直接|好写|难写)/
|
|
32
36
|
const FORWARD_REF = /(?:留在|归|交给|由)\s*`?[A-Z][A-Za-z0-9_]*`?/
|
|
@@ -38,12 +42,12 @@ const DECL_KEYWORDS = /^(class|struct|union|enum|namespace|using|typedef|templat
|
|
|
38
42
|
|
|
39
43
|
function usage(message) {
|
|
40
44
|
if (message) process.stderr.write(`错误:${message}\n`)
|
|
41
|
-
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')
|
|
42
46
|
return EXIT.usage
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
function parseArgs(argv) {
|
|
46
|
-
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 }
|
|
47
51
|
for (let index = 0; index < argv.length; index += 1) {
|
|
48
52
|
const arg = argv[index]
|
|
49
53
|
if (arg === '--root') {
|
|
@@ -59,6 +63,13 @@ function parseArgs(argv) {
|
|
|
59
63
|
index += 1
|
|
60
64
|
} else if (arg === '--no-header') {
|
|
61
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
|
|
62
73
|
} else if (arg === '--json') {
|
|
63
74
|
options.json = true
|
|
64
75
|
} else if (arg === '--quiet') {
|
|
@@ -205,16 +216,35 @@ export function checkText(text, options = {}) {
|
|
|
205
216
|
const inClass = classMemberLines(lines)
|
|
206
217
|
|
|
207
218
|
const firstContent = lines.findIndex((line) => line.trim() !== '')
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
const firstTrimmed = firstContent === -1 ? '' : lines[firstContent].trim()
|
|
220
|
+
const hasDoxyHeader = DOXY_FILE.test(firstTrimmed)
|
|
221
|
+
let doxyHeaderEnd = -1
|
|
222
|
+
if (hasDoxyHeader) {
|
|
223
|
+
doxyHeaderEnd = firstContent
|
|
224
|
+
while (doxyHeaderEnd + 1 < lines.length && lines[doxyHeaderEnd + 1].trim().startsWith('///')) doxyHeaderEnd += 1
|
|
225
|
+
}
|
|
226
|
+
if (requireHeader && firstContent !== -1 && !BANNER.test(firstTrimmed) && !hasDoxyHeader) {
|
|
227
|
+
add(firstContent + 1, 'header-missing', '文件开头缺少文件头:写 /// @file <文件名>,下一行写 /// @brief')
|
|
210
228
|
}
|
|
211
|
-
if (firstContent !== -1 && BANNER.test(
|
|
229
|
+
if (firstContent !== -1 && (BANNER.test(firstTrimmed) || hasDoxyHeader)) {
|
|
212
230
|
const own = options.fileName ? options.fileName.split(/[\\/]/).pop() : ''
|
|
231
|
+
const bannerHeader = BANNER.test(firstTrimmed)
|
|
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())) {
|
|
239
|
+
add(firstContent + 2, 'header-brief', '文件头第二行缺少 /// @brief 一句话说明')
|
|
240
|
+
}
|
|
213
241
|
for (let index = firstContent + 1; index < lines.length; index += 1) {
|
|
214
242
|
const trimmed = lines[index].trim()
|
|
215
|
-
if (BANNER.test(trimmed) || !trimmed.startsWith('//')) break
|
|
243
|
+
if (bannerHeader ? BANNER.test(trimmed) || !trimmed.startsWith('//') : !trimmed.startsWith('///')) break
|
|
216
244
|
const names = trimmed.match(/\b[A-Za-z0-9_-]+\.(?:h|hpp|cpp|cc)\b/g) ?? []
|
|
217
|
-
|
|
245
|
+
const fileMatches = trimmed.match(/@file\s+(\S+)/)
|
|
246
|
+
const namesToCheck = fileMatches ? names.filter((name) => name !== fileMatches[1]) : names
|
|
247
|
+
for (const name of namesToCheck) {
|
|
218
248
|
if (name !== own) add(index + 1, 'header-cross-file', `文件头提到了别的源文件 ${name}:只介绍本文件,删掉跨文件句子`)
|
|
219
249
|
}
|
|
220
250
|
if (/\bapps\//.test(trimmed)) add(index + 1, 'header-cross-file', '文件头指向了另一个目录:只介绍本文件')
|
|
@@ -232,12 +262,17 @@ export function checkText(text, options = {}) {
|
|
|
232
262
|
if (trimmed.startsWith('///') || (trimmed.startsWith('//') && !trimmed.startsWith('//!'))) {
|
|
233
263
|
if (displayWidth(trimmed) > width) add(index + 1, 'comment-width', `注释行 ${displayWidth(trimmed)} 列,超过 ${width} 列`)
|
|
234
264
|
for (const tag of FORBIDDEN_TAGS) {
|
|
265
|
+
if (options.allowTags?.has(tag.slice(1))) continue
|
|
235
266
|
if (trimmed.includes(tag)) add(index + 1, 'forbidden-tag', `禁止使用 ${tag},按 references/comments.md 的标签表改写`)
|
|
236
267
|
}
|
|
237
268
|
}
|
|
238
269
|
}
|
|
239
270
|
|
|
240
271
|
for (let index = 0; index < lines.length; index += 1) {
|
|
272
|
+
if (doxyHeaderEnd !== -1 && index <= doxyHeaderEnd) {
|
|
273
|
+
index = doxyHeaderEnd
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
241
276
|
if (!lines[index].trim().startsWith('///')) continue
|
|
242
277
|
if (lines[index].trim().startsWith('///<')) continue
|
|
243
278
|
const start = index
|
|
@@ -410,6 +445,11 @@ function main(argv) {
|
|
|
410
445
|
}
|
|
411
446
|
if (options.help) return usage()
|
|
412
447
|
const root = options.root ? path.resolve(options.root) : process.cwd()
|
|
448
|
+
try {
|
|
449
|
+
statSync(root)
|
|
450
|
+
} catch {
|
|
451
|
+
return usage(`根目录不存在或无法读取:${root}`)
|
|
452
|
+
}
|
|
413
453
|
const columnLimit = readColumnLimit(root)
|
|
414
454
|
if (columnLimit !== undefined) {
|
|
415
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
|
|