@optique/core 0.10.0-dev.363 → 0.10.0-dev.367

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.
@@ -6,6 +6,57 @@ const require_suggestion = require('./suggestion.cjs');
6
6
 
7
7
  //#region src/constructs.ts
8
8
  /**
9
+ * Collects option names and command names that are valid at the current
10
+ * parse position by walking the usage tree. Only "leading" candidates
11
+ * (those reachable before a required positional argument) are collected.
12
+ */
13
+ function collectLeadingCandidates(terms, optionNames, commandNames) {
14
+ if (!terms || !Array.isArray(terms)) return true;
15
+ for (const term of terms) {
16
+ if (term.type === "option") {
17
+ for (const name of term.names) optionNames.add(name);
18
+ return false;
19
+ }
20
+ if (term.type === "command") {
21
+ commandNames.add(term.name);
22
+ return false;
23
+ }
24
+ if (term.type === "argument") return false;
25
+ if (term.type === "optional") {
26
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
27
+ continue;
28
+ }
29
+ if (term.type === "multiple") {
30
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
31
+ if (term.min === 0) continue;
32
+ return false;
33
+ }
34
+ if (term.type === "exclusive") {
35
+ let allAlternativesSkippable = true;
36
+ for (const exclusiveUsage of term.terms) {
37
+ const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
38
+ allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
39
+ }
40
+ if (allAlternativesSkippable) continue;
41
+ return false;
42
+ }
43
+ }
44
+ return true;
45
+ }
46
+ function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
47
+ const options = /* @__PURE__ */ new Set();
48
+ const commands = /* @__PURE__ */ new Set();
49
+ for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
50
+ const candidates = new Set([...options, ...commands]);
51
+ const suggestions = require_suggestion.findSimilar(invalidInput, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS);
52
+ const suggestionMsg = customFormatter ? customFormatter(suggestions) : require_suggestion.createSuggestionMessage(suggestions);
53
+ return suggestionMsg.length > 0 ? [
54
+ ...baseError,
55
+ require_message.text("\n\n"),
56
+ ...suggestionMsg
57
+ ] : baseError;
58
+ }
59
+ /**
9
60
  * Checks if the given token is an option name that requires a value
10
61
  * (i.e., has a metavar) within the given usage terms.
11
62
  * @param usage The usage terms to search through.
@@ -200,16 +251,6 @@ function getNoMatchError(options, noMatchContext) {
200
251
  return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
201
252
  }
202
253
  /**
203
- * Creates default error for parse() method when buffer is not empty.
204
- * Shared by or() and longestMatch().
205
- * @internal
206
- */
207
- function createUnexpectedInputError(token, usage, options) {
208
- const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
209
- if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
210
- return require_suggestion.createErrorWithSuggestions(defaultMsg, token, usage, "both", options?.errors?.suggestions);
211
- }
212
- /**
213
254
  * @since 0.5.0
214
255
  */
215
256
  function or(...args) {
@@ -227,7 +268,12 @@ function or(...args) {
227
268
  const syncParsers = parsers;
228
269
  const getInitialError = (context) => ({
229
270
  consumed: 0,
230
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
271
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
272
+ const token = context.buffer[0];
273
+ const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
274
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
275
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
276
+ })()
231
277
  });
232
278
  const parseSync = (context) => {
233
279
  let error = getInitialError(context);
@@ -423,7 +469,12 @@ function longestMatch(...args) {
423
469
  const syncParsers = parsers;
424
470
  const getInitialError = (context) => ({
425
471
  consumed: 0,
426
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
472
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
473
+ const token = context.buffer[0];
474
+ const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
475
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
476
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
477
+ })()
427
478
  });
428
479
  const parseSync = (context) => {
429
480
  let bestMatch = null;
@@ -1,11 +1,62 @@
1
- import { message, optionName, values } from "./message.js";
1
+ import { message, optionName, text, values } from "./message.js";
2
2
  import { DependencyRegistry, dependencyId, isDeferredParseState, isDependencySourceState, isPendingDependencySourceState, isWrappedDependencySource, parseWithDependency, wrappedDependencySourceMarker } from "./dependency.js";
3
3
  import { dispatchByMode, dispatchIterableByMode } from "./mode-dispatch.js";
4
4
  import { extractArgumentMetavars, extractCommandNames, extractOptionNames } from "./usage.js";
5
- import { createErrorWithSuggestions, deduplicateSuggestions } from "./suggestion.js";
5
+ import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, findSimilar } from "./suggestion.js";
6
6
 
7
7
  //#region src/constructs.ts
