@jsbot-tellus/core 2.9.1 → 2.10.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/dist/core/plugin/params/declaration.d.ts +2 -2
- package/dist/core/plugin/params/declaration.d.ts.map +1 -1
- package/dist/core/plugin/params/declaration.js +189 -24
- package/dist/core/plugin/params/declaration.js.map +1 -1
- package/dist/core/plugin/params/input.d.ts.map +1 -1
- package/dist/core/plugin/params/input.js +35 -12
- package/dist/core/plugin/params/input.js.map +1 -1
- package/dist/core/plugin/params/type-level.d.ts +13 -3
- package/dist/core/plugin/params/type-level.d.ts.map +1 -1
- package/dist/core/plugin/params/types.d.ts +13 -2
- package/dist/core/plugin/params/types.d.ts.map +1 -1
- package/dist/core/plugin/params/types.js +22 -0
- package/dist/core/plugin/params/types.js.map +1 -1
- package/docs/plugin-api.md +27 -3
- package/package.json +1 -1
- package/src/core/plugin/params/declaration.ts +269 -31
- package/src/core/plugin/params/input.ts +63 -12
- package/src/core/plugin/params/type-level.ts +114 -6
- package/src/core/plugin/params/types.ts +46 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/core/plugin/params/types.ts"],"names":[],"mappings":"AAsBA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAChC,KAAK,CAAQ;IAEtB,YAAY,OAAe,EAAE,KAAa;QACtC,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAA;QAChC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACtB,CAAC;CACJ;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/core/plugin/params/types.ts"],"names":[],"mappings":"AAsBA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAChC,KAAK,CAAQ;IAEtB,YAAY,OAAe,EAAE,KAAa;QACtC,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAA;QAChC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACtB,CAAC;CACJ;AAkCD,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC5C,OAAO,IAAI,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,SAAyB;IACpD,IAAI,SAAS,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK;aAC1B,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;aACtB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QAEzB,OAAO,IAAI,OAAO,GAAG,CAAA;IACzB,CAAC;IAED,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAA;IAE/B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC,IAAI,CAAA;IACzB,CAAC;IAED,OAAO,GAAG,SAAS,CAAC,IAAI,IAAI,MAAM,CAAC,GAAG,IAAI,EAAE,KAAK,MAAM,CAAC,GAAG,IAAI,EAAE,GAAG,CAAA;AACxE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAC/B,UAAqC;IAErC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;IAE7C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IAElC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAE/C,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;IAE5C,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAA;AACvD,CAAC"}
|
package/docs/plugin-api.md
CHANGED
|
@@ -250,6 +250,8 @@ ctx.commands.register(
|
|
|
250
250
|
| `<arg:type>` | 指定类型的必填参数 |
|
|
251
251
|
| `[arg:type=default]` | 可选参数及默认值 |
|
|
252
252
|
| `<arg:string(2..10)>` | 带长度限制的字符串参数 |
|
|
253
|
+
| `<arg:type1\|type2>` | 联合类型,按声明顺序尝试 |
|
|
254
|
+
| `<arg:'a'\|'b'>` | 字面量联合,输入必须匹配其中一个值 |
|
|
253
255
|
| `<arg:text>` | 文本参数,消费全部剩余输入,必须放在最后 |
|
|
254
256
|
| `[...rest]` | 变长参数,每个剩余 token 作为一个字符串 |
|
|
255
257
|
|
|
@@ -277,20 +279,38 @@ params: '<message:text(2..200)> [nickname:string(..16)]'
|
|
|
277
279
|
- 只能用于 `string` / `text`;其他类型和 `[...rest]` 会报声明错误。
|
|
278
280
|
- 默认值也会在注册时校验;usage 不会显示长度限制。
|
|
279
281
|
|
|
282
|
+
联合类型:
|
|
283
|
+
|
|
284
|
+
`|` 可以把多个内置类型或字面量组合成一个参数,解析时按声明顺序尝试,第一个成功的候选生效:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
params: "<value:string|integer> [mode:'fast'|'slow'='fast']"
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- 内置类型联合:`<value:string|integer>`,handler 推导为 `string | number`。
|
|
291
|
+
- 字面量联合:`<mode:'fast'|'slow'>`,输入必须精确等于其中一个值,区分大小写;handler 推导为 `'fast' | 'slow'`。字面量必须用单引号或双引号包裹,支持空格、`|`、`=`、`:` 和转义引号。
|
|
292
|
+
- 顺序很重要:`<value:string|integer>` 会把 `123` 解析为字符串 `'123'`;需要数字时写成 `<value:integer|string>`。
|
|
293
|
+
- `string` 候选可以带长度限制,例如 `<name:string(2..10)|integer>`。
|
|
294
|
+
- `text` 和 `reply` 不能用于联合类型;`[...rest]` 仍然只收集字符串。
|
|
295
|
+
- 默认值也按顺序尝试,例如 `[mode:'fast'|'slow'='fast']`。`at` 候选不支持默认值,会被跳过;如果所有候选都无法解析默认值,注册时抛错。
|
|
296
|
+
- 重复的候选会被去重。
|
|
297
|
+
- 可选字面量联合 `[mode:'fast'|'slow']` 推导为 `'fast' | 'slow' | undefined`;`CommandParamArgs<"<mode:'fast'|'slow'>">` 推导为 `['fast' | 'slow']`。
|
|
298
|
+
|
|
280
299
|
固定参数可以用双引号或单引号包裹,例如 `/ask "hello world" 2` 会把 `hello world` 作为一个 token;引号内的反斜杠用于转义。
|
|
281
300
|
|
|
282
301
|
解析规则:
|
|
283
302
|
|
|
284
303
|
- `match.args` 和 `match.rest` 保持原始内容,不会被参数解析结果替换。
|
|
285
304
|
- 声明槽位填完后,多余 token 会被忽略并记录 debug 日志,不会报错。`text` 和 `rest` 会消费全部剩余输入,因此不存在多余 token。
|
|
286
|
-
-
|
|
305
|
+
- 必填参数缺失、已声明槽位类型错误、联合类型的所有候选都不匹配、长度不符合限制或引号未闭合时,Router 会自动回复错误和用法,handler 不会执行。
|
|
287
306
|
- usage 会自动生成,并显示在 `/help` 和指令组根命令的用法提示中。
|
|
288
307
|
- 必填参数不能出现在可选参数之后;参数名不能重复;`text` 和变长参数必须放在最后;必填参数不能声明默认值;长度限制只能用于 `string` / `text`,上下限必须是非负整数且下限不能大于上限。
|
|
289
|
-
-
|
|
308
|
+
- 联合类型的候选不能为空;`text` 和 `reply` 不能参与联合类型;字面量必须使用单引号或双引号包裹。
|
|
309
|
+
- 当 `params` 是字符串字面量时,handler 从第四个参数开始的位置参数类型会自动推导。例如 `<question:string> [count:integer=1]` 会推导为 `(question: string, count: number)`,`[name:string]` 会推导为 `string | undefined`,`[...rest]` 会推导为 `...string[]`。联合类型会推导为 TypeScript 联合,例如 `<value:string|integer>` 推导为 `string | number`,`<mode:'fast'|'slow'>` 推导为 `'fast' | 'slow'`。
|
|
290
310
|
- 需要在类型位置复用推导结果时,可以使用导出的 `CommandParamArgs<'<question:string>'>`。
|
|
291
311
|
- 也可以继续手写 handler 参数注解;注解与声明不一致时会在编译期报错。
|
|
292
312
|
- 自动推导只对字面量生效:运行时拼接的字符串,或显式声明为 `string` 的变量会回退为 `any[]`。需要保留字面量类型时,使用 `const` 变量或 `as const`。
|
|
293
|
-
-
|
|
313
|
+
- 类型层解析覆盖上面的声明语法,包括联合类型和引号字面量;如果字面量或默认值包含 `>`、`]` 等会干扰声明边界的字符,可能回退为 `any[]`,但不影响运行时解析。非法声明仍然在注册时抛错,而不是变成类型错误。
|
|
294
314
|
|
|
295
315
|
参数错误示例:
|
|
296
316
|
|
|
@@ -298,6 +318,10 @@ params: '<message:text(2..200)> [nickname:string(..16)]'
|
|
|
298
318
|
/ask hello abc
|
|
299
319
|
-> 参数 count 需要是整数
|
|
300
320
|
-> 用法:/ask <question> [count]
|
|
321
|
+
|
|
322
|
+
/mode medium
|
|
323
|
+
-> 参数 mode 需要是 'fast' 或 'slow'
|
|
324
|
+
-> 用法:/mode <mode>
|
|
301
325
|
```
|
|
302
326
|
|
|
303
327
|
### 指令组
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
candidateLabel,
|
|
2
3
|
declarationError,
|
|
4
|
+
joinCandidateLabels,
|
|
3
5
|
type CommandParamType,
|
|
6
|
+
type ParamCandidate,
|
|
4
7
|
type ParamLength,
|
|
5
8
|
type ParamSpec,
|
|
9
|
+
type ParamTypeCandidate,
|
|
6
10
|
} from './types.js'
|
|
7
11
|
import {
|
|
8
12
|
measureLength,
|
|
@@ -30,11 +34,6 @@ export function parseType(value: string): CommandParamType {
|
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
interface ParsedType {
|
|
34
|
-
type: CommandParamType
|
|
35
|
-
length?: ParamLength | undefined
|
|
36
|
-
}
|
|
37
|
-
|
|
38
37
|
function parseLengthNumber(raw: string, token: string): number {
|
|
39
38
|
const value = Number(raw)
|
|
40
39
|
|
|
@@ -70,12 +69,34 @@ function parseLength(raw: string, token: string): ParamLength {
|
|
|
70
69
|
return { min, max }
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
function
|
|
74
|
-
const
|
|
72
|
+
function isQuoted(value: string): boolean {
|
|
73
|
+
const quote = value[0]
|
|
74
|
+
|
|
75
|
+
return quote === '"' || quote === "'"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseLiteral(value: string, token: string): string {
|
|
79
|
+
const quote = value[0]
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
(quote !== '"' && quote !== "'") ||
|
|
83
|
+
value.length < 2 ||
|
|
84
|
+
value[value.length - 1] !== quote
|
|
85
|
+
) {
|
|
86
|
+
throw declarationError(`非法的字面量 "${value}":${token}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return unquote(value)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseBuiltinCandidate(
|
|
93
|
+
value: string,
|
|
94
|
+
token: string
|
|
95
|
+
): ParamTypeCandidate {
|
|
75
96
|
const open = value.indexOf('(')
|
|
76
97
|
|
|
77
98
|
if (open < 0) {
|
|
78
|
-
return { type: parseType(value) }
|
|
99
|
+
return { kind: 'type', type: parseType(value) }
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
if (!value.endsWith(')')) {
|
|
@@ -89,7 +110,112 @@ function parseTypeDeclaration(raw: string, token: string): ParsedType {
|
|
|
89
110
|
throw declarationError(`只有 string/text 参数支持长度限制:${token}`)
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
return { type, length }
|
|
113
|
+
return { kind: 'type', type, length }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function candidateKey(candidate: ParamCandidate): string {
|
|
117
|
+
if (candidate.kind === 'literal') {
|
|
118
|
+
return `literal:${candidate.value}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const length = candidate.length
|
|
122
|
+
const range =
|
|
123
|
+
length === undefined ? '' : `${length.min ?? ''}..${length.max ?? ''}`
|
|
124
|
+
|
|
125
|
+
return `type:${candidate.type}:${range}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function splitUnion(raw: string, token: string): string[] {
|
|
129
|
+
const parts: string[] = []
|
|
130
|
+
let current = ''
|
|
131
|
+
let quote: string | null = null
|
|
132
|
+
|
|
133
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
134
|
+
const char = raw[index]
|
|
135
|
+
|
|
136
|
+
if (char === undefined) break
|
|
137
|
+
|
|
138
|
+
if (quote !== null) {
|
|
139
|
+
current += char
|
|
140
|
+
|
|
141
|
+
if (char === '\\') {
|
|
142
|
+
const next = raw[index + 1]
|
|
143
|
+
|
|
144
|
+
if (next !== undefined) {
|
|
145
|
+
current += next
|
|
146
|
+
index += 1
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (char === quote) {
|
|
153
|
+
quote = null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (char === '"' || char === "'") {
|
|
160
|
+
quote = char
|
|
161
|
+
current += char
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (char === '|') {
|
|
166
|
+
parts.push(current)
|
|
167
|
+
current = ''
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
current += char
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (quote !== null) {
|
|
175
|
+
throw declarationError(`参数 "${token}" 的引号未闭合`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
parts.push(current)
|
|
179
|
+
|
|
180
|
+
return parts
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseCandidates(raw: string, token: string): ParamCandidate[] {
|
|
184
|
+
const candidates: ParamCandidate[] = []
|
|
185
|
+
const seen = new Set<string>()
|
|
186
|
+
|
|
187
|
+
for (const part of splitUnion(raw, token)) {
|
|
188
|
+
const value = part.trim()
|
|
189
|
+
|
|
190
|
+
if (!value) {
|
|
191
|
+
throw declarationError(`联合类型中存在空的类型:${token}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const candidate = isQuoted(value)
|
|
195
|
+
? { kind: 'literal' as const, value: parseLiteral(value, token) }
|
|
196
|
+
: parseBuiltinCandidate(value, token)
|
|
197
|
+
const key = candidateKey(candidate)
|
|
198
|
+
|
|
199
|
+
if (seen.has(key)) continue
|
|
200
|
+
|
|
201
|
+
seen.add(key)
|
|
202
|
+
candidates.push(candidate)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (candidates.length > 1) {
|
|
206
|
+
for (const candidate of candidates) {
|
|
207
|
+
if (
|
|
208
|
+
candidate.kind === 'type' &&
|
|
209
|
+
(candidate.type === 'text' || candidate.type === 'reply')
|
|
210
|
+
) {
|
|
211
|
+
throw declarationError(
|
|
212
|
+
`${candidate.type} 不能用于联合类型:${token}`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return candidates
|
|
93
219
|
}
|
|
94
220
|
|
|
95
221
|
function validateDefaultLength(
|
|
@@ -187,6 +313,84 @@ export function splitDeclaration(input: string): string[] {
|
|
|
187
313
|
return parts
|
|
188
314
|
}
|
|
189
315
|
|
|
316
|
+
function hasUnquotedEllipsis(value: string): boolean {
|
|
317
|
+
let quote: string | null = null
|
|
318
|
+
|
|
319
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
320
|
+
const char = value[index]
|
|
321
|
+
|
|
322
|
+
if (char === undefined) break
|
|
323
|
+
|
|
324
|
+
if (quote !== null) {
|
|
325
|
+
if (char === '\\') {
|
|
326
|
+
index += 1
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (char === quote) {
|
|
331
|
+
quote = null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (char === '"' || char === "'") {
|
|
338
|
+
quote = char
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (value.startsWith('...', index)) {
|
|
343
|
+
return true
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function splitDefault(
|
|
351
|
+
inner: string,
|
|
352
|
+
token: string
|
|
353
|
+
): { head: string; defaultRaw?: string | undefined } {
|
|
354
|
+
let quote: string | null = null
|
|
355
|
+
|
|
356
|
+
for (let index = 0; index < inner.length; index += 1) {
|
|
357
|
+
const char = inner[index]
|
|
358
|
+
|
|
359
|
+
if (char === undefined) break
|
|
360
|
+
|
|
361
|
+
if (quote !== null) {
|
|
362
|
+
if (char === '\\') {
|
|
363
|
+
index += 1
|
|
364
|
+
continue
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (char === quote) {
|
|
368
|
+
quote = null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (char === '"' || char === "'") {
|
|
375
|
+
quote = char
|
|
376
|
+
continue
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (char === '=') {
|
|
380
|
+
return {
|
|
381
|
+
head: inner.slice(0, index).trim(),
|
|
382
|
+
defaultRaw: inner.slice(index + 1).trim(),
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (quote !== null) {
|
|
388
|
+
throw declarationError(`参数 "${token}" 的引号未闭合`)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { head: inner.trim() }
|
|
392
|
+
}
|
|
393
|
+
|
|
190
394
|
export function parseToken(token: string): ParamSpec {
|
|
191
395
|
const open = token[0]
|
|
192
396
|
const close = token[token.length - 1]
|
|
@@ -221,23 +425,22 @@ export function parseToken(token: string): ParamSpec {
|
|
|
221
425
|
|
|
222
426
|
return {
|
|
223
427
|
name,
|
|
224
|
-
type: 'string',
|
|
428
|
+
candidates: [{ kind: 'type', type: 'string' }],
|
|
225
429
|
required: false,
|
|
226
430
|
rest: true,
|
|
227
431
|
hasDefault: false,
|
|
228
432
|
}
|
|
229
433
|
}
|
|
230
434
|
|
|
231
|
-
|
|
435
|
+
const { head, defaultRaw } = splitDefault(inner, token)
|
|
436
|
+
|
|
437
|
+
if (hasUnquotedEllipsis(head)) {
|
|
232
438
|
throw declarationError(`非法的参数声明 "${token}"`)
|
|
233
439
|
}
|
|
234
440
|
|
|
235
|
-
const equals = inner.indexOf('=')
|
|
236
|
-
const head = equals >= 0 ? inner.slice(0, equals).trim() : inner.trim()
|
|
237
|
-
const defaultRaw = equals >= 0 ? inner.slice(equals + 1).trim() : undefined
|
|
238
441
|
const colon = head.indexOf(':')
|
|
239
442
|
const name = validateName(colon >= 0 ? head.slice(0, colon) : head)
|
|
240
|
-
const
|
|
443
|
+
const candidates = parseCandidates(
|
|
241
444
|
colon >= 0 ? head.slice(colon + 1) : 'string',
|
|
242
445
|
token
|
|
243
446
|
)
|
|
@@ -247,33 +450,33 @@ export function parseToken(token: string): ParamSpec {
|
|
|
247
450
|
}
|
|
248
451
|
|
|
249
452
|
if (defaultRaw !== undefined) {
|
|
250
|
-
const defaultValue = parseDefaultValue(
|
|
251
|
-
|
|
252
|
-
if (typeof defaultValue === 'string') {
|
|
253
|
-
validateDefaultLength(name, defaultValue, length)
|
|
254
|
-
}
|
|
453
|
+
const defaultValue = parseDefaultValue(candidates, defaultRaw, name)
|
|
255
454
|
|
|
256
455
|
return {
|
|
257
456
|
name,
|
|
258
|
-
|
|
457
|
+
candidates,
|
|
259
458
|
required,
|
|
260
459
|
rest: false,
|
|
261
460
|
hasDefault: true,
|
|
262
461
|
defaultValue,
|
|
263
|
-
length,
|
|
264
462
|
}
|
|
265
463
|
}
|
|
266
464
|
|
|
267
465
|
return {
|
|
268
466
|
name,
|
|
269
|
-
|
|
467
|
+
candidates,
|
|
270
468
|
required,
|
|
271
469
|
rest: false,
|
|
272
470
|
hasDefault: false,
|
|
273
|
-
length,
|
|
274
471
|
}
|
|
275
472
|
}
|
|
276
473
|
|
|
474
|
+
function hasTextCandidate(spec: ParamSpec): boolean {
|
|
475
|
+
return spec.candidates.some(
|
|
476
|
+
(candidate) => candidate.kind === 'type' && candidate.type === 'text'
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
277
480
|
export function validateSpecs(specs: ParamSpec[]): void {
|
|
278
481
|
const names = new Set<string>()
|
|
279
482
|
let seenOptional = false
|
|
@@ -308,7 +511,7 @@ export function validateSpecs(specs: ParamSpec[]): void {
|
|
|
308
511
|
continue
|
|
309
512
|
}
|
|
310
513
|
|
|
311
|
-
if (spec
|
|
514
|
+
if (hasTextCandidate(spec)) {
|
|
312
515
|
if (index !== specs.length - 1) {
|
|
313
516
|
throw declarationError('文本参数必须放在最后')
|
|
314
517
|
}
|
|
@@ -333,23 +536,32 @@ export function usageOf(specs: ParamSpec[]): string {
|
|
|
333
536
|
return `[...${spec.name}]`
|
|
334
537
|
}
|
|
335
538
|
|
|
336
|
-
const name = spec
|
|
539
|
+
const name = hasTextCandidate(spec) ? `${spec.name}...` : spec.name
|
|
337
540
|
|
|
338
541
|
return spec.required ? `<${name}>` : `[${name}]`
|
|
339
542
|
})
|
|
340
543
|
.join(' ')
|
|
341
544
|
}
|
|
342
545
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
546
|
+
function parseDefaultCandidate(
|
|
547
|
+
candidate: ParamCandidate,
|
|
548
|
+
value: string,
|
|
346
549
|
name: string
|
|
347
550
|
): unknown {
|
|
348
|
-
|
|
551
|
+
if (candidate.kind === 'literal') {
|
|
552
|
+
if (value === candidate.value) {
|
|
553
|
+
return value
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
throw declarationError(
|
|
557
|
+
`参数 ${name} 的默认值需要是 ${candidateLabel(candidate)}`
|
|
558
|
+
)
|
|
559
|
+
}
|
|
349
560
|
|
|
350
|
-
switch (type) {
|
|
561
|
+
switch (candidate.type) {
|
|
351
562
|
case 'string':
|
|
352
563
|
case 'text':
|
|
564
|
+
validateDefaultLength(name, value, candidate.length)
|
|
353
565
|
return value
|
|
354
566
|
case 'number': {
|
|
355
567
|
const number = value.trim() === '' ? Number.NaN : Number(value)
|
|
@@ -392,3 +604,29 @@ export function parseDefaultValue(
|
|
|
392
604
|
}
|
|
393
605
|
}
|
|
394
606
|
}
|
|
607
|
+
|
|
608
|
+
export function parseDefaultValue(
|
|
609
|
+
candidates: ParamCandidate[],
|
|
610
|
+
raw: string,
|
|
611
|
+
name: string
|
|
612
|
+
): unknown {
|
|
613
|
+
const value = unquote(raw)
|
|
614
|
+
|
|
615
|
+
if (candidates.length === 1) {
|
|
616
|
+
return parseDefaultCandidate(candidates[0]!, value, name)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
for (const candidate of candidates) {
|
|
620
|
+
try {
|
|
621
|
+
return parseDefaultCandidate(candidate, value, name)
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (!(error instanceof Error)) {
|
|
624
|
+
throw error
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
throw declarationError(
|
|
630
|
+
`参数 ${name} 的默认值需要是 ${joinCandidateLabels(candidates)}`
|
|
631
|
+
)
|
|
632
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
candidateLabel,
|
|
2
3
|
CommandParamsError,
|
|
4
|
+
joinCandidateLabels,
|
|
3
5
|
type InputToken,
|
|
6
|
+
type ParamCandidate,
|
|
7
|
+
type ParamLength,
|
|
4
8
|
type ParamSpec,
|
|
5
9
|
type ParsedCommandParams,
|
|
6
10
|
} from './types.js'
|
|
@@ -101,9 +105,12 @@ export function tokenizeInput(input: string, usage: string): InputToken[] {
|
|
|
101
105
|
return tokens
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
function enforceLength(
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
function enforceLength(
|
|
109
|
+
name: string,
|
|
110
|
+
length: ParamLength | undefined,
|
|
111
|
+
value: string,
|
|
112
|
+
usage: string
|
|
113
|
+
): string {
|
|
107
114
|
if (length === undefined) {
|
|
108
115
|
return value
|
|
109
116
|
}
|
|
@@ -112,14 +119,14 @@ function enforceLength(spec: ParamSpec, value: string, usage: string): string {
|
|
|
112
119
|
|
|
113
120
|
if (length.min !== undefined && measured < length.min) {
|
|
114
121
|
throw new CommandParamsError(
|
|
115
|
-
`参数 ${
|
|
122
|
+
`参数 ${name} 至少需要 ${length.min} 个字符`,
|
|
116
123
|
usage
|
|
117
124
|
)
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
if (length.max !== undefined && measured > length.max) {
|
|
121
128
|
throw new CommandParamsError(
|
|
122
|
-
`参数 ${
|
|
129
|
+
`参数 ${name} 不能超过 ${length.max} 个字符`,
|
|
123
130
|
usage
|
|
124
131
|
)
|
|
125
132
|
}
|
|
@@ -127,16 +134,27 @@ function enforceLength(spec: ParamSpec, value: string, usage: string): string {
|
|
|
127
134
|
return value
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
|
|
137
|
+
function parseCandidate(
|
|
138
|
+
candidate: ParamCandidate,
|
|
131
139
|
spec: ParamSpec,
|
|
132
140
|
raw: string,
|
|
133
141
|
usage: string
|
|
134
142
|
): unknown {
|
|
135
|
-
|
|
143
|
+
if (candidate.kind === 'literal') {
|
|
144
|
+
if (raw === candidate.value) {
|
|
145
|
+
return raw
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new CommandParamsError(
|
|
149
|
+
`参数 ${spec.name} 需要是 ${candidateLabel(candidate)}`,
|
|
150
|
+
usage
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
switch (candidate.type) {
|
|
136
155
|
case 'string':
|
|
137
|
-
return enforceLength(spec, raw, usage)
|
|
138
156
|
case 'text':
|
|
139
|
-
return raw
|
|
157
|
+
return enforceLength(spec.name, candidate.length, raw, usage)
|
|
140
158
|
case 'number': {
|
|
141
159
|
const value = raw.trim() === '' ? Number.NaN : Number(raw)
|
|
142
160
|
|
|
@@ -175,11 +193,41 @@ export function parseValue(
|
|
|
175
193
|
}
|
|
176
194
|
case 'at':
|
|
177
195
|
return parseAt(raw, spec.name, usage)
|
|
196
|
+
case 'reply':
|
|
197
|
+
throw new CommandParamsError(
|
|
198
|
+
`参数 ${spec.name} 需要引用一条消息`,
|
|
199
|
+
usage
|
|
200
|
+
)
|
|
178
201
|
case 'time':
|
|
179
202
|
return parseTime(raw, spec.name, usage)
|
|
180
203
|
}
|
|
181
204
|
}
|
|
182
205
|
|
|
206
|
+
export function parseValue(
|
|
207
|
+
spec: ParamSpec,
|
|
208
|
+
raw: string,
|
|
209
|
+
usage: string
|
|
210
|
+
): unknown {
|
|
211
|
+
if (spec.candidates.length === 1) {
|
|
212
|
+
return parseCandidate(spec.candidates[0]!, spec, raw, usage)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const candidate of spec.candidates) {
|
|
216
|
+
try {
|
|
217
|
+
return parseCandidate(candidate, spec, raw, usage)
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (!(error instanceof CommandParamsError)) {
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
throw new CommandParamsError(
|
|
226
|
+
`参数 ${spec.name} 需要是 ${joinCandidateLabels(spec.candidates)}`,
|
|
227
|
+
usage
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
183
231
|
export function parseInput(
|
|
184
232
|
specs: ParamSpec[],
|
|
185
233
|
input: string,
|
|
@@ -191,7 +239,10 @@ export function parseInput(
|
|
|
191
239
|
let index = 0
|
|
192
240
|
|
|
193
241
|
for (const spec of specs) {
|
|
194
|
-
|
|
242
|
+
const sole =
|
|
243
|
+
spec.candidates.length === 1 ? spec.candidates[0] : undefined
|
|
244
|
+
|
|
245
|
+
if (sole?.kind === 'type' && sole.type === 'reply') {
|
|
195
246
|
if (quoteId !== undefined) {
|
|
196
247
|
args.push(quoteId)
|
|
197
248
|
} else if (spec.required) {
|
|
@@ -223,7 +274,7 @@ export function parseInput(
|
|
|
223
274
|
continue
|
|
224
275
|
}
|
|
225
276
|
|
|
226
|
-
if (
|
|
277
|
+
if (sole?.kind === 'type' && sole.type === 'text') {
|
|
227
278
|
const token = tokens[index]
|
|
228
279
|
|
|
229
280
|
if (!token) {
|
|
@@ -245,7 +296,7 @@ export function parseInput(
|
|
|
245
296
|
const value =
|
|
246
297
|
remaining === 1 ? token.value : input.slice(token.start).trim()
|
|
247
298
|
|
|
248
|
-
args.push(enforceLength(spec, value, usage))
|
|
299
|
+
args.push(enforceLength(spec.name, sole.length, value, usage))
|
|
249
300
|
index = tokens.length
|
|
250
301
|
continue
|
|
251
302
|
}
|