@jsbot-tellus/core 2.9.0 → 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/app/runtime.d.ts +1 -0
- package/dist/app/runtime.d.ts.map +1 -1
- package/dist/app/runtime.js +1 -0
- package/dist/app/runtime.js.map +1 -1
- package/dist/app.js +6 -1
- package/dist/app.js.map +1 -1
- 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/dist/core/web/config-store.d.ts.map +1 -1
- package/dist/core/web/config-store.js +14 -10
- package/dist/core/web/config-store.js.map +1 -1
- package/dist/core/web/routes/plugins.d.ts.map +1 -1
- package/dist/core/web/routes/plugins.js +24 -13
- package/dist/core/web/routes/plugins.js.map +1 -1
- package/dist/core/web/types.d.ts +6 -0
- package/dist/core/web/types.d.ts.map +1 -1
- package/docs/plugin-api.md +27 -3
- package/docs/webui.md +1 -1
- package/package.json +1 -1
- package/src/app/runtime.ts +2 -0
- package/src/app.ts +6 -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
- package/src/core/web/config-store.ts +18 -12
- package/src/core/web/routes/plugins.ts +30 -19
- package/src/core/web/types.ts +7 -0
|
@@ -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
|
}
|
|
@@ -11,7 +11,46 @@ type Trim<Source extends string> = TrimLeft<TrimRight<Source>>
|
|
|
11
11
|
type StripLength<Type extends string> =
|
|
12
12
|
Trim<Type> extends `${infer Base}(${string})` ? Trim<Base> : Trim<Type>
|
|
13
13
|
|
|
14
|
-
type
|
|
14
|
+
type Unescape<Source extends string> =
|
|
15
|
+
Source extends `${infer Head}\\${infer Next}${infer Rest}`
|
|
16
|
+
? `${Head}${Next}${Unescape<Rest>}`
|
|
17
|
+
: Source
|
|
18
|
+
|
|
19
|
+
type SplitUnionScan<
|
|
20
|
+
Source extends string,
|
|
21
|
+
Quote extends string | null = null,
|
|
22
|
+
Current extends string = '',
|
|
23
|
+
Acc extends string[] = [],
|
|
24
|
+
> = Source extends `${infer Char}${infer Rest}`
|
|
25
|
+
? Char extends '\\'
|
|
26
|
+
? Rest extends `${infer Next}${infer Tail}`
|
|
27
|
+
? SplitUnionScan<Tail, Quote, `${Current}${Char}${Next}`, Acc>
|
|
28
|
+
: [...Acc, `${Current}${Char}`]
|
|
29
|
+
: Char extends '"' | "'"
|
|
30
|
+
? Quote extends null
|
|
31
|
+
? SplitUnionScan<Rest, Char, `${Current}${Char}`, Acc>
|
|
32
|
+
: Char extends Quote
|
|
33
|
+
? SplitUnionScan<Rest, null, `${Current}${Char}`, Acc>
|
|
34
|
+
: SplitUnionScan<Rest, Quote, `${Current}${Char}`, Acc>
|
|
35
|
+
: Char extends '|'
|
|
36
|
+
? Quote extends null
|
|
37
|
+
? SplitUnionScan<Rest, null, '', [...Acc, Current]>
|
|
38
|
+
: SplitUnionScan<Rest, Quote, `${Current}${Char}`, Acc>
|
|
39
|
+
: SplitUnionScan<Rest, Quote, `${Current}${Char}`, Acc>
|
|
40
|
+
: [...Acc, Current]
|
|
41
|
+
|
|
42
|
+
type SplitUnion<Source extends string> = Source extends `${string}|${string}`
|
|
43
|
+
? SplitUnionScan<Source>
|
|
44
|
+
: [Source]
|
|
45
|
+
|
|
46
|
+
type LiteralValue<Type extends string> =
|
|
47
|
+
Trim<Type> extends `'${infer Inner}'`
|
|
48
|
+
? Unescape<Inner>
|
|
49
|
+
: Trim<Type> extends `"${infer Inner}"`
|
|
50
|
+
? Unescape<Inner>
|
|
51
|
+
: never
|
|
52
|
+
|
|
53
|
+
type BuiltinValue<Type extends string> =
|
|
15
54
|
StripLength<Type> extends 'string' | 'text'
|
|
16
55
|
? string
|
|
17
56
|
: StripLength<Type> extends
|
|
@@ -21,19 +60,88 @@ type ParamValue<Type extends string> =
|
|
|
21
60
|
? boolean
|
|
22
61
|
: unknown
|
|
23
62
|
|
|
63
|
+
type ParamValuePart<Type extends string> =
|
|
64
|
+
Trim<Type> extends `'${string}'` | `"${string}"`
|
|
65
|
+
? LiteralValue<Type>
|
|
66
|
+
: BuiltinValue<Type>
|
|
67
|
+
|
|
68
|
+
type ParamValue<Type extends string> =
|
|
69
|
+
SplitUnion<Type>[number] extends infer Part
|
|
70
|
+
? Part extends string
|
|
71
|
+
? ParamValuePart<Part>
|
|
72
|
+
: never
|
|
73
|
+
: never
|
|
74
|
+
|
|
75
|
+
type SplitTypeScan<
|
|
76
|
+
Source extends string,
|
|
77
|
+
Quote extends string | null = null,
|
|
78
|
+
Name extends string = '',
|
|
79
|
+
> = Source extends `${infer Char}${infer Rest}`
|
|
80
|
+
? Char extends '\\'
|
|
81
|
+
? Rest extends `${infer Next}${infer Tail}`
|
|
82
|
+
? SplitTypeScan<Tail, Quote, `${Name}${Char}${Next}`>
|
|
83
|
+
: [Name, null]
|
|
84
|
+
: Char extends '"' | "'"
|
|
85
|
+
? Quote extends null
|
|
86
|
+
? SplitTypeScan<Rest, Char, `${Name}${Char}`>
|
|
87
|
+
: Char extends Quote
|
|
88
|
+
? SplitTypeScan<Rest, null, `${Name}${Char}`>
|
|
89
|
+
: SplitTypeScan<Rest, Quote, `${Name}${Char}`>
|
|
90
|
+
: Char extends ':'
|
|
91
|
+
? Quote extends null
|
|
92
|
+
? [Name, Rest]
|
|
93
|
+
: SplitTypeScan<Rest, Quote, `${Name}${Char}`>
|
|
94
|
+
: SplitTypeScan<Rest, Quote, `${Name}${Char}`>
|
|
95
|
+
: [Name, null]
|
|
96
|
+
|
|
97
|
+
type SplitType<Source extends string> = Source extends `${string}:${string}`
|
|
98
|
+
? SplitTypeScan<Source>
|
|
99
|
+
: [Source, null]
|
|
100
|
+
|
|
101
|
+
type SplitDefaultScan<
|
|
102
|
+
Source extends string,
|
|
103
|
+
Quote extends string | null = null,
|
|
104
|
+
Head extends string = '',
|
|
105
|
+
> = Source extends `${infer Char}${infer Rest}`
|
|
106
|
+
? Char extends '\\'
|
|
107
|
+
? Rest extends `${infer Next}${infer Tail}`
|
|
108
|
+
? SplitDefaultScan<Tail, Quote, `${Head}${Char}${Next}`>
|
|
109
|
+
: [Head, false]
|
|
110
|
+
: Char extends '"' | "'"
|
|
111
|
+
? Quote extends null
|
|
112
|
+
? SplitDefaultScan<Rest, Char, `${Head}${Char}`>
|
|
113
|
+
: Char extends Quote
|
|
114
|
+
? SplitDefaultScan<Rest, null, `${Head}${Char}`>
|
|
115
|
+
: SplitDefaultScan<Rest, Quote, `${Head}${Char}`>
|
|
116
|
+
: Char extends '='
|
|
117
|
+
? Quote extends null
|
|
118
|
+
? [Head, true]
|
|
119
|
+
: SplitDefaultScan<Rest, Quote, `${Head}${Char}`>
|
|
120
|
+
: SplitDefaultScan<Rest, Quote, `${Head}${Char}`>
|
|
121
|
+
: [Head, false]
|
|
122
|
+
|
|
123
|
+
type SplitDefault<Source extends string> = Source extends `${string}=${string}`
|
|
124
|
+
? SplitDefaultScan<Source>
|
|
125
|
+
: [Source, false]
|
|
126
|
+
|
|
24
127
|
interface VariadicParam {
|
|
25
128
|
readonly variadic: true
|
|
26
129
|
}
|
|
27
130
|
|
|
28
|
-
type ParamType<Head extends string> =
|
|
29
|
-
|
|
30
|
-
|
|
131
|
+
type ParamType<Head extends string> =
|
|
132
|
+
SplitType<Head> extends [unknown, infer Type]
|
|
133
|
+
? Type extends string
|
|
134
|
+
? ParamValue<Type>
|
|
135
|
+
: string
|
|
136
|
+
: string
|
|
31
137
|
|
|
32
138
|
type ParamElement<Body extends string, Required extends boolean> =
|
|
33
139
|
Trim<Body> extends `...${string}`
|
|
34
140
|
? VariadicParam
|
|
35
|
-
: Trim<Body
|
|
36
|
-
?
|
|
141
|
+
: SplitDefault<Trim<Body>> extends [infer Head, true]
|
|
142
|
+
? Head extends string
|
|
143
|
+
? ParamType<Head>
|
|
144
|
+
: never
|
|
37
145
|
: Required extends true
|
|
38
146
|
? ParamType<Body>
|
|
39
147
|
: ParamType<Body> | undefined
|