@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.
Files changed (39) hide show
  1. package/dist/app/runtime.d.ts +1 -0
  2. package/dist/app/runtime.d.ts.map +1 -1
  3. package/dist/app/runtime.js +1 -0
  4. package/dist/app/runtime.js.map +1 -1
  5. package/dist/app.js +6 -1
  6. package/dist/app.js.map +1 -1
  7. package/dist/core/plugin/params/declaration.d.ts +2 -2
  8. package/dist/core/plugin/params/declaration.d.ts.map +1 -1
  9. package/dist/core/plugin/params/declaration.js +189 -24
  10. package/dist/core/plugin/params/declaration.js.map +1 -1
  11. package/dist/core/plugin/params/input.d.ts.map +1 -1
  12. package/dist/core/plugin/params/input.js +35 -12
  13. package/dist/core/plugin/params/input.js.map +1 -1
  14. package/dist/core/plugin/params/type-level.d.ts +13 -3
  15. package/dist/core/plugin/params/type-level.d.ts.map +1 -1
  16. package/dist/core/plugin/params/types.d.ts +13 -2
  17. package/dist/core/plugin/params/types.d.ts.map +1 -1
  18. package/dist/core/plugin/params/types.js +22 -0
  19. package/dist/core/plugin/params/types.js.map +1 -1
  20. package/dist/core/web/config-store.d.ts.map +1 -1
  21. package/dist/core/web/config-store.js +14 -10
  22. package/dist/core/web/config-store.js.map +1 -1
  23. package/dist/core/web/routes/plugins.d.ts.map +1 -1
  24. package/dist/core/web/routes/plugins.js +24 -13
  25. package/dist/core/web/routes/plugins.js.map +1 -1
  26. package/dist/core/web/types.d.ts +6 -0
  27. package/dist/core/web/types.d.ts.map +1 -1
  28. package/docs/plugin-api.md +27 -3
  29. package/docs/webui.md +1 -1
  30. package/package.json +1 -1
  31. package/src/app/runtime.ts +2 -0
  32. package/src/app.ts +6 -1
  33. package/src/core/plugin/params/declaration.ts +269 -31
  34. package/src/core/plugin/params/input.ts +63 -12
  35. package/src/core/plugin/params/type-level.ts +114 -6
  36. package/src/core/plugin/params/types.ts +46 -2
  37. package/src/core/web/config-store.ts +18 -12
  38. package/src/core/web/routes/plugins.ts +30 -19
  39. 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 parseTypeDeclaration(raw: string, token: string): ParsedType {
74
- const value = raw.trim()
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
- if (inner.includes('...')) {
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 { type, length } = parseTypeDeclaration(
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(type, defaultRaw, name)
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
- type,
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
- type,
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.type === 'text') {
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.type === 'text' ? `${spec.name}...` : spec.name
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
- export function parseDefaultValue(
344
- type: CommandParamType,
345
- raw: string,
546
+ function parseDefaultCandidate(
547
+ candidate: ParamCandidate,
548
+ value: string,
346
549
  name: string
347
550
  ): unknown {
348
- const value = unquote(raw)
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(spec: ParamSpec, value: string, usage: string): string {
105
- const length = spec.length
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
- `参数 ${spec.name} 至少需要 ${length.min} 个字符`,
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
- `参数 ${spec.name} 不能超过 ${length.max} 个字符`,
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
- export function parseValue(
137
+ function parseCandidate(
138
+ candidate: ParamCandidate,
131
139
  spec: ParamSpec,
132
140
  raw: string,
133
141
  usage: string
134
142
  ): unknown {
135
- switch (spec.type) {
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
- if (spec.type === 'reply') {
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 (spec.type === 'text') {
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 ParamValue<Type extends string> =
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> = Head extends `${string}:${infer Type}`
29
- ? ParamValue<Type>
30
- : string
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> extends `${infer Head}=${string}`
36
- ? ParamType<Head>
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