@optique/core 0.10.2 → 0.10.4-dev.408

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.
@@ -3,50 +3,13 @@ const require_dependency = require('./dependency.cjs');
3
3
  const require_mode_dispatch = require('./mode-dispatch.cjs');
4
4
  const require_usage = require('./usage.cjs');
5
5
  const require_suggestion = require('./suggestion.cjs');
6
+ const require_usage_internals = require('./usage-internals.cjs');
6
7
 
7
8
  //#region src/constructs.ts
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
9
  function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
47
10
  const options = /* @__PURE__ */ new Set();
48
11
  const commands = /* @__PURE__ */ new Set();
49
- for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
12
+ for (const parser of parsers) require_usage_internals.collectLeadingCandidates(parser.usage, options, commands);
50
13
  const candidates = new Set([...options, ...commands]);
51
14
  const suggestions = require_suggestion.findSimilar(invalidInput, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS);
52
15
  const suggestionMsg = customFormatter ? customFormatter(suggestions) : require_suggestion.createSuggestionMessage(suggestions);
@@ -2006,7 +1969,7 @@ function group(label, parser) {
2006
1969
  complete: (state) => parser.complete(state),
2007
1970
  suggest: (context, prefix) => parser.suggest(context, prefix),
2008
1971
  getDocFragments: (state, defaultValue) => {
2009
- const { description, fragments } = parser.getDocFragments(state, defaultValue);
1972
+ const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
2010
1973
  const allEntries = [];
2011
1974
  const titledSections = [];
2012
1975
  for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
@@ -2016,15 +1979,22 @@ function group(label, parser) {
2016
1979
  kind: "available",
2017
1980
  state: parser.initialState
2018
1981
  }, void 0);
2019
- const initialHasCommands = initialFragments.fragments.some((f) => f.type === "entry" && f.term.type === "command" || f.type === "section" && f.entries.some((e) => e.term.type === "command"));
2020
- const currentHasCommands = allEntries.some((e) => e.term.type === "command");
2021
- const applyLabel = !initialHasCommands || currentHasCommands;
1982
+ const initialCommandNames = /* @__PURE__ */ new Set();
1983
+ for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
1984
+ else if (f.type === "section") {
1985
+ for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
1986
+ }
1987
+ const initialHasCommands = initialCommandNames.size > 0;
1988
+ const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
1989
+ const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
2022
1990
  const labeledSection = applyLabel ? {
2023
1991
  title: label,
2024
1992
  entries: allEntries
2025
1993
  } : { entries: allEntries };
2026
1994
  return {
1995
+ brief,
2027
1996
  description,
1997
+ footer,
2028
1998
  fragments: [...titledSections.map((s) => ({
2029
1999
  ...s,
2030
2000
  type: "section"
@@ -3,46 +3,9 @@ import { DependencyRegistry, dependencyId, isDeferredParseState, isDependencySou
3
3
  import { dispatchByMode, dispatchIterableByMode } from "./mode-dispatch.js";
4
4
  import { extractArgumentMetavars, extractCommandNames, extractOptionNames } from "./usage.js";
5
5
  import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, findSimilar } from "./suggestion.js";
6
+ import { collectLeadingCandidates } from "./usage-internals.js";
6
7
 
7
8
  //#region src/constructs.ts
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
9
  function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
47
10
  const options = /* @__PURE__ */ new Set();
48
11
  const commands = /* @__PURE__ */ new Set();
@@ -2006,7 +1969,7 @@ function group(label, parser) {
2006
1969
  complete: (state) => parser.complete(state),
2007
1970
  suggest: (context, prefix) => parser.suggest(context, prefix),
2008
1971
  getDocFragments: (state, defaultValue) => {
2009
- const { description, fragments } = parser.getDocFragments(state, defaultValue);
1972
+ const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
2010
1973
  const allEntries = [];
2011
1974
  const titledSections = [];
2012
1975
  for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
@@ -2016,15 +1979,22 @@ function group(label, parser) {
2016
1979
  kind: "available",
2017
1980
  state: parser.initialState
2018
1981
  }, void 0);
2019
- const initialHasCommands = initialFragments.fragments.some((f) => f.type === "entry" && f.term.type === "command" || f.type === "section" && f.entries.some((e) => e.term.type === "command"));
2020
- const currentHasCommands = allEntries.some((e) => e.term.type === "command");
2021
- const applyLabel = !initialHasCommands || currentHasCommands;
1982
+ const initialCommandNames = /* @__PURE__ */ new Set();
1983
+ for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
1984
+ else if (f.type === "section") {
1985
+ for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
1986
+ }
1987
+ const initialHasCommands = initialCommandNames.size > 0;
1988
+ const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
1989
+ const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
2022
1990
  const labeledSection = applyLabel ? {
2023
1991
  title: label,
2024
1992
  entries: allEntries
2025
1993
  } : { entries: allEntries };
2026
1994
  return {
1995
+ brief,
2027
1996
  description,
1997
+ footer,
2028
1998
  fragments: [...titledSections.map((s) => ({
2029
1999
  ...s,
2030
2000
  type: "section"
package/dist/facade.cjs CHANGED
@@ -625,8 +625,8 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
625
625
  const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
626
626
  const augmentedDoc = {
627
627
  ...doc,
628
- brief: shouldOverride ? brief ?? doc.brief : doc.brief ?? brief,
629
- description: shouldOverride ? description ?? doc.description : doc.description ?? description,
628
+ brief: shouldOverride ? brief ?? doc.brief : doc.brief,
629
+ description: shouldOverride ? description ?? doc.description : doc.description,
630
630
  examples: isTopLevel && !isMetaCommandHelp ? examples ?? doc.examples : void 0,
631
631
  author: isTopLevel && !isMetaCommandHelp ? author ?? doc.author : void 0,
632
632
  bugs: isTopLevel && !isMetaCommandHelp ? bugs ?? doc.bugs : void 0,
package/dist/facade.js CHANGED
@@ -625,8 +625,8 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
625
625
  const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
626
626
  const augmentedDoc = {
627
627
  ...doc,
628
- brief: shouldOverride ? brief ?? doc.brief : doc.brief ?? brief,
629
- description: shouldOverride ? description ?? doc.description : doc.description ?? description,
628
+ brief: shouldOverride ? brief ?? doc.brief : doc.brief,
629
+ description: shouldOverride ? description ?? doc.description : doc.description,
630
630
  examples: isTopLevel && !isMetaCommandHelp ? examples ?? doc.examples : void 0,
631
631
  author: isTopLevel && !isMetaCommandHelp ? author ?? doc.author : void 0,
632
632
  bugs: isTopLevel && !isMetaCommandHelp ? bugs ?? doc.bugs : void 0,
package/dist/message.cjs CHANGED
@@ -253,115 +253,124 @@ function formatMessage(msg, options = {}) {
253
253
  const resetSequence = `\x1b[0m${resetSuffix}`;
254
254
  function* stream() {
255
255
  const wordPattern = /\s*\S+\s*/g;
256
- for (const term of msg) if (term.type === "text") if (term.text.includes("\n\n")) {
257
- const paragraphs = term.text.split(/\n\n+/);
258
- for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
259
- if (paragraphIndex > 0) yield {
260
- text: "\n",
261
- width: -1
262
- };
263
- const paragraph = paragraphs[paragraphIndex].replace(/\n/g, " ");
264
- wordPattern.lastIndex = 0;
265
- while (true) {
266
- const match = wordPattern.exec(paragraph);
267
- if (match == null) break;
268
- yield {
269
- text: match[0],
270
- width: match[0].length
256
+ let prevWasLineBreak = false;
257
+ for (const term of msg) {
258
+ const isAfterLineBreak = prevWasLineBreak;
259
+ prevWasLineBreak = false;
260
+ if (term.type === "text") {
261
+ const rawText = isAfterLineBreak ? term.text.replace(/^\n(?!\n)/, "") : term.text;
262
+ if (rawText.includes("\n\n")) {
263
+ const paragraphs = rawText.split(/\n\n+/);
264
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
265
+ if (paragraphIndex > 0) yield {
266
+ text: "\n",
267
+ width: -1
268
+ };
269
+ const paragraph = paragraphs[paragraphIndex].replace(/\n/g, " ");
270
+ wordPattern.lastIndex = 0;
271
+ while (true) {
272
+ const match = wordPattern.exec(paragraph);
273
+ if (match == null) break;
274
+ yield {
275
+ text: match[0],
276
+ width: match[0].length
277
+ };
278
+ }
279
+ }
280
+ } else {
281
+ const normalizedText = rawText.replace(/\n/g, " ");
282
+ if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
283
+ text: " ",
284
+ width: 1
271
285
  };
286
+ else {
287
+ wordPattern.lastIndex = 0;
288
+ while (true) {
289
+ const match = wordPattern.exec(normalizedText);
290
+ if (match == null) break;
291
+ yield {
292
+ text: match[0],
293
+ width: match[0].length
294
+ };
295
+ }
296
+ }
272
297
  }
273
- }
274
- } else {
275
- const normalizedText = term.text.replace(/\n/g, " ");
276
- if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
277
- text: " ",
278
- width: 1
279
- };
280
- else {
281
- wordPattern.lastIndex = 0;
282
- while (true) {
283
- const match = wordPattern.exec(normalizedText);
284
- if (match == null) break;
298
+ } else if (term.type === "optionName") {
299
+ const name = useQuotes ? `\`${term.optionName}\`` : term.optionName;
300
+ yield {
301
+ text: useColors ? `\x1b[3m${name}${resetSequence}` : name,
302
+ width: name.length
303
+ };
304
+ } else if (term.type === "optionNames") {
305
+ const names = term.optionNames.map((name) => useQuotes ? `\`${name}\`` : name);
306
+ let i = 0;
307
+ for (const name of names) {
308
+ if (i > 0) yield {
309
+ text: "/",
310
+ width: 1
311
+ };
285
312
  yield {
286
- text: match[0],
287
- width: match[0].length
313
+ text: useColors ? `\x1b[3m${name}${resetSequence}` : name,
314
+ width: name.length
288
315
  };
316
+ i++;
289
317
  }
290
- }
291
- }
292
- else if (term.type === "optionName") {
293
- const name = useQuotes ? `\`${term.optionName}\`` : term.optionName;
294
- yield {
295
- text: useColors ? `\x1b[3m${name}${resetSequence}` : name,
296
- width: name.length
297
- };
298
- } else if (term.type === "optionNames") {
299
- const names = term.optionNames.map((name) => useQuotes ? `\`${name}\`` : name);
300
- let i = 0;
301
- for (const name of names) {
318
+ } else if (term.type === "metavar") {
319
+ const metavar$1 = useQuotes ? `\`${term.metavar}\`` : term.metavar;
320
+ yield {
321
+ text: useColors ? `\x1b[1m${metavar$1}${resetSequence}` : metavar$1,
322
+ width: metavar$1.length
323
+ };
324
+ } else if (term.type === "value") {
325
+ const value$1 = useQuotes ? `${JSON.stringify(term.value)}` : term.value;
326
+ yield {
327
+ text: useColors ? `\x1b[32m${value$1}${resetSequence}` : value$1,
328
+ width: value$1.length
329
+ };
330
+ } else if (term.type === "values") for (let i = 0; i < term.values.length; i++) {
302
331
  if (i > 0) yield {
303
- text: "/",
332
+ text: " ",
304
333
  width: 1
305
334
  };
335
+ const value$1 = useQuotes ? JSON.stringify(term.values[i]) : term.values[i];
306
336
  yield {
307
- text: useColors ? `\x1b[3m${name}${resetSequence}` : name,
308
- width: name.length
337
+ text: useColors ? i <= 0 ? `\x1b[32m${value$1}` : i + 1 >= term.values.length ? `${value$1}${resetSequence}` : value$1 : value$1,
338
+ width: value$1.length
309
339
  };
310
- i++;
311
340
  }
312
- } else if (term.type === "metavar") {
313
- const metavar$1 = useQuotes ? `\`${term.metavar}\`` : term.metavar;
314
- yield {
315
- text: useColors ? `\x1b[1m${metavar$1}${resetSequence}` : metavar$1,
316
- width: metavar$1.length
317
- };
318
- } else if (term.type === "value") {
319
- const value$1 = useQuotes ? `${JSON.stringify(term.value)}` : term.value;
320
- yield {
321
- text: useColors ? `\x1b[32m${value$1}${resetSequence}` : value$1,
322
- width: value$1.length
323
- };
324
- } else if (term.type === "values") for (let i = 0; i < term.values.length; i++) {
325
- if (i > 0) yield {
326
- text: " ",
327
- width: 1
328
- };
329
- const value$1 = useQuotes ? JSON.stringify(term.values[i]) : term.values[i];
330
- yield {
331
- text: useColors ? i <= 0 ? `\x1b[32m${value$1}` : i + 1 >= term.values.length ? `${value$1}${resetSequence}` : value$1 : value$1,
332
- width: value$1.length
333
- };
334
- }
335
- else if (term.type === "envVar") {
336
- const envVar$1 = useQuotes ? `\`${term.envVar}\`` : term.envVar;
337
- yield {
338
- text: useColors ? `\x1b[1;4m${envVar$1}${resetSequence}` : envVar$1,
339
- width: envVar$1.length
340
- };
341
- } else if (term.type === "commandLine") {
342
- const cmd = useQuotes ? `\`${term.commandLine}\`` : term.commandLine;
343
- yield {
344
- text: useColors ? `\x1b[36m${cmd}${resetSequence}` : cmd,
345
- width: cmd.length
346
- };
347
- } else if (term.type === "lineBreak") yield {
348
- text: "\n",
349
- width: -1
350
- };
351
- else if (term.type === "url") {
352
- const urlString = term.url.href;
353
- const displayText = useQuotes ? `<${urlString}>` : urlString;
354
- if (useColors) {
355
- const hyperlink = `\x1b]8;;${urlString}\x1b\\${displayText}\x1b]8;;\x1b\\${resetSuffix}`;
341
+ else if (term.type === "envVar") {
342
+ const envVar$1 = useQuotes ? `\`${term.envVar}\`` : term.envVar;
343
+ yield {
344
+ text: useColors ? `\x1b[1;4m${envVar$1}${resetSequence}` : envVar$1,
345
+ width: envVar$1.length
346
+ };
347
+ } else if (term.type === "commandLine") {
348
+ const cmd = useQuotes ? `\`${term.commandLine}\`` : term.commandLine;
349
+ yield {
350
+ text: useColors ? `\x1b[36m${cmd}${resetSequence}` : cmd,
351
+ width: cmd.length
352
+ };
353
+ } else if (term.type === "lineBreak") {
356
354
  yield {
357
- text: hyperlink,
355
+ text: "\n",
356
+ width: -1
357
+ };
358
+ prevWasLineBreak = true;
359
+ } else if (term.type === "url") {
360
+ const urlString = term.url.href;
361
+ const displayText = useQuotes ? `<${urlString}>` : urlString;
362
+ if (useColors) {
363
+ const hyperlink = `\x1b]8;;${urlString}\x1b\\${displayText}\x1b]8;;\x1b\\${resetSuffix}`;
364
+ yield {
365
+ text: hyperlink,
366
+ width: displayText.length
367
+ };
368
+ } else yield {
369
+ text: displayText,
358
370
  width: displayText.length
359
371
  };
360
- } else yield {
361
- text: displayText,
362
- width: displayText.length
363
- };
364
- } else throw new TypeError(`Invalid MessageTerm type: ${term["type"]}.`);
372
+ } else throw new TypeError(`Invalid MessageTerm type: ${term["type"]}.`);
373
+ }
365
374
  }
366
375
  let output = "";
367
376
  let totalWidth = 0;
package/dist/message.js CHANGED
@@ -252,115 +252,124 @@ function formatMessage(msg, options = {}) {
252
252
  const resetSequence = `\x1b[0m${resetSuffix}`;
253
253
  function* stream() {
254
254
  const wordPattern = /\s*\S+\s*/g;
255
- for (const term of msg) if (term.type === "text") if (term.text.includes("\n\n")) {
256
- const paragraphs = term.text.split(/\n\n+/);
257
- for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
258
- if (paragraphIndex > 0) yield {
259
- text: "\n",
260
- width: -1
261
- };
262
- const paragraph = paragraphs[paragraphIndex].replace(/\n/g, " ");
263
- wordPattern.lastIndex = 0;
264
- while (true) {
265
- const match = wordPattern.exec(paragraph);
266
- if (match == null) break;
267
- yield {
268
- text: match[0],
269
- width: match[0].length
255
+ let prevWasLineBreak = false;
256
+ for (const term of msg) {
257
+ const isAfterLineBreak = prevWasLineBreak;
258
+ prevWasLineBreak = false;
259
+ if (term.type === "text") {
260
+ const rawText = isAfterLineBreak ? term.text.replace(/^\n(?!\n)/, "") : term.text;
261
+ if (rawText.includes("\n\n")) {
262
+ const paragraphs = rawText.split(/\n\n+/);
263
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
264
+ if (paragraphIndex > 0) yield {
265
+ text: "\n",
266
+ width: -1
267
+ };
268
+ const paragraph = paragraphs[paragraphIndex].replace(/\n/g, " ");
269
+ wordPattern.lastIndex = 0;
270
+ while (true) {
271
+ const match = wordPattern.exec(paragraph);
272
+ if (match == null) break;
273
+ yield {
274
+ text: match[0],
275
+ width: match[0].length
276
+ };
277
+ }
278
+ }
279
+ } else {
280
+ const normalizedText = rawText.replace(/\n/g, " ");
281
+ if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
282
+ text: " ",
283
+ width: 1
270
284
  };
285
+ else {
286
+ wordPattern.lastIndex = 0;
287
+ while (true) {
288
+ const match = wordPattern.exec(normalizedText);
289
+ if (match == null) break;
290
+ yield {
291
+ text: match[0],
292
+ width: match[0].length
293
+ };
294
+ }
295
+ }
271
296
  }
272
- }
273
- } else {
274
- const normalizedText = term.text.replace(/\n/g, " ");
275
- if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
276
- text: " ",
277
- width: 1
278
- };
279
- else {
280
- wordPattern.lastIndex = 0;
281
- while (true) {
282
- const match = wordPattern.exec(normalizedText);
283
- if (match == null) break;
297
+ } else if (term.type === "optionName") {
298
+ const name = useQuotes ? `\`${term.optionName}\`` : term.optionName;
299
+ yield {
300
+ text: useColors ? `\x1b[3m${name}${resetSequence}` : name,
301
+ width: name.length
302
+ };
303
+ } else if (term.type === "optionNames") {
304
+ const names = term.optionNames.map((name) => useQuotes ? `\`${name}\`` : name);
305
+ let i = 0;
306
+ for (const name of names) {
307
+ if (i > 0) yield {
308
+ text: "/",
309
+ width: 1
310
+ };
284
311
  yield {
285
- text: match[0],
286
- width: match[0].length
312
+ text: useColors ? `\x1b[3m${name}${resetSequence}` : name,
313
+ width: name.length
287
314
  };
315
+ i++;
288
316
  }
289
- }
290
- }
291
- else if (term.type === "optionName") {
292
- const name = useQuotes ? `\`${term.optionName}\`` : term.optionName;
293
- yield {
294
- text: useColors ? `\x1b[3m${name}${resetSequence}` : name,
295
- width: name.length
296
- };
297
- } else if (term.type === "optionNames") {
298
- const names = term.optionNames.map((name) => useQuotes ? `\`${name}\`` : name);
299
- let i = 0;
300
- for (const name of names) {
317
+ } else if (term.type === "metavar") {
318
+ const metavar$1 = useQuotes ? `\`${term.metavar}\`` : term.metavar;
319
+ yield {
320
+ text: useColors ? `\x1b[1m${metavar$1}${resetSequence}` : metavar$1,
321
+ width: metavar$1.length
322
+ };
323
+ } else if (term.type === "value") {
324
+ const value$1 = useQuotes ? `${JSON.stringify(term.value)}` : term.value;
325
+ yield {
326
+ text: useColors ? `\x1b[32m${value$1}${resetSequence}` : value$1,
327
+ width: value$1.length
328
+ };
329
+ } else if (term.type === "values") for (let i = 0; i < term.values.length; i++) {
301
330
  if (i > 0) yield {
302
- text: "/",
331
+ text: " ",
303
332
  width: 1
304
333
  };
334
+ const value$1 = useQuotes ? JSON.stringify(term.values[i]) : term.values[i];
305
335
  yield {
306
- text: useColors ? `\x1b[3m${name}${resetSequence}` : name,
307
- width: name.length
336
+ text: useColors ? i <= 0 ? `\x1b[32m${value$1}` : i + 1 >= term.values.length ? `${value$1}${resetSequence}` : value$1 : value$1,
337
+ width: value$1.length
308
338
  };
309
- i++;
310
339
  }
311
- } else if (term.type === "metavar") {
312
- const metavar$1 = useQuotes ? `\`${term.metavar}\`` : term.metavar;
313
- yield {
314
- text: useColors ? `\x1b[1m${metavar$1}${resetSequence}` : metavar$1,
315
- width: metavar$1.length
316
- };
317
- } else if (term.type === "value") {
318
- const value$1 = useQuotes ? `${JSON.stringify(term.value)}` : term.value;
319
- yield {
320
- text: useColors ? `\x1b[32m${value$1}${resetSequence}` : value$1,
321
- width: value$1.length
322
- };
323
- } else if (term.type === "values") for (let i = 0; i < term.values.length; i++) {
324
- if (i > 0) yield {
325
- text: " ",
326
- width: 1
327
- };
328
- const value$1 = useQuotes ? JSON.stringify(term.values[i]) : term.values[i];
329
- yield {
330
- text: useColors ? i <= 0 ? `\x1b[32m${value$1}` : i + 1 >= term.values.length ? `${value$1}${resetSequence}` : value$1 : value$1,
331
- width: value$1.length
332
- };
333
- }
334
- else if (term.type === "envVar") {
335
- const envVar$1 = useQuotes ? `\`${term.envVar}\`` : term.envVar;
336
- yield {
337
- text: useColors ? `\x1b[1;4m${envVar$1}${resetSequence}` : envVar$1,
338
- width: envVar$1.length
339
- };
340
- } else if (term.type === "commandLine") {
341
- const cmd = useQuotes ? `\`${term.commandLine}\`` : term.commandLine;
342
- yield {
343
- text: useColors ? `\x1b[36m${cmd}${resetSequence}` : cmd,
344
- width: cmd.length
345
- };
346
- } else if (term.type === "lineBreak") yield {
347
- text: "\n",
348
- width: -1
349
- };
350
- else if (term.type === "url") {
351
- const urlString = term.url.href;
352
- const displayText = useQuotes ? `<${urlString}>` : urlString;
353
- if (useColors) {
354
- const hyperlink = `\x1b]8;;${urlString}\x1b\\${displayText}\x1b]8;;\x1b\\${resetSuffix}`;
340
+ else if (term.type === "envVar") {
341
+ const envVar$1 = useQuotes ? `\`${term.envVar}\`` : term.envVar;
342
+ yield {
343
+ text: useColors ? `\x1b[1;4m${envVar$1}${resetSequence}` : envVar$1,
344
+ width: envVar$1.length
345
+ };
346
+ } else if (term.type === "commandLine") {
347
+ const cmd = useQuotes ? `\`${term.commandLine}\`` : term.commandLine;
348
+ yield {
349
+ text: useColors ? `\x1b[36m${cmd}${resetSequence}` : cmd,
350
+ width: cmd.length
351
+ };
352
+ } else if (term.type === "lineBreak") {
355
353
  yield {
356
- text: hyperlink,
354
+ text: "\n",
355
+ width: -1
356
+ };
357
+ prevWasLineBreak = true;
358
+ } else if (term.type === "url") {
359
+ const urlString = term.url.href;
360
+ const displayText = useQuotes ? `<${urlString}>` : urlString;
361
+ if (useColors) {
362
+ const hyperlink = `\x1b]8;;${urlString}\x1b\\${displayText}\x1b]8;;\x1b\\${resetSuffix}`;
363
+ yield {
364
+ text: hyperlink,
365
+ width: displayText.length
366
+ };
367
+ } else yield {
368
+ text: displayText,
357
369
  width: displayText.length
358
370
  };
359
- } else yield {
360
- text: displayText,
361
- width: displayText.length
362
- };
363
- } else throw new TypeError(`Invalid MessageTerm type: ${term["type"]}.`);
371
+ } else throw new TypeError(`Invalid MessageTerm type: ${term["type"]}.`);
372
+ }
364
373
  }
365
374
  let output = "";
366
375
  let totalWidth = 0;
@@ -2,6 +2,7 @@ const require_message = require('./message.cjs');
2
2
  const require_dependency = require('./dependency.cjs');
3
3
  const require_usage = require('./usage.cjs');
4
4
  const require_suggestion = require('./suggestion.cjs');
5
+ const require_usage_internals = require('./usage-internals.cjs');
5
6
  const require_valueparser = require('./valueparser.cjs');
6
7
 
7
8
  //#region src/primitives.ts
@@ -881,11 +882,10 @@ function command(name, parser, options = {}) {
881
882
  if (context.state === void 0) {
882
883
  if (context.buffer.length < 1 || context.buffer[0] !== name) {
883
884
  const actual = context.buffer.length > 0 ? context.buffer[0] : null;
885
+ const leadingCmds = require_usage_internals.extractLeadingCommandNames(context.usage);
886
+ const suggestions = actual ? require_suggestion.findSimilar(actual, leadingCmds, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
884
887
  if (options.errors?.notMatched) {
885
888
  const errorMessage = options.errors.notMatched;
886
- const candidates = /* @__PURE__ */ new Set();
887
- for (const cmdName of require_usage.extractCommandNames(context.usage)) candidates.add(cmdName);
888
- const suggestions = actual ? require_suggestion.findSimilar(actual, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
889
889
  return {
890
890
  success: false,
891
891
  consumed: 0,
@@ -898,10 +898,15 @@ function command(name, parser, options = {}) {
898
898
  error: require_message.message`Expected command ${require_message.optionName(name)}, but got end of input.`
899
899
  };
900
900
  const baseError = require_message.message`Expected command ${require_message.optionName(name)}, but got ${actual}.`;
901
+ const suggestionMsg = require_suggestion.createSuggestionMessage(suggestions);
901
902
  return {
902
903
  success: false,
903
904
  consumed: 0,
904
- error: require_suggestion.createErrorWithSuggestions(baseError, actual, context.usage, "command")
905
+ error: suggestionMsg.length > 0 ? [
906
+ ...baseError,
907
+ require_message.text("\n\n"),
908
+ ...suggestionMsg
909
+ ] : baseError
905
910
  };
906
911
  }
907
912
  return {
@@ -1,7 +1,8 @@
1
- import { message, metavar, optionName, optionNames, valueSet } from "./message.js";
1
+ import { message, metavar, optionName, optionNames, text, valueSet } from "./message.js";
2
2
  import { createDeferredParseState, createDependencySourceState, createPendingDependencySourceState, dependencyId, getDefaultValuesFunction, getDependencyIds, isDeferredParseState, isDependencySource, isDependencySourceState, isDerivedValueParser, isPendingDependencySourceState, suggestWithDependency } from "./dependency.js";
3
- import { extractCommandNames, extractOptionNames } from "./usage.js";
4
- import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, findSimilar } from "./suggestion.js";
3
+ import { extractOptionNames } from "./usage.js";
4
+ import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, findSimilar } from "./suggestion.js";
5
+ import { extractLeadingCommandNames } from "./usage-internals.js";
5
6
  import { isValueParser } from "./valueparser.js";
6
7
 
7
8
  //#region src/primitives.ts
@@ -881,11 +882,10 @@ function command(name, parser, options = {}) {
881
882
  if (context.state === void 0) {
882
883
  if (context.buffer.length < 1 || context.buffer[0] !== name) {
883
884
  const actual = context.buffer.length > 0 ? context.buffer[0] : null;
885
+ const leadingCmds = extractLeadingCommandNames(context.usage);
886
+ const suggestions = actual ? findSimilar(actual, leadingCmds, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
884
887
  if (options.errors?.notMatched) {
885
888
  const errorMessage = options.errors.notMatched;
886
- const candidates = /* @__PURE__ */ new Set();
887
- for (const cmdName of extractCommandNames(context.usage)) candidates.add(cmdName);
888
- const suggestions = actual ? findSimilar(actual, candidates, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
889
889
  return {
890
890
  success: false,
891
891
  consumed: 0,
@@ -898,10 +898,15 @@ function command(name, parser, options = {}) {
898
898
  error: message`Expected command ${optionName(name)}, but got end of input.`
899
899
  };
900
900
  const baseError = message`Expected command ${optionName(name)}, but got ${actual}.`;
901
+ const suggestionMsg = createSuggestionMessage(suggestions);
901
902
  return {
902
903
  success: false,
903
904
  consumed: 0,
904
- error: createErrorWithSuggestions(baseError, actual, context.usage, "command")
905
+ error: suggestionMsg.length > 0 ? [
906
+ ...baseError,
907
+ text("\n\n"),
908
+ ...suggestionMsg
909
+ ] : baseError
905
910
  };
906
911
  }
907
912
  return {
@@ -0,0 +1,73 @@
1
+
2
+ //#region src/usage-internals.ts
3
+ /**
4
+ * Collects option names and command names that are valid as the *immediate*
5
+ * next token at the current parse position ("leading candidates").
6
+ *
7
+ * Unlike the full-tree extractors in `usage.ts`, this function stops
8
+ * descending into a branch as soon as it hits a required (blocking) term —
9
+ * an option, a command, or a required argument. Optional and zero-or-more
10
+ * terms are traversed but do not block.
11
+ *
12
+ * @param terms The usage terms to inspect.
13
+ * @param optionNames Accumulator for leading option names.
14
+ * @param commandNames Accumulator for leading command names.
15
+ * @returns `true` if every term in `terms` is skippable (i.e., the caller
16
+ * may continue scanning the next sibling term), `false` otherwise.
17
+ */
18
+ function collectLeadingCandidates(terms, optionNames, commandNames) {
19
+ if (!terms || !Array.isArray(terms)) return true;
20
+ for (const term of terms) {
21
+ if (term.type === "option") {
22
+ for (const name of term.names) optionNames.add(name);
23
+ return false;
24
+ }
25
+ if (term.type === "command") {
26
+ commandNames.add(term.name);
27
+ return false;
28
+ }
29
+ if (term.type === "argument") return false;
30
+ if (term.type === "optional") {
31
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
32
+ continue;
33
+ }
34
+ if (term.type === "multiple") {
35
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
36
+ if (term.min === 0) continue;
37
+ return false;
38
+ }
39
+ if (term.type === "exclusive") {
40
+ let allSkippable = true;
41
+ for (const branch of term.terms) {
42
+ const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames);
43
+ allSkippable = allSkippable && branchSkippable;
44
+ }
45
+ if (allSkippable) continue;
46
+ return false;
47
+ }
48
+ }
49
+ return true;
50
+ }
51
+ /**
52
+ * Returns the set of command names that are valid as the *immediate* next
53
+ * token, derived from the leading candidates of `usage`.
54
+ *
55
+ * This is the command-only projection of {@link collectLeadingCandidates}
56
+ * and is used to generate accurate "Did you mean?" suggestions in
57
+ * `command()` error messages — suggestions are scoped to commands actually
58
+ * reachable at the current parse position rather than all commands anywhere
59
+ * in the usage tree.
60
+ *
61
+ * @param usage The usage tree to inspect.
62
+ * @returns A `Set` of command names valid as the next input token.
63
+ */
64
+ function extractLeadingCommandNames(usage) {
65
+ const options = /* @__PURE__ */ new Set();
66
+ const commands = /* @__PURE__ */ new Set();
67
+ collectLeadingCandidates(usage, options, commands);
68
+ return commands;
69
+ }
70
+
71
+ //#endregion
72
+ exports.collectLeadingCandidates = collectLeadingCandidates;
73
+ exports.extractLeadingCommandNames = extractLeadingCommandNames;
@@ -0,0 +1,71 @@
1
+ //#region src/usage-internals.ts
2
+ /**
3
+ * Collects option names and command names that are valid as the *immediate*
4
+ * next token at the current parse position ("leading candidates").
5
+ *
6
+ * Unlike the full-tree extractors in `usage.ts`, this function stops
7
+ * descending into a branch as soon as it hits a required (blocking) term —
8
+ * an option, a command, or a required argument. Optional and zero-or-more
9
+ * terms are traversed but do not block.
10
+ *
11
+ * @param terms The usage terms to inspect.
12
+ * @param optionNames Accumulator for leading option names.
13
+ * @param commandNames Accumulator for leading command names.
14
+ * @returns `true` if every term in `terms` is skippable (i.e., the caller
15
+ * may continue scanning the next sibling term), `false` otherwise.
16
+ */
17
+ function collectLeadingCandidates(terms, optionNames, commandNames) {
18
+ if (!terms || !Array.isArray(terms)) return true;
19
+ for (const term of terms) {
20
+ if (term.type === "option") {
21
+ for (const name of term.names) optionNames.add(name);
22
+ return false;
23
+ }
24
+ if (term.type === "command") {
25
+ commandNames.add(term.name);
26
+ return false;
27
+ }
28
+ if (term.type === "argument") return false;
29
+ if (term.type === "optional") {
30
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
31
+ continue;
32
+ }
33
+ if (term.type === "multiple") {
34
+ collectLeadingCandidates(term.terms, optionNames, commandNames);
35
+ if (term.min === 0) continue;
36
+ return false;
37
+ }
38
+ if (term.type === "exclusive") {
39
+ let allSkippable = true;
40
+ for (const branch of term.terms) {
41
+ const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames);
42
+ allSkippable = allSkippable && branchSkippable;
43
+ }
44
+ if (allSkippable) continue;
45
+ return false;
46
+ }
47
+ }
48
+ return true;
49
+ }
50
+ /**
51
+ * Returns the set of command names that are valid as the *immediate* next
52
+ * token, derived from the leading candidates of `usage`.
53
+ *
54
+ * This is the command-only projection of {@link collectLeadingCandidates}
55
+ * and is used to generate accurate "Did you mean?" suggestions in
56
+ * `command()` error messages — suggestions are scoped to commands actually
57
+ * reachable at the current parse position rather than all commands anywhere
58
+ * in the usage tree.
59
+ *
60
+ * @param usage The usage tree to inspect.
61
+ * @returns A `Set` of command names valid as the next input token.
62
+ */
63
+ function extractLeadingCommandNames(usage) {
64
+ const options = /* @__PURE__ */ new Set();
65
+ const commands = /* @__PURE__ */ new Set();
66
+ collectLeadingCandidates(usage, options, commands);
67
+ return commands;
68
+ }
69
+
70
+ //#endregion
71
+ export { collectLeadingCandidates, extractLeadingCommandNames };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "0.10.2",
3
+ "version": "0.10.4-dev.408+296f11c0",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",