@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/plugin.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { checkEnvBinding, claimVariables } from './bindings.js';
|
|
2
|
+
import { captureDeclaration, elidedRead, unreadableArgument, unreadableFault } from './capture.js';
|
|
2
3
|
import { notACommand } from './command-rules.js';
|
|
3
4
|
import { attach, commandCode, commandNode } from './command.js';
|
|
4
5
|
import { elided, quoteString, spelled } from './diagnostic-text.js';
|
|
5
6
|
import { DeclarationError, InternalError, quoted, reasonOf } from './errors.js';
|
|
6
7
|
import { appliesTo, buildExtensions, isDescriptor, registerDescriptor } from './extension.js';
|
|
7
|
-
import { checkDeprecated, checkDescription, checkHidden,
|
|
8
|
-
import { boundOptions, pluginSites } from './globals.js';
|
|
8
|
+
import { checkDeprecated, checkDescription, checkHidden, partFinding, pluginOptionSite, slotSite, } from './facts.js';
|
|
9
|
+
import { boundOptions, checkNoPresenceRule, pluginSites } from './globals.js';
|
|
9
10
|
import { checkIdentity } from './identity.js';
|
|
10
11
|
import { coreViews } from './lanes.js';
|
|
11
|
-
import {
|
|
12
|
-
import { isPlainObject } from './plain.js';
|
|
13
|
-
import { foreignValue, middlewareActivation, notAFunction, notAList, notAnObject, pluginInstalledTwice,
|
|
12
|
+
import { compileOptions } from './options.js';
|
|
13
|
+
import { copyOwnKeys, decidePlain, declaring, isPlainObject, shallowList, shallowRecord, } from './plain.js';
|
|
14
|
+
import { foreignValue, middlewareActivation, notAFunction, notAList, notAnObject, pluginInstalledTwice, signalClaimedTwice, slotTaken, sourceBinding, sourceBoundOwnOption, unknownSignal, } from './plugin-rules.js';
|
|
14
15
|
import { pluginLoaderFailed } from './rules.js';
|
|
15
16
|
import { isProcessSignal } from './signals.js';
|
|
16
17
|
import { buildTheme } from './theme.js';
|
|
17
18
|
import { readTranslations } from './translators.js';
|
|
18
|
-
import { captureConfig, checkDeclarations } from './validation.js';
|
|
19
|
+
import { captureConfig, checkDeclarations, defaultDepthFault } from './validation.js';
|
|
19
20
|
import { buildViews, viewIdentities } from './view.js';
|
|
21
|
+
/** The slots of a definition that hold a list, each copied with its entries as they were built. */
|
|
22
|
+
const definitionLists = ['extensions', 'signals', 'commands', 'views', 'translators'];
|
|
20
23
|
/** Authored values register here, so the public type publishes no state to reach or replace. */
|
|
21
24
|
const nodes = new WeakMap();
|
|
22
25
|
/**
|
|
@@ -34,17 +37,47 @@ class PluginDeclaration {
|
|
|
34
37
|
* One plugin: an identity and the contributions it carries. Creating and installing the value runs
|
|
35
38
|
* none of its code: `onCommandAttach` runs at graph build, `onFailure` runs when `run()` renders a
|
|
36
39
|
* failure, and the middleware runs inside an invocation, so an installed plugin an invocation never
|
|
37
|
-
* reaches costs that invocation its hooks alone.
|
|
38
|
-
* that one definition carries on its own throws here, before the value exists.
|
|
40
|
+
* reaches costs that invocation its hooks alone. The definition is read once, after the identity,
|
|
41
|
+
* and every rule that one definition carries on its own throws here, before the value exists.
|
|
39
42
|
*/
|
|
40
43
|
function plugin(identity, definition) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
return new PluginDeclaration(declaring(() => readPlugin(identity, definition)));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The one copy of a definition that `plugin()` reads: the definition, its theme mapping, options
|
|
48
|
+
* record, middleware with its activation list, and source, and each list it holds. An entry of a
|
|
49
|
+
* list is what its factory built and is not copied, and each option's config is captured where its
|
|
50
|
+
* own rules read it, so a fault about it names the option. A read that throws is the unreadable
|
|
51
|
+
* fault of the definition, and a definition that is not a plain object is the not-an-object fault.
|
|
52
|
+
*/
|
|
53
|
+
function captureDefinition(identity, definition) {
|
|
54
|
+
return captureDeclaration(definition, (copy, read) => {
|
|
55
|
+
read.nested(copy, 'theme', shallowRecord);
|
|
56
|
+
read.nested(copy, 'options', shallowRecord);
|
|
57
|
+
read.nested(copy, 'middleware', copyMiddleware);
|
|
58
|
+
read.nested(copy, 'source', shallowRecord);
|
|
59
|
+
for (const list of definitionLists) {
|
|
60
|
+
read.nested(copy, list, shallowList);
|
|
61
|
+
}
|
|
62
|
+
}, {
|
|
63
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
64
|
+
correction: 'Supply { options, middleware, extensions, views }.',
|
|
65
|
+
findings: [{ arguments: [identity, definition], call: 'plugin', mark: '1' }],
|
|
66
|
+
sentence: `${pluginSentence(identity)} declares a definition that is not an object.`,
|
|
67
|
+
}),
|
|
68
|
+
unreadable: unreadableArgument({ call: 'plugin', named: identity, subject: pluginSentence(identity) }, 'definition'),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** A middleware object's copy, its activation list copied too, or any other value as declared. */
|
|
72
|
+
function copyMiddleware(middleware) {
|
|
73
|
+
if (!decidePlain(middleware)) {
|
|
74
|
+
return middleware;
|
|
75
|
+
}
|
|
76
|
+
const copy = copyOwnKeys(middleware);
|
|
77
|
+
if (Object.hasOwn(copy, 'activate')) {
|
|
78
|
+
copy.activate = shallowList(copy.activate);
|
|
79
|
+
}
|
|
80
|
+
return copy;
|
|
48
81
|
}
|
|
49
82
|
/** How every plugin diagnostic names one plugin at the start of a sentence. */
|
|
50
83
|
function pluginSentence(identity) {
|
|
@@ -71,17 +104,6 @@ function entryCode(value) {
|
|
|
71
104
|
const node = nodeOf(value);
|
|
72
105
|
return node ? spelled(`plugin(${quoteString(node.identity)}, ${elided})`) : value;
|
|
73
106
|
}
|
|
74
|
-
/** The declarations one plugin value carries, which a JavaScript author reaches as any value. */
|
|
75
|
-
function definitionOf(identity, definition) {
|
|
76
|
-
if (!isPlainObject(definition)) {
|
|
77
|
-
throw new DeclarationError(notAnObject, {
|
|
78
|
-
correction: 'Supply { options, middleware, extensions, views }.',
|
|
79
|
-
findings: [{ arguments: [identity, definition], call: 'plugin', mark: '1' }],
|
|
80
|
-
sentence: `${pluginSentence(identity)} declares a definition that is not an object.`,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return definition;
|
|
84
|
-
}
|
|
85
107
|
/** How a second claim on one slot reads: its clause, the note on the owner's entry, and the fix. */
|
|
86
108
|
const slotWords = {
|
|
87
109
|
signals: {
|
|
@@ -116,9 +138,9 @@ function slotFault(site, slot, claim) {
|
|
|
116
138
|
/**
|
|
117
139
|
* The installed list in composition order, with every rule that reads two plugins together: an
|
|
118
140
|
* identity installed twice, a second claim on the theme slot, the signals slot, or the
|
|
119
|
-
* configuration source, and two distinct descriptors under one identity. The slot is
|
|
120
|
-
*
|
|
121
|
-
* already ran at its `plugin()` call.
|
|
141
|
+
* configuration source, and two distinct descriptors under one identity. The slot is the copy the
|
|
142
|
+
* Application's constructor took, which a JavaScript author fills with any value. Each plugin's own
|
|
143
|
+
* rules already ran at its `plugin()` call.
|
|
122
144
|
*/
|
|
123
145
|
function installPlugins(application, plugins) {
|
|
124
146
|
const printed = Array.isArray(plugins) ? Array.from(plugins, entryCode) : plugins;
|
|
@@ -186,29 +208,38 @@ function installPlugins(application, plugins) {
|
|
|
186
208
|
}
|
|
187
209
|
return { descriptors, plugins: installed };
|
|
188
210
|
}
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
211
|
+
/**
|
|
212
|
+
* The rules a plugin's option answers before every rule an ordinary declaration carries, read from
|
|
213
|
+
* the one copy of its config that `captureConfig` takes, which it answers with for every later read.
|
|
214
|
+
* `siteOf` places the option with the value its entry prints as: the config as declared for a value
|
|
215
|
+
* that is not one, elided for a config whose read threw, and the copy for every later rule.
|
|
216
|
+
*/
|
|
217
|
+
function checkPluginOption(siteOf, declared) {
|
|
218
|
+
const sentence = siteOf(declared).subject;
|
|
219
|
+
const config = captureConfig(declared, {
|
|
220
|
+
notAnObject: () => new DeclarationError(notAnObject, {
|
|
196
221
|
correction: 'Supply { type, ... }.',
|
|
197
|
-
findings: [partFinding(
|
|
222
|
+
findings: [partFinding(siteOf(declared), [])],
|
|
198
223
|
sentence: `${sentence} is not an option declaration.`,
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
224
|
+
}),
|
|
225
|
+
// The default's walk stopped at the limit, so the finding prints it elided.
|
|
226
|
+
tooDeep: () => {
|
|
227
|
+
const { keys, shown } = elidedRead('default');
|
|
228
|
+
return defaultDepthFault(sentence, [partFinding(siteOf(shown), keys)]);
|
|
229
|
+
},
|
|
230
|
+
// A part whose read threw is never read again, so the finding prints it elided.
|
|
231
|
+
unreadable: (thrown, slot) => {
|
|
232
|
+
const { keys, shown } = elidedRead(slot);
|
|
233
|
+
return unreadableFault({ declared: 'config', findings: [partFinding(siteOf(shown), keys)], subject: sentence }, thrown);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
const site = siteOf(config);
|
|
237
|
+
// A plugin's options are global options, so they answer the global option's presence rule.
|
|
238
|
+
checkNoPresenceRule(site, config);
|
|
209
239
|
checkDescription(site, config.description);
|
|
210
240
|
checkHidden(site, config.hidden);
|
|
211
241
|
checkDeprecated(site, config.deprecated);
|
|
242
|
+
return config;
|
|
212
243
|
}
|
|
213
244
|
/**
|
|
214
245
|
* One plugin's option declarations, in declaration order. They join the globals table, so the rules
|
|
@@ -223,12 +254,23 @@ function readOptions(identity, declared, build) {
|
|
|
223
254
|
});
|
|
224
255
|
}
|
|
225
256
|
const inputs = [];
|
|
226
|
-
|
|
257
|
+
// A finding prints each option's captured config once it is taken, and each later one elided.
|
|
258
|
+
const shown = Object.fromEntries(Object.keys(declared ?? {}).map((name) => [name, spelled(elided)]));
|
|
259
|
+
for (const [name, entry] of Object.entries(declared ?? {})) {
|
|
227
260
|
const sentence = `${pluginSentence(identity)} option ${quoted(name)}`;
|
|
228
|
-
|
|
229
|
-
|
|
261
|
+
// A computed key defines its own property even for `__proto__`, where assignment would not.
|
|
262
|
+
const siteOf = (value) => pluginOptionSite({ identity, options: { ...shown, [name]: value } }, name, sentence);
|
|
263
|
+
const config = checkPluginOption(siteOf, entry);
|
|
264
|
+
// Defining the key keeps its place, and holds even for a key of `__proto__`.
|
|
265
|
+
Object.defineProperty(shown, name, {
|
|
266
|
+
configurable: true,
|
|
267
|
+
enumerable: true,
|
|
268
|
+
value: config,
|
|
269
|
+
writable: true,
|
|
270
|
+
});
|
|
271
|
+
const site = siteOf(config);
|
|
230
272
|
checkEnvBinding(site, config);
|
|
231
|
-
const input = { config
|
|
273
|
+
const input = { config, kind: 'option', name };
|
|
232
274
|
// The shared rules name the plugin and the option, so a fault reads with its contributor.
|
|
233
275
|
checkDeclarations([{ input, site }], sentence);
|
|
234
276
|
build.records.set(input, buildExtensions({
|
|
@@ -243,11 +285,12 @@ function readOptions(identity, declared, build) {
|
|
|
243
285
|
}));
|
|
244
286
|
inputs.push(input);
|
|
245
287
|
}
|
|
246
|
-
// Two options of one plugin meet in the
|
|
288
|
+
// Two options of one plugin meet in the globals table every Command's table holds.
|
|
289
|
+
// They therefore share its rules.
|
|
247
290
|
const siteOf = pluginSites(identity, inputs);
|
|
248
291
|
compileOptions(inputs, { siteOf, subject: `plugin ${quoted(identity)}` });
|
|
249
292
|
claimVariables(boundOptions(inputs, (input) => ({
|
|
250
|
-
phrase: `plugin ${quoted(identity)} option
|
|
293
|
+
phrase: `plugin ${quoted(identity)} option ${quoted(input.name)}`,
|
|
251
294
|
site: siteOf(input),
|
|
252
295
|
})));
|
|
253
296
|
return inputs;
|
|
@@ -532,7 +575,8 @@ function readSource(identity, declaration, own) {
|
|
|
532
575
|
}
|
|
533
576
|
const carrier = own.inputs.find((input) => Object.hasOwn(own.build.records.get(input) ?? {}, bindingIdentity));
|
|
534
577
|
if (carrier) {
|
|
535
|
-
|
|
578
|
+
// The record prints each option's captured config, which is what its rules read.
|
|
579
|
+
const option = pluginSites(identity, own.inputs)(carrier);
|
|
536
580
|
throw new DeclarationError(sourceBoundOwnOption, {
|
|
537
581
|
correction: "Remove the value; the source's own options resolve before it loads.",
|
|
538
582
|
findings: [partFinding(option, ['extensions'])],
|
|
@@ -552,7 +596,8 @@ function defineExtensions(identity, declaration, build) {
|
|
|
552
596
|
sentence: `${pluginSentence(identity)} declares extensions that are not an array.`,
|
|
553
597
|
});
|
|
554
598
|
}
|
|
555
|
-
|
|
599
|
+
const list = Array.isArray(extensions) ? extensions : [];
|
|
600
|
+
for (const [index, descriptor] of list.entries()) {
|
|
556
601
|
if (!isDescriptor(descriptor)) {
|
|
557
602
|
throw new DeclarationError(foreignValue, {
|
|
558
603
|
correction: 'Supply the value returned by extension(identity, config).',
|
|
@@ -567,14 +612,15 @@ function defineExtensions(identity, declaration, build) {
|
|
|
567
612
|
}
|
|
568
613
|
}
|
|
569
614
|
/**
|
|
570
|
-
* Every rule one definition carries on its own, in the order the definition's slots are read
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
615
|
+
* Every rule one definition carries on its own, in the order the definition's slots are read, each
|
|
616
|
+
* from the one copy `captureDefinition` takes once the identity is judged. The plugin's own
|
|
617
|
+
* extensions register before any declaration carries a value, so a duplicated package copy is
|
|
618
|
+
* reported from the list that defines it. The views list is read against core's view identities,
|
|
619
|
+
* and the Application reads the same copy again against every other contributor's.
|
|
574
620
|
*/
|
|
575
621
|
function readPlugin(named, definition) {
|
|
576
622
|
checkIdentity('plugin', named);
|
|
577
|
-
const declaration =
|
|
623
|
+
const declaration = captureDefinition(named, definition);
|
|
578
624
|
const theme = declaration.theme === undefined ? undefined : buildTheme(declaration.theme, named);
|
|
579
625
|
const build = { descriptors: new Map(), records: new Map() };
|
|
580
626
|
defineExtensions(named, declaration, build);
|
|
@@ -608,34 +654,6 @@ function readPlugin(named, definition) {
|
|
|
608
654
|
function ownedSignals(plugins) {
|
|
609
655
|
return plugins.find((entry) => entry.signals.length > 0)?.signals ?? [];
|
|
610
656
|
}
|
|
611
|
-
/** A collected value, or the declared array default, as this run's own copy. */
|
|
612
|
-
function collectedValue(collected, declared) {
|
|
613
|
-
if (collected) {
|
|
614
|
-
return [...collected];
|
|
615
|
-
}
|
|
616
|
-
return Array.isArray(declared) ? [...declared] : [];
|
|
617
|
-
}
|
|
618
|
-
/** One plugin option's value for one run: what a tier supplied, or the declared default. */
|
|
619
|
-
function pluginValue({ config, name }, values) {
|
|
620
|
-
const declared = config.default;
|
|
621
|
-
if (config.type === 'boolean') {
|
|
622
|
-
return booleanValue(values, name, config);
|
|
623
|
-
}
|
|
624
|
-
if (config.multiple === true) {
|
|
625
|
-
return collectedValue(values.lists.get(name), declared);
|
|
626
|
-
}
|
|
627
|
-
// Build already proved that a string option without a validator declares a string default.
|
|
628
|
-
return values.strings.get(name) ?? (typeof declared === 'string' ? declared : undefined);
|
|
629
|
-
}
|
|
630
|
-
/**
|
|
631
|
-
* One plugin's own option values for one run: what argv or an input source supplied, or the
|
|
632
|
-
* declared default, filled without validation. A collected value and an array default are copied,
|
|
633
|
-
* so a plugin that writes to what it received changes neither the declaration nor the next run.
|
|
634
|
-
* Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
635
|
-
*/
|
|
636
|
-
function pluginValues(inputs, values) {
|
|
637
|
-
return Object.fromEntries(inputs.map((input) => [input.name, pluginValue(input, values)]));
|
|
638
|
-
}
|
|
639
657
|
/** One plugin's own spellings for one run, frozen, so no plugin writes what another reads. */
|
|
640
658
|
function pluginSpellings(inputs, values) {
|
|
641
659
|
// Entries become own keys even for a name such as `__proto__`, which assignment would not.
|
|
@@ -645,4 +663,4 @@ function pluginSpellings(inputs, values) {
|
|
|
645
663
|
});
|
|
646
664
|
return Object.freeze(Object.fromEntries(supplied));
|
|
647
665
|
}
|
|
648
|
-
export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews,
|
|
666
|
+
export { installPlugins, loadDefault, ownedSignals, plugin, pluginSentence, pluginSpellings, pluginViews, };
|
package/dist/sources.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { OptionValues } from './options.js';
|
|
|
5
5
|
import type { BuiltPlugin } from './plugin.js';
|
|
6
6
|
import type { ContextualStyle } from './style.js';
|
|
7
7
|
import type { Host, Out } from './types.js';
|
|
8
|
-
import type { OptionInput } from './validation.js';
|
|
8
|
+
import type { OptionInput, Provenance, Validation } from './validation.js';
|
|
9
9
|
/**
|
|
10
10
|
* One scope the stage fills: its options in declaration order, and the values argv supplied them.
|
|
11
11
|
* The stage writes each fill into `values`, so the caller hands it the run's own copy.
|
|
@@ -35,23 +35,32 @@ interface SourceStage {
|
|
|
35
35
|
signal: AbortSignal;
|
|
36
36
|
/** The contextual style an action receives, so a source escapes raw data before it warns. */
|
|
37
37
|
style: ContextualStyle;
|
|
38
|
+
/**
|
|
39
|
+
* Validates the listed options alone, from the values the stage has filled so far, with what the
|
|
40
|
+
* environment tier found about them, under the context every validator of the run reads. A
|
|
41
|
+
* configuration source's own options pass through it before the source loads.
|
|
42
|
+
*/
|
|
43
|
+
validate: (inputs: readonly OptionInput[], provenance: Provenance) => Promise<Validation>;
|
|
38
44
|
}
|
|
39
45
|
/**
|
|
40
46
|
* What the stage found beside the values it filled. `labels` names where each filled option's value
|
|
41
|
-
* came from, by option name, and `rejected` names the variable of each Boolean
|
|
42
|
-
* is outside
|
|
47
|
+
* came from, by option name, and `rejected` names the variable of each Boolean or counted option
|
|
48
|
+
* whose value is outside its grammar. Both are internal to core's failure messages. `fault` is a configuration
|
|
43
49
|
* source's own fault, or the failure its resolver threw, which stops the stage and takes the place
|
|
44
|
-
* of every validation problem.
|
|
50
|
+
* of every validation problem. `validated` is the pass over the source's own options ahead of its
|
|
51
|
+
* call, whose values and problems the run's one validation pass reads.
|
|
45
52
|
*/
|
|
46
|
-
interface SourceOutcome {
|
|
53
|
+
interface SourceOutcome extends Provenance {
|
|
47
54
|
fault: LoomError | undefined;
|
|
48
|
-
|
|
49
|
-
rejected: ReadonlyMap<string, string>;
|
|
55
|
+
validated: Validation | undefined;
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
52
58
|
* The input-source stage: the environment, then the configuration source, for every option in
|
|
53
|
-
* scope that argv left unfilled. When no option is left to ask, the source never loads.
|
|
54
|
-
*
|
|
59
|
+
* scope that argv left unfilled. When no option is left to ask, the source never loads. The
|
|
60
|
+
* source's own options pass their validators before it loads, and when one is rejected the source
|
|
61
|
+
* is never called and the problem reports with every other validation problem, while each option
|
|
62
|
+
* the source would have been asked to fill reports no missing value. A source fault stops the
|
|
63
|
+
* stage and fills nothing more.
|
|
55
64
|
*/
|
|
56
65
|
declare function fillInputs(stage: SourceStage): Promise<SourceOutcome>;
|
|
57
66
|
export type { SourceOutcome, SourceStage, StageScope };
|
package/dist/sources.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { asSentence, foreignFailure, InternalError, LoomError, reasonOf } from './errors.js';
|
|
1
|
+
import { asSentence, foreignFailure, InternalError, LoomError, quoted, reasonOf, } from './errors.js';
|
|
2
2
|
import { isProseLine } from './facts.js';
|
|
3
|
+
import { frozenValues } from './globals.js';
|
|
3
4
|
import { isSupplied } from './options.js';
|
|
4
5
|
import { isPlainObject } from './plain.js';
|
|
5
|
-
import { loadDefault, pluginSentence
|
|
6
|
+
import { loadDefault, pluginSentence } from './plugin.js';
|
|
6
7
|
import { foreignThrow, foreignThrowCorrection, sourceAnswers, sourceAnswersCorrection, } from './rules.js';
|
|
7
8
|
/** The Boolean grammar a bound variable is read through: the whole value, case-insensitive. */
|
|
8
9
|
const truths = new Map([
|
|
@@ -11,10 +12,13 @@ const truths = new Map([
|
|
|
11
12
|
['false', false],
|
|
12
13
|
['true', true],
|
|
13
14
|
]);
|
|
15
|
+
/** The count grammar a bound variable is read through: the whole value, one or more ASCII digits. */
|
|
16
|
+
const countText = /^[0-9]+$/u;
|
|
14
17
|
/**
|
|
15
|
-
* Reads one option's bound variable. An empty variable is unset and falls through, a string
|
|
16
|
-
* receives the raw string,
|
|
17
|
-
* value states the option's value and not a spelling
|
|
18
|
+
* Reads one option's bound variable. An empty variable is unset and falls through, and a string
|
|
19
|
+
* option receives the raw string, never its implied value. A Boolean option reads the whole value
|
|
20
|
+
* through the grammar, so the value states the option's value and not a spelling, and a counted
|
|
21
|
+
* option reads the whole value as decimal digits.
|
|
18
22
|
*/
|
|
19
23
|
function readVariable(input, env) {
|
|
20
24
|
const variable = input.config.env;
|
|
@@ -25,6 +29,9 @@ function readVariable(input, env) {
|
|
|
25
29
|
if (input.config.type === 'string') {
|
|
26
30
|
return { kind: 'fill', value: raw };
|
|
27
31
|
}
|
|
32
|
+
if (input.config.type === 'count') {
|
|
33
|
+
return countText.test(raw) ? { kind: 'fill', value: Number(raw) } : { kind: 'rejected' };
|
|
34
|
+
}
|
|
28
35
|
const value = truths.get(raw.toLowerCase());
|
|
29
36
|
return value === undefined ? { kind: 'rejected' } : { kind: 'fill', value };
|
|
30
37
|
}
|
|
@@ -33,6 +40,9 @@ function fill(values, name, value) {
|
|
|
33
40
|
if (typeof value === 'boolean') {
|
|
34
41
|
values.booleans.set(name, value);
|
|
35
42
|
}
|
|
43
|
+
else if (typeof value === 'number') {
|
|
44
|
+
values.counts.set(name, value);
|
|
45
|
+
}
|
|
36
46
|
else if (typeof value === 'string') {
|
|
37
47
|
values.strings.set(name, value);
|
|
38
48
|
}
|
|
@@ -43,12 +53,13 @@ function fill(values, name, value) {
|
|
|
43
53
|
/** How a fault names the value an answer owed, by the shape the option takes. */
|
|
44
54
|
const owed = {
|
|
45
55
|
boolean: 'a Boolean',
|
|
56
|
+
count: 'a whole number of 0 or more',
|
|
46
57
|
list: 'an array of strings',
|
|
47
58
|
string: 'a string',
|
|
48
59
|
};
|
|
49
60
|
function rawKind(input) {
|
|
50
|
-
if (input.config.type
|
|
51
|
-
return
|
|
61
|
+
if (input.config.type !== 'string') {
|
|
62
|
+
return input.config.type;
|
|
52
63
|
}
|
|
53
64
|
return input.config.multiple === true ? 'list' : 'string';
|
|
54
65
|
}
|
|
@@ -78,6 +89,9 @@ function rawValue(kind, value) {
|
|
|
78
89
|
if (kind === 'boolean') {
|
|
79
90
|
return typeof value === 'boolean' ? value : undefined;
|
|
80
91
|
}
|
|
92
|
+
if (kind === 'count') {
|
|
93
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
94
|
+
}
|
|
81
95
|
return typeof value === 'string' ? value : undefined;
|
|
82
96
|
}
|
|
83
97
|
/**
|
|
@@ -100,12 +114,12 @@ function readAnswer(sentence, input, answer) {
|
|
|
100
114
|
const shape = isPlainObject(answer) ? answer : undefined;
|
|
101
115
|
const label = shape?.label;
|
|
102
116
|
if (!shape || !isProseLine(label)) {
|
|
103
|
-
throw ruleFault(`${sentence} answered option
|
|
117
|
+
throw ruleFault(`${sentence} answered option ${quoted(input.name)} with an answer that is not { value, label }.`);
|
|
104
118
|
}
|
|
105
119
|
const kind = rawKind(input);
|
|
106
120
|
const value = rawValue(kind, shape.value);
|
|
107
121
|
if (value === undefined) {
|
|
108
|
-
throw ruleFault(`${sentence} answered option
|
|
122
|
+
throw ruleFault(`${sentence} answered option ${quoted(input.name)} with a value that is not ${owed[kind]}.`);
|
|
109
123
|
}
|
|
110
124
|
return { label, value };
|
|
111
125
|
}
|
|
@@ -121,7 +135,7 @@ function readAnswers(sentence, answers, requested) {
|
|
|
121
135
|
return Object.entries(answers).map(([name, answer]) => {
|
|
122
136
|
const target = byName.get(name);
|
|
123
137
|
if (!target) {
|
|
124
|
-
throw ruleFault(`${sentence} answered option
|
|
138
|
+
throw ruleFault(`${sentence} answered option ${quoted(name)}, which core did not request.`);
|
|
125
139
|
}
|
|
126
140
|
const { label, value } = readAnswer(sentence, target.input, answer);
|
|
127
141
|
return { label, target, value };
|
|
@@ -150,7 +164,7 @@ function sourceFailure(sentence, error) {
|
|
|
150
164
|
* Core builds the context before the call, so a fault of its own is never the plugin's.
|
|
151
165
|
*/
|
|
152
166
|
async function askSource(stage, call) {
|
|
153
|
-
const { owner, requested } = call;
|
|
167
|
+
const { options, owner, requested } = call;
|
|
154
168
|
const resolver = await loadDefault(owner.identity, owner.source.load, {
|
|
155
169
|
guard: isResolverExport,
|
|
156
170
|
noun: 'source',
|
|
@@ -162,7 +176,8 @@ async function askSource(stage, call) {
|
|
|
162
176
|
const context = {
|
|
163
177
|
graph: stage.inspected(),
|
|
164
178
|
host: stage.host,
|
|
165
|
-
|
|
179
|
+
// Validation produced the record the plugin's option types describe.
|
|
180
|
+
options,
|
|
166
181
|
out: stage.out,
|
|
167
182
|
requests: requested.map(({ input, scope }) => stage.request(input, scope.global)),
|
|
168
183
|
style: stage.style,
|
|
@@ -193,8 +208,8 @@ async function askSource(stage, call) {
|
|
|
193
208
|
}
|
|
194
209
|
/**
|
|
195
210
|
* The environment tier: each option in scope that argv left unfilled reads its bound variable. A
|
|
196
|
-
* filled option is labeled with its variable, and a Boolean one outside
|
|
197
|
-
* as rejected, which fills nothing.
|
|
211
|
+
* filled option is labeled with its variable, and a Boolean or counted one outside its grammar is
|
|
212
|
+
* recorded as rejected, which fills nothing.
|
|
198
213
|
*/
|
|
199
214
|
function fillFromEnvironment(scopes, env) {
|
|
200
215
|
const labels = new Map();
|
|
@@ -228,8 +243,11 @@ function requestedOf(scopes, excluded, bound) {
|
|
|
228
243
|
}
|
|
229
244
|
/**
|
|
230
245
|
* The input-source stage: the environment, then the configuration source, for every option in
|
|
231
|
-
* scope that argv left unfilled. When no option is left to ask, the source never loads.
|
|
232
|
-
*
|
|
246
|
+
* scope that argv left unfilled. When no option is left to ask, the source never loads. The
|
|
247
|
+
* source's own options pass their validators before it loads, and when one is rejected the source
|
|
248
|
+
* is never called and the problem reports with every other validation problem, while each option
|
|
249
|
+
* the source would have been asked to fill reports no missing value. A source fault stops the
|
|
250
|
+
* stage and fills nothing more.
|
|
233
251
|
*/
|
|
234
252
|
async function fillInputs(stage) {
|
|
235
253
|
const scopes = stage.locals ? [stage.globals, stage.locals] : [stage.globals];
|
|
@@ -239,20 +257,32 @@ async function fillInputs(stage) {
|
|
|
239
257
|
? requestedOf(scopes, rejected, { binding: owner.source.binding, extensions: stage.extensions })
|
|
240
258
|
: [];
|
|
241
259
|
if (!owner || requested.length === 0) {
|
|
242
|
-
return { fault: undefined, labels, rejected };
|
|
260
|
+
return { fault: undefined, labels, rejected, validated: undefined };
|
|
243
261
|
}
|
|
262
|
+
let validated = undefined;
|
|
244
263
|
try {
|
|
245
|
-
|
|
264
|
+
validated = await stage.validate(owner.inputs, { labels, rejected });
|
|
265
|
+
// A rejected own option skips the source, and the options it would have filled report no omission.
|
|
266
|
+
if (validated.failure) {
|
|
267
|
+
const unanswered = new Set(requested.map(({ input }) => input));
|
|
268
|
+
return { fault: undefined, labels, rejected, unanswered, validated };
|
|
269
|
+
}
|
|
270
|
+
// A run cancelled while the source's own options were validated loads nothing.
|
|
271
|
+
if (stage.signal.aborted) {
|
|
272
|
+
return { fault: undefined, labels, rejected, validated };
|
|
273
|
+
}
|
|
274
|
+
const options = frozenValues(owner.inputs, validated.values);
|
|
275
|
+
for (const { label, target, value } of await askSource(stage, { options, owner, requested })) {
|
|
246
276
|
fill(target.scope.values, target.input.name, value);
|
|
247
277
|
labels.set(target.input.name, label);
|
|
248
278
|
}
|
|
249
|
-
return { fault: undefined, labels, rejected };
|
|
279
|
+
return { fault: undefined, labels, rejected, validated };
|
|
250
280
|
}
|
|
251
281
|
catch (error) {
|
|
252
282
|
// The resolver's own failure keeps its class, and so its code.
|
|
253
283
|
// Every other fault above is raised as an internal error, and anything else is wrapped the same way.
|
|
254
284
|
const fault = error instanceof LoomError ? error : foreignFailure(error);
|
|
255
|
-
return { fault, labels, rejected };
|
|
285
|
+
return { fault, labels, rejected, validated };
|
|
256
286
|
}
|
|
257
287
|
}
|
|
258
288
|
export { fillInputs };
|
package/dist/style-layout.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { graphemeWidths } from '
|
|
1
|
+
import { graphemeWidths } from './style-width.js';
|
|
2
2
|
const offsets = [0, 1, 2, 3, 4, 5, 6, 7];
|
|
3
3
|
const empty = {
|
|
4
4
|
advance: offsets.map(() => 0),
|
|
@@ -117,6 +117,7 @@ function padBlock(block, padding, attributes) {
|
|
|
117
117
|
if (!block.tabs && block.minimum >= padding.width) {
|
|
118
118
|
return block;
|
|
119
119
|
}
|
|
120
|
+
const space = (count) => leaf({ attributes, kind: 'text', text: ' '.repeat(count) }, count);
|
|
120
121
|
const lines = block.lines.map((line, index) => {
|
|
121
122
|
// A terminal newline has no extra line to align; interior blank lines do.
|
|
122
123
|
if (index > 0 && index === block.lines.length - 1 && !line.content.text) {
|
|
@@ -134,7 +135,6 @@ function padBlock(block, padding, attributes) {
|
|
|
134
135
|
else if (padding.align === 'center') {
|
|
135
136
|
before = Math.floor(missing / 2);
|
|
136
137
|
}
|
|
137
|
-
const space = (count) => leaf({ attributes, kind: 'text', text: ' '.repeat(count) }, count);
|
|
138
138
|
// Re-segment inserted spaces too: a trailing Unicode Prepend character can absorb one.
|
|
139
139
|
const spaced = join([space(before), content, space(missing - before)]);
|
|
140
140
|
const row = measured([{ content: spaced, ending: [] }]);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** One grapheme cluster of a string: where it starts and ends, and the columns it occupies. */
|
|
2
|
+
interface GraphemeWidth {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
width: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Each grapheme cluster of a string with the terminal columns it occupies. Printable ASCII that no
|
|
9
|
+
* non-ASCII character follows is its own one-column cluster. Any other run of clusters is read by
|
|
10
|
+
* one cursor, which starts at the run and carries the break state from cluster to cluster.
|
|
11
|
+
*/
|
|
12
|
+
export declare function graphemeWidths(text: string): Generator<GraphemeWidth>;
|
|
13
|
+
export {};
|