@loomcli/core 0.6.0 → 0.8.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/NOTICE +34 -0
- package/dist/application.d.ts +11 -6
- package/dist/application.js +57 -19
- package/dist/capture.d.ts +65 -0
- package/dist/capture.js +99 -0
- package/dist/chain.d.ts +28 -17
- package/dist/chain.js +11 -10
- package/dist/command-rules.d.ts +1 -1
- package/dist/command-rules.js +2 -2
- package/dist/command.d.ts +31 -47
- package/dist/command.js +204 -209
- package/dist/errors.d.ts +22 -21
- package/dist/errors.js +38 -18
- package/dist/facts.d.ts +5 -0
- package/dist/facts.js +8 -1
- package/dist/globals.d.ts +48 -22
- package/dist/globals.js +72 -29
- package/dist/glyphs.generated.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/input-rules.d.ts +20 -2
- package/dist/input-rules.js +47 -5
- package/dist/inspect.d.ts +33 -17
- package/dist/inspect.js +74 -87
- package/dist/locate.js +52 -60
- package/dist/options.d.ts +101 -83
- package/dist/options.js +311 -267
- package/dist/parse.d.ts +162 -0
- package/dist/parse.js +601 -0
- package/dist/plain.d.ts +52 -2
- package/dist/plain.js +228 -2
- package/dist/plugin-rules.d.ts +8 -4
- package/dist/plugin-rules.js +13 -9
- package/dist/plugin-settings.d.ts +18 -0
- package/dist/plugin-settings.js +38 -0
- package/dist/plugin.d.ts +32 -27
- package/dist/plugin.js +107 -89
- package/dist/sources.d.ts +18 -9
- package/dist/sources.js +50 -20
- package/dist/style-layout.js +2 -2
- package/dist/style-width.d.ts +13 -0
- package/dist/style-width.js +170 -0
- package/dist/types.d.ts +70 -30
- package/dist/unicode.generated.d.ts +27 -0
- package/dist/unicode.generated.js +1036 -0
- package/dist/validation.d.ts +108 -34
- package/dist/validation.js +282 -125
- package/dist/view.d.ts +16 -11
- package/dist/view.js +13 -4
- package/licenses/unicode-LICENSE.txt +41 -0
- package/licenses/uucode-LICENSE.md +35 -0
- package/package.json +9 -5
package/dist/parse.js
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { MisplacedOptionError, MissingValueError, RepeatedOptionError, UnexpectedArgumentError, UnexpectedValueError, UnknownCommandError, UnknownOptionError, } from './errors.js';
|
|
2
|
+
import { emptyValues, isSupplied } from './options.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a word is an option word: `--` and at least one more character, or `-` and an ASCII
|
|
5
|
+
* letter. Every other word is a plain word, so `-`, `-5`, and `-.5` are values or arguments. The
|
|
6
|
+
* parser and `locate` read each word through this rule.
|
|
7
|
+
*/
|
|
8
|
+
function isOptionWord(word) {
|
|
9
|
+
return word.startsWith('--') ? word.length > 2 : /^-[A-Za-z]/u.test(word);
|
|
10
|
+
}
|
|
11
|
+
/** A word the grammar never consumes as a separate value: an option word or the bare `--`. */
|
|
12
|
+
function refusesValue(word) {
|
|
13
|
+
return word === '--' || isOptionWord(word);
|
|
14
|
+
}
|
|
15
|
+
/** Whether an option collects every occurrence: a multiple string option. */
|
|
16
|
+
function collects(option) {
|
|
17
|
+
return option.type === 'string' && option.multiple;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether every occurrence of an option adds to it: a multiple string option collects each, and a
|
|
21
|
+
* counted option counts each, so neither is ever a repeat.
|
|
22
|
+
*/
|
|
23
|
+
function accumulates(option) {
|
|
24
|
+
return option.type === 'count' || collects(option);
|
|
25
|
+
}
|
|
26
|
+
/** Whether an occurrence of an option repeats one an earlier word supplied. */
|
|
27
|
+
function repeats(context, option) {
|
|
28
|
+
const values = option.global ? context.values.globals : context.values.locals;
|
|
29
|
+
return !accumulates(option) && isSupplied(values, option.name);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* What a spelling with nothing attached supplies, by its value class: a Boolean's own value, one
|
|
33
|
+
* occurrence of a count, or a string option's implied value. A string option that takes the next
|
|
34
|
+
* word has no value of its own, so it answers `undefined`.
|
|
35
|
+
*/
|
|
36
|
+
function bareValue(option, spelling) {
|
|
37
|
+
switch (option.valueClass) {
|
|
38
|
+
case 'boolean': {
|
|
39
|
+
return { kind: 'value', option, spelling, value: option.value };
|
|
40
|
+
}
|
|
41
|
+
case 'count': {
|
|
42
|
+
return { kind: 'value', option, spelling, value: 1 };
|
|
43
|
+
}
|
|
44
|
+
case 'implied': {
|
|
45
|
+
return { implied: true, kind: 'value', option, spelling, value: option.implied };
|
|
46
|
+
}
|
|
47
|
+
case 'separate': {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
default: {
|
|
51
|
+
const exhaustive = option;
|
|
52
|
+
return exhaustive;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A repeated occurrence supplies nothing, and a string option still takes the value its form
|
|
58
|
+
* names, so the words after it read as they would have.
|
|
59
|
+
*/
|
|
60
|
+
function repeated(option, spelling, takesNext) {
|
|
61
|
+
return { occurrences: [{ kind: 'repeated', option, spelling }], takesNext };
|
|
62
|
+
}
|
|
63
|
+
/** Whether a string option with no value in its own word takes the next word as its value. */
|
|
64
|
+
function takesSeparate(next) {
|
|
65
|
+
return next !== undefined && !refusesValue(next);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A string option whose value is the next word: that word, unless the words ran out or it is a word
|
|
69
|
+
* the grammar never consumes as a value.
|
|
70
|
+
*/
|
|
71
|
+
function separateValue(option, spelling, next) {
|
|
72
|
+
if (next === undefined) {
|
|
73
|
+
return { occurrences: [{ kind: 'awaiting', option, spelling }], takesNext: false };
|
|
74
|
+
}
|
|
75
|
+
if (refusesValue(next)) {
|
|
76
|
+
return { occurrences: [{ kind: 'missing', option, spelling }], takesNext: false };
|
|
77
|
+
}
|
|
78
|
+
return { occurrences: [{ kind: 'value', option, spelling, value: next }], takesNext: true };
|
|
79
|
+
}
|
|
80
|
+
/** A word that starts with `--`, split at its first `=` into the spelling and the value it carries. */
|
|
81
|
+
function readLong(context, word, next) {
|
|
82
|
+
const equals = word.indexOf('=');
|
|
83
|
+
const spelling = equals === -1 ? word : word.slice(0, equals);
|
|
84
|
+
const inline = equals === -1 ? undefined : word.slice(equals + 1);
|
|
85
|
+
const option = context.table.get(spelling);
|
|
86
|
+
if (!option) {
|
|
87
|
+
return { occurrences: [{ kind: 'unknown', spelling }], takesNext: false };
|
|
88
|
+
}
|
|
89
|
+
if (repeats(context, option)) {
|
|
90
|
+
const separate = option.valueClass === 'separate' && inline === undefined && takesSeparate(next);
|
|
91
|
+
return repeated(option, spelling, separate);
|
|
92
|
+
}
|
|
93
|
+
return longValue({ inline, option, spelling }, next);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The value a declared long spelling supplies: a Boolean's own value or one occurrence of a count,
|
|
97
|
+
* neither of which takes `=`, or a string option's value after `=`, else its implied value, else
|
|
98
|
+
* the next word.
|
|
99
|
+
*/
|
|
100
|
+
function longValue(long, next) {
|
|
101
|
+
const { inline, option, spelling } = long;
|
|
102
|
+
if (inline === undefined) {
|
|
103
|
+
const bare = bareValue(option, spelling);
|
|
104
|
+
return bare ? { occurrences: [bare], takesNext: false } : separateValue(option, spelling, next);
|
|
105
|
+
}
|
|
106
|
+
const occurrence = option.type === 'string'
|
|
107
|
+
? { kind: 'value', lead: `${spelling}=`, option, spelling, value: inline }
|
|
108
|
+
: { kind: 'unexpected', option, spelling, value: inline };
|
|
109
|
+
return { occurrences: [occurrence], takesNext: false };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A Boolean letter is set, and a counted letter adds one, and the walk continues; a `=` after
|
|
113
|
+
* either is a value it cannot take.
|
|
114
|
+
*/
|
|
115
|
+
function valuelessLetter(option, spelling, rest) {
|
|
116
|
+
const occurrence = rest.startsWith('=')
|
|
117
|
+
? { kind: 'unexpected', option, spelling, value: rest.slice(1) }
|
|
118
|
+
: { kind: 'value', option, spelling, value: option.type === 'boolean' ? option.value : 1 };
|
|
119
|
+
return { ends: occurrence.kind !== 'value', occurrences: [occurrence], takesNext: false };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A value letter ends the group: the rest of the word is its value, with one leading `=` stripped.
|
|
123
|
+
* When nothing remains, a letter with an implied value supplies it, and any other takes the next
|
|
124
|
+
* word as its value.
|
|
125
|
+
*/
|
|
126
|
+
function valueLetter(group, letter, rest) {
|
|
127
|
+
const { option, spelling } = letter;
|
|
128
|
+
if (rest === '') {
|
|
129
|
+
const bare = bareValue(option, spelling);
|
|
130
|
+
return bare
|
|
131
|
+
? { ends: true, occurrences: [bare], takesNext: false }
|
|
132
|
+
: { ends: true, ...separateValue(option, spelling, group.next) };
|
|
133
|
+
}
|
|
134
|
+
const value = rest.startsWith('=') ? rest.slice(1) : rest;
|
|
135
|
+
const lead = group.word.slice(0, group.word.length - value.length);
|
|
136
|
+
return {
|
|
137
|
+
ends: true,
|
|
138
|
+
occurrences: [{ kind: 'value', lead, option, spelling, value }],
|
|
139
|
+
takesNext: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Whether a letter repeats an option an earlier letter of its group or an earlier word supplied. A
|
|
144
|
+
* counted letter adds another occurrence instead, and so would a multiple option's.
|
|
145
|
+
*/
|
|
146
|
+
function repeatsInGroup(group, option) {
|
|
147
|
+
return (group.set.has(option.name) && !accumulates(option)) || repeats(group.context, option);
|
|
148
|
+
}
|
|
149
|
+
/** The letter at one index of a group, read against the group's table. */
|
|
150
|
+
function readLetter(group, index) {
|
|
151
|
+
const spelling = `-${group.letters[index] ?? ''}`;
|
|
152
|
+
const option = group.context.table.get(spelling);
|
|
153
|
+
const rest = group.letters.slice(index + 1).join('');
|
|
154
|
+
if (!option) {
|
|
155
|
+
return { ends: true, occurrences: [{ kind: 'unknown', spelling }], takesNext: false };
|
|
156
|
+
}
|
|
157
|
+
if (repeatsInGroup(group, option)) {
|
|
158
|
+
const takesNext = option.valueClass === 'separate' && rest === '' && takesSeparate(group.next);
|
|
159
|
+
return { ends: true, ...repeated(option, spelling, takesNext) };
|
|
160
|
+
}
|
|
161
|
+
group.set.add(option.name);
|
|
162
|
+
return option.type === 'string'
|
|
163
|
+
? valueLetter(group, { option, spelling }, rest)
|
|
164
|
+
: valuelessLetter(option, spelling, rest);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A short group under the `getopt` rule, one code point per letter. The walk stops at a value
|
|
168
|
+
* letter or at the first letter that faults, a repeated one included, so the characters after it
|
|
169
|
+
* are never read as letters.
|
|
170
|
+
*/
|
|
171
|
+
function readGroup(context, word, next) {
|
|
172
|
+
const letters = word.slice(1).match(/./gsu) ?? [];
|
|
173
|
+
const group = { context, letters, next, set: new Set(), word };
|
|
174
|
+
const occurrences = [];
|
|
175
|
+
for (const index of group.letters.keys()) {
|
|
176
|
+
const letter = readLetter(group, index);
|
|
177
|
+
occurrences.push(...letter.occurrences);
|
|
178
|
+
if (letter.ends) {
|
|
179
|
+
return { occurrences, takesNext: letter.takesNext };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { occurrences, takesNext: false };
|
|
183
|
+
}
|
|
184
|
+
/** One option word read against one table, with the word after it as its possible value. */
|
|
185
|
+
function readOptionWord(context, word, next) {
|
|
186
|
+
return word.startsWith('--') ? readLong(context, word, next) : readGroup(context, word, next);
|
|
187
|
+
}
|
|
188
|
+
/** Whether an occurrence is of a Command's own option, which routing holds back until it ends. */
|
|
189
|
+
function isOwnOccurrence(occurrence) {
|
|
190
|
+
return 'option' in occurrence && occurrence.kind !== 'awaiting' && !occurrence.option.global;
|
|
191
|
+
}
|
|
192
|
+
/** Stamps the next occurrence or positional with its order in the words. */
|
|
193
|
+
function stamp(state) {
|
|
194
|
+
state.at = state.read;
|
|
195
|
+
state.read += 1;
|
|
196
|
+
}
|
|
197
|
+
/** Holds a fault unless one read earlier is held already. */
|
|
198
|
+
function keep(state, fault) {
|
|
199
|
+
if (state.fault === undefined || fault.at < state.fault.at) {
|
|
200
|
+
state.fault = fault;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The visible Commands below one Command that declare a spelling, as paths from the root in
|
|
205
|
+
* authoring order. A hidden or deprecated Command, and every Command below it, is left out, as a
|
|
206
|
+
* listing leaves it out.
|
|
207
|
+
*/
|
|
208
|
+
function declarers(command, path, spelling) {
|
|
209
|
+
const found = [];
|
|
210
|
+
for (const [name, child] of command.children) {
|
|
211
|
+
const below = [...path, name];
|
|
212
|
+
if (!child.hidden && child.deprecated === undefined) {
|
|
213
|
+
if (child.table.has(spelling)) {
|
|
214
|
+
found.push(below);
|
|
215
|
+
}
|
|
216
|
+
found.push(...declarers(child, below, spelling));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return found;
|
|
220
|
+
}
|
|
221
|
+
/** The fault a spelling the routed Command's table does not hold reports. */
|
|
222
|
+
function unplaced(state, spelling) {
|
|
223
|
+
const commands = declarers(state.command, state.path, spelling);
|
|
224
|
+
return commands.length > 0
|
|
225
|
+
? new MisplacedOptionError(spelling, commands)
|
|
226
|
+
: new UnknownOptionError(spelling);
|
|
227
|
+
}
|
|
228
|
+
/** Holds a fault unless an earlier word already holds one, and notes a fault on a global option. */
|
|
229
|
+
function hold(state, error, global) {
|
|
230
|
+
keep(state, { at: state.at, error });
|
|
231
|
+
state.globalFault ||= global;
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Records one supplied value in the map its kind and its declaration decide: a count adds its
|
|
236
|
+
* occurrence, and a bare spelling's implied value notes its position, so validation supplies the
|
|
237
|
+
* prepared output there.
|
|
238
|
+
*/
|
|
239
|
+
function write(values, occurrence) {
|
|
240
|
+
const { option, value } = occurrence;
|
|
241
|
+
const { name } = option;
|
|
242
|
+
if (typeof value === 'boolean') {
|
|
243
|
+
values.booleans.set(name, value);
|
|
244
|
+
}
|
|
245
|
+
else if (typeof value === 'number') {
|
|
246
|
+
values.counts.set(name, (values.counts.get(name) ?? 0) + value);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const position = writeString(values, option, value);
|
|
250
|
+
if (occurrence.implied) {
|
|
251
|
+
values.implied.set(name, [...(values.implied.get(name) ?? []), position]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Records one string value: the option's one value, or the next of a multiple option's list.
|
|
257
|
+
* Answers the position the value took, `0` for a scalar and its index in the list otherwise.
|
|
258
|
+
*/
|
|
259
|
+
function writeString(values, option, value) {
|
|
260
|
+
const { name } = option;
|
|
261
|
+
if (!collects(option)) {
|
|
262
|
+
values.strings.set(name, value);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
const list = values.lists.get(name);
|
|
266
|
+
if (list) {
|
|
267
|
+
list.push(value);
|
|
268
|
+
return list.length - 1;
|
|
269
|
+
}
|
|
270
|
+
values.lists.set(name, [value]);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Lists one supplied option name at the order it was read in. A parent's own option binds after
|
|
275
|
+
* routing, so it can join the list behind names read after it.
|
|
276
|
+
*/
|
|
277
|
+
function note(state, entry) {
|
|
278
|
+
const later = state.supplied.findIndex(({ at }) => at > entry.at);
|
|
279
|
+
state.supplied.splice(later === -1 ? state.supplied.length : later, 0, entry);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Lists the held-back options while routing is still open, each under the name the reached
|
|
283
|
+
* Command's own entry gives its spelling, so a reader offers none of them again. A spelling that
|
|
284
|
+
* Command does not hold lists nothing.
|
|
285
|
+
*/
|
|
286
|
+
function noteHeld(state) {
|
|
287
|
+
for (const { at, occurrence } of state.pending) {
|
|
288
|
+
const entry = state.command.table.get(occurrence.spelling);
|
|
289
|
+
if (entry && !state.supplied.some(({ name }) => name === entry.name)) {
|
|
290
|
+
note(state, { at, name: entry.name });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** Writes one value an occurrence supplied. The reading already ended its walk at a repeat. */
|
|
295
|
+
function supply(state, occurrence) {
|
|
296
|
+
const { option, spelling } = occurrence;
|
|
297
|
+
const values = option.global ? state.values.globals : state.values.locals;
|
|
298
|
+
if (!values.spellings.has(option.name)) {
|
|
299
|
+
note(state, { at: state.at, name: option.name });
|
|
300
|
+
}
|
|
301
|
+
// A collecting option records its last occurrence, because each one overwrites the entry.
|
|
302
|
+
values.spellings.set(option.name, spelling);
|
|
303
|
+
write(values, occurrence);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* The failure a spelling reports where the routed Command reads it: one its table lacks, or one it
|
|
308
|
+
* declares with another value class than the parent's declaration routing read it by.
|
|
309
|
+
*/
|
|
310
|
+
function spellingFault(state, occurrence) {
|
|
311
|
+
const { spelling } = occurrence;
|
|
312
|
+
return occurrence.kind === 'unknown'
|
|
313
|
+
? unplaced(state, spelling)
|
|
314
|
+
: new MisplacedOptionError(spelling, [[...state.path]]);
|
|
315
|
+
}
|
|
316
|
+
/** Applies one occurrence, and answers whether the word's next occurrence is read. */
|
|
317
|
+
function applyOccurrence(state, occurrence) {
|
|
318
|
+
if (occurrence.kind === 'value') {
|
|
319
|
+
return supply(state, occurrence);
|
|
320
|
+
}
|
|
321
|
+
if (occurrence.kind === 'unknown' || occurrence.kind === 'misplaced') {
|
|
322
|
+
return hold(state, spellingFault(state, occurrence), false);
|
|
323
|
+
}
|
|
324
|
+
const { option, spelling } = occurrence;
|
|
325
|
+
if (occurrence.kind === 'awaiting') {
|
|
326
|
+
state.awaiting = { global: option.global, name: option.name, spelling };
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return hold(state, optionFault(occurrence), option.global);
|
|
330
|
+
}
|
|
331
|
+
/** The failure a faulted occurrence of a declared option reports. */
|
|
332
|
+
function optionFault(occurrence) {
|
|
333
|
+
const { spelling } = occurrence;
|
|
334
|
+
if (occurrence.kind === 'unexpected') {
|
|
335
|
+
return new UnexpectedValueError(spelling, occurrence.value, occurrence.option.type);
|
|
336
|
+
}
|
|
337
|
+
return occurrence.kind === 'repeated'
|
|
338
|
+
? new RepeatedOptionError(spelling)
|
|
339
|
+
: new MissingValueError(spelling, 'attached');
|
|
340
|
+
}
|
|
341
|
+
/** Applies a word's occurrences in order, up to the first that faults. A faulted one supplies nothing. */
|
|
342
|
+
function apply(state, reading) {
|
|
343
|
+
for (const occurrence of reading.occurrences) {
|
|
344
|
+
stamp(state);
|
|
345
|
+
const held = !state.routed && isOwnOccurrence(occurrence);
|
|
346
|
+
if (held) {
|
|
347
|
+
state.pending.push({ at: state.at, occurrence });
|
|
348
|
+
}
|
|
349
|
+
if (held ? occurrence.kind !== 'value' : !applyOccurrence(state, occurrence)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The occurrence routing read against a parent's own option, bound to the routed Command's
|
|
356
|
+
* declaration of the same spelling. A Command without it reports the spelling as it reports any its
|
|
357
|
+
* table lacks, and a declaration of another value class is misplaced, because the parent's
|
|
358
|
+
* declaration already decided whether the next word was the value, and the words are never read
|
|
359
|
+
* again. A declaration of the same class keeps the fault the parent's declaration found, if any.
|
|
360
|
+
*/
|
|
361
|
+
function rebound(state, pending) {
|
|
362
|
+
const { spelling } = pending;
|
|
363
|
+
const option = state.command.table.get(spelling);
|
|
364
|
+
if (!option) {
|
|
365
|
+
return { kind: 'unknown', spelling };
|
|
366
|
+
}
|
|
367
|
+
if (option.valueClass !== pending.option.valueClass) {
|
|
368
|
+
return { kind: 'misplaced', spelling };
|
|
369
|
+
}
|
|
370
|
+
return pending.kind === 'value'
|
|
371
|
+
? boundValue(state, pending, option)
|
|
372
|
+
: boundFault(pending, option);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* A fault routing held back, bound to the routed Command's declaration of the same class, which
|
|
376
|
+
* keeps it. A value after a spelling that takes none reaches here only with a Boolean or counted
|
|
377
|
+
* option, because a class mismatch is misplaced before this binds; the string branch exists only so
|
|
378
|
+
* the bound occurrence stays typed, and no invocation reaches it.
|
|
379
|
+
*/
|
|
380
|
+
function boundFault(pending, option) {
|
|
381
|
+
if (pending.kind !== 'unexpected') {
|
|
382
|
+
return { ...pending, option };
|
|
383
|
+
}
|
|
384
|
+
return option.type === 'string'
|
|
385
|
+
? { kind: 'misplaced', spelling: pending.spelling }
|
|
386
|
+
: { ...pending, option };
|
|
387
|
+
}
|
|
388
|
+
/** A value routing held back, bound to the routed Command's declaration of the same class. */
|
|
389
|
+
function boundValue(state, pending, option) {
|
|
390
|
+
const { spelling } = pending;
|
|
391
|
+
if (repeats({ table: state.command.table, values: state.values }, option)) {
|
|
392
|
+
return { kind: 'repeated', option, spelling };
|
|
393
|
+
}
|
|
394
|
+
// The bound declaration decides what a spelling with nothing attached supplies.
|
|
395
|
+
// That is a Boolean's own value, one occurrence of a count, or its own implied value.
|
|
396
|
+
// A value in the next word or attached is the word the parent's declaration read.
|
|
397
|
+
const bare = pending.lead === undefined ? bareValue(option, spelling) : undefined;
|
|
398
|
+
return { ...pending, option, value: bare?.value ?? pending.value };
|
|
399
|
+
}
|
|
400
|
+
/** Binds each occurrence routing read against a parent's own option to the Command it reached. */
|
|
401
|
+
function bindPending(state) {
|
|
402
|
+
for (const { at, occurrence } of state.pending) {
|
|
403
|
+
state.at = at;
|
|
404
|
+
applyOccurrence(state, rebound(state, occurrence));
|
|
405
|
+
}
|
|
406
|
+
state.pending = [];
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The names a routing failure offers: the canonical names of the visible, current children, in
|
|
410
|
+
* authoring order. A candidate list is a listing, so a hidden or a deprecated child is absent from
|
|
411
|
+
* it, as completion leaves them out, and a parent whose children are all hidden or deprecated
|
|
412
|
+
* offers none. A deprecated child typed in full still routes.
|
|
413
|
+
*/
|
|
414
|
+
function candidatesOf(command) {
|
|
415
|
+
return [...command.children]
|
|
416
|
+
.filter(([, child]) => !child.hidden && child.deprecated === undefined)
|
|
417
|
+
.map(([name]) => name);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* The slot the positional at one index fills: the slot at that index, else a variadic last slot,
|
|
421
|
+
* which accepts every later positional, else none. Binding and `locate` read positions through it.
|
|
422
|
+
*/
|
|
423
|
+
function argumentSlot(slots, position) {
|
|
424
|
+
const last = slots.at(-1);
|
|
425
|
+
return slots[position] ?? (last?.variadic ? last : undefined);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Whether routing reads a Command's own options: it has an action and children. A Command with
|
|
429
|
+
* children takes no arguments, so a plain word after its own option can only name a child, and its
|
|
430
|
+
* own declaration says whether the next word is that option's value.
|
|
431
|
+
*/
|
|
432
|
+
function readsOwnOptions(command) {
|
|
433
|
+
return command.dispatch !== undefined && command.children.size > 0;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* An option word under routing, read against the global options, and at a Command with an action
|
|
437
|
+
* and children against its whole table, whose own options bind once routing ends. A walk that meets
|
|
438
|
+
* a spelling those options do not declare ends routing at the Command reached, which reads the word
|
|
439
|
+
* again against its own table. Answers how many words it read, and `0` where routing ends.
|
|
440
|
+
*/
|
|
441
|
+
function routeOption(routing, state, index) {
|
|
442
|
+
const { graph, words } = routing;
|
|
443
|
+
const { command } = state;
|
|
444
|
+
const table = readsOwnOptions(command) ? command.table : graph.globals.table;
|
|
445
|
+
const context = { table, values: state.values };
|
|
446
|
+
const reading = readOptionWord(context, words[index] ?? '', words[index + 1]);
|
|
447
|
+
if (reading.occurrences.some((occurrence) => occurrence.kind === 'unknown')) {
|
|
448
|
+
return 0;
|
|
449
|
+
}
|
|
450
|
+
apply(state, reading);
|
|
451
|
+
return reading.takesNext ? 2 : 1;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* A plain word under routing names a child or an alias and descends, and an unknown child is the
|
|
455
|
+
* one fault raised here. Under a Command with no children it ends routing as that Command's first
|
|
456
|
+
* argument. `walked` hears the path as each name routes, so an unknown Command leaves its caller
|
|
457
|
+
* holding the partial path.
|
|
458
|
+
*/
|
|
459
|
+
function descend(routing, state, word) {
|
|
460
|
+
const { command } = state;
|
|
461
|
+
if (command.children.size === 0) {
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
const child = command.routes.get(word);
|
|
465
|
+
if (!child) {
|
|
466
|
+
throw new UnknownCommandError(word, candidatesOf(command));
|
|
467
|
+
}
|
|
468
|
+
// An alias routes like the canonical name, and the path it walks reports that name alone.
|
|
469
|
+
state.command = child.command;
|
|
470
|
+
state.path.push(child.name);
|
|
471
|
+
routing.walked?.(Object.freeze([...state.path]));
|
|
472
|
+
return 1;
|
|
473
|
+
}
|
|
474
|
+
/** One word routing reads: how many words it took, or `0` where routing ends. */
|
|
475
|
+
function routeWord(routing, state, index) {
|
|
476
|
+
const word = routing.words[index];
|
|
477
|
+
if (word === undefined || word === '--') {
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
return isOptionWord(word) ? routeOption(routing, state, index) : descend(routing, state, word);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Routing: the words before the first bare `--`, read from the root against the global options
|
|
484
|
+
* alone. Answers the index of the first word it did not read.
|
|
485
|
+
*/
|
|
486
|
+
function route(routing, state) {
|
|
487
|
+
let index = 0;
|
|
488
|
+
for (let read = routeWord(routing, state, index); read > 0; read = routeWord(routing, state, index)) {
|
|
489
|
+
index += read;
|
|
490
|
+
}
|
|
491
|
+
return index;
|
|
492
|
+
}
|
|
493
|
+
/** One plain word after routing: the routed Command's next positional, even one that names a child. */
|
|
494
|
+
function positional(state, word) {
|
|
495
|
+
stamp(state);
|
|
496
|
+
state.positionals.push(word);
|
|
497
|
+
const position = state.positionals.length - 1;
|
|
498
|
+
if (!argumentSlot(state.command.arguments, position)) {
|
|
499
|
+
keep(state, { at: state.at, extra: position });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** One word of the routed Command, read against its table. Answers how many later words it took. */
|
|
503
|
+
function readCommandWord(state, word, next) {
|
|
504
|
+
if (!isOptionWord(word)) {
|
|
505
|
+
positional(state, word);
|
|
506
|
+
return 0;
|
|
507
|
+
}
|
|
508
|
+
const reading = readOptionWord({ table: state.command.table, values: state.values }, word, next);
|
|
509
|
+
apply(state, reading);
|
|
510
|
+
return reading.takesNext ? 1 : 0;
|
|
511
|
+
}
|
|
512
|
+
/** The routed Command's words from where routing ended, up to the first bare `--`. */
|
|
513
|
+
function readCommandWords(routing, state, start) {
|
|
514
|
+
const { words } = routing;
|
|
515
|
+
for (let index = start; index < words.length; index += 1) {
|
|
516
|
+
const word = words[index] ?? '';
|
|
517
|
+
if (word === '--') {
|
|
518
|
+
return { delimited: true, passthrough: words.slice(index + 1) };
|
|
519
|
+
}
|
|
520
|
+
index += readCommandWord(state, word, words[index + 1]);
|
|
521
|
+
}
|
|
522
|
+
return { delimited: false, passthrough: [] };
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Whether routing ended where the words did: they stopped it, the list is complete, no later word
|
|
526
|
+
* can continue routing, or a parent's own option already faulted, which binding reports wherever
|
|
527
|
+
* routing ends.
|
|
528
|
+
*/
|
|
529
|
+
function routingEnded(state, start, read) {
|
|
530
|
+
return (start < read.words.length ||
|
|
531
|
+
read.partial !== true ||
|
|
532
|
+
state.command.children.size === 0 ||
|
|
533
|
+
state.pending.some(({ occurrence }) => occurrence.kind !== 'value'));
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Reads a word list through routing, against the global options and the own options of each
|
|
537
|
+
* Command with an action and children it passes through, binds those own options to the Command
|
|
538
|
+
* routing reached once routing has ended, and then reads that Command's own words, up to the first
|
|
539
|
+
* bare `--`, against the one table it holds.
|
|
540
|
+
* Parsing continues past a fault, so every global option is found wherever it sits, and only the
|
|
541
|
+
* first fault in word order is held. Only an unknown Command throws.
|
|
542
|
+
*/
|
|
543
|
+
function readWords(graph, words, options = {}) {
|
|
544
|
+
const { partial, walked } = options;
|
|
545
|
+
const routing = { graph, walked, words };
|
|
546
|
+
const state = {
|
|
547
|
+
at: 0,
|
|
548
|
+
awaiting: undefined,
|
|
549
|
+
command: graph.root,
|
|
550
|
+
fault: undefined,
|
|
551
|
+
globalFault: false,
|
|
552
|
+
path: [],
|
|
553
|
+
pending: [],
|
|
554
|
+
positionals: [],
|
|
555
|
+
read: 0,
|
|
556
|
+
routed: false,
|
|
557
|
+
supplied: [],
|
|
558
|
+
values: { globals: emptyValues(), locals: emptyValues() },
|
|
559
|
+
};
|
|
560
|
+
const start = route(routing, state);
|
|
561
|
+
if (routingEnded(state, start, { partial, words })) {
|
|
562
|
+
state.routed = true;
|
|
563
|
+
bindPending(state);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
noteHeld(state);
|
|
567
|
+
}
|
|
568
|
+
const tail = readCommandWords(routing, state, start);
|
|
569
|
+
return {
|
|
570
|
+
...state,
|
|
571
|
+
...tail,
|
|
572
|
+
committed: start < words.length && words[start] !== '--',
|
|
573
|
+
supplied: state.supplied.map(({ name }) => name),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/** The failure a held fault reports once every word is read. */
|
|
577
|
+
function heldFailure(read, fault) {
|
|
578
|
+
return 'error' in fault
|
|
579
|
+
? fault.error
|
|
580
|
+
: new UnexpectedArgumentError(read.path, read.command.arguments.length, read.positionals.slice(fault.extra));
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Parses a complete invocation, where a string option still waiting for its value at the end of
|
|
584
|
+
* the words is a missing value.
|
|
585
|
+
*/
|
|
586
|
+
function parseInvocation(graph, argv, walked) {
|
|
587
|
+
const read = readWords(graph, argv, { walked });
|
|
588
|
+
const { awaiting, command, passthrough, path, positionals, values } = read;
|
|
589
|
+
const missing = awaiting && { at: read.read, error: new MissingValueError(awaiting.spelling) };
|
|
590
|
+
const fault = read.fault ?? missing;
|
|
591
|
+
return {
|
|
592
|
+
command,
|
|
593
|
+
fault: fault && heldFailure(read, fault),
|
|
594
|
+
globalFault: read.globalFault || awaiting?.global === true,
|
|
595
|
+
passthrough,
|
|
596
|
+
path,
|
|
597
|
+
positionals,
|
|
598
|
+
values,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
export { argumentSlot, candidatesOf, isOptionWord, parseInvocation, readOptionWord, readWords, refusesValue, };
|
package/dist/plain.d.ts
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs one declaring call with verdicts of its own, dropped when it returns or throws. A call made
|
|
3
|
+
* inside another, such as a `plugin()` a getter makes, is part of the outer read and shares them.
|
|
4
|
+
*/
|
|
5
|
+
declare function declaring<Result>(call: () => Result): Result;
|
|
1
6
|
/**
|
|
2
7
|
* A structural value core reads as plain data: an object literal, and never a declaration that
|
|
3
8
|
* carries state of its own. The options slots read it to reject a value that is not an options
|
|
4
|
-
* object, and
|
|
9
|
+
* object, and `snapshot` reads it to copy a declared value faithfully. A value a declaring call
|
|
10
|
+
* already judged answers with that verdict.
|
|
11
|
+
*/
|
|
12
|
+
declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a declaring call reads one part of a declaration as plain data, decided once: the first
|
|
15
|
+
* verdict is recorded, and `isPlainObject` answers with it for the same value from then on.
|
|
16
|
+
*/
|
|
17
|
+
declare function decidePlain(value: unknown): value is Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
|
|
20
|
+
* consumer cannot reach the source through the copy. A value that holds itself is copied with the
|
|
21
|
+
* same cycle, because each source object is judged and copied once and every path to it reaches
|
|
22
|
+
* that copy. Primitives and library objects, such as a class instance or a `Date` a schema
|
|
23
|
+
* produced, are reported as they are, because core cannot copy them meaningfully. Build reads it
|
|
24
|
+
* for a converter's schema, and the chain for the request one middleware holds.
|
|
25
|
+
*/
|
|
26
|
+
declare function snapshot(value: unknown): unknown;
|
|
27
|
+
/** The snapshot of one plain object, under the record type the caller already established. */
|
|
28
|
+
declare function snapshotRecord(value: Record<string, unknown>): Readonly<Record<string, unknown>>;
|
|
29
|
+
/**
|
|
30
|
+
* The snapshot `snapshot` takes, of a value whose paths may hold at most `levels` arrays and plain
|
|
31
|
+
* objects, the value itself included. Every path through a container the value holds twice counts
|
|
32
|
+
* it, and a value that holds itself has a path without end. Either kind of path past `levels`
|
|
33
|
+
* throws `NestedTooDeepError` before the walk goes deeper than `levels`, so neither the walk nor
|
|
34
|
+
* any later reader of the copy goes deeper either. A declaring call reads it for a declared default.
|
|
35
|
+
*/
|
|
36
|
+
declare function boundedSnapshot(value: unknown, levels: number): unknown;
|
|
37
|
+
/** What `boundedSnapshot` throws for a value with a path longer than its limit. */
|
|
38
|
+
declare class NestedTooDeepError extends Error {
|
|
39
|
+
name: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A copy of every own string key of one object, enumerable or not, each described once and read
|
|
43
|
+
* once, in the order the object lists its keys. `entering` hears each key before it is read, so a
|
|
44
|
+
* read that throws can name it. A symbol key is not copied, because no declaration declares one.
|
|
45
|
+
*/
|
|
46
|
+
declare function copyOwnKeys(declared: object, entering?: (key: string) => void): Record<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* The copy of one part of a declaration that `decidePlain` judges plain, by `copyOwnKeys`. Any other
|
|
49
|
+
* value is answered as it is, so the rule for its slot reports it.
|
|
5
50
|
*/
|
|
6
|
-
|
|
51
|
+
declare function shallowRecord(value: unknown): unknown;
|
|
52
|
+
/** A copy of one list's entries by `fillList`. Each entry is what its factory built, so it is kept. */
|
|
53
|
+
declare function copyList<Entry>(list: readonly Entry[]): Entry[];
|
|
54
|
+
/** The copy of a value that is a list, by `copyList`. Any other value is answered as it is. */
|
|
55
|
+
declare function shallowList(value: unknown): unknown;
|
|
56
|
+
export { boundedSnapshot, copyList, copyOwnKeys, decidePlain, declaring, isPlainObject, NestedTooDeepError, shallowList, shallowRecord, snapshot, snapshotRecord, };
|