@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.
Files changed (52) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +11 -6
  3. package/dist/application.js +57 -19
  4. package/dist/capture.d.ts +65 -0
  5. package/dist/capture.js +99 -0
  6. package/dist/chain.d.ts +28 -17
  7. package/dist/chain.js +11 -10
  8. package/dist/command-rules.d.ts +1 -1
  9. package/dist/command-rules.js +2 -2
  10. package/dist/command.d.ts +31 -47
  11. package/dist/command.js +204 -209
  12. package/dist/errors.d.ts +22 -21
  13. package/dist/errors.js +38 -18
  14. package/dist/facts.d.ts +5 -0
  15. package/dist/facts.js +8 -1
  16. package/dist/globals.d.ts +48 -22
  17. package/dist/globals.js +72 -29
  18. package/dist/glyphs.generated.js +1 -1
  19. package/dist/index.d.ts +5 -3
  20. package/dist/index.js +3 -1
  21. package/dist/input-rules.d.ts +20 -2
  22. package/dist/input-rules.js +47 -5
  23. package/dist/inspect.d.ts +33 -17
  24. package/dist/inspect.js +74 -87
  25. package/dist/locate.js +52 -60
  26. package/dist/options.d.ts +101 -83
  27. package/dist/options.js +311 -267
  28. package/dist/parse.d.ts +162 -0
  29. package/dist/parse.js +601 -0
  30. package/dist/plain.d.ts +52 -2
  31. package/dist/plain.js +228 -2
  32. package/dist/plugin-rules.d.ts +8 -4
  33. package/dist/plugin-rules.js +13 -9
  34. package/dist/plugin-settings.d.ts +18 -0
  35. package/dist/plugin-settings.js +38 -0
  36. package/dist/plugin.d.ts +32 -27
  37. package/dist/plugin.js +107 -89
  38. package/dist/sources.d.ts +18 -9
  39. package/dist/sources.js +50 -20
  40. package/dist/style-layout.js +2 -2
  41. package/dist/style-width.d.ts +13 -0
  42. package/dist/style-width.js +170 -0
  43. package/dist/types.d.ts +70 -30
  44. package/dist/unicode.generated.d.ts +27 -0
  45. package/dist/unicode.generated.js +1036 -0
  46. package/dist/validation.d.ts +108 -34
  47. package/dist/validation.js +282 -125
  48. package/dist/view.d.ts +16 -11
  49. package/dist/view.js +13 -4
  50. package/licenses/unicode-LICENSE.txt +41 -0
  51. package/licenses/uucode-LICENSE.md +35 -0
  52. package/package.json +9 -5
