@optique/core 0.5.0-dev.79 → 0.5.0-dev.80

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.
@@ -0,0 +1,591 @@
1
+ import { message, metavar, optionName, optionNames } from "./message.js";
2
+ import { isValueParser } from "./valueparser.js";
3
+
4
+ //#region src/primitives.ts
5
+ /**
6
+ * Creates a parser that always succeeds without consuming any input and
7
+ * produces a constant value of the type {@link T}.
8
+ * @template T The type of the constant value produced by the parser.
9
+ */
10
+ function constant(value) {
11
+ return {
12
+ $valueType: [],
13
+ $stateType: [],
14
+ priority: 0,
15
+ usage: [],
16
+ initialState: value,
17
+ parse(context) {
18
+ return {
19
+ success: true,
20
+ next: context,
21
+ consumed: []
22
+ };
23
+ },
24
+ complete(state) {
25
+ return {
26
+ success: true,
27
+ value: state
28
+ };
29
+ },
30
+ getDocFragments(_state, _defaultValue) {
31
+ return { fragments: [] };
32
+ }
33
+ };
34
+ }
35
+ function option(...args) {
36
+ const lastArg = args.at(-1);
37
+ const secondLastArg = args.at(-2);
38
+ let valueParser;
39
+ let optionNames$1;
40
+ let options = {};
41
+ if (isValueParser(lastArg)) {
42
+ valueParser = lastArg;
43
+ optionNames$1 = args.slice(0, -1);
44
+ } else if (typeof lastArg === "object" && lastArg != null) {
45
+ options = lastArg;
46
+ if (isValueParser(secondLastArg)) {
47
+ valueParser = secondLastArg;
48
+ optionNames$1 = args.slice(0, -2);
49
+ } else {
50
+ valueParser = void 0;
51
+ optionNames$1 = args.slice(0, -1);
52
+ }
53
+ } else {
54
+ optionNames$1 = args;
55
+ valueParser = void 0;
56
+ }
57
+ return {
58
+ $valueType: [],
59
+ $stateType: [],
60
+ priority: 10,
61
+ usage: [valueParser == null ? {
62
+ type: "optional",
63
+ terms: [{
64
+ type: "option",
65
+ names: optionNames$1
66
+ }]
67
+ } : {
68
+ type: "option",
69
+ names: optionNames$1,
70
+ metavar: valueParser.metavar
71
+ }],
72
+ initialState: valueParser == null ? {
73
+ success: true,
74
+ value: false
75
+ } : {
76
+ success: false,
77
+ error: options.errors?.missing ? typeof options.errors.missing === "function" ? options.errors.missing(optionNames$1) : options.errors.missing : message`Missing option ${optionNames(optionNames$1)}.`
78
+ },
79
+ parse(context) {
80
+ if (context.optionsTerminated) return {
81
+ success: false,
82
+ consumed: 0,
83
+ error: options.errors?.optionsTerminated ?? message`No more options can be parsed.`
84
+ };
85
+ else if (context.buffer.length < 1) return {
86
+ success: false,
87
+ consumed: 0,
88
+ error: options.errors?.endOfInput ?? message`Expected an option, but got end of input.`
89
+ };
90
+ if (context.buffer[0] === "--") return {
91
+ success: true,
92
+ next: {
93
+ ...context,
94
+ buffer: context.buffer.slice(1),
95
+ state: context.state,
96
+ optionsTerminated: true
97
+ },
98
+ consumed: context.buffer.slice(0, 1)
99
+ };
100
+ if (optionNames$1.includes(context.buffer[0])) {
101
+ if (context.state.success && (valueParser != null || context.state.value)) return {
102
+ success: false,
103
+ consumed: 1,
104
+ error: options.errors?.duplicate ? typeof options.errors.duplicate === "function" ? options.errors.duplicate(context.buffer[0]) : options.errors.duplicate : message`${context.buffer[0]} cannot be used multiple times.`
105
+ };
106
+ if (valueParser == null) return {
107
+ success: true,
108
+ next: {
109
+ ...context,
110
+ state: {
111
+ success: true,
112
+ value: true
113
+ },
114
+ buffer: context.buffer.slice(1)
115
+ },
116
+ consumed: context.buffer.slice(0, 1)
117
+ };
118
+ if (context.buffer.length < 2) return {
119
+ success: false,
120
+ consumed: 1,
121
+ error: message`Option ${optionName(context.buffer[0])} requires a value, but got no value.`
122
+ };
123
+ const result = valueParser.parse(context.buffer[1]);
124
+ return {
125
+ success: true,
126
+ next: {
127
+ ...context,
128
+ state: result,
129
+ buffer: context.buffer.slice(2)
130
+ },
131
+ consumed: context.buffer.slice(0, 2)
132
+ };
133
+ }
134
+ const prefixes = optionNames$1.filter((name) => name.startsWith("--") || name.startsWith("/")).map((name) => name.startsWith("/") ? `${name}:` : `${name}=`);
135
+ for (const prefix of prefixes) {
136
+ if (!context.buffer[0].startsWith(prefix)) continue;
137
+ if (context.state.success && (valueParser != null || context.state.value)) return {
138
+ success: false,
139
+ consumed: 1,
140
+ error: options.errors?.duplicate ? typeof options.errors.duplicate === "function" ? options.errors.duplicate(prefix) : options.errors.duplicate : message`${optionName(prefix)} cannot be used multiple times.`
141
+ };
142
+ const value = context.buffer[0].slice(prefix.length);
143
+ if (valueParser == null) return {
144
+ success: false,
145
+ consumed: 1,
146
+ error: options.errors?.unexpectedValue ? typeof options.errors.unexpectedValue === "function" ? options.errors.unexpectedValue(value) : options.errors.unexpectedValue : message`Option ${optionName(prefix)} is a Boolean flag, but got a value: ${value}.`
147
+ };
148
+ const result = valueParser.parse(value);
149
+ return {
150
+ success: true,
151
+ next: {
152
+ ...context,
153
+ state: result,
154
+ buffer: context.buffer.slice(1)
155
+ },
156
+ consumed: context.buffer.slice(0, 1)
157
+ };
158
+ }
159
+ if (valueParser == null) {
160
+ const shortOptions = optionNames$1.filter((name) => name.match(/^-[^-]$/));
161
+ for (const shortOption of shortOptions) {
162
+ if (!context.buffer[0].startsWith(shortOption)) continue;
163
+ if (context.state.success && (valueParser != null || context.state.value)) return {
164
+ success: false,
165
+ consumed: 1,
166
+ error: options.errors?.duplicate ? typeof options.errors.duplicate === "function" ? options.errors.duplicate(shortOption) : options.errors.duplicate : message`${optionName(shortOption)} cannot be used multiple times.`
167
+ };
168
+ return {
169
+ success: true,
170
+ next: {
171
+ ...context,
172
+ state: {
173
+ success: true,
174
+ value: true
175
+ },
176
+ buffer: [`-${context.buffer[0].slice(2)}`, ...context.buffer.slice(1)]
177
+ },
178
+ consumed: [context.buffer[0].slice(0, 2)]
179
+ };
180
+ }
181
+ }
182
+ return {
183
+ success: false,
184
+ consumed: 0,
185
+ error: message`No matched option for ${optionName(context.buffer[0])}.`
186
+ };
187
+ },
188
+ complete(state) {
189
+ if (state == null) return valueParser == null ? {
190
+ success: true,
191
+ value: false
192
+ } : {
193
+ success: false,
194
+ error: options.errors?.missing ? typeof options.errors.missing === "function" ? options.errors.missing(optionNames$1) : options.errors.missing : message`Missing option ${optionNames(optionNames$1)}.`
195
+ };
196
+ if (state.success) return state;
197
+ return {
198
+ success: false,
199
+ error: options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(state.error) : options.errors.invalidValue : message`${optionNames(optionNames$1)}: ${state.error}`
200
+ };
201
+ },
202
+ getDocFragments(_state, defaultValue) {
203
+ const fragments = [{
204
+ type: "entry",
205
+ term: {
206
+ type: "option",
207
+ names: optionNames$1,
208
+ metavar: valueParser?.metavar
209
+ },
210
+ description: options.description,
211
+ default: defaultValue != null && valueParser != null ? message`${valueParser.format(defaultValue)}` : void 0
212
+ }];
213
+ return {
214
+ fragments,
215
+ description: options.description
216
+ };
217
+ },
218
+ [Symbol.for("Deno.customInspect")]() {
219
+ return `option(${optionNames$1.map((o) => JSON.stringify(o)).join(", ")})`;
220
+ }
221
+ };
222
+ }
223
+ /**
224
+ * Creates a parser for command-line flags that must be explicitly provided.
225
+ * Unlike {@link option}, this parser fails if the flag is not present, making
226
+ * it suitable for required boolean flags that don't have a meaningful default.
227
+ *
228
+ * The key difference from {@link option} is:
229
+ * - {@link option} without a value parser: Returns `false` when not present
230
+ * - {@link flag}: Fails parsing when not present, only produces `true`
231
+ *
232
+ * This is useful for dependent options where the presence of a flag changes
233
+ * the shape of the result type.
234
+ *
235
+ * @param args The {@link OptionName}s to parse, followed by an optional
236
+ * {@link FlagOptions} object that allows you to specify
237
+ * a description or other metadata.
238
+ * @returns A {@link Parser} that produces `true` when the flag is present
239
+ * and fails when it is not present.
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * // Basic flag usage
244
+ * const parser = flag("-f", "--force");
245
+ * // Succeeds with true: parse(parser, ["-f"])
246
+ * // Fails: parse(parser, [])
247
+ *
248
+ * // With description
249
+ * const verboseFlag = flag("-v", "--verbose", {
250
+ * description: "Enable verbose output"
251
+ * });
252
+ * ```
253
+ */
254
+ function flag(...args) {
255
+ const lastArg = args.at(-1);
256
+ let optionNames$1;
257
+ let options = {};
258
+ if (typeof lastArg === "object" && lastArg != null && !Array.isArray(lastArg)) {
259
+ options = lastArg;
260
+ optionNames$1 = args.slice(0, -1);
261
+ } else optionNames$1 = args;
262
+ return {
263
+ $valueType: [],
264
+ $stateType: [],
265
+ priority: 10,
266
+ usage: [{
267
+ type: "option",
268
+ names: optionNames$1
269
+ }],
270
+ initialState: void 0,
271
+ parse(context) {
272
+ if (context.optionsTerminated) return {
273
+ success: false,
274
+ consumed: 0,
275
+ error: options.errors?.optionsTerminated ?? message`No more options can be parsed.`
276
+ };
277
+ else if (context.buffer.length < 1) return {
278
+ success: false,
279
+ consumed: 0,
280
+ error: options.errors?.endOfInput ?? message`Expected an option, but got end of input.`
281
+ };
282
+ if (context.buffer[0] === "--") return {
283
+ success: true,
284
+ next: {
285
+ ...context,
286
+ buffer: context.buffer.slice(1),
287
+ state: context.state,
288
+ optionsTerminated: true
289
+ },
290
+ consumed: context.buffer.slice(0, 1)
291
+ };
292
+ if (optionNames$1.includes(context.buffer[0])) {
293
+ if (context.state?.success) return {
294
+ success: false,
295
+ consumed: 1,
296
+ error: options.errors?.duplicate ? typeof options.errors.duplicate === "function" ? options.errors.duplicate(context.buffer[0]) : options.errors.duplicate : message`${optionName(context.buffer[0])} cannot be used multiple times.`
297
+ };
298
+ return {
299
+ success: true,
300
+ next: {
301
+ ...context,
302
+ state: {
303
+ success: true,
304
+ value: true
305
+ },
306
+ buffer: context.buffer.slice(1)
307
+ },
308
+ consumed: context.buffer.slice(0, 1)
309
+ };
310
+ }
311
+ const prefixes = optionNames$1.filter((name) => name.startsWith("--") || name.startsWith("/")).map((name) => name.startsWith("/") ? `${name}:` : `${name}=`);
312
+ for (const prefix of prefixes) if (context.buffer[0].startsWith(prefix)) {
313
+ const value = context.buffer[0].slice(prefix.length);
314
+ return {
315
+ success: false,
316
+ consumed: 1,
317
+ error: message`Flag ${optionName(prefix.slice(0, -1))} does not accept a value, but got: ${value}.`
318
+ };
319
+ }
320
+ const shortOptions = optionNames$1.filter((name) => name.match(/^-[^-]$/));
321
+ for (const shortOption of shortOptions) {
322
+ if (!context.buffer[0].startsWith(shortOption)) continue;
323
+ if (context.state?.success) return {
324
+ success: false,
325
+ consumed: 1,
326
+ error: options.errors?.duplicate ? typeof options.errors.duplicate === "function" ? options.errors.duplicate(shortOption) : options.errors.duplicate : message`${optionName(shortOption)} cannot be used multiple times.`
327
+ };
328
+ return {
329
+ success: true,
330
+ next: {
331
+ ...context,
332
+ state: {
333
+ success: true,
334
+ value: true
335
+ },
336
+ buffer: [`-${context.buffer[0].slice(2)}`, ...context.buffer.slice(1)]
337
+ },
338
+ consumed: [context.buffer[0].slice(0, 2)]
339
+ };
340
+ }
341
+ return {
342
+ success: false,
343
+ consumed: 0,
344
+ error: message`No matched option for ${optionName(context.buffer[0])}.`
345
+ };
346
+ },
347
+ complete(state) {
348
+ if (state == null) return {
349
+ success: false,
350
+ error: options.errors?.missing ? typeof options.errors.missing === "function" ? options.errors.missing(optionNames$1) : options.errors.missing : message`Required flag ${optionNames(optionNames$1)} is missing.`
351
+ };
352
+ if (state.success) return {
353
+ success: true,
354
+ value: true
355
+ };
356
+ return {
357
+ success: false,
358
+ error: message`${optionNames(optionNames$1)}: ${state.error}`
359
+ };
360
+ },
361
+ getDocFragments(_state, _defaultValue) {
362
+ const fragments = [{
363
+ type: "entry",
364
+ term: {
365
+ type: "option",
366
+ names: optionNames$1
367
+ },
368
+ description: options.description
369
+ }];
370
+ return {
371
+ fragments,
372
+ description: options.description
373
+ };
374
+ },
375
+ [Symbol.for("Deno.customInspect")]() {
376
+ return `flag(${optionNames$1.map((o) => JSON.stringify(o)).join(", ")})`;
377
+ }
378
+ };
379
+ }
380
+ /**
381
+ * Creates a parser that expects a single argument value.
382
+ * This parser is typically used for positional arguments
383
+ * that are not options or flags.
384
+ * @template T The type of the value produced by the parser.
385
+ * @param valueParser The {@link ValueParser} that defines how to parse
386
+ * the argument value.
387
+ * @param options Optional configuration for the argument parser,
388
+ * allowing you to specify a description or other metadata.
389
+ * @returns A {@link Parser} that expects a single argument value and produces
390
+ * the parsed value of type {@link T}.
391
+ */
392
+ function argument(valueParser, options = {}) {
393
+ const optionPattern = /^--?[a-z0-9-]+$/i;
394
+ const term = {
395
+ type: "argument",
396
+ metavar: valueParser.metavar
397
+ };
398
+ return {
399
+ $valueType: [],
400
+ $stateType: [],
401
+ priority: 5,
402
+ usage: [term],
403
+ initialState: void 0,
404
+ parse(context) {
405
+ if (context.buffer.length < 1) return {
406
+ success: false,
407
+ consumed: 0,
408
+ error: options.errors?.endOfInput ?? message`Expected an argument, but got end of input.`
409
+ };
410
+ let i = 0;
411
+ let optionsTerminated = context.optionsTerminated;
412
+ if (!optionsTerminated) {
413
+ if (context.buffer[i] === "--") {
414
+ optionsTerminated = true;
415
+ i++;
416
+ } else if (context.buffer[i].match(optionPattern)) return {
417
+ success: false,
418
+ consumed: i,
419
+ error: message`Expected an argument, but got an option: ${optionName(context.buffer[i])}.`
420
+ };
421
+ }
422
+ if (context.buffer.length < i + 1) return {
423
+ success: false,
424
+ consumed: i,
425
+ error: message`Expected an argument, but got end of input.`
426
+ };
427
+ if (context.state != null) return {
428
+ success: false,
429
+ consumed: i,
430
+ error: options.errors?.multiple ? typeof options.errors.multiple === "function" ? options.errors.multiple(valueParser.metavar) : options.errors.multiple : message`The argument ${metavar(valueParser.metavar)} cannot be used multiple times.`
431
+ };
432
+ const result = valueParser.parse(context.buffer[i]);
433
+ return {
434
+ success: true,
435
+ next: {
436
+ ...context,
437
+ buffer: context.buffer.slice(i + 1),
438
+ state: result,
439
+ optionsTerminated
440
+ },
441
+ consumed: context.buffer.slice(0, i + 1)
442
+ };
443
+ },
444
+ complete(state) {
445
+ if (state == null) return {
446
+ success: false,
447
+ error: options.errors?.endOfInput ?? message`Expected a ${metavar(valueParser.metavar)}, but too few arguments.`
448
+ };
449
+ else if (state.success) return state;
450
+ return {
451
+ success: false,
452
+ error: options.errors?.invalidValue ? typeof options.errors.invalidValue === "function" ? options.errors.invalidValue(state.error) : options.errors.invalidValue : message`${metavar(valueParser.metavar)}: ${state.error}`
453
+ };
454
+ },
455
+ getDocFragments(_state, defaultValue) {
456
+ const fragments = [{
457
+ type: "entry",
458
+ term,
459
+ description: options.description,
460
+ default: defaultValue == null ? void 0 : message`${valueParser.format(defaultValue)}`
461
+ }];
462
+ return {
463
+ fragments,
464
+ description: options.description
465
+ };
466
+ },
467
+ [Symbol.for("Deno.customInspect")]() {
468
+ return `argument()`;
469
+ }
470
+ };
471
+ }
472
+ /**
473
+ * Creates a parser that matches a specific subcommand name and then applies
474
+ * an inner parser to the remaining arguments.
475
+ * This is useful for building CLI tools with subcommands like git, npm, etc.
476
+ * @template T The type of the value returned by the inner parser.
477
+ * @template TState The type of the state used by the inner parser.
478
+ * @param name The subcommand name to match (e.g., `"show"`, `"edit"`).
479
+ * @param parser The {@link Parser} to apply after the command is matched.
480
+ * @param options Optional configuration for the command parser, such as
481
+ * a description for documentation.
482
+ * @returns A {@link Parser} that matches the command name and delegates
483
+ * to the inner parser for the remaining arguments.
484
+ */
485
+ function command(name, parser, options = {}) {
486
+ return {
487
+ $valueType: [],
488
+ $stateType: [],
489
+ priority: 15,
490
+ usage: [{
491
+ type: "command",
492
+ name
493
+ }, ...parser.usage],
494
+ initialState: void 0,
495
+ parse(context) {
496
+ if (context.state === void 0) {
497
+ if (context.buffer.length < 1 || context.buffer[0] !== name) {
498
+ const actual = context.buffer.length > 0 ? context.buffer[0] : null;
499
+ const errorMessage = options.errors?.notMatched ?? message`Expected command ${optionName(name)}, but got ${actual ?? "end of input"}.`;
500
+ return {
501
+ success: false,
502
+ consumed: 0,
503
+ error: typeof errorMessage === "function" ? errorMessage(name, actual) : errorMessage
504
+ };
505
+ }
506
+ return {
507
+ success: true,
508
+ next: {
509
+ ...context,
510
+ buffer: context.buffer.slice(1),
511
+ state: ["matched", name]
512
+ },
513
+ consumed: context.buffer.slice(0, 1)
514
+ };
515
+ } else if (context.state[0] === "matched") {
516
+ const result = parser.parse({
517
+ ...context,
518
+ state: parser.initialState
519
+ });
520
+ if (result.success) return {
521
+ success: true,
522
+ next: {
523
+ ...result.next,
524
+ state: ["parsing", result.next.state]
525
+ },
526
+ consumed: result.consumed
527
+ };
528
+ return result;
529
+ } else if (context.state[0] === "parsing") {
530
+ const result = parser.parse({
531
+ ...context,
532
+ state: context.state[1]
533
+ });
534
+ if (result.success) return {
535
+ success: true,
536
+ next: {
537
+ ...result.next,
538
+ state: ["parsing", result.next.state]
539
+ },
540
+ consumed: result.consumed
541
+ };
542
+ return result;
543
+ }
544
+ return {
545
+ success: false,
546
+ consumed: 0,
547
+ error: options.errors?.invalidState ?? message`Invalid command state.`
548
+ };
549
+ },
550
+ complete(state) {
551
+ if (typeof state === "undefined") return {
552
+ success: false,
553
+ error: options.errors?.notFound ?? message`Command ${optionName(name)} was not matched.`
554
+ };
555
+ else if (state[0] === "matched") return parser.complete(parser.initialState);
556
+ else if (state[0] === "parsing") return parser.complete(state[1]);
557
+ return {
558
+ success: false,
559
+ error: options.errors?.invalidState ?? message`Invalid command state during completion.`
560
+ };
561
+ },
562
+ getDocFragments(state, defaultValue) {
563
+ if (state.kind === "unavailable" || typeof state.state === "undefined") return {
564
+ description: options.description,
565
+ fragments: [{
566
+ type: "entry",
567
+ term: {
568
+ type: "command",
569
+ name
570
+ },
571
+ description: options.description
572
+ }]
573
+ };
574
+ const innerState = state.state[0] === "parsing" ? {
575
+ kind: "available",
576
+ state: state.state[1]
577
+ } : { kind: "unavailable" };
578
+ const innerFragments = parser.getDocFragments(innerState, defaultValue);
579
+ return {
580
+ ...innerFragments,
581
+ description: innerFragments.description ?? options.description
582
+ };
583
+ },
584
+ [Symbol.for("Deno.customInspect")]() {
585
+ return `command(${JSON.stringify(name)})`;
586
+ }
587
+ };
588
+ }
589
+
590
+ //#endregion
591
+ export { argument, command, constant, flag, option };