@optique/core 0.9.4 → 0.9.6

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.
@@ -202,16 +253,6 @@ function getNoMatchError(options, noMatchContext) {
202
253
  return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
203
254
  }
204
255
  /**
205
- * Creates default error for parse() method when buffer is not empty.
206
- * Shared by or() and longestMatch().
207
- * @internal
208
- */
209
- function createUnexpectedInputError(token, usage, options) {
210
- const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
211
- if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
212
- return require_suggestion.createErrorWithSuggestions(defaultMsg, token, usage, "both", options?.errors?.suggestions);
213
- }
214
- /**
215
256
  * @since 0.5.0
216
257
  */
217
258
  function or(...args) {
@@ -230,7 +271,12 @@ function or(...args) {
230
271
  const syncParsers = parsers;
231
272
  const getInitialError = (context) => ({
232
273
  consumed: 0,
233
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
274
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
275
+ const token = context.buffer[0];
276
+ const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
277
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
278
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
279
+ })()
234
280
  });
235
281
  const parseSync = (context) => {
236
282
  let error = getInitialError(context);
@@ -372,7 +418,12 @@ function longestMatch(...args) {
372
418
  const syncParsers = parsers;
373
419
  const getInitialError = (context) => ({
374
420
  consumed: 0,
375
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
421
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
422
+ const token = context.buffer[0];
423
+ const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
424
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
425
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
426
+ })()
376
427
  });
