@optique/core 0.8.13 → 0.8.15
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.
- package/dist/constructs.cjs +47 -53
- package/dist/constructs.js +46 -52
- package/dist/facade.cjs +2 -2
- package/dist/facade.js +2 -2
- package/dist/primitives.cjs +9 -4
- package/dist/primitives.js +12 -7
- package/dist/usage-internals.cjs +73 -0
- package/dist/usage-internals.js +71 -0
- package/package.json +1 -1
package/dist/constructs.cjs
CHANGED
|
@@ -1,50 +1,13 @@
|
|
|
1
1
|
const require_message = require('./message.cjs');
|
|
2
2
|
const require_usage = require('./usage.cjs');
|
|
3
3
|
const require_suggestion = require('./suggestion.cjs');
|
|
4
|
+
const require_usage_internals = require('./usage-internals.cjs');
|
|
4
5
|
|
|
5
6
|
//#region src/constructs.ts
|
|
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
7
|
function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
|
|
45
8
|
const options = /* @__PURE__ */ new Set();
|
|
46
9
|
const commands = /* @__PURE__ */ new Set();
|
|
47
|
-
for (const parser of parsers) collectLeadingCandidates(parser.usage, options, commands);
|
|
10
|
+
for (const parser of parsers) require_usage_internals.collectLeadingCandidates(parser.usage, options, commands);
|
|
48
11
|
const candidates = new Set([...options, ...commands]);
|
|
49
12
|
const suggestions = require_suggestion.findSimilar(invalidInput, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS);
|
|
50
13
|
const suggestionMsg = customFormatter ? customFormatter(suggestions) : require_suggestion.createSuggestionMessage(suggestions);
|
|
@@ -838,6 +801,9 @@ function merge(...args) {
|
|
|
838
801
|
return require_suggestion.deduplicateSuggestions(suggestions);
|
|
839
802
|
},
|
|
840
803
|
getDocFragments(state, _defaultValue) {
|
|
804
|
+
let brief;
|
|
805
|
+
let description;
|
|
806
|
+
let footer;
|
|
841
807
|
const fragments = parsers.flatMap((p, i) => {
|
|
842
808
|
let parserState;
|
|
843
809
|
if (p.initialState === void 0) {
|
|
@@ -851,7 +817,11 @@ function merge(...args) {
|
|
|
851
817
|
kind: "available",
|
|
852
818
|
state: state.state
|
|
853
819
|
};
|
|
854
|
-
|
|
820
|
+
const docFragments = p.getDocFragments(parserState, void 0);
|
|
821
|
+
brief ??= docFragments.brief;
|
|
822
|
+
description ??= docFragments.description;
|
|
823
|
+
footer ??= docFragments.footer;
|
|
824
|
+
return docFragments.fragments;
|
|
855
825
|
});
|
|
856
826
|
const entries = fragments.filter((f) => f.type === "entry");
|
|
857
827
|
const sections = [];
|
|
@@ -866,18 +836,28 @@ function merge(...args) {
|
|
|
866
836
|
entries
|
|
867
837
|
};
|
|
868
838
|
sections.push(labeledSection);
|
|
869
|
-
return {
|
|
839
|
+
return {
|
|
840
|
+
brief,
|
|
841
|
+
description,
|
|
842
|
+
footer,
|
|
843
|
+
fragments: sections.map((s) => ({
|
|
844
|
+
...s,
|
|
845
|
+
type: "section"
|
|
846
|
+
}))
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
brief,
|
|
851
|
+
description,
|
|
852
|
+
footer,
|
|
853
|
+
fragments: [...sections.map((s) => ({
|
|
870
854
|
...s,
|
|
871
855
|
type: "section"
|
|
872
|
-
}))
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
})), {
|
|
878
|
-
type: "section",
|
|
879
|
-
entries
|
|
880
|
-
}] };
|
|
856
|
+
})), {
|
|
857
|
+
type: "section",
|
|
858
|
+
entries
|
|
859
|
+
}]
|
|
860
|
+
};
|
|
881
861
|
}
|
|
882
862
|
};
|
|
883
863
|
}
|
|
@@ -1056,18 +1036,32 @@ function group(label, parser) {
|
|
|
1056
1036
|
complete: (state) => parser.complete(state),
|
|
1057
1037
|
suggest: (context, prefix) => parser.suggest(context, prefix),
|
|
1058
1038
|
getDocFragments: (state, defaultValue) => {
|
|
1059
|
-
const { description, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1039
|
+
const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1060
1040
|
const allEntries = [];
|
|
1061
1041
|
const titledSections = [];
|
|
1062
1042
|
for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
|
|
1063
1043
|
else if (fragment.type === "section") if (fragment.title) titledSections.push(fragment);
|
|
1064
1044
|
else allEntries.push(...fragment.entries);
|
|
1065
|
-
const
|
|
1045
|
+
const initialFragments = parser.getDocFragments({
|
|
1046
|
+
kind: "available",
|
|
1047
|
+
state: parser.initialState
|
|
1048
|
+
}, void 0);
|
|
1049
|
+
const initialCommandNames = /* @__PURE__ */ new Set();
|
|
1050
|
+
for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
|
|
1051
|
+
else if (f.type === "section") {
|
|
1052
|
+
for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
|
|
1053
|
+
}
|
|
1054
|
+
const initialHasCommands = initialCommandNames.size > 0;
|
|
1055
|
+
const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
|
|
1056
|
+
const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
|
|
1057
|
+
const labeledSection = applyLabel ? {
|
|
1066
1058
|
title: label,
|
|
1067
1059
|
entries: allEntries
|
|
1068
|
-
};
|
|
1060
|
+
} : { entries: allEntries };
|
|
1069
1061
|
return {
|
|
1062
|
+
brief,
|
|
1070
1063
|
description,
|
|
1064
|
+
footer,
|
|
1071
1065
|
fragments: [...titledSections.map((s) => ({
|
|
1072
1066
|
...s,
|
|
1073
1067
|
type: "section"
|
package/dist/constructs.js
CHANGED
|
@@ -1,46 +1,9 @@
|
|
|
1
1
|
import { message, optionName, text, values } from "./message.js";
|
|
2
2
|
import { extractArgumentMetavars, extractCommandNames, extractOptionNames } from "./usage.js";
|
|
3
3
|
import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, deduplicateSuggestions, findSimilar } from "./suggestion.js";
|
|
4
|
+
import { collectLeadingCandidates } from "./usage-internals.js";
|
|
4
5
|
|
|
5
6
|
//#region src/constructs.ts
|
|
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
7
|
function createUnexpectedInputErrorWithScopedSuggestions(baseError, invalidInput, parsers, customFormatter) {
|
|
45
8
|
const options = /* @__PURE__ */ new Set();
|
|
46
9
|
const commands = /* @__PURE__ */ new Set();
|
|
@@ -838,6 +801,9 @@ function merge(...args) {
|
|
|
838
801
|
return deduplicateSuggestions(suggestions);
|
|
839
802
|
},
|
|
840
803
|
getDocFragments(state, _defaultValue) {
|
|
804
|
+
let brief;
|
|
805
|
+
let description;
|
|
806
|
+
let footer;
|
|
841
807
|
const fragments = parsers.flatMap((p, i) => {
|
|
842
808
|
let parserState;
|
|
843
809
|
if (p.initialState === void 0) {
|
|
@@ -851,7 +817,11 @@ function merge(...args) {
|
|
|
851
817
|
kind: "available",
|
|
852
818
|
state: state.state
|
|
853
819
|
};
|
|
854
|
-
|
|
820
|
+
const docFragments = p.getDocFragments(parserState, void 0);
|
|
821
|
+
brief ??= docFragments.brief;
|
|
822
|
+
description ??= docFragments.description;
|
|
823
|
+
footer ??= docFragments.footer;
|
|
824
|
+
return docFragments.fragments;
|
|
855
825
|
});
|
|
856
826
|
const entries = fragments.filter((f) => f.type === "entry");
|
|
857
827
|
const sections = [];
|
|
@@ -866,18 +836,28 @@ function merge(...args) {
|
|
|
866
836
|
entries
|
|
867
837
|
};
|
|
868
838
|
sections.push(labeledSection);
|
|
869
|
-
return {
|
|
839
|
+
return {
|
|
840
|
+
brief,
|
|
841
|
+
description,
|
|
842
|
+
footer,
|
|
843
|
+
fragments: sections.map((s) => ({
|
|
844
|
+
...s,
|
|
845
|
+
type: "section"
|
|
846
|
+
}))
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
brief,
|
|
851
|
+
description,
|
|
852
|
+
footer,
|
|
853
|
+
fragments: [...sections.map((s) => ({
|
|
870
854
|
...s,
|
|
871
855
|
type: "section"
|
|
872
|
-
}))
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
})), {
|
|
878
|
-
type: "section",
|
|
879
|
-
entries
|
|
880
|
-
}] };
|
|
856
|
+
})), {
|
|
857
|
+
type: "section",
|
|
858
|
+
entries
|
|
859
|
+
}]
|
|
860
|
+
};
|
|
881
861
|
}
|
|
882
862
|
};
|
|
883
863
|
}
|
|
@@ -1056,18 +1036,32 @@ function group(label, parser) {
|
|
|
1056
1036
|
complete: (state) => parser.complete(state),
|
|
1057
1037
|
suggest: (context, prefix) => parser.suggest(context, prefix),
|
|
1058
1038
|
getDocFragments: (state, defaultValue) => {
|
|
1059
|
-
const { description, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1039
|
+
const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1060
1040
|
const allEntries = [];
|
|
1061
1041
|
const titledSections = [];
|
|
1062
1042
|
for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
|
|
1063
1043
|
else if (fragment.type === "section") if (fragment.title) titledSections.push(fragment);
|
|
1064
1044
|
else allEntries.push(...fragment.entries);
|
|
1065
|
-
const
|
|
1045
|
+
const initialFragments = parser.getDocFragments({
|
|
1046
|
+
kind: "available",
|
|
1047
|
+
state: parser.initialState
|
|
1048
|
+
}, void 0);
|
|
1049
|
+
const initialCommandNames = /* @__PURE__ */ new Set();
|
|
1050
|
+
for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
|
|
1051
|
+
else if (f.type === "section") {
|
|
1052
|
+
for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
|
|
1053
|
+
}
|
|
1054
|
+
const initialHasCommands = initialCommandNames.size > 0;
|
|
1055
|
+
const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
|
|
1056
|
+
const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
|
|
1057
|
+
const labeledSection = applyLabel ? {
|
|
1066
1058
|
title: label,
|
|
1067
1059
|
entries: allEntries
|
|
1068
|
-
};
|
|
1060
|
+
} : { entries: allEntries };
|
|
1069
1061
|
return {
|
|
1062
|
+
brief,
|
|
1070
1063
|
description,
|
|
1064
|
+
footer,
|
|
1071
1065
|
fragments: [...titledSections.map((s) => ({
|
|
1072
1066
|
...s,
|
|
1073
1067
|
type: "section"
|
package/dist/facade.cjs
CHANGED
|
@@ -582,8 +582,8 @@ function run(parser, programName, args, options = {}) {
|
|
|
582
582
|
const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
|
|
583
583
|
const augmentedDoc = {
|
|
584
584
|
...doc,
|
|
585
|
-
brief: shouldOverride ? brief ?? doc.brief : doc.brief
|
|
586
|
-
description: shouldOverride ? description ?? doc.description : doc.description
|
|
585
|
+
brief: shouldOverride ? brief ?? doc.brief : doc.brief,
|
|
586
|
+
description: shouldOverride ? description ?? doc.description : doc.description,
|
|
587
587
|
footer: shouldOverride ? footer ?? doc.footer : doc.footer ?? footer
|
|
588
588
|
};
|
|
589
589
|
stdout(require_doc.formatDocPage(programName, augmentedDoc, {
|
package/dist/facade.js
CHANGED
|
@@ -582,8 +582,8 @@ function run(parser, programName, args, options = {}) {
|
|
|
582
582
|
const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
|
|
583
583
|
const augmentedDoc = {
|
|
584
584
|
...doc,
|
|
585
|
-
brief: shouldOverride ? brief ?? doc.brief : doc.brief
|
|
586
|
-
description: shouldOverride ? description ?? doc.description : doc.description
|
|
585
|
+
brief: shouldOverride ? brief ?? doc.brief : doc.brief,
|
|
586
|
+
description: shouldOverride ? description ?? doc.description : doc.description,
|
|
587
587
|
footer: shouldOverride ? footer ?? doc.footer : doc.footer ?? footer
|
|
588
588
|
};
|
|
589
589
|
stdout(formatDocPage(programName, augmentedDoc, {
|
package/dist/primitives.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const require_message = require('./message.cjs');
|
|
2
2
|
const require_usage = require('./usage.cjs');
|
|
3
3
|
const require_suggestion = require('./suggestion.cjs');
|
|
4
|
+
const require_usage_internals = require('./usage-internals.cjs');
|
|
4
5
|
const require_valueparser = require('./valueparser.cjs');
|
|
5
6
|
|
|
6
7
|
//#region src/primitives.ts
|
|
@@ -593,11 +594,10 @@ function command(name, parser, options = {}) {
|
|
|
593
594
|
if (context.state === void 0) {
|
|
594
595
|
if (context.buffer.length < 1 || context.buffer[0] !== name) {
|
|
595
596
|
const actual = context.buffer.length > 0 ? context.buffer[0] : null;
|
|
597
|
+
const leadingCmds = require_usage_internals.extractLeadingCommandNames(context.usage);
|
|
598
|
+
const suggestions = actual ? require_suggestion.findSimilar(actual, leadingCmds, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
596
599
|
if (options.errors?.notMatched) {
|
|
597
600
|
const errorMessage = options.errors.notMatched;
|
|
598
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
599
|
-
for (const cmdName of require_usage.extractCommandNames(context.usage)) candidates.add(cmdName);
|
|
600
|
-
const suggestions = actual ? require_suggestion.findSimilar(actual, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
601
601
|
return {
|
|
602
602
|
success: false,
|
|
603
603
|
consumed: 0,
|
|
@@ -610,10 +610,15 @@ function command(name, parser, options = {}) {
|
|
|
610
610
|
error: require_message.message`Expected command ${require_message.optionName(name)}, but got end of input.`
|
|
611
611
|
};
|
|
612
612
|
const baseError = require_message.message`Expected command ${require_message.optionName(name)}, but got ${actual}.`;
|
|
613
|
+
const suggestionMsg = require_suggestion.createSuggestionMessage(suggestions);
|
|
613
614
|
return {
|
|
614
615
|
success: false,
|
|
615
616
|
consumed: 0,
|
|
616
|
-
error:
|
|
617
|
+
error: suggestionMsg.length > 0 ? [
|
|
618
|
+
...baseError,
|
|
619
|
+
require_message.text("\n\n"),
|
|
620
|
+
...suggestionMsg
|
|
621
|
+
] : baseError
|
|
617
622
|
};
|
|
618
623
|
}
|
|
619
624
|
return {
|
package/dist/primitives.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { message, metavar, optionName, optionNames } from "./message.js";
|
|
2
|
-
import {
|
|
3
|
-
import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, findSimilar } from "./suggestion.js";
|
|
1
|
+
import { message, metavar, optionName, optionNames, text } from "./message.js";
|
|
2
|
+
import { extractOptionNames } from "./usage.js";
|
|
3
|
+
import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, findSimilar } from "./suggestion.js";
|
|
4
|
+
import { extractLeadingCommandNames } from "./usage-internals.js";
|
|
4
5
|
import { isValueParser } from "./valueparser.js";
|
|
5
6
|
|
|
6
7
|
//#region src/primitives.ts
|
|
@@ -593,11 +594,10 @@ function command(name, parser, options = {}) {
|
|
|
593
594
|
if (context.state === void 0) {
|
|
594
595
|
if (context.buffer.length < 1 || context.buffer[0] !== name) {
|
|
595
596
|
const actual = context.buffer.length > 0 ? context.buffer[0] : null;
|
|
597
|
+
const leadingCmds = extractLeadingCommandNames(context.usage);
|
|
598
|
+
const suggestions = actual ? findSimilar(actual, leadingCmds, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
596
599
|
if (options.errors?.notMatched) {
|
|
597
600
|
const errorMessage = options.errors.notMatched;
|
|
598
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
599
|
-
for (const cmdName of extractCommandNames(context.usage)) candidates.add(cmdName);
|
|
600
|
-
const suggestions = actual ? findSimilar(actual, candidates, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
601
601
|
return {
|
|
602
602
|
success: false,
|
|
603
603
|
consumed: 0,
|
|
@@ -610,10 +610,15 @@ function command(name, parser, options = {}) {
|
|
|
610
610
|
error: message`Expected command ${optionName(name)}, but got end of input.`
|
|
611
611
|
};
|
|
612
612
|
const baseError = message`Expected command ${optionName(name)}, but got ${actual}.`;
|
|
613
|
+
const suggestionMsg = createSuggestionMessage(suggestions);
|
|
613
614
|
return {
|
|
614
615
|
success: false,
|
|
615
616
|
consumed: 0,
|
|
616
|
-
error:
|
|
617
|
+
error: suggestionMsg.length > 0 ? [
|
|
618
|
+
...baseError,
|
|
619
|
+
text("\n\n"),
|
|
620
|
+
...suggestionMsg
|
|
621
|
+
] : baseError
|
|
617
622
|
};
|
|
618
623
|
}
|
|
619
624
|
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 };
|