@@ -0,0 +1,170 @@
1
+ /*!
2
+ Ported from the grapheme width iterator of @rockorager/uucode 2.2.1, src/width.ts.
3
+ Copyright (c) 2026 Tim Culverhouse. Licensed under the MIT License,
4
+ in licenses/uucode-LICENSE.md of @loomcli/core.
5
+ */
6
+ import { graphemeTable, widthTable } from './unicode.generated.js';
7
+ /** The break state after a regional indicator, which pairs with the next one into a flag. */
8
+ const regionalIndicatorState = 1;
9
+ /** The break state inside an extended pictographic sequence, which a ZWJ continues. */
10
+ const pictographicState = 2;
11
+ // The table members, read once, so each lookup indexes the arrays directly.
12
+ const { stage1, stage1Shift, stage2, stage2Mask, stage3, widthMask, zeroWidthFlag, emojiVSFlag } = widthTable;
13
+ const { breakTable, graphemeBreakPropertyCount } = graphemeTable;
14
+ /** A code point's packed row: its grapheme break property, width, and emoji flags. */
15
+ function lookup(codePoint) {
16
+ const offset = stage1[codePoint >> stage1Shift] ?? 0;
17
+ return stage3[stage2[offset + (codePoint & stage2Mask)] ?? 0] ?? 0;
18
+ }
19
+ /** The row an empty string starts from: the Other break property at width one. */
20
+ const defaultRow = 1 << 5;
21
+ const graphemeBreak = (row) => row & 0x1f;
22
+ const rowWidth = (row) => (row >> 5) & widthMask;
23
+ const zeroInGrapheme = (row) => ((row >> 5) & zeroWidthFlag) !== 0;
24
+ const emojiSelectorBase = (row) => ((row >> 8) & emojiVSFlag) !== 0;
25
+ /** The next break state shifted left once, with whether a cluster boundary falls between. */
26
+ function transition(before, after, state) {
27
+ const count = graphemeBreakPropertyCount;
28
+ return breakTable[(state * count + before) * count + after] ?? 0;
29
+ }
30
+ /** A cursor at the start of a run, with the run's first code point read ahead. */
31
+ function cursor(text, start) {
32
+ const hasNext = start < text.length;
33
+ const codePoint = hasNext ? (text.codePointAt(start) ?? 0) : 0;
34
+ return {
35
+ codePoint: 0,
36
+ hasNext,
37
+ index: start,
38
+ isBreak: false,
39
+ nextCodePoint: codePoint,
40
+ nextIndex: hasNext ? start + (codePoint > 0xff_ff ? 2 : 1) : start,
41
+ nextRow: hasNext ? lookup(codePoint) : defaultRow,
42
+ row: defaultRow,
43
+ state: 0,
44
+ text,
45
+ };
46
+ }
47
+ /** Moves to the next code point and reads whether a cluster boundary follows it. */
48
+ function advance(at) {
49
+ if (!at.hasNext) {
50
+ return false;
51
+ }
52
+ at.codePoint = at.nextCodePoint;
53
+ at.row = at.nextRow;
54
+ const index = at.nextIndex;
55
+ at.index = index;
56
+ if (index >= at.text.length) {
57
+ at.hasNext = false;
58
+ at.isBreak = true;
59
+ return true;
60
+ }
61
+ const first = at.text.charCodeAt(index);
62
+ let codePoint = first;
63
+ let nextIndex = index + 1;
64
+ if (first >= 0xd8_00 && first <= 0xdb_ff && nextIndex < at.text.length) {
65
+ const second = at.text.charCodeAt(nextIndex);
66
+ if (second >= 0xdc_00 && second <= 0xdf_ff) {
67
+ codePoint = ((first - 0xd8_00) << 10) + second - 0xdc_00 + 0x1_00_00;
68
+ nextIndex += 1;
69
+ }
70
+ }
71
+ const row = lookup(codePoint);
72
+ const packed = transition(graphemeBreak(at.row), graphemeBreak(row), at.state);
73
+ at.state = packed >> 1;
74
+ at.nextCodePoint = codePoint;
75
+ at.nextIndex = nextIndex;
76
+ at.nextRow = row;
77
+ at.isBreak = (packed & 1) !== 0;
78
+ return true;
79
+ }
80
+ /** The width of the next grapheme cluster, leaving the cursor at its end. */
81
+ function clusterWidth(at) {
82
+ if (!advance(at)) {
83
+ return 0;
84
+ }
85
+ const standalone = rowWidth(at.row);
86
+ if (at.isBreak) {
87
+ return standalone;
88
+ }
89
+ let width = zeroInGrapheme(at.row) ? 0 : standalone;
90
+ let previousRow = at.row;
91
+ let previousState = at.state;
92
+ for (;;) {
93
+ if (!advance(at)) {
94
+ break;
95
+ }
96
+ switch (at.codePoint) {
97
+ case 0xfe_0f: {
98
+ if (emojiSelectorBase(previousRow)) {
99
+ width = 2;
100
+ }
101
+ break;
102
+ }
103
+ case 0xfe_0e: {
104
+ if (emojiSelectorBase(previousRow)) {
105
+ width = 1;
106
+ }
107
+ break;
108
+ }
109
+ case 0x20_0d: {
110
+ if (previousState === pictographicState && !at.isBreak) {
111
+ if (!advance(at) || at.isBreak) {
112
+ return width;
113
+ }
114
+ previousRow = at.row;
115
+ previousState = at.state;
116
+ continue;
117
+ }
118
+ break;
119
+ }
120
+ case 0x1_f3_fb:
121
+ case 0x1_f3_fc:
122
+ case 0x1_f3_fd:
123
+ case 0x1_f3_fe:
124
+ case 0x1_f3_ff: {
125
+ width = 2;
126
+ break;
127
+ }
128
+ default: {
129
+ if (previousState === regionalIndicatorState) {
130
+ width = 2;
131
+ }
132
+ else if (!zeroInGrapheme(at.row)) {
133
+ width += rowWidth(at.row);
134
+ }
135
+ }
136
+ }
137
+ if (at.isBreak) {
138
+ break;
139
+ }
140
+ previousRow = at.row;
141
+ previousState = at.state;
142
+ }
143
+ return width;
144
+ }
145
+ /** Whether the character at an index is printable ASCII that no non-ASCII character follows. */
146
+ function asciiAlone(text, index) {
147
+ const code = text.charCodeAt(index);
148
+ return (code >= 0x20 && code < 0x7f && (index + 1 >= text.length || text.charCodeAt(index + 1) < 0x80));
149
+ }
150
+ /**
151
+ * Each grapheme cluster of a string with the terminal columns it occupies. Printable ASCII that no
152
+ * non-ASCII character follows is its own one-column cluster. Any other run of clusters is read by
153
+ * one cursor, which starts at the run and carries the break state from cluster to cluster.
154
+ */
155
+ export function* graphemeWidths(text) {
156
+ let start = 0;
157
+ while (start < text.length) {
158
+ if (asciiAlone(text, start)) {
159
+ yield { end: start + 1, start, width: 1 };
160
+ start += 1;
161
+ continue;
162
+ }
163
+ const at = cursor(text, start);
164
+ do {
165
+ const width = clusterWidth(at);
166
+ yield { end: at.index, start, width };
167
+ start = at.index;
168
+ } while (start < text.length && !asciiAlone(text, start));
169
+ }
170
+ }
package/dist/types.d.ts CHANGED
@@ -8,12 +8,18 @@ import type { RenderingPolicy } from './rendering.js';
8
8
  import type { ContextualStyle } from './style.js';
