@optique/core 0.7.11 → 0.7.13-dev.361

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.
@@ -24,6 +24,52 @@ function isOptionRequiringValue(usage, token) {
24
24
  }
25
25
  return traverse(usage);
26
26
  }
27
+ function collectLeadingCandidates(terms, optionNames, commandNames) {
28
+ if (!terms || !Array.isArray(terms)) return true;
29
+ for (const term of terms) {
30
+ if (term.type === "option") {
31
+ for (const name of term.names) optionNames.add(name);
32
+ return false;
33
+ }
34
+ if (term.type === "command") {
35
+ commandNames.add(term.name);
36
+ return false;
37
+ }
38
+ if (term.type === "argument") return false;
39
+ if (term.type === "optional") {
40
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
41
+ continue;
42
+ }
43
+ if (term.type === "multiple") {
44
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
45
+ if (term.min === 0) continue;
46
+ return false;
47
+ }
48
+ if (term.type === "exclusive") {
49
+ let allAlternativesSkippable = true;
50
+ for (const exclusiveUsage of term.terms) {
51
+ const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
52
+ allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
53
+ }
54
+ if (allAlternativesSkippable) continue;
55
+ return false;
56
+ }
57
+ }
58
+ return true;
59
+ }
60
+ function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
61
+ const options = /* @__PURE__ */ new Set();
62
+ const commands = /* @__PURE__ */ new Set();
63
+ for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
64
+ const candidates = new Set([...options, ...commands]);
65
+ const suggestions = require_suggestion.findSimilar(invalidInput, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS);
66
+ const suggestionMsg = customFormatter ? customFormatter(suggestions) : require_suggestion.createSuggestionMessage(suggestions);
67
+ return suggestionMsg.length > 0 ? [
68
+ ...baseError,
69
+ require_message.text("\n\n"),
70
+ ...suggestionMsg
71
+ ] : baseError;
72
+ }
27
73
  /**
28
74
  * Extracts required (non-optional) usage terms from a usage array.
29
75
  * @param usage The usage to extract required terms from
@@ -132,7 +178,7 @@ function or(...args) {
132
178
  const token = context.buffer[0];
133
179
  const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
134
180
  if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
135
- return require_suggestion.createErrorWithSuggestions(defaultMsg, token, context.usage, "both", options?.errors?.suggestions);
181
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
136
182
  })()
137
183
  };
138
184
  const orderedParsers = parsers.map((p, i) => [p, i]);
@@ -193,7 +239,9 @@ function or(...args) {
193
239
  });
194
240
  },
195
241
  getDocFragments(state, _defaultValue) {
242
+ let brief;
196
243
  let description;
244
+ let footer;
197
245
  let fragments;
198
246
  if (state.kind === "unavailable" || state.state == null) fragments = parsers.flatMap((p) => p.getDocFragments({ kind: "unavailable" }, void 0).fragments);
199
247
  else {
@@ -203,7 +251,9 @@ function or(...args) {
203
251
  state: parserResult.next.state
204
252
  } : { kind: "unavailable" };
205
253
  const docFragments = parsers[index].getDocFragments(innerState, void 0);
254
+ brief = docFragments.brief;
206
255
  description = docFragments.description;
256
+ footer = docFragments.footer;
207
257
  fragments = docFragments.fragments;
208
258
  }
209
259
  const entries = fragments.filter((f) => f.type === "entry");
@@ -214,7 +264,9 @@ function or(...args) {
214
264
  else sections.push(fragment);
215
265
  }
216
266
  return {
267
+ brief,
217
268
  description,
269
+ footer,
218
270
  fragments: [...sections.map((s) => ({
219
271
  ...s,
220
272
  type: "section"
@@ -276,7 +328,7 @@ function longestMatch(...args) {
276
328
  const token = context.buffer[0];
277
329
  const defaultMsg = require_message.message`Unexpected option or subcommand: ${require_message.optionName(token)}.`;
278
330
  if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
279
- return require_suggestion.createErrorWithSuggestions(defaultMsg, token, context.usage, "both", options?.errors?.suggestions);
331
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
280
332
  })()
281
333
  };
282
334
  for (let i = 0; i < parsers.length; i++) {
@@ -337,6 +389,7 @@ function longestMatch(...args) {
337
389
  });
338
390
  },
339
391
  getDocFragments(state, _defaultValue) {
392
+ let brief;
340
393
  let description;
341
394
  let footer;
342
395
  let fragments;
@@ -348,12 +401,14 @@ function longestMatch(...args) {
348
401
  kind: "available",
349
402
  state: result.next.state
350
403
  });
404
+ brief = docResult.brief;
351
405
  description = docResult.description;
352
406
  footer = docResult.footer;
353
407
  fragments = docResult.fragments;
354
408
  } else fragments = parsers.flatMap((p) => p.getDocFragments({ kind: "unavailable" }).fragments);
355
409
  }
356
410
  return {
411
+ brief,
357
412
  description,
358
413
  fragments,
359
414
  footer
@@ -1,6 +1,6 @@
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 } from "./suggestion.js";
3
+ import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, findSimilar } from "./suggestion.js";
4
4
 
5
5
  //#region src/constructs.ts
6
6
  /**
@@ -24,6 +24,52 @@ function isOptionRequiringValue(usage, token) {
24
24
  }
25
25
  return traverse(usage);
26
26
  }
27
+ function collectLeadingCandidates(terms, optionNames, commandNames) {
28
+ if (!terms || !Array.isArray(terms)) return true;
29
+ for (const term of terms) {
30
+ if (term.type === "option") {
31
+ for (const name of term.names) optionNames.add(name);
32
+ return false;
33
+ }
34
+ if (term.type === "command") {
35
+ commandNames.add(term.name);
36
+ return false;
37
+ }
38
+ if (term.type === "argument") return false;
39
+ if (term.type === "optional") {
40
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
41
+ continue;
42
+ }
43
+ if (term.type === "multiple") {
44
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
45
+ if (term.min === 0) continue;
46
+ return false;
47
+ }
48
+ if (term.type === "exclusive") {
49
+ let allAlternativesSkippable = true;
50
+ for (const exclusiveUsage of term.terms) {
51
+ const alternativeSkippable = collectLeadingCandidates(exclusiveUsage, optionNames, commandNames);
52
+ allAlternativesSkippable = allAlternativesSkippable && alternativeSkippable;
53
+ }
54
+ if (allAlternativesSkippable) continue;
55
+ return false;
56
+ }
57
+ }
58
+ return true;
59
+ }
60
+ function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
61
+ const options = /* @__PURE__ */ new Set();
62
+ const commands = /* @__PURE__ */ new Set();
63
+ for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
64
+ const candidates = new Set([...options, ...commands]);
65
+ const suggestions = findSimilar(invalidInput, candidates, DEFAULT_FIND_SIMILAR_OPTIONS);
66
+ const suggestionMsg = customFormatter ? customFormatter(suggestions) : createSuggestionMessage(suggestions);
67
+ return suggestionMsg.length > 0 ? [
68
+ ...baseError,
69
+ text("\n\n"),
70
+ ...suggestionMsg
71
+ ] : baseError;
72
+ }
27
73
  /**
28
74
  * Extracts required (non-optional) usage terms from a usage array.
29
75
  * @param usage The usage to extract required terms from
@@ -132,7 +178,7 @@ function or(...args) {
132
178
  const token = context.buffer[0];
133
179
  const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
134
180
  if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
135
- return createErrorWithSuggestions(defaultMsg, token, context.usage, "both", options?.errors?.suggestions);
181
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
136
182
  })()
137
183
  };
138
184
  const orderedParsers = parsers.map((p, i) => [p, i]);
@@ -193,7 +239,9 @@ function or(...args) {
193
239
  });
194
240
  },
195
241
  getDocFragments(state, _defaultValue) {
242
+ let brief;
196
243
  let description;
244
+ let footer;
197
245
  let fragments;
198
246
  if (state.kind === "unavailable" || state.state == null) fragments = parsers.flatMap((p) => p.getDocFragments({ kind: "unavailable" }, void 0).fragments);
199
247
  else {
@@ -203,7 +251,9 @@ function or(...args) {
203
251
  state: parserResult.next.state
204
252
  } : { kind: "unavailable" };
205
253
  const docFragments = parsers[index].getDocFragments(innerState, void 0);
254
+ brief = docFragments.brief;
206
255
  description = docFragments.description;
256
+ footer = docFragments.footer;
207
257
  fragments = docFragments.fragments;
208
258
  }
209
259
  const entries = fragments.filter((f) => f.type === "entry");
@@ -214,7 +264,9 @@ function or(...args) {
214
264
  else sections.push(fragment);
215
265
  }
216
266
  return {
267
+ brief,
217
268
  description,
269
+ footer,
218
270
  fragments: [...sections.map((s) => ({
219
271
  ...s,
220
272
  type: "section"
@@ -276,7 +328,7 @@ function longestMatch(...args) {
276
328
  const token = context.buffer[0];
277
329
  const defaultMsg = message`Unexpected option or subcommand: ${optionName(token)}.`;
278
330
  if (options?.errors?.unexpectedInput != null) return typeof options.errors.unexpectedInput === "function" ? options.errors.unexpectedInput(token) : options.errors.unexpectedInput;
279
- return createErrorWithSuggestions(defaultMsg, token, context.usage, "both", options?.errors?.suggestions);
331
+ return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
280
332
  })()
281
333
  };
282
334
  for (let i = 0; i < parsers.length; i++) {
@@ -337,6 +389,7 @@ function longestMatch(...args) {
337
389
  });
338
390
  },
339
391
  getDocFragments(state, _defaultValue) {
392
+ let brief;
340
393
  let description;
341
394
  let footer;
342
395
  let fragments;
@@ -348,12 +401,14 @@ function longestMatch(...args) {
348
401
  kind: "available",
349
402
  state: result.next.state
350
403
  });
404
+ brief = docResult.brief;
351
405
  description = docResult.description;
352
406
  footer = docResult.footer;
353
407
  fragments = docResult.fragments;
354
408
  } else fragments = parsers.flatMap((p) => p.getDocFragments({ kind: "unavailable" }).fragments);
355
409
  }
356
410
  return {
411
+ brief,
357
412
  description,
358
413
  fragments,
359
414
  footer
package/dist/doc.d.cts CHANGED
@@ -60,6 +60,12 @@ type DocFragment = {
60
60
  * a final document page.
61
61
  */
