@optique/core 0.9.7 → 0.9.9
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);
|
|
@@ -1300,6 +1263,9 @@ function merge(...args) {
|
|
|
1300
1263
|
}();
|
|
1301
1264
|
},
|
|
1302
1265
|
getDocFragments(state, _defaultValue) {
|
|
1266
|
+
let brief;
|
|
1267
|
+
let description;
|
|
1268
|
+
let footer;
|
|
1303
1269
|
const fragments = parsers.flatMap((p, i) => {
|
|
1304
1270
|
let parserState;
|
|
1305
1271
|
if (p.initialState === void 0) {
|
|
@@ -1313,7 +1279,11 @@ function merge(...args) {
|
|
|
1313
1279
|
kind: "available",
|
|
1314
1280
|
state: state.state
|
|
1315
1281
|
};
|
|
1316
|
-
|
|
1282
|
+
const docFragments = p.getDocFragments(parserState, void 0);
|
|
1283
|
+
brief ??= docFragments.brief;
|
|
1284
|
+
description ??= docFragments.description;
|
|
1285
|
+
footer ??= docFragments.footer;
|
|
1286
|
+
return docFragments.fragments;
|
|
1317
1287
|
});
|
|
1318
1288
|
const entries = fragments.filter((f) => f.type === "entry");
|
|
1319
1289
|
const sections = [];
|
|
@@ -1328,18 +1298,28 @@ function merge(...args) {
|
|
|
1328
1298
|
entries
|
|
1329
1299
|
};
|
|
1330
1300
|
sections.push(labeledSection);
|
|
1331
|
-
return {
|
|
1301
|
+
return {
|
|
1302
|
+
brief,
|
|
1303
|
+
description,
|
|
1304
|
+
footer,
|
|
1305
|
+
fragments: sections.map((s) => ({
|
|
1306
|
+
...s,
|
|
1307
|
+
type: "section"
|
|
1308
|
+
}))
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
brief,
|
|
1313
|
+
description,
|
|
1314
|
+
footer,
|
|
1315
|
+
fragments: [...sections.map((s) => ({
|
|
1332
1316
|
...s,
|
|
1333
1317
|
type: "section"
|
|
1334
|
-
}))
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
})), {
|
|
1340
|
-
type: "section",
|
|
1341
|
-
entries
|
|
1342
|
-
}] };
|
|
1318
|
+
})), {
|
|
1319
|
+
type: "section",
|
|
1320
|
+
entries
|
|
1321
|
+
}]
|
|
1322
|
+
};
|
|
1343
1323
|
}
|
|
1344
1324
|
};
|
|
1345
1325
|
}
|
|
@@ -1630,18 +1610,32 @@ function group(label, parser) {
|
|
|
1630
1610
|
complete: (state) => parser.complete(state),
|
|
1631
1611
|
suggest: (context, prefix) => parser.suggest(context, prefix),
|
|
1632
1612
|
getDocFragments: (state, defaultValue) => {
|
|
1633
|
-
const { description, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1613
|
+
const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1634
1614
|
const allEntries = [];
|
|
1635
1615
|
const titledSections = [];
|
|
1636
1616
|
for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
|
|
1637
1617
|
else if (fragment.type === "section") if (fragment.title) titledSections.push(fragment);
|
|
1638
1618
|
else allEntries.push(...fragment.entries);
|
|
1639
|
-
const
|
|
1619
|
+
const initialFragments = parser.getDocFragments({
|
|
1620
|
+
kind: "available",
|
|
1621
|
+
state: parser.initialState
|
|
1622
|
+
}, void 0);
|
|
1623
|
+
const initialCommandNames = /* @__PURE__ */ new Set();
|
|
1624
|
+
for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
|
|
1625
|
+
else if (f.type === "section") {
|
|
1626
|
+
for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
|
|
1627
|
+
}
|
|
1628
|
+
const initialHasCommands = initialCommandNames.size > 0;
|
|
1629
|
+
const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
|
|
1630
|
+
const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
|
|
1631
|
+
const labeledSection = applyLabel ? {
|
|
1640
1632
|
title: label,
|
|
1641
1633
|
entries: allEntries
|
|
1642
|
-
};
|
|
1634
|
+
} : { entries: allEntries };
|
|
1643
1635
|
return {
|
|
1636
|
+
brief,
|
|
1644
1637
|
description,
|
|
1638
|
+
footer,
|
|
1645
1639
|
fragments: [...titledSections.map((s) => ({
|
|
1646
1640
|
...s,
|
|
1647
1641
|
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();
|
|
@@ -1300,6 +1263,9 @@ function merge(...args) {
|
|
|
1300
1263
|
}();
|
|
1301
1264
|
},
|
|
1302
1265
|
getDocFragments(state, _defaultValue) {
|
|
1266
|
+
let brief;
|
|
1267
|
+
let description;
|
|
1268
|
+
let footer;
|
|
1303
1269
|
const fragments = parsers.flatMap((p, i) => {
|
|
1304
1270
|
let parserState;
|
|
1305
1271
|
if (p.initialState === void 0) {
|
|
@@ -1313,7 +1279,11 @@ function merge(...args) {
|
|
|
1313
1279
|
kind: "available",
|
|
1314
1280
|
state: state.state
|
|
1315
1281
|
};
|
|
1316
|
-
|
|
1282
|
+
const docFragments = p.getDocFragments(parserState, void 0);
|
|
1283
|
+
brief ??= docFragments.brief;
|
|
1284
|
+
description ??= docFragments.description;
|
|
1285
|
+
footer ??= docFragments.footer;
|
|
1286
|
+
return docFragments.fragments;
|
|
1317
1287
|
});
|
|
1318
1288
|
const entries = fragments.filter((f) => f.type === "entry");
|
|
1319
1289
|
const sections = [];
|
|
@@ -1328,18 +1298,28 @@ function merge(...args) {
|
|
|
1328
1298
|
entries
|
|
1329
1299
|
};
|
|
1330
1300
|
sections.push(labeledSection);
|
|
1331
|
-
return {
|
|
1301
|
+
return {
|
|
1302
|
+
brief,
|
|
1303
|
+
description,
|
|
1304
|
+
footer,
|
|
1305
|
+
fragments: sections.map((s) => ({
|
|
1306
|
+
...s,
|
|
1307
|
+
type: "section"
|
|
1308
|
+
}))
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
brief,
|
|
1313
|
+
description,
|
|
1314
|
+
footer,
|
|
1315
|
+
fragments: [...sections.map((s) => ({
|
|
1332
1316
|
...s,
|
|
1333
1317
|
type: "section"
|
|
1334
|
-
}))
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
})), {
|
|
1340
|
-
type: "section",
|
|
1341
|
-
entries
|
|
1342
|
-
}] };
|
|
1318
|
+
})), {
|
|
1319
|
+
type: "section",
|
|
1320
|
+
entries
|
|
1321
|
+
}]
|
|
1322
|
+
};
|
|
1343
1323
|
}
|
|
1344
1324
|
};
|
|
1345
1325
|
}
|
|
@@ -1630,18 +1610,32 @@ function group(label, parser) {
|
|
|
1630
1610
|
complete: (state) => parser.complete(state),
|
|
1631
1611
|
suggest: (context, prefix) => parser.suggest(context, prefix),
|
|
1632
1612
|
getDocFragments: (state, defaultValue) => {
|
|
1633
|
-
const { description, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1613
|
+
const { brief, description, footer, fragments } = parser.getDocFragments(state, defaultValue);
|
|
1634
1614
|
const allEntries = [];
|
|
1635
1615
|
const titledSections = [];
|
|
1636
1616
|
for (const fragment of fragments) if (fragment.type === "entry") allEntries.push(fragment);
|
|
1637
1617
|
else if (fragment.type === "section") if (fragment.title) titledSections.push(fragment);
|
|
1638
1618
|
else allEntries.push(...fragment.entries);
|
|
1639
|
-
const
|
|
1619
|
+
const initialFragments = parser.getDocFragments({
|
|
1620
|
+
kind: "available",
|
|
1621
|
+
state: parser.initialState
|
|
1622
|
+
}, void 0);
|
|
1623
|
+
const initialCommandNames = /* @__PURE__ */ new Set();
|
|
1624
|
+
for (const f of initialFragments.fragments) if (f.type === "entry" && f.term.type === "command") initialCommandNames.add(f.term.name);
|
|
1625
|
+
else if (f.type === "section") {
|
|
1626
|
+
for (const e of f.entries) if (e.term.type === "command") initialCommandNames.add(e.term.name);
|
|
1627
|
+
}
|
|
1628
|
+
const initialHasCommands = initialCommandNames.size > 0;
|
|
1629
|
+
const currentCommandsAreGroupOwn = allEntries.some((e) => e.term.type === "command" && initialCommandNames.has(e.term.name));
|
|
1630
|
+
const applyLabel = !initialHasCommands || currentCommandsAreGroupOwn;
|
|
1631
|
+
const labeledSection = applyLabel ? {
|
|
1640
1632
|
title: label,
|
|
1641
1633
|
entries: allEntries
|
|
1642
|
-
};
|
|
1634
|
+
} : { entries: allEntries };
|
|
1643
1635
|
return {
|
|
1636
|
+
brief,
|
|
1644
1637
|
description,
|
|
1638
|
+
footer,
|
|
1645
1639
|
fragments: [...titledSections.map((s) => ({
|
|
1646
1640
|
...s,
|
|
1647
1641
|
type: "section"
|
package/dist/facade.cjs
CHANGED
|
@@ -577,8 +577,8 @@ function runParser(parser, programName, args, options = {}) {
|
|
|
577
577
|
const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
|
|
578
578
|
const augmentedDoc = {
|
|
579
579
|
...doc,
|
|
580
|
-
brief: shouldOverride ? brief ?? doc.brief : doc.brief
|
|
581
|
-
description: shouldOverride ? description ?? doc.description : doc.description
|
|
580
|
+
brief: shouldOverride ? brief ?? doc.brief : doc.brief,
|
|
581
|
+
description: shouldOverride ? description ?? doc.description : doc.description,
|
|
582
582
|
footer: shouldOverride ? footer ?? doc.footer : doc.footer ?? footer
|
|
583
583
|
};
|
|
584
584
|
stdout(require_doc.formatDocPage(programName, augmentedDoc, {
|
package/dist/facade.js
CHANGED
|
@@ -577,8 +577,8 @@ function runParser(parser, programName, args, options = {}) {
|
|
|
577
577
|
const shouldOverride = !isMetaCommandHelp && !isSubcommandHelp;
|
|
578
578
|
const augmentedDoc = {
|
|
579
579
|
...doc,
|
|
580
|
-
brief: shouldOverride ? brief ?? doc.brief : doc.brief
|
|
581
|
-
description: shouldOverride ? description ?? doc.description : doc.description
|
|
580
|
+
brief: shouldOverride ? brief ?? doc.brief : doc.brief,
|
|
581
|
+
description: shouldOverride ? description ?? doc.description : doc.description,
|
|
582
582
|
footer: shouldOverride ? footer ?? doc.footer : doc.footer ?? footer
|
|
583
583
|
};
|
|
584
584
|
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
|
|
@@ -752,11 +753,10 @@ function command(name, parser, options = {}) {
|
|
|
752
753
|
if (context.state === void 0) {
|
|
753
754
|
if (context.buffer.length < 1 || context.buffer[0] !== name) {
|
|
754
755
|
const actual = context.buffer.length > 0 ? context.buffer[0] : null;
|
|
756
|
+
const leadingCmds = require_usage_internals.extractLeadingCommandNames(context.usage);
|
|
757
|
+
const suggestions = actual ? require_suggestion.findSimilar(actual, leadingCmds, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
755
758
|
if (options.errors?.notMatched) {
|
|
756
759
|
const errorMessage = options.errors.notMatched;
|
|
757
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
758
|
-
for (const cmdName of require_usage.extractCommandNames(context.usage)) candidates.add(cmdName);
|
|
759
|
-
const suggestions = actual ? require_suggestion.findSimilar(actual, candidates, require_suggestion.DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
760
760
|
return {
|
|
761
761
|
success: false,
|
|
762
762
|
consumed: 0,
|
|
@@ -769,10 +769,15 @@ function command(name, parser, options = {}) {
|
|
|
769
769
|
error: require_message.message`Expected command ${require_message.optionName(name)}, but got end of input.`
|
|
770
770
|
};
|
|
771
771
|
const baseError = require_message.message`Expected command ${require_message.optionName(name)}, but got ${actual}.`;
|
|
772
|
+
const suggestionMsg = require_suggestion.createSuggestionMessage(suggestions);
|
|
772
773
|
return {
|
|
773
774
|
success: false,
|
|
774
775
|
consumed: 0,
|
|
775
|
-
error:
|
|
776
|
+
error: suggestionMsg.length > 0 ? [
|
|
777
|
+
...baseError,
|
|
778
|
+
require_message.text("\n\n"),
|
|
779
|
+
...suggestionMsg
|
|
780
|
+
] : baseError
|
|
776
781
|
};
|
|
777
782
|
}
|
|
778
783
|
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
|
|
@@ -752,11 +753,10 @@ function command(name, parser, options = {}) {
|
|
|
752
753
|
if (context.state === void 0) {
|
|
753
754
|
if (context.buffer.length < 1 || context.buffer[0] !== name) {
|
|
754
755
|
const actual = context.buffer.length > 0 ? context.buffer[0] : null;
|
|
756
|
+
const leadingCmds = extractLeadingCommandNames(context.usage);
|
|
757
|
+
const suggestions = actual ? findSimilar(actual, leadingCmds, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
755
758
|
if (options.errors?.notMatched) {
|
|
756
759
|
const errorMessage = options.errors.notMatched;
|
|
757
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
758
|
-
for (const cmdName of extractCommandNames(context.usage)) candidates.add(cmdName);
|
|
759
|
-
const suggestions = actual ? findSimilar(actual, candidates, DEFAULT_FIND_SIMILAR_OPTIONS) : [];
|
|
760
760
|
return {
|
|
761
761
|
success: false,
|
|
762
762
|
consumed: 0,
|
|
@@ -769,10 +769,15 @@ function command(name, parser, options = {}) {
|
|
|
769
769
|
error: message`Expected command ${optionName(name)}, but got end of input.`
|
|
770
770
|
};
|
|
771
771
|
const baseError = message`Expected command ${optionName(name)}, but got ${actual}.`;
|
|
772
|
+
const suggestionMsg = createSuggestionMessage(suggestions);
|
|
772
773
|
return {
|
|
773
774
|
success: false,
|
|
774
775
|
consumed: 0,
|
|
775
|
-
error:
|
|
776
|
+
error: suggestionMsg.length > 0 ? [
|
|
777
|
+
...baseError,
|
|
778
|
+
text("\n\n"),
|
|
779
|
+
...suggestionMsg
|
|
780
|
+
] : baseError
|
|
776
781
|
};
|
|
777
782
|
}
|
|
778
783
|
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 };
|