@loomcli/core 0.7.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/dist/application.d.ts +11 -6
- package/dist/application.js +11 -5
- 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 +136 -170
- package/dist/errors.d.ts +22 -21
- package/dist/errors.js +38 -18
- package/dist/globals.d.ts +48 -22
- package/dist/globals.js +63 -22
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/input-rules.d.ts +16 -2
- package/dist/input-rules.js +40 -5
- package/dist/inspect.d.ts +35 -11
- package/dist/inspect.js +69 -66
- package/dist/locate.js +52 -60
- package/dist/options.d.ts +94 -83
- package/dist/options.js +298 -260
- package/dist/parse.d.ts +162 -0
- package/dist/parse.js +601 -0
- package/dist/plugin-rules.d.ts +2 -4
- package/dist/plugin-rules.js +3 -8
- package/dist/plugin.d.ts +26 -21
- package/dist/plugin.js +10 -45
- package/dist/sources.d.ts +18 -9
- package/dist/sources.js +46 -16
- package/dist/types.d.ts +68 -28
- package/dist/validation.d.ts +84 -31
- package/dist/validation.js +222 -110
- package/package.json +1 -1
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/plugin-rules.d.ts
CHANGED
|
@@ -19,8 +19,6 @@ declare const pluginInstalledTwice: import("./diagnostic-text.js").DiagnosticRul
|
|
|
19
19
|
declare const slotTaken: import("./diagnostic-text.js").DiagnosticRule;
|
|
20
20
|
/** A plugin, extension, or view identity outside the identity grammar. */
|
|
21
21
|
declare const invalidIdentity: import("./diagnostic-text.js").DiagnosticRule;
|
|
22
|
-
/** A validator or a presence rule on a plugin option. */
|
|
23
|
-
declare const pluginOptionRule: import("./diagnostic-text.js").DiagnosticRule;
|
|
24
22
|
/** A middleware activation that is missing, empty, or names an option the plugin lacks. */
|
|
25
23
|
declare const middlewareActivation: import("./diagnostic-text.js").DiagnosticRule;
|
|
26
24
|
/** A loader, a hook, or a translator that core cannot call. */
|
|
@@ -31,7 +29,7 @@ declare const unknownSignal: import("./diagnostic-text.js").DiagnosticRule;
|
|
|
31
29
|
declare const signalClaimedTwice: import("./diagnostic-text.js").DiagnosticRule;
|
|
32
30
|
/** A configuration source binding that is not one of the plugin's option extensions. */
|
|
33
31
|
declare const sourceBinding: import("./diagnostic-text.js").DiagnosticRule;
|
|
34
|
-
/** A plugin option that carries its own plugin's source binding. */
|
|
32
|
+
/** A plugin's option that carries its own plugin's source binding. */
|
|
35
33
|
declare const sourceBoundOwnOption: import("./diagnostic-text.js").DiagnosticRule;
|
|
36
34
|
/** Two distinct objects under one extension or declared-view identity. */
|
|
37
35
|
declare const twoPackageCopies: import("./diagnostic-text.js").DiagnosticRule;
|
|
@@ -65,4 +63,4 @@ declare const renderingPolicyRule: import("./diagnostic-text.js").DiagnosticRule
|
|
|
65
63
|
declare const themeMapping: import("./diagnostic-text.js").DiagnosticRule;
|
|
66
64
|
/** A theme name that a built-in style member already holds. */
|
|
67
65
|
declare const themeNameTaken: import("./diagnostic-text.js").DiagnosticRule;
|
|
68
|
-
export { asyncExtensionSchema, brokenAttachHook, extensionOutput, extensionTarget, extensionValueTwice, extensionWithoutSchema, foreignValue, invalidExtensionValue, invalidIdentity, invalidPacket, middlewareActivation, notAFunction, notAList, notAnObject, overrideKey, overrideTwice, pluginInstalledTwice,
|
|
66
|
+
export { asyncExtensionSchema, brokenAttachHook, extensionOutput, extensionTarget, extensionValueTwice, extensionWithoutSchema, foreignValue, invalidExtensionValue, invalidIdentity, invalidPacket, middlewareActivation, notAFunction, notAList, notAnObject, overrideKey, overrideTwice, pluginInstalledTwice, renderingPolicyRule, retiredApplicationOption, signalClaimedTwice, slotTaken, sourceBinding, sourceBoundOwnOption, themeMapping, themeNameTaken, translationKey, twoPackageCopies, unknownSignal, unreadableDeclaration, };
|
package/dist/plugin-rules.js
CHANGED
|
@@ -6,7 +6,7 @@ import { registerRule } from './diagnostic-text.js';
|
|
|
6
6
|
*/
|
|
7
7
|
/** A slot that holds a list, such as `plugins` or `commands`, holding a value of another kind. */
|
|
8
8
|
const notAList = registerRule('@loomcli/core/not-a-list', {
|
|
9
|
-
explanation: 'Core reads plugins, commands, extensions, views, translators, and
|
|
9
|
+
explanation: 'Core reads plugins, commands, extensions, views, translators, signals, and aliases each as a list, in order. A value of any other kind has no entries to read.',
|
|
10
10
|
headline: 'Not a list',
|
|
11
11
|
});
|
|
12
12
|
/**
|
|
@@ -46,11 +46,6 @@ const invalidIdentity = registerRule('@loomcli/core/invalid-identity', {
|
|
|
46
46
|
explanation: 'An identity keys what a plugin, an extension, or a view contributes, names it in every diagnostic, and prefixes the identities of the rules its package declares, so it is a package name as npm spells one, scoped or not, then any subpath segments, each after a / and each of lowercase letters and digits in words joined by single hyphens.',
|
|
47
47
|
headline: 'Invalid identity',
|
|
48
48
|
});
|
|
49
|
-
/** A validator or a presence rule on a plugin option. */
|
|
50
|
-
const pluginOptionRule = registerRule('@loomcli/core/plugin-option-rule', {
|
|
51
|
-
explanation: "A plugin's middleware interprets its own options' values, so a plugin option declares how it parses and nothing more: no validator and no presence rule.",
|
|
52
|
-
headline: 'Rule on a plugin option',
|
|
53
|
-
});
|
|
54
49
|
/** A middleware activation that is missing, empty, or names an option the plugin lacks. */
|
|
55
50
|
const middlewareActivation = registerRule('@loomcli/core/middleware-activation', {
|
|
56
51
|
explanation: "Activation decides when core loads a plugin's middleware: on every run with 'always', or only when an invocation supplies one of the plugin's own options the list names, so a middleware no invocation needs costs it nothing.",
|
|
@@ -76,7 +71,7 @@ const sourceBinding = registerRule('@loomcli/core/source-binding', {
|
|
|
76
71
|
explanation: 'A configuration source answers the options that carry its binding, an extension the plugin lists under extensions that applies to options. Core asks the source about those options without knowing what the binding means.',
|
|
77
72
|
headline: 'Invalid source binding',
|
|
78
73
|
});
|
|
79
|
-
/** A plugin option that carries its own plugin's source binding. */
|
|
74
|
+
/** A plugin's option that carries its own plugin's source binding. */
|
|
80
75
|
const sourceBoundOwnOption = registerRule('@loomcli/core/source-bound-own-option', {
|
|
81
76
|
explanation: "A plugin's own options resolve before its configuration source loads, because the source reads them, so none of them can take a value from that source.",
|
|
82
77
|
headline: 'Source bound to its own option',
|
|
@@ -161,4 +156,4 @@ const themeNameTaken = registerRule('@loomcli/core/theme-name-taken', {
|
|
|
161
156
|
explanation: 'Each theme name becomes a member of the style object beside the built-in members, so a name a built-in already holds would hide it.',
|
|
162
157
|
headline: 'Theme name taken',
|
|
163
158
|
});
|
|
164
|
-
export { asyncExtensionSchema, brokenAttachHook, extensionOutput, extensionTarget, extensionValueTwice, extensionWithoutSchema, foreignValue, invalidExtensionValue, invalidIdentity, invalidPacket, middlewareActivation, notAFunction, notAList, notAnObject, overrideKey, overrideTwice, pluginInstalledTwice,
|
|
159
|
+
export { asyncExtensionSchema, brokenAttachHook, extensionOutput, extensionTarget, extensionValueTwice, extensionWithoutSchema, foreignValue, invalidExtensionValue, invalidIdentity, invalidPacket, middlewareActivation, notAFunction, notAList, notAnObject, overrideKey, overrideTwice, pluginInstalledTwice, renderingPolicyRule, retiredApplicationOption, signalClaimedTwice, slotTaken, sourceBinding, sourceBoundOwnOption, themeMapping, themeNameTaken, translationKey, twoPackageCopies, unknownSignal, unreadableDeclaration, };
|