@optique/core 0.8.10 → 0.8.11

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.
@@ -4,6 +4,57 @@ const require_suggestion = require('./suggestion.cjs');
4
4
 
5
5
  //#region src/constructs.ts
6
6
  /**
7
+ * Collects option names and command names that are valid at the current
8
+ * parse position by walking the usage tree. Only "leading" candidates
9
+ * (those reachable before a required positional argument) are collected.
10
+ */
11
+ function collectLeadingCandidates(terms, optionNames, commandNames) {
12
+ if (!terms || !Array.isArray(terms)) return true;
13
+ for (const term of terms) {
14
+ if (term.type === "option") {
15
+ for (const name of term.names) optionNames.add(name);
16
+ return false;
17
+ }
18
+ if (term.type === "command") {
19
+ commandNames.add(term.name);
20
+ return false;
21
+ }
22
+ if (term.type === "argument") return false;
23
+ if (term.type === "optional") {
24
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
25
+ continue;
26
+ }
27
+ if (term.type === "multiple") {
28
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
29
+ if (term.min === 0) continue;
30
+ return false;
31
+ }
32
+ if (term.type === "exclusive") {
33
+ let allAlternativesSkippable = true;
34
+ for (const exclusiveUsage of term.terms) {
35
+ const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
36
+ allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
37
+ }
38
+ if (allAlternativesSkippable) continue;
39
+ return false;
40
+ }
41
+ }
42
+ return true;
43
+ }
44
+ function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
45
+ const options = /* @__PURE__ */ new Set();
46
+ const commands = /* @__PURE__ */ new Set();
47
+ for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
48
+ const candidates = new Set([...options, ...commands]);
49
+ const suggestions = require_suggestion.findSimilar(invalidInput, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS);
50
+ const suggestionMsg = customFormatter ? customFormatter(suggestions) : require_suggestion.createSuggestionMessage(suggestions);
51
+ return suggestionMsg.length > 0 ? [
52
+ ...baseError,
53
+ require_message.text("\n\n"),
54
+ ...suggestionMsg
55
+ ] : baseError;
56
+ }
57
+ /**
7
58
  * Checks if the given token is an option name that requires a value
8
59
  * (i.e., has a metavar) within the given usage terms.
9
60
  * @param usage The usage terms to search through.
@@ -168,16 +219,6 @@ function getNoMatchError(options, noMatchContext) {
168
219
  return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
169
220
  }
170
221
  /**
171
- * Creates default error for parse() method when buffer is not empty.
172
- * Shared by or() and longestMatch().
173
- * @internal
174
- */
175
- function createUnexpectedInputError(token, usage, options) {
176
- const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
177
- if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
178
- return require_suggestion.createErrorWithSuggestions(defaultMsg, token, usage, "both", options?.errors?.suggestions);
179
- }
180
- /**
181
222
  * @since 0.5.0
182
223
  */
183
224
  function or(...args) {
@@ -204,7 +245,12 @@ function or(...args) {
204
245
  parse(context) {
205
246
  let error = {
206
247
  consumed: 0,
207
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
248
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
249
+ const token = context.buffer[0];
250
+ const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
251
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
252
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
253
+ })()
208
254
  };
209
255
  const orderedParsers = parsers.map((p, i) => [p, i]);
210
256
  orderedParsers.sort(([_, a], [__, b]) => context.state?.[0] === a ? -1 : context.state?.[0] === b ? 1 : a - b);
@@ -305,7 +351,12 @@ function longestMatch(...args) {
305
351
  let bestMatch = null;
306
352
  let error = {
307
353
  consumed: 0,
308
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
354
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
355
+ const token = context.buffer[0];
356
+ const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
357
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
358
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
359
+ })()
309
360
  };
310
361
  for (let i = 0; i < parsers.length; i++) {
311
362
  const parser = parsers[i];
@@ -1,9 +1,60 @@
1
- import { message, optionName, values } from "./message.js";
1
+ import { message, optionName, text, values } from "./message.js";
2
2
  import { extractArgumentMetavars, extractCommandNames, extractOptionNames } from "./usage.js";
3
- import { createErrorWithSuggestions, deduplicateSuggestions } from "./suggestion.js";
3
+ import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, findSimilar } from "./suggestion.js";
4
4
 
5
5
  //#region src/constructs.ts
