@optique/core 0.8.0-dev.165 → 0.8.0-dev.167

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.
@@ -1,6 +1,6 @@
1
1
  import { message, optionName, values } from "./message.js";
2
2
  import { extractArgumentMetavars, extractCommandNames, extractOptionNames } from "./usage.js";
3
- import { createErrorWithSuggestions } from "./suggestion.js";
3
+ import { createErrorWithSuggestions, deduplicateSuggestions } from "./suggestion.js";
4
4
 
5
5
  //#region src/constructs.ts
6
6
  /**
@@ -47,6 +47,35 @@ function analyzeNoMatchContext(parsers) {
47
47
  };
48
48
  }
49
49
  /**
50
+ * Error class thrown when duplicate option names are detected during parser
51
+ * construction. This is a programmer error, not a user error.
52
+ */
53
+ var DuplicateOptionError = class extends Error {
54
+ constructor(optionName$1, sources) {
55
+ super(`Duplicate option name "${optionName$1}" found in fields: ${sources.join(", ")}. Each option name must be unique within a parser combinator.`);
56
+ this.optionName = optionName$1;
57
+ this.sources = sources;
58
+ this.name = "DuplicateOptionError";
59
+ }
60
+ };
61
+ /**
62
+ * Checks for duplicate option names across parser sources and throws an error
63
+ * if duplicates are found. This should be called at construction time.
64
+ * @param parserSources Array of [source, usage] tuples
65
+ * @throws DuplicateOptionError if duplicate option names are found
66
+ */
67
+ function checkDuplicateOptionNames(parserSources) {
68
+ const optionNameSources = /* @__PURE__ */ new Map();
69
+ for (const [source, usage] of parserSources) {
70
+ const names = extractOptionNames(usage);
71
+ for (const name of names) {
72
+ if (!optionNameSources.has(name)) optionNameSources.set(name, []);
73
+ optionNameSources.get(name).push(source);
74
+ }
75
+ }
76
+ for (const [name, sources] of optionNameSources) if (sources.length > 1) throw new DuplicateOptionError(name, sources);
77
+ }
78
+ /**
50
79
  * Generates a contextual error message based on what types of inputs
51
80
  * the parsers expect (options, commands, or arguments).
52
81
  * @param context Context about what types of inputs are expected
@@ -63,6 +92,70 @@ function generateNoMatchError(context) {
63
92
  else return message`No matching option, command, or argument found.`;
64
93
  }
65
94
  /**
95
+ * Creates a complete() method shared by or() and longestMatch().
96
+ * @internal
97
+ */
98
+ function createExclusiveComplete(parsers, options, noMatchContext) {
99
+ return (state) => {
100
+ if (state == null) return {
101
+ success: false,
102
+ error: getNoMatchError(options, noMatchContext)
103
+ };
104
+ const [i, result] = state;
105
+ if (result.success) return parsers[i].complete(result.next.state);
106
+ return {
107
+ success: false,
108
+ error: result.error
109
+ };
110
+ };
111
+ }
112
+ /**
113
+ * Creates a suggest() method shared by or() and longestMatch().
114
+ * @internal
115
+ */
116
+ function createExclusiveSuggest(parsers) {
117
+ return (context, prefix) => {
118
+ const suggestions = [];
119
+ if (context.state == null) for (const parser of parsers) {
120
+ const parserSuggestions = parser.suggest({
121
+ ...context,
122
+ state: parser.initialState
123
+ }, prefix);
124
+ suggestions.push(...parserSuggestions);
125
+ }
126
+ else {
127
+ const [index, parserResult] = context.state;
128
+ if (parserResult.success) {
129
+ const parserSuggestions = parsers[index].suggest({
130
+ ...context,
131
+ state: parserResult.next.state
132
+ }, prefix);
133
+ suggestions.push(...parserSuggestions);
134
+ }
135
+ }
136
+ return deduplicateSuggestions(suggestions);
137
+ };
138
+ }
139
+ /**
140
+ * Gets the no-match error, either from custom options or default.
141
+ * Shared by or() and longestMatch().
142
+ * @internal
143
+ */
144
+ function getNoMatchError(options, noMatchContext) {
145
+ const customNoMatch = options?.errors?.noMatch;
146
+ return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
147
+ }
148
+ /**
149
+ * Creates default error for parse() method when buffer is not empty.
150
+ * Shared by or() and longestMatch().
151
+ * @internal
152
+ */
153
+ function createUnexpectedInputError(token, usage, options) {
154
+ const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
155
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
156
+ return createErrorWithSuggestions(defaultMsg, token, usage, "both", options?.errors?.suggestions);
157
+ }
158
+ /**
66
159
  * @since 0.5.0
67
160
  */
