@loomcli/core 0.4.0 → 0.6.0
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/LICENSE +21 -0
- package/dist/application.d.ts +60 -31
- package/dist/application.js +356 -149
- package/dist/bindings.d.ts +31 -0
- package/dist/bindings.js +64 -0
- package/dist/chain.d.ts +26 -12
- package/dist/chain.js +59 -92
- package/dist/command-rules.d.ts +55 -0
- package/dist/command-rules.js +142 -0
- package/dist/command.d.ts +212 -93
- package/dist/command.js +1224 -454
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +23 -0
- package/dist/defect.d.ts +18 -0
- package/dist/defect.js +272 -0
- package/dist/developer.d.ts +24 -0
- package/dist/developer.js +52 -0
- package/dist/diagnostic-text.d.ts +81 -0
- package/dist/diagnostic-text.js +283 -0
- package/dist/diagnostic.d.ts +11 -0
- package/dist/diagnostic.js +70 -0
- package/dist/errors.d.ts +115 -26
- package/dist/errors.js +333 -54
- package/dist/exit-codes.d.ts +45 -0
- package/dist/exit-codes.js +46 -0
- package/dist/extension.d.ts +44 -9
- package/dist/extension.js +147 -65
- package/dist/facts.d.ts +71 -11
- package/dist/facts.js +108 -25
- package/dist/globals.d.ts +63 -22
- package/dist/globals.js +164 -39
- package/dist/hints.d.ts +79 -0
- package/dist/hints.js +247 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +43 -1
- package/dist/identity.d.ts +19 -0
- package/dist/identity.js +72 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +6 -0
- package/dist/input-rules.d.ts +64 -0
- package/dist/input-rules.js +145 -0
- package/dist/inspect.d.ts +36 -5
- package/dist/inspect.js +125 -27
- package/dist/lanes.js +1 -1
- package/dist/locate.d.ts +41 -0
- package/dist/locate.js +121 -0
- package/dist/options.d.ts +96 -2
- package/dist/options.js +259 -71
- package/dist/output.d.ts +11 -2
- package/dist/output.js +23 -3
- package/dist/plain.d.ts +6 -0
- package/dist/plain.js +12 -0
- package/dist/plugin-rules.d.ts +62 -0
- package/dist/plugin-rules.js +155 -0
- package/dist/plugin.d.ts +121 -55
- package/dist/plugin.js +496 -126
- package/dist/prototypes.d.ts +7 -0
- package/dist/prototypes.js +29 -0
- package/dist/rendering.d.ts +6 -1
- package/dist/rendering.js +23 -5
- package/dist/rules.d.ts +51 -0
- package/dist/rules.js +115 -0
- package/dist/sequence.js +6 -1
- package/dist/sources.d.ts +58 -0
- package/dist/sources.js +258 -0
- package/dist/style-wire.js +1 -1
- package/dist/style.js +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.js +25 -5
- package/dist/thenable.d.ts +15 -0
- package/dist/thenable.js +29 -0
- package/dist/translators.d.ts +69 -0
- package/dist/translators.js +253 -0
- package/dist/types.d.ts +76 -21
- package/dist/validation.d.ts +65 -10
- package/dist/validation.js +314 -108
- package/dist/view.d.ts +49 -15
- package/dist/view.js +157 -79
- package/package.json +3 -2
package/dist/options.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputSite } from './facts.js';
|
|
1
2
|
import type { OptionConfig } from './types.js';
|
|
2
3
|
export interface OptionDeclaration {
|
|
3
4
|
name: string;
|
|
@@ -7,9 +8,13 @@ export interface OptionValues {
|
|
|
7
8
|
strings: Map<string, string>;
|
|
8
9
|
lists: Map<string, string[]>;
|
|
9
10
|
booleans: Map<string, boolean>;
|
|
11
|
+
/** The spelling of the token that supplied each parsed option, which no input source writes. */
|
|
12
|
+
spellings: Map<string, string>;
|
|
10
13
|
}
|
|
14
|
+
/** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
|
|
15
|
+
export declare function emptyValues(): OptionValues;
|
|
11
16
|
/** Which accepted form a table entry is. The table owns the convention, so readers never re-derive it. */
|
|
12
|
-
type SpellingRole = 'long' | 'negative' | 'short';
|
|
17
|
+
export type SpellingRole = 'long' | 'negative' | 'short';
|
|
13
18
|
type OptionForm = {
|
|
14
19
|
type: 'string';
|
|
15
20
|
name: string;
|
|
@@ -22,6 +27,22 @@ type OptionForm = {
|
|
|
22
27
|
type OptionSpelling = OptionForm & {
|
|
23
28
|
role: SpellingRole;
|
|
24
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* The part of one declaration that yields a spelling of the given role, which a spelling fault
|
|
32
|
+
* marks: the declared name for the long form, `short` for the short alias, and the `polarity` that
|
|
33
|
+
* generates a negative form.
|
|
34
|
+
*/
|
|
35
|
+
export declare function spellingMark(site: InputSite, role: SpellingRole): string;
|
|
36
|
+
/** The declared name answers the declared-name rule an argument's name answers. */
|
|
37
|
+
export declare function checkOptionName(name: unknown, site: InputSite): void;
|
|
38
|
+
/**
|
|
39
|
+
* The scope one table compiles: the phrase a repeated name names it by, and where each of its
|
|
40
|
+
* options was declared, which every fault's findings rebuild.
|
|
41
|
+
*/
|
|
42
|
+
export interface CompileScope<Declaration extends OptionDeclaration> {
|
|
43
|
+
readonly subject: string;
|
|
44
|
+
readonly siteOf: (declaration: Declaration) => InputSite;
|
|
45
|
+
}
|
|
25
46
|
/**
|
|
26
47
|
* One Boolean option's value for one invocation: the value the parser consumed, or the value its
|
|
27
48
|
* declared polarity gives an absent option. A negative-only option is absent as `true`, because its
|
|
@@ -29,14 +50,87 @@ type OptionSpelling = OptionForm & {
|
|
|
29
50
|
* declaration answer the same rule.
|
|
30
51
|
*/
|
|
31
52
|
export declare function booleanValue(values: OptionValues, name: string, config: OptionConfig): boolean;
|
|
32
|
-
|
|
53
|
+
/**
|
|
54
|
+
* One scope's options compiled into the spelling table the parser reads, with every rule one
|
|
55
|
+
* declaration answers alone and every rule two of them answer together.
|
|
56
|
+
*/
|
|
57
|
+
export declare function compileOptions<Declaration extends OptionDeclaration>(declarations: readonly Declaration[], scope: CompileScope<Declaration>): Map<string, OptionSpelling>;
|
|
58
|
+
/**
|
|
59
|
+
* Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
|
|
60
|
+
* separate value is never one. The parser and `locate` read each token through this rule.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isOptionToken(token: string): boolean;
|
|
63
|
+
/** A long option token and the inline value it carries after its first `=`, if any. */
|
|
64
|
+
export interface LongToken {
|
|
65
|
+
spelling: string;
|
|
66
|
+
inline: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A token that starts with `--` split at its first `=` into the spelling and the inline value, or
|
|
70
|
+
* `undefined` for any other token. The parser and `locate` split long tokens through this rule.
|
|
71
|
+
*/
|
|
72
|
+
export declare function longToken(token: string): LongToken | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The name of the string option one long spelling names in a table, which takes its value after
|
|
75
|
+
* `=`, or `undefined` for a Boolean, negative, short, or unknown spelling.
|
|
76
|
+
*/
|
|
77
|
+
export declare function longStringOption(spellings: ReadonlyMap<string, OptionSpelling>, spelling: string): string | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* A string option whose value the next token supplies, where the tokens ended first. A complete
|
|
80
|
+
* invocation reports it as a missing value; a partial one reads the next word as that value.
|
|
81
|
+
*/
|
|
82
|
+
export interface AwaitingValue {
|
|
83
|
+
name: string;
|
|
84
|
+
spelling: string;
|
|
85
|
+
}
|
|
86
|
+
/** One option name a token newly supplied, with the index of that token in the list read. */
|
|
87
|
+
export interface SuppliedOption {
|
|
88
|
+
name: string;
|
|
89
|
+
token: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The pre-scan's reading of a token list that may stop short. `positions` holds the index in
|
|
93
|
+
* `tokens` of each `rest` token, and `awaiting` is the global option the last token left without
|
|
94
|
+
* its value.
|
|
95
|
+
*/
|
|
96
|
+
export interface GlobalScan {
|
|
97
|
+
awaiting: AwaitingValue | undefined;
|
|
98
|
+
positions: number[];
|
|
99
|
+
rest: string[];
|
|
100
|
+
supplied: SuppliedOption[];
|
|
101
|
+
values: OptionValues;
|
|
102
|
+
}
|
|
33
103
|
/** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
|
|
104
|
+
export declare function scanGlobals(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): GlobalScan;
|
|
105
|
+
/** The pre-scan of a complete invocation, where a global still waiting for its value is a fault. */
|
|
34
106
|
export declare function extractGlobals(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
|
|
35
107
|
rest: string[];
|
|
36
108
|
values: OptionValues;
|
|
37
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* Whether one option holds a value a tier supplied: a token in any spelling it accepts, or a fill
|
|
112
|
+
* from an input source. A declared default is never in these maps, so it never counts.
|
|
113
|
+
*/
|
|
114
|
+
export declare function isSupplied(values: OptionValues, name: string): boolean;
|
|
115
|
+
/** One run's own copy of parsed values, which the input-source stage fills without touching argv's. */
|
|
116
|
+
export declare function copyValues(values: OptionValues): OptionValues;
|
|
38
117
|
/** Global and local keys never overlap, so one merged view feeds a single validation pass. */
|
|
39
118
|
export declare function mergeValues(globals: OptionValues, locals: OptionValues): OptionValues;
|
|
119
|
+
/**
|
|
120
|
+
* One Command's reading of its own tokens, which may stop short. `delimited` says a bare `--` was
|
|
121
|
+
* read, and `awaiting` is the option the last token left without its value.
|
|
122
|
+
*/
|
|
123
|
+
export interface InputScan {
|
|
124
|
+
awaiting: AwaitingValue | undefined;
|
|
125
|
+
delimited: boolean;
|
|
126
|
+
options: OptionValues;
|
|
127
|
+
passthrough: string[];
|
|
128
|
+
positionals: string[];
|
|
129
|
+
supplied: SuppliedOption[];
|
|
130
|
+
}
|
|
131
|
+
/** Reads one Command's tokens into options, positionals, and the passthrough tail. */
|
|
132
|
+
export declare function scanInputs(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): InputScan;
|
|
133
|
+
/** Parses a complete invocation's local tokens, where a waiting option is a missing value. */
|
|
40
134
|
export declare function parseInputs(spellings: ReadonlyMap<string, OptionSpelling>, tokens: readonly string[]): {
|
|
41
135
|
options: OptionValues;
|
|
42
136
|
passthrough: string[];
|
package/dist/options.js
CHANGED
|
@@ -1,52 +1,123 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { declaredName } from './command-rules.js';
|
|
2
|
+
import { valueCode } from './diagnostic-text.js';
|
|
3
|
+
import { DeclarationError, MissingValueError, quoted, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
|
|
4
|
+
import { factFault, flagFault, siteFinding } from './facts.js';
|
|
5
|
+
import { booleanOptionMultiple, optionDeclaredTwice, optionPolarity, optionType, polarityOnString, shortAlias, shortOnlyBothPolarities, shortOnlyWithoutShort, spellingTaken, } from './input-rules.js';
|
|
2
6
|
/** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
|
|
3
|
-
function emptyValues() {
|
|
4
|
-
return { booleans: new Map(), lists: new Map(), strings: new Map() };
|
|
7
|
+
export function emptyValues() {
|
|
8
|
+
return { booleans: new Map(), lists: new Map(), spellings: new Map(), strings: new Map() };
|
|
5
9
|
}
|
|
6
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The part of one declaration that yields a spelling of the given role, which a spelling fault
|
|
12
|
+
* marks: the declared name for the long form, `short` for the short alias, and the `polarity` that
|
|
13
|
+
* generates a negative form.
|
|
14
|
+
*/
|
|
15
|
+
export function spellingMark(site, role) {
|
|
16
|
+
if (role === 'long') {
|
|
17
|
+
return site.named;
|
|
18
|
+
}
|
|
19
|
+
return `${site.at}.${role === 'short' ? 'short' : 'polarity'}`;
|
|
20
|
+
}
|
|
21
|
+
/** The declared name answers the declared-name rule an argument's name answers. */
|
|
22
|
+
export function checkOptionName(name, site) {
|
|
23
|
+
const findings = [siteFinding(site, site.named)];
|
|
7
24
|
if (typeof name !== 'string') {
|
|
8
|
-
throw new DeclarationError(
|
|
25
|
+
throw new DeclarationError(declaredName, {
|
|
26
|
+
correction: 'Supply a string name.',
|
|
27
|
+
findings,
|
|
28
|
+
sentence: `Option name ${valueCode(name)} is not a string.`,
|
|
29
|
+
});
|
|
9
30
|
}
|
|
10
31
|
if (!name || name.startsWith('-') || /[\s=]/u.test(name)) {
|
|
11
|
-
throw new DeclarationError(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
throw new DeclarationError(declaredName, {
|
|
33
|
+
correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
|
|
34
|
+
findings,
|
|
35
|
+
sentence: `Option name ${quoted(name)} is invalid.`,
|
|
36
|
+
});
|
|
15
37
|
}
|
|
38
|
+
}
|
|
39
|
+
/** The short alias, and `shortOnly`, which leaves the option that alias alone. */
|
|
40
|
+
function checkShortForms(config, site, subject) {
|
|
16
41
|
if (config.short !== undefined &&
|
|
17
42
|
(typeof config.short !== 'string' || !/^[A-Za-z]$/u.test(config.short))) {
|
|
18
|
-
throw
|
|
43
|
+
throw factFault(shortAlias, site, {
|
|
44
|
+
correction: 'Supply one ASCII letter.',
|
|
45
|
+
fact: 'short',
|
|
46
|
+
sentence: `${subject} declares a short alias that is not one ASCII letter.`,
|
|
47
|
+
});
|
|
19
48
|
}
|
|
20
49
|
if (config.shortOnly !== undefined && typeof config.shortOnly !== 'boolean') {
|
|
21
|
-
throw
|
|
50
|
+
throw flagFault(site, 'shortOnly');
|
|
22
51
|
}
|
|
23
52
|
if (config.shortOnly && config.short === undefined) {
|
|
24
|
-
throw
|
|
53
|
+
throw factFault(shortOnlyWithoutShort, site, {
|
|
54
|
+
correction: 'Add short or remove shortOnly.',
|
|
55
|
+
fact: 'shortOnly',
|
|
56
|
+
sentence: `${subject} declares shortOnly and no short alias.`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** A Boolean option's polarity, which a string option does not declare. */
|
|
61
|
+
function checkPolarity(config, site, subject) {
|
|
62
|
+
if (config.polarity === undefined) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (config.type !== 'boolean') {
|
|
66
|
+
throw factFault(polarityOnString, site, {
|
|
67
|
+
correction: 'Remove polarity or use type "boolean".',
|
|
68
|
+
fact: 'polarity',
|
|
69
|
+
sentence: `${subject} declares polarity but is not Boolean.`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (!['positive', 'both', 'negative'].includes(config.polarity)) {
|
|
73
|
+
throw factFault(optionPolarity, site, {
|
|
74
|
+
correction: 'Use "positive", "both", or "negative".',
|
|
75
|
+
fact: 'polarity',
|
|
76
|
+
sentence: `${subject} has an invalid polarity.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (config.polarity === 'both' && config.shortOnly) {
|
|
80
|
+
throw factFault(shortOnlyBothPolarities, site, {
|
|
81
|
+
correction: 'Enable long forms or select one polarity.',
|
|
82
|
+
fact: 'shortOnly',
|
|
83
|
+
sentence: `${subject} cannot express both polarities with shortOnly.`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Every rule one option declaration answers alone, before the table meets it. */
|
|
88
|
+
function validateDeclaration({ name, config }, site) {
|
|
89
|
+
checkOptionName(name, site);
|
|
90
|
+
const subject = `Option ${quoted(name)}`;
|
|
91
|
+
if (!['string', 'boolean'].includes(config.type)) {
|
|
92
|
+
throw factFault(optionType, site, {
|
|
93
|
+
correction: 'Use "string" or "boolean".',
|
|
94
|
+
fact: 'type',
|
|
95
|
+
sentence: `${subject} has an invalid type.`,
|
|
96
|
+
});
|
|
25
97
|
}
|
|
98
|
+
checkShortForms(config, site, subject);
|
|
26
99
|
if (config.type === 'boolean' && config.multiple !== undefined) {
|
|
27
|
-
throw
|
|
100
|
+
throw factFault(booleanOptionMultiple, site, {
|
|
101
|
+
correction: 'Remove multiple or declare a string option.',
|
|
102
|
+
fact: 'multiple',
|
|
103
|
+
sentence: `${subject} is a boolean option and declares multiple.`,
|
|
104
|
+
});
|
|
28
105
|
}
|
|
29
106
|
if (config.multiple !== undefined && typeof config.multiple !== 'boolean') {
|
|
30
|
-
throw
|
|
31
|
-
}
|
|
32
|
-
if (config.polarity !== undefined) {
|
|
33
|
-
if (config.type !== 'boolean') {
|
|
34
|
-
throw new DeclarationError(`Option "${name}" declares polarity but is not Boolean. Remove polarity or use type "boolean".`);
|
|
35
|
-
}
|
|
36
|
-
if (!['positive', 'both', 'negative'].includes(config.polarity)) {
|
|
37
|
-
throw new DeclarationError(`Option "${name}" has an invalid polarity. Use "positive", "both", or "negative".`);
|
|
38
|
-
}
|
|
39
|
-
if (config.polarity === 'both' && config.shortOnly) {
|
|
40
|
-
throw new DeclarationError(`Option "${name}" cannot express both polarities with shortOnly. Enable long forms or select one polarity.`);
|
|
41
|
-
}
|
|
107
|
+
throw flagFault(site, 'multiple');
|
|
42
108
|
}
|
|
109
|
+
checkPolarity(config, site, subject);
|
|
43
110
|
}
|
|
44
|
-
function addSpelling(
|
|
45
|
-
const existing =
|
|
111
|
+
function addSpelling(claims, spelling, claim) {
|
|
112
|
+
const existing = claims.get(spelling);
|
|
46
113
|
if (existing) {
|
|
47
|
-
throw new DeclarationError(
|
|
114
|
+
throw new DeclarationError(spellingTaken, {
|
|
115
|
+
correction: 'Change one declaration.',
|
|
116
|
+
findings: [existing, claim].map(({ option, site }) => siteFinding(site, spellingMark(site, option.role))),
|
|
117
|
+
sentence: `Option spelling ${quoted(spelling)} is used by both ${quoted(existing.option.name)} and ${quoted(claim.option.name)}.`,
|
|
118
|
+
});
|
|
48
119
|
}
|
|
49
|
-
|
|
120
|
+
claims.set(spelling, claim);
|
|
50
121
|
}
|
|
51
122
|
/**
|
|
52
123
|
* One Boolean option's value for one invocation: the value the parser consumed, or the value its
|
|
@@ -57,37 +128,70 @@ function addSpelling(spellings, spelling, option) {
|
|
|
57
128
|
export function booleanValue(values, name, config) {
|
|
58
129
|
return values.booleans.get(name) ?? config.polarity === 'negative';
|
|
59
130
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
131
|
+
/**
|
|
132
|
+
* One scope's options compiled into the spelling table the parser reads, with every rule one
|
|
133
|
+
* declaration answers alone and every rule two of them answer together.
|
|
134
|
+
*/
|
|
135
|
+
export function compileOptions(declarations, scope) {
|
|
136
|
+
const claims = new Map();
|
|
137
|
+
const names = new Map();
|
|
138
|
+
for (const declaration of declarations) {
|
|
139
|
+
const { name, config } = declaration;
|
|
140
|
+
const site = scope.siteOf(declaration);
|
|
141
|
+
validateDeclaration({ config, name }, site);
|
|
142
|
+
const first = names.get(name);
|
|
143
|
+
if (first) {
|
|
144
|
+
const earlier = scope.siteOf(first);
|
|
145
|
+
throw new DeclarationError(optionDeclaredTwice, {
|
|
146
|
+
correction: 'Remove or rename the duplicate.',
|
|
147
|
+
findings: [
|
|
148
|
+
siteFinding(earlier, earlier.named, 'the first declaration'),
|
|
149
|
+
siteFinding(site, site.named, 'the second declaration'),
|
|
150
|
+
],
|
|
151
|
+
sentence: `Option ${quoted(name)} is declared more than once on ${scope.subject}.`,
|
|
152
|
+
});
|
|
67
153
|
}
|
|
68
|
-
names.
|
|
154
|
+
names.set(name, declaration);
|
|
69
155
|
const positive = config.type === 'string'
|
|
70
156
|
? { multiple: config.multiple === true, name, type: 'string' }
|
|
71
157
|
: { name, type: 'boolean', value: config.polarity !== 'negative' };
|
|
72
158
|
if (!config.shortOnly) {
|
|
73
159
|
if (config.type === 'string' || config.polarity !== 'negative') {
|
|
74
|
-
addSpelling(
|
|
160
|
+
addSpelling(claims, `--${name}`, { option: { ...positive, role: 'long' }, site });
|
|
75
161
|
}
|
|
76
162
|
if (config.type === 'boolean' &&
|
|
77
163
|
(config.polarity === 'both' || config.polarity === 'negative')) {
|
|
78
|
-
addSpelling(
|
|
79
|
-
name,
|
|
80
|
-
|
|
81
|
-
type: 'boolean',
|
|
82
|
-
value: false,
|
|
164
|
+
addSpelling(claims, `--no-${name}`, {
|
|
165
|
+
option: { name, role: 'negative', type: 'boolean', value: false },
|
|
166
|
+
site,
|
|
83
167
|
});
|
|
84
168
|
}
|
|
85
169
|
}
|
|
86
170
|
if (config.short !== undefined) {
|
|
87
|
-
addSpelling(
|
|
171
|
+
addSpelling(claims, `-${config.short}`, { option: { ...positive, role: 'short' }, site });
|
|
88
172
|
}
|
|
89
173
|
}
|
|
90
|
-
return
|
|
174
|
+
return new Map([...claims].map(([spelling, { option }]) => [spelling, option]));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
|
|
178
|
+
* separate value is never one. The parser and `locate` read each token through this rule.
|
|
179
|
+
*/
|
|
180
|
+
export function isOptionToken(token) {
|
|
181
|
+
return token.startsWith('-');
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A token that starts with `--` split at its first `=` into the spelling and the inline value, or
|
|
185
|
+
* `undefined` for any other token. The parser and `locate` split long tokens through this rule.
|
|
186
|
+
*/
|
|
187
|
+
export function longToken(token) {
|
|
188
|
+
if (!token.startsWith('--')) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
const equals = token.indexOf('=');
|
|
192
|
+
return equals === -1
|
|
193
|
+
? { inline: undefined, spelling: token }
|
|
194
|
+
: { inline: token.slice(equals + 1), spelling: token.slice(0, equals) };
|
|
91
195
|
}
|
|
92
196
|
function lookup(spellings, spelling) {
|
|
93
197
|
const option = spellings.get(spelling);
|
|
@@ -96,20 +200,33 @@ function lookup(spellings, spelling) {
|
|
|
96
200
|
}
|
|
97
201
|
return option;
|
|
98
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* The name of the string option one long spelling names in a table, which takes its value after
|
|
205
|
+
* `=`, or `undefined` for a Boolean, negative, short, or unknown spelling.
|
|
206
|
+
*/
|
|
207
|
+
export function longStringOption(spellings, spelling) {
|
|
208
|
+
const option = spellings.get(spelling);
|
|
209
|
+
return option?.type === 'string' && option.role === 'long' ? option.name : undefined;
|
|
210
|
+
}
|
|
99
211
|
function acceptValue({ option, spelling, values, next, inline, }) {
|
|
100
212
|
const repeatable = option.type === 'string' && option.multiple;
|
|
101
213
|
if (!repeatable && (values.strings.has(option.name) || values.booleans.has(option.name))) {
|
|
102
214
|
throw new RepeatedOptionError(spelling);
|
|
103
215
|
}
|
|
216
|
+
// A repeatable option records its last occurrence, because each one overwrites the entry.
|
|
217
|
+
values.spellings.set(option.name, spelling);
|
|
104
218
|
if (option.type === 'boolean') {
|
|
105
219
|
if (inline !== undefined) {
|
|
106
220
|
throw new UnexpectedValueError(spelling, inline);
|
|
107
221
|
}
|
|
108
222
|
values.booleans.set(option.name, option.value);
|
|
109
|
-
return
|
|
223
|
+
return 'alone';
|
|
110
224
|
}
|
|
111
225
|
const value = inline ?? next;
|
|
112
|
-
if (value === undefined
|
|
226
|
+
if (value === undefined) {
|
|
227
|
+
return { name: option.name, spelling };
|
|
228
|
+
}
|
|
229
|
+
if (inline === undefined && isOptionToken(value)) {
|
|
113
230
|
throw new MissingValueError(spelling);
|
|
114
231
|
}
|
|
115
232
|
if (repeatable) {
|
|
@@ -120,14 +237,13 @@ function acceptValue({ option, spelling, values, next, inline, }) {
|
|
|
120
237
|
else {
|
|
121
238
|
values.strings.set(option.name, value);
|
|
122
239
|
}
|
|
123
|
-
return inline === undefined;
|
|
240
|
+
return inline === undefined ? 'next' : 'alone';
|
|
124
241
|
}
|
|
125
242
|
function parseOption(spellings, input, values) {
|
|
126
243
|
const { token, next } = input;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const inline = equals === -1 ? undefined : token.slice(equals + 1);
|
|
244
|
+
const long = longToken(token);
|
|
245
|
+
if (long) {
|
|
246
|
+
const { inline, spelling } = long;
|
|
131
247
|
return acceptValue({ inline, next, option: lookup(spellings, spelling), spelling, values });
|
|
132
248
|
}
|
|
133
249
|
if (token === '-') {
|
|
@@ -141,28 +257,52 @@ function parseOption(spellings, input, values) {
|
|
|
141
257
|
throw new ShortGroupError({ reason: 'value-position', token: spelling });
|
|
142
258
|
}
|
|
143
259
|
const inline = suffix.startsWith('=') ? suffix.slice(1) : undefined;
|
|
144
|
-
|
|
145
|
-
|
|
260
|
+
const reading = acceptValue({ inline, next, option, spelling, values });
|
|
261
|
+
if (reading !== 'alone') {
|
|
262
|
+
return reading;
|
|
146
263
|
}
|
|
147
264
|
}
|
|
148
|
-
return
|
|
265
|
+
return 'alone';
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Reads one option token into the scan and answers whether it took the next token too. The names
|
|
269
|
+
* it newly supplied join `supplied` in the order the values map first recorded them, so a repeated
|
|
270
|
+
* option keeps its first position.
|
|
271
|
+
*/
|
|
272
|
+
function readOption(spellings, input, state) {
|
|
273
|
+
const known = state.values.spellings.size;
|
|
274
|
+
const reading = parseOption(spellings, input, state.values);
|
|
275
|
+
for (const name of [...state.values.spellings.keys()].slice(known)) {
|
|
276
|
+
state.supplied.push({ name, token: input.index });
|
|
277
|
+
}
|
|
278
|
+
if (typeof reading === 'object') {
|
|
279
|
+
state.awaiting = reading;
|
|
280
|
+
}
|
|
281
|
+
return reading === 'next';
|
|
149
282
|
}
|
|
150
283
|
/**
|
|
151
284
|
* A hyphen token belongs to the globals when its long spelling or every short letter does. The
|
|
152
285
|
* pre-scan reads the globals alone, so a letter it does not own is only "not a global option".
|
|
286
|
+
* The scan stops at a global value option, because the letters after it may be the value the
|
|
287
|
+
* operator meant to pass: the token then belongs to the globals, and parsing reports the
|
|
288
|
+
* value-position fault that names that option alone.
|
|
153
289
|
*/
|
|
154
290
|
function isGlobalToken(spellings, token) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return spellings.has(
|
|
291
|
+
const long = longToken(token);
|
|
292
|
+
if (long) {
|
|
293
|
+
return spellings.has(long.spelling);
|
|
158
294
|
}
|
|
159
295
|
const group = token.slice(1).split('=')[0] ?? '';
|
|
160
296
|
let global = '';
|
|
161
297
|
let other = '';
|
|
162
298
|
for (let index = 0; index < group.length; index += 1) {
|
|
163
299
|
const letter = group.charAt(index);
|
|
164
|
-
|
|
300
|
+
const option = spellings.get(`-${letter}`);
|
|
301
|
+
if (option) {
|
|
165
302
|
global = global === '' ? letter : global;
|
|
303
|
+
if (option.type === 'string') {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
166
306
|
}
|
|
167
307
|
else {
|
|
168
308
|
other = other === '' ? letter : other;
|
|
@@ -177,52 +317,100 @@ function isGlobalToken(spellings, token) {
|
|
|
177
317
|
return true;
|
|
178
318
|
}
|
|
179
319
|
/** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
|
|
180
|
-
export function
|
|
181
|
-
const
|
|
320
|
+
export function scanGlobals(spellings, tokens) {
|
|
321
|
+
const state = { awaiting: undefined, supplied: [], values: emptyValues() };
|
|
182
322
|
const rest = [];
|
|
323
|
+
const positions = [];
|
|
183
324
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
184
325
|
const token = tokens[index];
|
|
185
326
|
if (token === undefined) {
|
|
186
327
|
break;
|
|
187
328
|
}
|
|
188
329
|
if (token === '--') {
|
|
189
|
-
|
|
190
|
-
|
|
330
|
+
// One push per token, because a spread call would overflow the stack on a long list.
|
|
331
|
+
for (const [offset, tail] of tokens.slice(index).entries()) {
|
|
332
|
+
rest.push(tail);
|
|
333
|
+
positions.push(index + offset);
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
191
336
|
}
|
|
192
|
-
if (!token
|
|
337
|
+
if (!isOptionToken(token) || !isGlobalToken(spellings, token)) {
|
|
193
338
|
rest.push(token);
|
|
339
|
+
positions.push(index);
|
|
194
340
|
}
|
|
195
|
-
else if (
|
|
341
|
+
else if (readOption(spellings, { index, next: tokens[index + 1], token }, state)) {
|
|
196
342
|
index += 1;
|
|
197
343
|
}
|
|
198
344
|
}
|
|
345
|
+
return { ...state, positions, rest };
|
|
346
|
+
}
|
|
347
|
+
/** The pre-scan of a complete invocation, where a global still waiting for its value is a fault. */
|
|
348
|
+
export function extractGlobals(spellings, tokens) {
|
|
349
|
+
const { awaiting, rest, values } = scanGlobals(spellings, tokens);
|
|
350
|
+
if (awaiting) {
|
|
351
|
+
throw new MissingValueError(awaiting.spelling);
|
|
352
|
+
}
|
|
199
353
|
return { rest, values };
|
|
200
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Whether one option holds a value a tier supplied: a token in any spelling it accepts, or a fill
|
|
357
|
+
* from an input source. A declared default is never in these maps, so it never counts.
|
|
358
|
+
*/
|
|
359
|
+
export function isSupplied(values, name) {
|
|
360
|
+
return values.strings.has(name) || values.lists.has(name) || values.booleans.has(name);
|
|
361
|
+
}
|
|
362
|
+
/** One run's own copy of parsed values, which the input-source stage fills without touching argv's. */
|
|
363
|
+
export function copyValues(values) {
|
|
364
|
+
return {
|
|
365
|
+
booleans: new Map(values.booleans),
|
|
366
|
+
lists: new Map([...values.lists].map(([name, list]) => [name, [...list]])),
|
|
367
|
+
spellings: new Map(values.spellings),
|
|
368
|
+
strings: new Map(values.strings),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
201
371
|
/** Global and local keys never overlap, so one merged view feeds a single validation pass. */
|
|
202
372
|
export function mergeValues(globals, locals) {
|
|
203
373
|
return {
|
|
204
374
|
booleans: new Map([...globals.booleans, ...locals.booleans]),
|
|
205
375
|
lists: new Map([...globals.lists, ...locals.lists]),
|
|
376
|
+
spellings: new Map([...globals.spellings, ...locals.spellings]),
|
|
206
377
|
strings: new Map([...globals.strings, ...locals.strings]),
|
|
207
378
|
};
|
|
208
379
|
}
|
|
209
|
-
|
|
210
|
-
|
|
380
|
+
/** Reads one Command's tokens into options, positionals, and the passthrough tail. */
|
|
381
|
+
export function scanInputs(spellings, tokens) {
|
|
382
|
+
const state = { awaiting: undefined, supplied: [], values: emptyValues() };
|
|
211
383
|
const positionals = [];
|
|
384
|
+
const read = (passthrough, delimited) => ({
|
|
385
|
+
awaiting: state.awaiting,
|
|
386
|
+
delimited,
|
|
387
|
+
options: state.values,
|
|
388
|
+
passthrough,
|
|
389
|
+
positionals,
|
|
390
|
+
supplied: state.supplied,
|
|
391
|
+
});
|
|
212
392
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
213
393
|
const token = tokens[index];
|
|
214
394
|
if (token === undefined) {
|
|
215
395
|
break;
|
|
216
396
|
}
|
|
217
397
|
if (token === '--') {
|
|
218
|
-
return
|
|
398
|
+
return read(tokens.slice(index + 1), true);
|
|
219
399
|
}
|
|
220
|
-
if (!token
|
|
400
|
+
if (!isOptionToken(token)) {
|
|
221
401
|
positionals.push(token);
|
|
222
402
|
}
|
|
223
|
-
else if (
|
|
403
|
+
else if (readOption(spellings, { index, next: tokens[index + 1], token }, state)) {
|
|
224
404
|
index += 1;
|
|
225
405
|
}
|
|
226
406
|
}
|
|
227
|
-
return
|
|
407
|
+
return read([], false);
|
|
408
|
+
}
|
|
409
|
+
/** Parses a complete invocation's local tokens, where a waiting option is a missing value. */
|
|
410
|
+
export function parseInputs(spellings, tokens) {
|
|
411
|
+
const { awaiting, options, passthrough, positionals } = scanInputs(spellings, tokens);
|
|
412
|
+
if (awaiting) {
|
|
413
|
+
throw new MissingValueError(awaiting.spelling);
|
|
414
|
+
}
|
|
415
|
+
return { options, passthrough, positionals };
|
|
228
416
|
}
|
package/dist/output.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { RenderingPolicy } from './rendering.js';
|
|
|
4
4
|
import type { Palette } from './style-state.js';
|
|
5
5
|
import type { ActionChannel, Host, OpenResult, Out, ResultBinding, ViewContext } from './types.js';
|
|
6
6
|
import type { ViewRegistry } from './view.js';
|
|
7
|
-
type WriteState = {
|
|
7
|
+
export type WriteState = {
|
|
8
8
|
kind: 'ok';
|
|
9
9
|
} | {
|
|
10
10
|
kind: 'failed';
|
|
@@ -19,6 +19,7 @@ export declare class Output {
|
|
|
19
19
|
private readonly signal;
|
|
20
20
|
private readonly destinations;
|
|
21
21
|
private renderFault;
|
|
22
|
+
private readonly viewFaults;
|
|
22
23
|
private readonly stops;
|
|
23
24
|
private route;
|
|
24
25
|
/**
|
|
@@ -26,6 +27,8 @@ export declare class Output {
|
|
|
26
27
|
* declaration a call answers to is checked where the action was authored.
|
|
27
28
|
*/
|
|
28
29
|
readonly out: Out<OpenResult>;
|
|
30
|
+
/** The channel a configuration source receives: `out` with the results call naming a source. */
|
|
31
|
+
readonly sourceOut: Out<OpenResult>;
|
|
29
32
|
private palette;
|
|
30
33
|
private policy;
|
|
31
34
|
private registry;
|
|
@@ -54,7 +57,7 @@ export declare class Output {
|
|
|
54
57
|
private resultFault;
|
|
55
58
|
/** The registry one invocation resolves through, republished as each contributor is read. */
|
|
56
59
|
useViews(registry: ViewRegistry): void;
|
|
57
|
-
/** The
|
|
60
|
+
/** The path routing walked, updated as each name routes, which an incomplete sequence names. */
|
|
58
61
|
useRoute(path: readonly string[]): void;
|
|
59
62
|
/** What this invocation's output raised beside its calls, in the order it was raised. */
|
|
60
63
|
get stopped(): readonly unknown[];
|
|
@@ -110,6 +113,12 @@ export declare class Output {
|
|
|
110
113
|
* awaits the call must not end the process with an unhandled rejection.
|
|
111
114
|
*/
|
|
112
115
|
private renderFailed;
|
|
116
|
+
/**
|
|
117
|
+
* Whether one value is what a view or a message check failed with during this invocation. A
|
|
118
|
+
* broken view is a defect in the code that broke the view contract, so an action or a source that
|
|
119
|
+
* lets its rejection propagate reports it as that defect.
|
|
120
|
+
*/
|
|
121
|
+
raisedByView(value: unknown): boolean;
|
|
113
122
|
/** What a view failed with during this invocation, if one did. */
|
|
114
123
|
get fault(): {
|
|
115
124
|
cause: unknown;
|