6
6
  /**
7
+ * Collects option names and command names that are valid at the current
8
+ * parse position by walking the usage tree. Only "leading" candidates
9
+ * (those reachable before a required positional argument) are collected.
10
+ */
11
+ function collectLeadingCandidates(terms, optionNames, commandNames) {
12
+ if (!terms || !Array.isArray(terms)) return true;
13
+ for (const term of terms) {
14
+ if (term.type === "option") {
15
+ for (const name of term.names) optionNames.add(name);
16
+ return false;
17
+ }
18
+ if (term.type === "command") {
19
+ commandNames.add(term.name);
20
+ return false;
21
+ }
22
+ if (term.type === "argument") return false;
23
+ if (term.type === "optional") {
24
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
25
+ continue;
26
+ }
27
+ if (term.type === "multiple") {
28
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
29
+ if (term.min === 0) continue;
30
+ return false;
31
+ }
32
+ if (term.type === "exclusive") {
33
+ let allAlternativesSkippable = true;
34
+ for (const exclusiveUsage of term.terms) {
35
+ const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
36
+ allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
37
+ }
38
+ if (allAlternativesSkippable) continue;
39
+ return false;
40
+ }
41
+ }
42
+ return true;
43
+ }
44
+ function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
45
+ const options = /* @__PURE__ */ new Set();
46
+ const commands = /* @__PURE__ */ new Set();
47
+ for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
48
+ const candidates = new Set([...options, ...commands]);
49
+ const suggestions = findSimilar(invalidInput, candidates, DEFAULT_FIND_SIMILAR_OPTIONS);
50
+ const suggestionMsg = customFormatter ? customFormatter(suggestions) : createSuggestionMessage(suggestions);
51
+ return suggestionMsg.length > 0 ? [
52
+ ...baseError,
53
+ text("\n\n"),
54
+ ...suggestionMsg
55
+ ] : baseError;
56
+ }
57
+ /**
7
58
  * Checks if the given token is an option name that requires a value
8
59
  * (i.e., has a metavar) within the given usage terms.
9
60
  * @param usage The usage terms to search through.
@@ -168,16 +219,6 @@ function getNoMatchError(options, noMatchContext) {
168
219
  return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
169
220
  }
170
221
  /**
171
- * Creates default error for parse() method when buffer is not empty.
172
- * Shared by or() and longestMatch().
173
- * @internal
174
- */
175
- function createUnexpectedInputError(token, usage, options) {
176
- const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
177
- if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
178
- return createErrorWithSuggestions(defaultMsg, token, usage, "both", options?.errors?.suggestions);
179
- }
180
- /**
181
222
  * @since 0.5.0
182
223
  */
183
224
  function or(...args) {
@@ -204,7 +245,12 @@ function or(...args) {
204
245
  parse(context) {
205
246
  let error = {
206
247
  consumed: 0,
207
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
248
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
249
+ const token = context.buffer[0];
250
+ const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
251
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
252
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
253
+ })()
208
254
  };
209
255
  const orderedParsers = parsers.map((p, i) => [p, i]);
210
256
  orderedParsers.sort(([_, a], [__, b]) => context.state?.[0] === a ? -1 : context.state?.[0] === b ? 1 : a - b);
@@ -305,7 +351,12 @@ function longestMatch(...args) {
305
351
  let bestMatch = null;
306
352
  let error = {
307
353
  consumed: 0,
308
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
354
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
355
+ const token = context.buffer[0];
356
+ const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
357
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
358
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
359
+ })()
309
360
  };
310
361
  for (let i = 0; i < parsers.length; i++) {
311
362
  const parser = parsers[i];
package/dist/facade.cjs CHANGED
@@ -470,9 +470,9 @@ function run(parser, programName, args, options = {}) {
470
470
  } else {
471
471
  const singularMatchExact = completionName === "singular" || completionName === "both" ? arg === "--completion" : false;
472
472
  const pluralMatchExact = completionName === "plural" || completionName === "both" ? arg === "--completions" : false;
473
- if ((singularMatchExact || pluralMatchExact) && i + 1 < args.length) {
474
- const shell = args[i + 1];
475
- const completionArgs = args.slice(i + 2);
473
+ if (singularMatchExact || pluralMatchExact) {
474
+ const shell = i + 1 < args.length ? args[i + 1] : "";
475
+ const completionArgs = i + 1 < args.length ? args.slice(i + 2) : [];
476
476
  return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
477
477
  }
478
478
  }
package/dist/facade.js CHANGED
@@ -470,9 +470,9 @@ function run(parser, programName, args, options = {}) {
470
470
  } else {
471
471
  const singularMatchExact = completionName === "singular" || completionName === "both" ? arg === "--completion" : false;
472
472
  const pluralMatchExact = completionName === "plural" || completionName === "both" ? arg === "--completions" : false;
473
- if ((singularMatchExact || pluralMatchExact) && i + 1 < args.length) {
474
- const shell = args[i + 1];
475
- const completionArgs = args.slice(i + 2);
473
+ if (singularMatchExact || pluralMatchExact) {
474
+ const shell = i + 1 < args.length ? args[i + 1] : "";
475
+ const completionArgs = i + 1 < args.length ? args.slice(i + 2) : [];
476
476
  return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
477
477
  }
478
478
  }
@@ -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.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",