68
161
  function or(...args) {
@@ -85,34 +178,11 @@ function or(...args) {
85
178
  terms: parsers.map((p) => p.usage)
86
179
  }],
87
180
  initialState: void 0,
88
- complete(state) {
89
- if (state == null) {
90
- const customNoMatch = options?.errors?.noMatch;
91
- const error = customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
92
- return {
93
- success: false,
94
- error
95
- };
96
- }
97
- const [i, result] = state;
98
- if (result.success) return parsers[i].complete(result.next.state);
99
- return {
100
- success: false,
101
- error: result.error
102
- };
103
- },
181
+ complete: createExclusiveComplete(parsers, options, noMatchContext),
104
182
  parse(context) {
105
183
  let error = {
106
184
  consumed: 0,
107
- error: context.buffer.length < 1 ? (() => {
108
- const customNoMatch = options?.errors?.noMatch;
109
- return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
110
- })() : (() => {
111
- const token = context.buffer[0];
112
- const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
113
- if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
114
- return createErrorWithSuggestions(defaultMsg, token, context.usage, "both", options?.errors?.suggestions);
115
- })()
185
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
116
186
  };
117
187
  const orderedParsers = parsers.map((p, i) => [p, i]);
118
188
  orderedParsers.sort(([_, a], [__, b]) => context.state?.[0] === a ? -1 : context.state?.[0] === b ? 1 : a - b);
@@ -144,33 +214,7 @@ function or(...args) {
144
214
  success: false
145
215
  };
146
216
  },
147
- suggest(context, prefix) {
148
- const suggestions = [];
149
- if (context.state == null) for (const parser of parsers) {
150
- const parserSuggestions = parser.suggest({
151
- ...context,
152
- state: parser.initialState
153
- }, prefix);
154
- suggestions.push(...parserSuggestions);
155
- }
156
- else {
157
- const [index, parserResult] = context.state;
158
- if (parserResult.success) {
159
- const parserSuggestions = parsers[index].suggest({
160
- ...context,
161
- state: parserResult.next.state
162
- }, prefix);
163
- suggestions.push(...parserSuggestions);
164
- }
165
- }
166
- const seen = /* @__PURE__ */ new Set();
167
- return suggestions.filter((suggestion) => {
168
- const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
169
- if (seen.has(key)) return false;
170
- seen.add(key);
171
- return true;
172
- });
173
- },
217
+ suggest: createExclusiveSuggest(parsers),
174
218
  getDocFragments(state, _defaultValue) {
175
219
  let description;
176
220
  let fragments;
@@ -228,35 +272,12 @@ function longestMatch(...args) {
228
272
  terms: parsers.map((p) => p.usage)
229
273
  }],
230
274
  initialState: void 0,