9
9
  type LowercaseLetter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
10
10
  type ShortAlias = LowercaseLetter | Uppercase<LowercaseLetter>;
11
+ /**
12
+ * The spellings an option declares beyond its long form. `aliases` adds a long spelling for each
13
+ * name, so `shortOnly`, which removes every long spelling, never declares one.
14
+ */
11
15
  type OptionSpelling = {
12
16
  short?: ShortAlias;
13
17
  shortOnly?: false;
18
+ aliases?: readonly string[];
14
19
  } | {
15
20
  short: ShortAlias;
16
21
  shortOnly: true;
22
+ aliases?: never;
17
23
  };
18
24
  type Presence = {
19
25
  required: true;
@@ -48,6 +54,24 @@ type Omission = {
48
54
  } | {
49
55
  validateOmitted?: false;
50
56
  };
57
+ /**
58
+ * The presence rules a global option never declares, because its validation runs on every Command.
59
+ * `GlobalOptionConfig` and `GlobalOmissionConstraint` both read the rule from here.
60
+ */
61
+ type PresenceRuleKey = 'required' | 'validateOmitted';
62
+ /** The fault each presence rule names when a global option declares it. */
63
+ interface PresenceRuleFaults {
64
+ required: {
65
+ 'A global option declares no required; the Commands that read it check for it': never;
66
+ };
67
+ validateOmitted: {
68
+ 'A global option declares no validateOmitted; its omission is plain absence': never;
69
+ };
70
+ }
71
+ /** The first presence rule a config declares, in the order the faults name them. */
72
+ type DeclaredPresenceRule<Config> = {
73
+ [Key in PresenceRuleKey]: [Extract<Config, Record<Key, unknown>>] extends [never] ? never : Key;
74
+ }[PresenceRuleKey];
51
75
  /**
52
76
  * The one-line summary every projection reads. It is a core fact: optional, and a string that holds
53
77
  * a character other than whitespace and no line terminator.
@@ -156,8 +180,11 @@ export interface InputIdentity {
156
180
  export interface SuppliedInputs {
157
181
  /** Raw positional values of the routed Command: one string, or the tokens of a variadic. */
158
182
  args: Readonly<Record<string, string | readonly string[] | undefined>>;
159
- /** Raw option values, globals and locals: a string, every occurrence, or a Boolean presence. */
160
- options: Readonly<Record<string, string | readonly string[] | boolean | undefined>>;
183
+ /**
184
+ * Raw option values, globals and locals: a string, every occurrence, a Boolean presence, or the
185
+ * number of times a counted option was supplied.
186
+ */
187
+ options: Readonly<Record<string, string | readonly string[] | boolean | number | undefined>>;
161
188
  }
162
189
  /**
163
190
  * What core knows when it calls one schema. A default validates before any token is read, so its
@@ -185,7 +212,7 @@ export interface ViewContext {
185
212
  export interface View<Data> {
186
213
  render: (data: Readonly<Data>, context: ViewContext) => string;
187
214
  /** A view has one shape; the row view of Results is the other. */
188
- row?: never;
215
+ row?: undefined;
189
216
  }
190
217
  /**
191
218
  * A row view renders a sequence one row at a time.
@@ -197,7 +224,7 @@ export interface RowView<Row> {
197
224
  head?: (context: ViewContext) => string;
198
225
  tail?: (count: number, context: ViewContext) => string;
199
226
  /** A row view has one shape; the whole view of Rendered output is the other. */