377
428
  const parseSync = (context) => {
378
429
  let bestMatch = null;
@@ -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.
@@ -202,16 +253,6 @@ function getNoMatchError(options, noMatchContext) {
202
253
  return customNoMatch ? typeof customNoMatch === "function" ? customNoMatch(noMatchContext) : customNoMatch : generateNoMatchError(noMatchContext);
203
254
  }
204
255
  /**
205
- * Creates default error for parse() method when buffer is not empty.
206
- * Shared by or() and longestMatch().
207
- * @internal
208
- */
209
- function createUnexpectedInputError(token, usage, options) {
210
- const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
211
- if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
212
- return createErrorWithSuggestions(defaultMsg, token, usage, "both", options?.errors?.suggestions);
213
- }
214
- /**
215
256
  * @since 0.5.0
216
257
  */
217
258
  function or(...args) {
@@ -230,7 +271,12 @@ function or(...args) {
230
271
  const syncParsers = parsers;
231
272
  const getInitialError = (context) => ({
232
273
  consumed: 0,
233
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
274
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
275
+ const token = context.buffer[0];
276
+ const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
277
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
278
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
279
+ })()
234
280
  });
235
281
  const parseSync = (context) => {
236
282
  let error = getInitialError(context);
@@ -372,7 +418,12 @@ function longestMatch(...args) {
372
418
  const syncParsers = parsers;
373
419
  const getInitialError = (context) => ({
374
420
  consumed: 0,
375
- error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : createUnexpectedInputError(context.buffer[0], context.usage, options)
421
+ error: context.buffer.length < 1 ? getNoMatchError(options, noMatchContext) : (() => {
422
+ const token = context.buffer[0];
423
+ const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
424
+ if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
425
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
426
+ })()
376
427
  });
377
428
  const parseSync = (context) => {
378
429
  let bestMatch = null;
package/dist/facade.cjs CHANGED
@@ -284,9 +284,26 @@ function combineWithHelpVersion(originalParser, helpParsers, versionParsers, com
284
284
  completion: require_primitives.constant(false),
285
285
  result: originalParser
286
286
  }));
287
+ const mainParserIndex = parsers.length - 1;
287
288
  if (parsers.length === 1) return parsers[0];
288
- else if (parsers.length === 2) return require_constructs.longestMatch(parsers[0], parsers[1]);
289
- else return require_constructs.longestMatch(...parsers);
289
+ let combined;
290
+ if (parsers.length === 2) combined = require_constructs.longestMatch(parsers[0], parsers[1]);
291
+ else combined = require_constructs.longestMatch(...parsers);
292
+ const topUsage = combined.usage[0];
293
+ if (topUsage?.type === "exclusive" && mainParserIndex > 0) {
294
+ const terms = [...topUsage.terms];
295
+ const [mainTerm] = terms.splice(mainParserIndex, 1);
296
+ const lenientCount = (helpParsers.helpOption ? 1 : 0) + (versionParsers.versionOption ? 1 : 0);
297
+ terms.splice(lenientCount, 0, mainTerm);
298
+ combined = {
299
+ ...combined,
300
+ usage: [{
301
+ ...topUsage,
302
+ terms
303
+ }]
304
+ };
305
+ }
306
+ return combined;
290
307
  }
291
308
  /**
292
309
  * Classifies the parsing result into a discriminated union for cleaner handling.
@@ -455,9 +472,9 @@ function runParser(parser, programName, args, options = {}) {
455
472
  } else {
456
473
  const singularMatchExact = completionName === "singular" || completionName === "both" ? arg === "--completion" : false;
457
474
  const pluralMatchExact = completionName === "plural" || completionName === "both" ? arg === "--completions" : false;
458
- if ((singularMatchExact || pluralMatchExact) && i + 1 < args.length) {
459
- const shell = args[i + 1];
460
- const completionArgs = args.slice(i + 2);
475
+ if (singularMatchExact || pluralMatchExact) {
476
+ const shell = i + 1 < args.length ? args[i + 1] : "";
477
+ const completionArgs = i + 1 < args.length ? args.slice(i + 2) : [];
461
478
  return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
462
479
  }
463
480
  }
package/dist/facade.js CHANGED
@@ -284,9 +284,26 @@ function combineWithHelpVersion(originalParser, helpParsers, versionParsers, com
284
284
  completion: constant(false),
285
285
  result: originalParser
286
286
  }));
287
+ const mainParserIndex = parsers.length - 1;
287
288
  if (parsers.length === 1) return parsers[0];
288
- else if (parsers.length === 2) return longestMatch(parsers[0], parsers[1]);
289
- else return longestMatch(...parsers);
289
+ let combined;
290
+ if (parsers.length === 2) combined = longestMatch(parsers[0], parsers[1]);
291
+ else combined = longestMatch(...parsers);
292
+ const topUsage = combined.usage[0];
293
+ if (topUsage?.type === "exclusive" && mainParserIndex > 0) {
294
+ const terms = [...topUsage.terms];
295
+ const [mainTerm] = terms.splice(mainParserIndex, 1);
296
+ const lenientCount = (helpParsers.helpOption ? 1 : 0) + (versionParsers.versionOption ? 1 : 0);
297
+ terms.splice(lenientCount, 0, mainTerm);
298
+ combined = {
299
+ ...combined,
300
+ usage: [{
301
+ ...topUsage,
302
+ terms
303
+ }]
304
+ };
305
+ }
306
+ return combined;
290
307
  }
291
308
  /**
292
309
  * Classifies the parsing result into a discriminated union for cleaner handling.
@@ -455,9 +472,9 @@ function runParser(parser, programName, args, options = {}) {
455
472
  } else {
456
473
  const singularMatchExact = completionName === "singular" || completionName === "both" ? arg === "--completion" : false;
457
474
  const pluralMatchExact = completionName === "plural" || completionName === "both" ? arg === "--completions" : false;
458
- if ((singularMatchExact || pluralMatchExact) && i + 1 < args.length) {
459
- const shell = args[i + 1];
460
- const completionArgs = args.slice(i + 2);
475
+ if (singularMatchExact || pluralMatchExact) {
476
+ const shell = i + 1 < args.length ? args[i + 1] : "";
477
+ const completionArgs = i + 1 < args.length ? args.slice(i + 2) : [];
461
478
  return handleCompletion([shell, ...completionArgs], programName, parser, completionParsers.completionCommand, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, completionMode, completionName);
462
479
  }
463
480
  }
@@ -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.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",