8
8
  /**
9
+ * Collects option names and command names that are valid at the current
10
+ * parse position by walking the usage tree. Only "leading" candidates
11
+ * (those reachable before a required positional argument) are collected.
12
+ */
13
+ function collectLeadingCandidates(terms, optionNames, commandNames) {
14
+ if (!terms || !Array.isArray(terms)) return true;
15
+ for (const term of terms) {
16
+ if (term.type === "option") {
17
+ for (const name of term.names) optionNames.add(name);
18
+ return false;
19
+ }
20
+ if (term.type === "command") {
21
+ commandNames.add(term.name);
22
+ return false;
23
+ }
24
+ if (term.type === "argument") return false;
25
+ if (term.type === "optional") {
26
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
27
+ continue;
28
+ }
29
+ if (term.type === "multiple") {
30
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
31
+ if (term.min === 0) continue;
32
+ return false;
33
+ }
34
+ if (term.type === "exclusive") {
35
+ let allAlternativesSkippable = true;
36
+ for (const exclusiveUsage of term.terms) {
37
+ const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
38
+ allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
39
+ }
40
+ if (allAlternativesSkippable) continue;
41
+ return false;
42
+ }
43
+ }
44
+ return true;
45
+ }
46
+ function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
47
+ const options = /* @__PURE__ */ new Set();
48
+ const commands = /* @__PURE__ */ new Set();
49
+ for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
50
+ const candidates = new Set([...options, ...commands]);
51
+ const suggestions = findSimilar(invalidInput, candidates, DEFAULT_FIND_SIMILAR_OPTIONS);
52
+ const suggestionMsg = customFormatter ? customFormatter(suggestions) : createSuggestionMessage(suggestions);
53
+ return suggestionMsg.length > 0 ? [
54
+ ...baseError,
55
+ text("\n\n"),
56
+ ...suggestionMsg
57
+ ] : baseError;
58
+ }
59
+ /**
9
60
  * Checks if the given token is an option name that requires a value
10
61
  * (i.e., has a metavar) within the given usage terms.
11
62
  * @param usage The usage terms to search through.
@@ -200,16 +251,6 @@ function getNoMatchError(options, noMatchContext) {
200
251
  return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
201
252
  }
202
253
  /**
203
- * Creates default error for parse() method when buffer is not empty.
204
- * Shared by or() and longestMatch().
205
- * @internal
206
- */
207
- function createUnexpectedInputError(token, usage, options) {
208
- const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
209
- if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
210
- return createErrorWithSuggestions(defaultMsg, token, usage, "both", options?.errors?.suggestions);
211
- }
212
- /**
213
254
  * @since 0.5.0
214
255
  */
215
256
  function or(...args) {
@@ -227,7 +268,12 @@ function or(...args) {
227
268
  const syncParsers = parsers;
228
269
  const getInitialError = (context) => ({
229
270
  consumed: 0,
230
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
271
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
272
+ const token = context.buffer[0];
273
+ const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
274
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
275
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
276
+ })()
231
277
  });
232
278
  const parseSync = (context) => {
233
279
  let error = getInitialError(context);
@@ -423,7 +469,12 @@ function longestMatch(...args) {
423
469
  const syncParsers = parsers;
424
470
  const getInitialError = (context) => ({
425
471
  consumed: 0,
426
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
472
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
473
+ const token = context.buffer[0];
474
+ const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
475
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
476
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
477
+ })()
427
478
  });
428
479
  const parseSync = (context) => {
429
480
  let bestMatch = null;
package/dist/facade.cjs CHANGED
@@ -492,9 +492,9 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
492
492
  } else {
493
493
  const singularMatchExact = completionName === "singular" || completionName === "both" ? arg === "--completion" : false;
494
494
  const pluralMatchExact = completionName === "plural" || completionName === "both" ? arg === "--completions" : false;
495
- if ((singularMatchExact || pluralMatchExact) && i + 1 < args.length) {
496
- const shell = args[i + 1];
497
- const completionArgs = args.slice(i + 2);
495
+ if (singularMatchExact || pluralMatchExact) {
496
+ const shell = i + 1 < args.length ? args[i + 1] : "";
497
+ const completionArgs = i + 1 < args.length ? args.slice(i + 2) : [];
498
498
  return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
499
499
  }
500
500
  }
package/dist/facade.js CHANGED
@@ -492,9 +492,9 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
492
492
  } else {
493
493
  const singularMatchExact = completionName === "singular" || completionName === "both" ? arg === "--completion" : false;
494
494
  const pluralMatchExact = completionName === "plural" || completionName === "both" ? arg === "--completions" : false;
495
- if ((singularMatchExact || pluralMatchExact) && i + 1 < args.length) {
496
- const shell = args[i + 1];
497
- const completionArgs = args.slice(i + 2);
495
+ if (singularMatchExact || pluralMatchExact) {
496
+ const shell = i + 1 < args.length ? args[i + 1] : "";
497
+ const completionArgs = i + 1 < args.length ? args.slice(i + 2) : [];
498
498
  return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
499
499
  }
500
500
  }
@@ -232,5 +232,6 @@ function deduplicateSuggestions(suggestions) {
232
232
  //#endregion
233
233
  exports.DEFAULT_FIND_SIMILAR_OPTIONS = DEFAULT_FIND_SIMILAR_OPTIONS;
234
234
  exports.createErrorWithSuggestions = createErrorWithSuggestions;
235
+ exports.createSuggestionMessage = createSuggestionMessage;
235
236
  exports.deduplicateSuggestions = deduplicateSuggestions;
236
237
  exports.findSimilar = findSimilar;
@@ -230,4 +230,4 @@ function deduplicateSuggestions(suggestions) {
230
230
  }
231
231
 
232
232
  //#endregion
233
- export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, deduplicateSuggestions, findSimilar };
233
+ export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, findSimilar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "0.10.0-dev.363+787c7d2e",
3
+ "version": "0.10.0-dev.367+dd14db26",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",