200
- render?: never;
227
+ render?: undefined;
201
228
  }
202
229
  /** The views record of a value result: every entry renders the whole value. */
203
230
  export type ResultViews<Value> = Readonly<Record<string, View<Value>>>;
@@ -317,7 +344,7 @@ export interface ResultBinding {
317
344
  /**
318
345
  * The routed Command's invocation after parsing and validation, which every middleware reads. The
319
346
  * values are what the action receives, the output of each declaration's schema, for that Command's
320
- * own arguments and local options; global and plugin option values are not here. The records are
347
+ * own arguments and local options; global option values are not here. The records are
321
348
  * untyped and frozen, because a middleware runs ahead of every action and the graph carries no
322
349
  * type for a value.
323
350
  */
@@ -341,6 +368,8 @@ export type StringOption = OptionSpelling & Presence & Multiplicity & Omission &
341
368
  type: 'string';
342
369
  polarity?: never;
343
370
  validate?: StandardSchemaV1;
371
+ /** The value a bare spelling supplies, so an explicit value is attached to the spelling. */
372
+ implied?: string;
344
373
  };
345
374
  /** A variadic argument collects the remaining tokens, so it follows the multiple option rules. */
346
375
  export type VariadicArgument = Presence & Described & ArgumentExtensions & {
@@ -357,6 +386,19 @@ export type ValidatedValue<Config, Raw> = Config extends unknown ? 'validate' ex
357
386
  export type DefaultConstraint<Config> = Config extends unknown ? Config & {
358
387
  default?: 'validate' extends keyof Config ? PerValue<Config, SchemaInput<Config['validate']>> : RawValue<Config>;
359
388
  } : never;
389
+ /**
390
+ * An implied value is the string an operator would otherwise attach, so it must be a string the
391
+ * validator's input type accepts, as a default must be. A multiple option's validator reads one
392
+ * value, so the implied value meets that one value's input type. A validator that declares no types
393
+ * infers `never`, so it states nothing to check against. The key names the fault, the way the
394
+ * other declaration constraints do, because an intersection with the literal would reduce the whole
395
+ * config to `never` and report every key.
396
+ */
397
+ export type ImpliedConstraint<Config> = Config extends {
398
+ implied: infer Implied;
399
+ } ? 'validate' extends keyof Config ? [SchemaInput<Config['validate']>] extends [never] ? unknown : Implied extends SchemaInput<Config['validate']> ? unknown : {
400
+ 'An implied value must be in the validator input type': Config['validate'];
401
+ } : unknown : unknown;
360
402
  /**
361
403
  * A multiple option or a variadic argument passes each value to its validator alone, so the
362
404
  * declared validator must accept one `string`. The key names the fault, the way the other
@@ -404,19 +446,9 @@ export type ValidateOmittedConstraint<Config> = Config extends {
404
446
  /**
405
447
  * A global option declares no presence rule, so its omission is always plain absence. A union
406
448
  * config fails when any member declares the key, and a wide `OptionConfig` passes, because its
407
- * members only allow the key.
449
+ * members only allow the key. `required` is named first when a config declares both.
408
450
  */
409
- export type GlobalOmissionConstraint<Config> = [Extract<Config, {
410
- required: unknown;
411
- }>] extends [
412
- never
413
- ] ? [Extract<Config, {
414
- validateOmitted: unknown;
415
- }>] extends [never] ? unknown : {
416
- 'A global option declares no validateOmitted; its omission is plain absence': never;
417
- } : {
418
- 'A global option declares no required; the Commands that read it check for it': never;
419
- };
451
+ export type GlobalOmissionConstraint<Config> = [DeclaredPresenceRule<Config>] extends [never] ? unknown : PresenceRuleFaults['required' extends DeclaredPresenceRule<Config> ? 'required' : 'validateOmitted'];
420
452
  export type ArgumentValue<Config extends ArgumentConfig> = Config extends {
421
453
  variadic: true;
422
454
  } ? ValidatedValue<Config, string[]> : ValidatedValue<Config, string> | (Config extends {
@@ -433,6 +465,7 @@ export type BooleanOption = (OptionSpelling & Described & Listed & EnvBinding &
433
465
  multiple?: never;
434
466
  required?: never;
435
467
  validateOmitted?: never;
468
+ implied?: never;
436
469
  polarity?: 'positive' | 'negative';
437
470
  }) | (Described & Listed & EnvBinding & OptionExtensions & {
438
471
  type: 'boolean';
@@ -441,27 +474,34 @@ export type BooleanOption = (OptionSpelling & Described & Listed & EnvBinding &
441
474
  multiple?: never;
442
475
  required?: never;
443
476
  validateOmitted?: never;
477
+ implied?: never;
444
478
  polarity: 'both';
445
479
  short?: ShortAlias;
446
480
  shortOnly?: false;
481
+ aliases?: readonly string[];
447
482
  });
448
- export type OptionConfig = StringOption | BooleanOption;
449
483
  /**
450
- * The parsing part of a string option config, which is all a plugin option declares. A plugin
451
- * option carries no schema and no presence rule, because the pre-scan consumes it ahead of routing,
452
- * where the validation context every schema is promised cannot exist. Its middleware interprets
453
- * the value.
454
- * A Boolean plugin option is an ordinary `BooleanOption`, which already declares none of them.
484
+ * A counted option takes no value and reads how many times it was supplied, across every spelling,
485
+ * so it declares no validator, default, presence rule, collection, polarity, or implied value.
455
486
  */
456
- export type PluginStringOption = OptionSpelling & Multiplicity & Described & Listed & OptionExtensions & {
457
- type: 'string';
458
- default?: string | string[];
459
- polarity?: never;
460
- required?: never;
487
+ export type CountOption = OptionSpelling & Described & Listed & EnvBinding & OptionExtensions & {
488
+ type: 'count';
461
489
  validate?: never;
490
+ default?: never;
491
+ multiple?: never;
492
+ required?: never;
462
493
  validateOmitted?: never;
494
+ polarity?: never;
495
+ implied?: never;
463
496
  };
464
- export type PluginOptionConfig = PluginStringOption | BooleanOption;
497
+ export type OptionConfig = StringOption | BooleanOption | CountOption;
498
+ /**
499
+ * The configuration of a global option a plugin declares: everything `option()` takes except the
500
+ * presence rules, which a global option never declares, because its validation runs on every
501
+ * Command. `globalOption()` states the same rule through `GlobalOmissionConstraint`, which also
502
+ * accepts a config typed as the wide `OptionConfig`.
503
+ */
504
+ export type GlobalOptionConfig = OptionConfig & Readonly<Partial<Record<PresenceRuleKey, never>>>;
465
505
  export type OptionValue<Config extends OptionConfig> = Config extends StringOption ? Config extends {
466
506
  multiple: true;
467
507
  } ? ValidatedValue<Config, string[]> : ValidatedValue<Config, string> | (Config extends {
@@ -470,7 +510,7 @@ export type OptionValue<Config extends OptionConfig> = Config extends StringOpti
470
510
  default: unknown;
471
511
  } | {
472
512
  validateOmitted: true;
473
- } ? never : undefined) : boolean;
513
+ } ? never : undefined) : Config extends CountOption ? number : boolean;
474
514
  /**
475
515
  * What an action receives. `graph` is the frozen graph `inspect()` returns for this run, and
476
516
  * `command` is the routed node inside it: the same two values the run's middleware receive.
@@ -0,0 +1,27 @@
1
+ /*!
2
+ The tables derive from the Unicode Character Database.
3
+ Copyright © 1991-2025 Unicode, Inc. Licensed under the Unicode License v3,
4
+ in licenses/unicode-LICENSE.txt of @loomcli/core.
5
+
6
+ They are generated from the tables of @rockorager/uucode.
7
+ Copyright (c) 2026 Tim Culverhouse. Licensed under the MIT License,
8
+ in licenses/uucode-LICENSE.md of @loomcli/core.
9
+ */
10
+ /** The grapheme break state machine, indexed by state and two break properties. */
11
+ export declare const graphemeTable: {
12
+ breakStateCount: number;
13
+ breakTable: Uint8Array<ArrayBuffer>;
14
+ graphemeBreakPropertyCount: number;
15
+ };
16
+ /** Each code point's width and grapheme break property, in a three-stage lookup. */
17
+ export declare const widthTable: {
18
+ emojiVSFlag: number;
19
+ maxCodePoint: number;
20
+ stage1: Uint16Array<ArrayBuffer>;
21
+ stage1Shift: number;
22
+ stage2: Uint8Array<ArrayBuffer>;
23
+ stage2Mask: number;
24
+ stage3: Uint16Array<ArrayBuffer>;
25
+ widthMask: number;
26
+ zeroWidthFlag: number;
27
+ };