62
62
  interface DocFragments {
63
+ /**
64
+ * An optional brief that provides a short summary for the collection
65
+ * of fragments.
66
+ * @since 0.7.12
67
+ */
68
+ readonly brief?: Message;
63
69
  /**
64
70
  * An optional description that applies to the entire collection of fragments.
65
71
  */
package/dist/doc.d.ts CHANGED
@@ -60,6 +60,12 @@ type DocFragment = {
60
60
  * a final document page.
61
61
  */
62
62
  interface DocFragments {
63
+ /**
64
+ * An optional brief that provides a short summary for the collection
65
+ * of fragments.
66
+ * @since 0.7.12
67
+ */
68
+ readonly brief?: Message;
63
69
  /**
64
70
  * An optional description that applies to the entire collection of fragments.
65
71
  */
package/dist/facade.cjs CHANGED
@@ -561,11 +561,13 @@ function run(parser, programName, args, options = {}) {
561
561
  const doc = require_parser.getDocPage(helpGeneratorParser, classified.commands);
562
562
  if (doc != null) {
563
563
  const isMetaCommandHelp = (completionName === "singular" || completionName === "both" ? requestedCommand === "completion" : false) || (completionName === "plural" || completionName === "both" ? requestedCommand === "completions" : false) || requestedCommand === "help" || requestedCommand === "version";
564
+ const isSubcommandHelp = classified.commands.length > 0;
565
+ const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
564
566
  const augmentedDoc = {
565
567
  ...doc,
566
- brief: !isMetaCommandHelp ? brief ?? doc.brief : doc.brief,
567
- description: !isMetaCommandHelp ? description ?? doc.description : doc.description,
568
- footer: !isMetaCommandHelp ? footer ?? doc.footer : doc.footer
568
+ brief: shouldOverride ? brief ?? doc.brief : doc.brief ?? brief,
569
+ description: shouldOverride ? description ?? doc.description : doc.description ?? description,
570
+ footer: shouldOverride ? footer ?? doc.footer : doc.footer ?? footer
569
571
  };
570
572
  stdout(require_doc.formatDocPage(programName, augmentedDoc, {
571
573
  colors,
package/dist/facade.js CHANGED
@@ -561,11 +561,13 @@ function run(parser, programName, args, options = {}) {
561
561
  const doc = getDocPage(helpGeneratorParser, classified.commands);
562
562
  if (doc != null) {
563
563
  const isMetaCommandHelp = (completionName === "singular" || completionName === "both" ? requestedCommand === "completion" : false) || (completionName === "plural" || completionName === "both" ? requestedCommand === "completions" : false) || requestedCommand === "help" || requestedCommand === "version";
564
+ const isSubcommandHelp = classified.commands.length > 0;
565
+ const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
564
566
  const augmentedDoc = {
565
567
  ...doc,
566
- brief: !isMetaCommandHelp ? brief ?? doc.brief : doc.brief,
567
- description: !isMetaCommandHelp ? description ?? doc.description : doc.description,
568
- footer: !isMetaCommandHelp ? footer ?? doc.footer : doc.footer
568
+ brief: shouldOverride ? brief ?? doc.brief : doc.brief ?? brief,
569
+ description: shouldOverride ? description ?? doc.description : doc.description ?? description,
570
+ footer: shouldOverride ? footer ?? doc.footer : doc.footer ?? footer
569
571
  };
570
572
  stdout(formatDocPage(programName, augmentedDoc, {
571
573
  colors,
package/dist/parser.cjs CHANGED
@@ -159,7 +159,7 @@ function getDocPage(parser, args = []) {
159
159
  if (!result.success) break;
160
160
  context = result.next;
161
161
  } while (context.buffer.length > 0);
162
- const { description, fragments, footer } = parser.getDocFragments({
162
+ const { brief, description, fragments, footer } = parser.getDocFragments({
163
163
  kind: "available",
164
164
  state: context.state
165
165
  }, void 0);
@@ -184,6 +184,7 @@ function getDocPage(parser, args = []) {
184
184
  return {
185
185
  usage,
186
186
  sections,
187
+ ...brief != null && { brief },
187
188
  ...description != null && { description },
188
189
  ...footer != null && { footer }
189
190
  };
package/dist/parser.js CHANGED
@@ -159,7 +159,7 @@ function getDocPage(parser, args = []) {
159
159
  if (!result.success) break;
160
160
  context = result.next;
161
161
  } while (context.buffer.length > 0);
162
- const { description, fragments, footer } = parser.getDocFragments({
162
+ const { brief, description, fragments, footer } = parser.getDocFragments({
163
163
  kind: "available",
164
164
  state: context.state
165
165
  }, void 0);
@@ -184,6 +184,7 @@ function getDocPage(parser, args = []) {
184
184
  return {
185
185
  usage,
186
186
  sections,
187
+ ...brief != null && { brief },
187
188
  ...description != null && { description },
188
189
  ...footer != null && { footer }
189
190
  };
@@ -725,6 +725,7 @@ function command(name, parser, options = {}) {
725
725
  const innerFragments = parser.getDocFragments(innerState, defaultValue);
726
726
  return {
727
727
  ...innerFragments,
728
+ brief: innerFragments.brief ?? options.brief,
728
729
  description: innerFragments.description ?? options.description,
729
730
  footer: innerFragments.footer ?? options.footer
730
731
  };
@@ -725,6 +725,7 @@ function command(name, parser, options = {}) {
725
725
  const innerFragments = parser.getDocFragments(innerState, defaultValue);
726
726
  return {
727
727
  ...innerFragments,
728
+ brief: innerFragments.brief ?? options.brief,
728
729
  description: innerFragments.description ?? options.description,
729
730
  footer: innerFragments.footer ?? options.footer
730
731
  };
@@ -183,4 +183,5 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
183
183
  //#endregion
184
184
  exports.DEFAULT_FIND_SIMILAR_OPTIONS = DEFAULT_FIND_SIMILAR_OPTIONS;
185
185
  exports.createErrorWithSuggestions = createErrorWithSuggestions;
186
+ exports.createSuggestionMessage = createSuggestionMessage;
186
187
  exports.findSimilar = findSimilar;
@@ -181,4 +181,4 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
181
181
  }
182
182
 
183
183
  //#endregion
184
- export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, findSimilar };
184
+ export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, findSimilar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "0.7.11",
3
+ "version": "0.7.13-dev.361+10f5601c",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",