231
- complete(state) {
232
- if (state == null) {
233
- const customNoMatch = options?.errors?.noMatch;
234
- const error = customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
235
- return {
236
- success: false,
237
- error
238
- };
239
- }
240
- const [i, result] = state;
241
- if (result.success) return parsers[i].complete(result.next.state);
242
- return {
243
- success: false,
244
- error: result.error
245
- };
246
- },
275
+ complete: createExclusiveComplete(parsers, options, noMatchContext),
247
276
  parse(context) {
248
277
  let bestMatch = null;
249
278
  let error = {
250
279
  consumed: 0,
251
- error: context.buffer.length < 1 ? (() => {
252
- const customNoMatch = options?.errors?.noMatch;
253
- return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
254
- })() : (() => {
255
- const token = context.buffer[0];
256
- const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
257
- if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
258
- return createErrorWithSuggestions(defaultMsg, token, context.usage, "both", options?.errors?.suggestions);
259
- })()
280
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
260
281
  };
261
282
  for (let i = 0; i < parsers.length; i++) {
262
283
  const parser = parsers[i];
@@ -288,33 +309,7 @@ function longestMatch(...args) {
288
309
  success: false
289
310
  };
290
311
  },
291
- suggest(context, prefix) {
292
- const suggestions = [];
293
- if (context.state == null) for (const parser of parsers) {
294
- const parserSuggestions = parser.suggest({
295
- ...context,
296
- state: parser.initialState
297
- }, prefix);
298
- suggestions.push(...parserSuggestions);
299
- }
300
- else {
301
- const [index, parserResult] = context.state;
302
- if (parserResult.success) {
303
- const parserSuggestions = parsers[index].suggest({
304
- ...context,
305
- state: parserResult.next.state
306
- }, prefix);
307
- suggestions.push(...parserSuggestions);
308
- }
309
- }
310
- const seen = /* @__PURE__ */ new Set();
311
- return suggestions.filter((suggestion) => {
312
- const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
313
- if (seen.has(key)) return false;
314
- seen.add(key);
315
- return true;
316
- });
317
- },
312
+ suggest: createExclusiveSuggest(parsers),
318
313
  getDocFragments(state, _defaultValue) {
319
314
  let description;
320
315
  let footer;
@@ -353,6 +348,7 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
353
348
  }
354
349
  const parserPairs = Object.entries(parsers);
355
350
  parserPairs.sort(([_, parserA], [__, parserB]) => parserB.priority - parserA.priority);
351
+ if (!options.allowDuplicates) checkDuplicateOptionNames(parserPairs.map(([field, parser]) => [field, parser.usage]));
356
352
  const noMatchContext = analyzeNoMatchContext(Object.values(parsers));
357
353
  return {
358
354
  $valueType: [],
@@ -361,21 +357,6 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
361
357
  usage: parserPairs.flatMap(([_, p]) => p.usage),
362
358
  initialState: Object.fromEntries(Object.entries(parsers).map(([key, parser]) => [key, parser.initialState])),
363
359
  parse(context) {
364
- if (!options.allowDuplicates) {
365
- const optionNameSources = /* @__PURE__ */ new Map();
366
- for (const [field, parser] of parserPairs) {
367
- const names = extractOptionNames(parser.usage);
368
- for (const name of names) {
369
- if (!optionNameSources.has(name)) optionNameSources.set(name, []);
370
- optionNameSources.get(name).push(field);
371
- }
372
- }
373
- for (const [name, sources] of optionNameSources) if (sources.length > 1) return {
374
- success: false,
375
- consumed: 0,
376
- error: message`Duplicate option name ${optionName(name)} found in fields: ${values(sources)}. Each option name must be unique within a parser combinator.`
377
- };
378
- }
379
360
  let error = {
380
361
  consumed: 0,
381
362
  error: context.buffer.length > 0 ? (() => {
@@ -469,13 +450,7 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
469
450
  }, prefix);
470
451
  suggestions.push(...fieldSuggestions);
471
452
  }
472
- const seen = /* @__PURE__ */ new Set();
473
- return suggestions.filter((suggestion) => {
474
- const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
475
- if (seen.has(key)) return false;
476
- seen.add(key);
477
- return true;
478
- });
453
+ return deduplicateSuggestions(suggestions);
479
454
  },
480
455
  getDocFragments(state, defaultValue) {
481
456
  const fragments = parserPairs.flatMap(([field, p]) => {
@@ -515,6 +490,7 @@ function tuple(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
515
490
  parsers = labelOrParsers;
516
491
  options = maybeParsersOrOptions ?? {};
517
492
  }
493
+ if (!options.allowDuplicates) checkDuplicateOptionNames(parsers.map((parser, index) => [String(index), parser.usage]));
518
494
  return {
519
495
  $valueType: [],
520
496
  $stateType: [],
@@ -522,21 +498,6 @@ function tuple(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
522
498
  priority: parsers.length > 0 ? Math.max(...parsers.map((p) => p.priority)) : 0,
523
499
  initialState: parsers.map((parser) => parser.initialState),
524
500
  parse(context) {
525
- if (!options.allowDuplicates) {
526
- const optionNameSources = /* @__PURE__ */ new Map();
527
- for (let i = 0; i < parsers.length; i++) {
528
- const names = extractOptionNames(parsers[i].usage);
529
- for (const name of names) {
530
- if (!optionNameSources.has(name)) optionNameSources.set(name, []);
531
- optionNameSources.get(name).push(i);
532
- }
533
- }
534
- for (const [name, indices] of optionNameSources) if (indices.length > 1) return {
535
- success: false,
536
- consumed: 0,
537
- error: message`Duplicate option name ${optionName(name)} found at positions: ${values(indices.map(String))}. Each option name must be unique within a parser combinator.`
538
- };
539
- }
540
501
  let currentContext = context;
541
502
  const allConsumed = [];
542
503
  const matchedParsers = /* @__PURE__ */ new Set();
@@ -621,13 +582,7 @@ function tuple(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
621
582
  }, prefix);
622
583
  suggestions.push(...parserSuggestions);
623
584
  }
624
- const seen = /* @__PURE__ */ new Set();
625
- return suggestions.filter((suggestion) => {
626
- const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
627
- if (seen.has(key)) return false;
628
- seen.add(key);
629
- return true;
630
- });
585
+ return deduplicateSuggestions(suggestions);
631
586
  },
632
587
  getDocFragments(state, defaultValue) {
633
588
  const fragments = parsers.flatMap((p, i) => {
@@ -670,6 +625,7 @@ function merge(...args) {
670
625
  const withIndex = rawParsers.map((p, i) => [p, i]);
671
626
  const sorted = withIndex.toSorted(([a], [b]) => b.priority - a.priority);
672
627
  const parsers = sorted.map(([p]) => p);
628
+ if (!options.allowDuplicates) checkDuplicateOptionNames(sorted.map(([parser, originalIndex]) => [String(originalIndex), parser.usage]));
673
629
  const initialState = {};
674
630
  for (const parser of parsers) if (parser.initialState && typeof parser.initialState === "object") for (const field in parser.initialState) initialState[field] = parser.initialState[field];
675
631
  return {
@@ -679,21 +635,6 @@ function merge(...args) {
679
635
  usage: parsers.flatMap((p) => p.usage),
680
636
  initialState,
681
637
  parse(context) {
682
- if (!options.allowDuplicates) {
683
- const optionNameSources = /* @__PURE__ */ new Map();
684
- for (const [parser, originalIndex] of sorted) {
685
- const names = extractOptionNames(parser.usage);
686
- for (const name of names) {
687
- if (!optionNameSources.has(name)) optionNameSources.set(name, []);
688
- optionNameSources.get(name).push(originalIndex);
689
- }
690
- }
691
- for (const [name, indices] of optionNameSources) if (indices.length > 1) return {
692
- success: false,
693
- consumed: 0,
694
- error: message`Duplicate option name ${optionName(name)} found in merged parsers at positions: ${values(indices.map(String))}. Each option name must be unique within a parser combinator.`
695
- };
696
- }
697
638
  for (let i = 0; i < parsers.length; i++) {
698
639
  const parser = parsers[i];
699
640
  let parserState;
@@ -782,13 +723,7 @@ function merge(...args) {
782
723
  }, prefix);
783
724
  suggestions.push(...parserSuggestions);
784
725
  }
785
- const seen = /* @__PURE__ */ new Set();
786
- return suggestions.filter((suggestion) => {
787
- const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
788
- if (seen.has(key)) return false;
789
- seen.add(key);
790
- return true;
791
- });
726
+ return deduplicateSuggestions(suggestions);
792
727
  },
793
728
  getDocFragments(state, _defaultValue) {
794
729
  const fragments = parsers.flatMap((p) => {
@@ -919,13 +854,7 @@ function concat(...parsers) {
919
854
  }, prefix);
920
855
  suggestions.push(...parserSuggestions);
921
856
  }
922
- const seen = /* @__PURE__ */ new Set();
923
- return suggestions.filter((suggestion) => {
924
- const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}:${suggestion.pattern}`;
925
- if (seen.has(key)) return false;
926
- seen.add(key);
927
- return true;
928
- });
857
+ return deduplicateSuggestions(suggestions);
929
858
  },
930
859
  getDocFragments(state, _defaultValue) {
931
860
  const fragments = parsers.flatMap((p, index) => {
@@ -1257,13 +1186,7 @@ function conditional(discriminator, branches, defaultBranch, options) {
1257
1186
  state: state.branchState
1258
1187
  }, prefix));
1259
1188
  }
1260
- const seen = /* @__PURE__ */ new Set();
1261
- return suggestions.filter((suggestion) => {
1262
- const key = suggestion.kind === "literal" ? suggestion.text : `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",")}`;
1263
- if (seen.has(key)) return false;
1264
- seen.add(key);
1265
- return true;
1266
- });
1189
+ return deduplicateSuggestions(suggestions);
1267
1190
  },
1268
1191
  getDocFragments(_state, _defaultValue) {
1269
1192
  const fragments = [];
@@ -1295,4 +1218,4 @@ function conditional(discriminator, branches, defaultBranch, options) {
1295
1218
  }
1296
1219
 
1297
1220
  //#endregion
1298
- export { concat, conditional, group, longestMatch, merge, object, or, tuple };
1221
+ export { DuplicateOptionError, concat, conditional, group, longestMatch, merge, object, or, tuple };
package/dist/index.cjs CHANGED
@@ -9,6 +9,7 @@ const require_primitives = require('./primitives.cjs');
9
9
  const require_parser = require('./parser.cjs');
10
10
  const require_facade = require('./facade.cjs');
11
11
 
12
+ exports.DuplicateOptionError = require_constructs.DuplicateOptionError;
12
13
  exports.RunError = require_facade.RunError;
13
14
  exports.WithDefaultError = require_modifiers.WithDefaultError;
14
15
  exports.argument = require_primitives.argument;
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ import { ChoiceOptions, FloatOptions, IntegerOptionsBigInt, IntegerOptionsNumber
5
5
  import { MultipleErrorOptions, MultipleOptions, WithDefaultError, WithDefaultOptions, map, multiple, optional, withDefault } from "./modifiers.cjs";
6
6
  import { ArgumentErrorOptions, ArgumentOptions, CommandErrorOptions, CommandOptions, FlagErrorOptions, FlagOptions, OptionErrorOptions, OptionOptions, PassThroughFormat, PassThroughOptions, argument, command, constant, flag, option, passThrough } from "./primitives.cjs";
7
7
  import { DocState, InferValue, Parser, ParserContext, ParserResult, Result, Suggestion, getDocPage, parse, suggest } from "./parser.cjs";
8
- import { ConditionalErrorOptions, ConditionalOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.cjs";
8
+ import { ConditionalErrorOptions, ConditionalOptions, DuplicateOptionError, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.cjs";
9
9
  import { ShellCompletion, bash, fish, nu, pwsh, zsh } from "./completion.cjs";
10
10
  import { RunError, RunOptions, run } from "./facade.cjs";
11
- export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, ConditionalErrorOptions, ConditionalOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, PassThroughFormat, PassThroughOptions, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, conditional, constant, envVar, extractArgumentMetavars, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, passThrough, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
11
+ export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, ConditionalErrorOptions, ConditionalOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, DuplicateOptionError, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, PassThroughFormat, PassThroughOptions, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, conditional, constant, envVar, extractArgumentMetavars, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, passThrough, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ import { ChoiceOptions, FloatOptions, IntegerOptionsBigInt, IntegerOptionsNumber
5
5
  import { MultipleErrorOptions, MultipleOptions, WithDefaultError, WithDefaultOptions, map, multiple, optional, withDefault } from "./modifiers.js";
6
6
  import { ArgumentErrorOptions, ArgumentOptions, CommandErrorOptions, CommandOptions, FlagErrorOptions, FlagOptions, OptionErrorOptions, OptionOptions, PassThroughFormat, PassThroughOptions, argument, command, constant, flag, option, passThrough } from "./primitives.js";
7
7
  import { DocState, InferValue, Parser, ParserContext, ParserResult, Result, Suggestion, getDocPage, parse, suggest } from "./parser.js";
8
- import { ConditionalErrorOptions, ConditionalOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
8
+ import { ConditionalErrorOptions, ConditionalOptions, DuplicateOptionError, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
9
9
  import { ShellCompletion, bash, fish, nu, pwsh, zsh } from "./completion.js";
10
10
  import { RunError, RunOptions, run } from "./facade.js";
11
- export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, ConditionalErrorOptions, ConditionalOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, PassThroughFormat, PassThroughOptions, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, conditional, constant, envVar, extractArgumentMetavars, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, passThrough, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
11
+ export { ArgumentErrorOptions, ArgumentOptions, ChoiceOptions, CommandErrorOptions, CommandOptions, ConditionalErrorOptions, ConditionalOptions, DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, DocState, DuplicateOptionError, FlagErrorOptions, FlagOptions, FloatOptions, InferValue, IntegerOptionsBigInt, IntegerOptionsNumber, LocaleOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, Message, MessageFormatOptions, MessageTerm, MultipleErrorOptions, MultipleOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionName, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, PassThroughFormat, PassThroughOptions, Result, RunError, RunOptions, ShellCompletion, ShowDefaultOptions, StringOptions, Suggestion, TupleOptions, UrlOptions, Usage, UsageFormatOptions, UsageTerm, UsageTermFormatOptions, Uuid, UuidOptions, ValueParser, ValueParserResult, WithDefaultError, WithDefaultOptions, argument, bash, choice, command, commandLine, concat, conditional, constant, envVar, extractArgumentMetavars, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, passThrough, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { commandLine, envVar, formatMessage, message, metavar, optionName, optionNames, text, value, values } from "./message.js";
2
2
  import { extractArgumentMetavars, extractCommandNames, extractOptionNames, formatUsage, formatUsageTerm, normalizeUsage } from "./usage.js";
3
- import { concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
3
+ import { DuplicateOptionError, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
4
4
  import { formatDocPage } from "./doc.js";
5
5
  import { bash, fish, nu, pwsh, zsh } from "./completion.js";
6
6
  import { WithDefaultError, map, multiple, optional, withDefault } from "./modifiers.js";
@@ -9,4 +9,4 @@ import { argument, command, constant, flag, option, passThrough } from "./primit
9
9
  import { getDocPage, parse, suggest } from "./parser.js";
10
10
  import { RunError, run } from "./facade.js";
11
11
 
12
- export { RunError, WithDefaultError, argument, bash, choice, command, commandLine, concat, conditional, constant, envVar, extractArgumentMetavars, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, passThrough, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
12
+ export { DuplicateOptionError, RunError, WithDefaultError, argument, bash, choice, command, commandLine, concat, conditional, constant, envVar, extractArgumentMetavars, extractCommandNames, extractOptionNames, fish, flag, float, formatDocPage, formatMessage, formatUsage, formatUsageTerm, getDocPage, group, integer, isValueParser, locale, longestMatch, map, merge, message, metavar, multiple, normalizeUsage, nu, object, option, optionName, optionNames, optional, or, parse, passThrough, pwsh, run, string, suggest, text, tuple, url, uuid, value, values, withDefault, zsh };
@@ -2,6 +2,45 @@ const require_message = require('./message.cjs');
2
2
 
3
3
  //#region src/modifiers.ts
4
4
  /**
5
+ * Internal helper for optional-style parsing logic shared by optional()
6
+ * and withDefault(). Handles the common pattern of:
7
+ * - Unwrapping optional state to inner parser state
8
+ * - Detecting if inner parser actually matched (state changed or no consumption)
9
+ * - Returning success with undefined state when inner parser fails without consuming
10
+ * @internal
11
+ */
12
+ function parseOptionalStyle(context, parser) {
13
+ const innerState = typeof context.state === "undefined" ? parser.initialState : context.state[0];
14
+ const result = parser.parse({
15
+ ...context,
16
+ state: innerState
17
+ });
18
+ if (result.success) {
19
+ if (result.next.state !== innerState || result.consumed.length === 0) return {
20
+ success: true,
21
+ next: {
22
+ ...result.next,
23
+ state: [result.next.state]
24
+ },
25
+ consumed: result.consumed
26
+ };
27
+ return {
28
+ success: true,
29
+ next: {
30
+ ...result.next,
31
+ state: context.state
32
+ },
33
+ consumed: result.consumed
34
+ };
35
+ }
36
+ if (result.consumed === 0) return {
37
+ success: true,
38
+ next: context,
39
+ consumed: []
40
+ };
41
+ return result;
42
+ }
43
+ /**
5
44
  * Creates a parser that makes another parser optional, allowing it to succeed
6
45
  * without consuming input if the wrapped parser fails to match.
7
46
  * If the wrapped parser succeeds, this returns its value.
@@ -23,35 +62,7 @@ function optional(parser) {
23
62
  }],
24
63
  initialState: void 0,
25
64
  parse(context) {
26
- const innerState = typeof context.state === "undefined" ? parser.initialState : context.state[0];
27
- const result = parser.parse({
28
- ...context,
29
- state: innerState
30
- });
31
- if (result.success) {
32
- if (result.next.state !== innerState || result.consumed.length === 0) return {
33
- success: true,
34
- next: {
35
- ...result.next,
36
- state: [result.next.state]
37
- },
38
- consumed: result.consumed
39
- };
40
- return {
41
- success: true,
42
- next: {
43
- ...result.next,
44
- state: context.state
45
- },
46
- consumed: result.consumed
47
- };
48
- }
49
- if (result.consumed === 0) return {
50
- success: true,
51
- next: context,
52
- consumed: []
53
- };
54
- return result;
65
+ return parseOptionalStyle(context, parser);
55
66
  },
56
67
  complete(state) {
57
68
  if (typeof state === "undefined") return {
@@ -121,35 +132,7 @@ function withDefault(parser, defaultValue, options) {
121
132
  }],
122
133
  initialState: void 0,
123
134
  parse(context) {
124
- const innerState = typeof context.state === "undefined" ? parser.initialState : context.state[0];
125
- const result = parser.parse({
126
- ...context,
127
- state: innerState
128
- });
129
- if (result.success) {
130
- if (result.next.state !== innerState || result.consumed.length === 0) return {
131
- success: true,
132
- next: {
133
- ...result.next,
134
- state: [result.next.state]
135
- },
136
- consumed: result.consumed
137
- };
138
- return {
139
- success: true,
140
- next: {
141
- ...result.next,
142
- state: context.state
143
- },
144
- consumed: result.consumed
145
- };
146
- }
147
- if (result.consumed === 0) return {
148
- success: true,
149
- next: context,
150
- consumed: []
151
- };
152
- return result;
135
+ return parseOptionalStyle(context, parser);
153
136
  },
154
137
  complete(state) {
155
138
  if (typeof state === "undefined") try {