@bomb.sh/router 0.0.1 → 0.7.1
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/README.md +293 -0
- package/esm/_dnt.polyfills.d.ts +11 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/lib/argument.d.ts +7 -0
- package/esm/lib/argument.js +66 -0
- package/esm/lib/bind.d.ts +41 -0
- package/esm/lib/bind.js +285 -0
- package/esm/lib/checkpoint.d.ts +3 -0
- package/esm/lib/checkpoint.js +5 -0
- package/esm/lib/command.d.ts +6 -0
- package/esm/lib/command.js +16 -0
- package/esm/lib/dasherize.d.ts +1 -0
- package/esm/lib/dasherize.js +7 -0
- package/esm/lib/decode.d.ts +6 -0
- package/esm/lib/decode.js +46 -0
- package/esm/lib/definition.d.ts +4 -0
- package/esm/lib/definition.js +10 -0
- package/esm/lib/dynamic.d.ts +6 -0
- package/esm/lib/dynamic.js +21 -0
- package/esm/lib/env.d.ts +34 -0
- package/esm/lib/env.js +83 -0
- package/esm/lib/extend.d.ts +2 -0
- package/esm/lib/extend.js +4 -0
- package/esm/lib/maybe.d.ts +9 -0
- package/esm/lib/maybe.js +12 -0
- package/esm/lib/multiple.d.ts +8 -0
- package/esm/lib/multiple.js +9 -0
- package/esm/lib/option.d.ts +7 -0
- package/esm/lib/option.js +31 -0
- package/esm/lib/param.d.ts +26 -0
- package/esm/lib/param.js +37 -0
- package/esm/lib/parse.d.ts +2 -0
- package/esm/lib/parse.js +338 -0
- package/esm/lib/pipeline.d.ts +182 -0
- package/esm/lib/pipeline.js +6 -0
- package/esm/lib/print.d.ts +7 -0
- package/esm/lib/print.js +156 -0
- package/esm/lib/read.d.ts +27 -0
- package/esm/lib/read.js +95 -0
- package/esm/lib/rest.d.ts +9 -0
- package/esm/lib/rest.js +1 -0
- package/esm/lib/result.d.ts +9 -0
- package/esm/lib/result.js +1 -0
- package/esm/lib/route.d.ts +9 -0
- package/esm/lib/route.js +45 -0
- package/esm/lib/toggle.d.ts +7 -0
- package/esm/lib/toggle.js +91 -0
- package/esm/lib/tokenize.d.ts +20 -0
- package/esm/lib/tokenize.js +41 -0
- package/esm/lib/tokenizer.d.ts +34 -0
- package/esm/lib/tokenizer.js +130 -0
- package/esm/lib/transform.d.ts +8 -0
- package/esm/lib/transform.js +34 -0
- package/esm/lib/types.d.ts +196 -0
- package/esm/lib/types.js +1 -0
- package/esm/lib/values.d.ts +29 -0
- package/esm/lib/values.js +83 -0
- package/esm/mod.d.ts +29 -0
- package/esm/mod.js +18 -0
- package/esm/package.json +3 -0
- package/package.json +27 -9
package/esm/lib/print.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 😵💫 This file has been vibe coded 😵💫
|
|
3
|
+
*/
|
|
4
|
+
export function printHelp(intent) {
|
|
5
|
+
let route = intent.definition;
|
|
6
|
+
let subject = title(intent);
|
|
7
|
+
let heading = route.version ? `${subject} ${route.version}` : subject;
|
|
8
|
+
let children = route.phases.flatMap((phase) => phase.routes);
|
|
9
|
+
let args = [];
|
|
10
|
+
let options = [];
|
|
11
|
+
for (let param of params(route)) {
|
|
12
|
+
let syntax = param.cli.syntax;
|
|
13
|
+
if (!syntax) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
let row = [syntax.label, param.description];
|
|
17
|
+
if (syntax.type === "argument") {
|
|
18
|
+
args.push(row);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
options.push(row);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let usage = [subject, "[OPTIONS]", ...args.map(([label]) => label)].join(" ");
|
|
25
|
+
if (children.length > 0) {
|
|
26
|
+
usage += route.methods.includes("execute") ? " [COMMAND]" : " <COMMAND>";
|
|
27
|
+
}
|
|
28
|
+
let lines = [heading];
|
|
29
|
+
if (route.description) {
|
|
30
|
+
lines.push(...wrap(route.description, width));
|
|
31
|
+
}
|
|
32
|
+
lines.push("", "Usage:", ` ${usage}`);
|
|
33
|
+
if (args.length > 0) {
|
|
34
|
+
lines.push("", "Arguments:", ...list(args));
|
|
35
|
+
}
|
|
36
|
+
if (children.length > 0) {
|
|
37
|
+
lines.push("", "Commands:", ...list(children.map((child) => [child.name, child.description])));
|
|
38
|
+
}
|
|
39
|
+
options.push(["-h, --help", "Print help"]);
|
|
40
|
+
if (route.methods.includes("version")) {
|
|
41
|
+
options.push(["-v, --version", "Print version"]);
|
|
42
|
+
}
|
|
43
|
+
lines.push("", "Options:", ...list(options));
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
export function printVersion(intent) {
|
|
47
|
+
let version = intent.definition.version;
|
|
48
|
+
if (!version) {
|
|
49
|
+
throw new TypeError(`route ${JSON.stringify(intent.route)} has no version`);
|
|
50
|
+
}
|
|
51
|
+
return `${title(intent)} ${version}`;
|
|
52
|
+
}
|
|
53
|
+
export function printErrors(result) {
|
|
54
|
+
let subject = title(result);
|
|
55
|
+
switch (result.code) {
|
|
56
|
+
case "method-not-allowed":
|
|
57
|
+
return [
|
|
58
|
+
`${subject} does not support ${result.method.toUpperCase()}`,
|
|
59
|
+
"",
|
|
60
|
+
"Available methods:",
|
|
61
|
+
...result.allowed.map((method) => ` ${method.toUpperCase()}`),
|
|
62
|
+
].join("\n");
|
|
63
|
+
case "unprocessable-content": {
|
|
64
|
+
return result.issues.map(problem).join("\n");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function params(route) {
|
|
69
|
+
return route.phases.flatMap((phase) => Object.values(phase.model.params));
|
|
70
|
+
}
|
|
71
|
+
function title(intent) {
|
|
72
|
+
return intent.path.length > 0
|
|
73
|
+
? intent.path.join(" ")
|
|
74
|
+
: intent.definition.name;
|
|
75
|
+
}
|
|
76
|
+
function list(rows) {
|
|
77
|
+
let size = Math.max(...rows.map(([label]) => label.length));
|
|
78
|
+
let available = Math.max(24, width - size - 4);
|
|
79
|
+
let lines = [];
|
|
80
|
+
for (let [label, description] of rows) {
|
|
81
|
+
let prefix = ` ${label.padEnd(size)}`;
|
|
82
|
+
if (!description) {
|
|
83
|
+
lines.push(prefix.trimEnd());
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
let [first, ...rest] = wrap(description, available);
|
|
87
|
+
lines.push(`${prefix} ${first}`);
|
|
88
|
+
lines.push(...rest.map((line) => `${" ".repeat(size + 4)}${line}`));
|
|
89
|
+
}
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
function wrap(text, size) {
|
|
93
|
+
let words = text.trim().split(/\s+/);
|
|
94
|
+
let lines = [];
|
|
95
|
+
let line = "";
|
|
96
|
+
for (let word of words) {
|
|
97
|
+
if (line.length === 0) {
|
|
98
|
+
line = word;
|
|
99
|
+
}
|
|
100
|
+
else if (line.length + word.length + 1 <= size) {
|
|
101
|
+
line += ` ${word}`;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
lines.push(line);
|
|
105
|
+
line = word;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (line) {
|
|
109
|
+
lines.push(line);
|
|
110
|
+
}
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
function problem(issue) {
|
|
114
|
+
let location = address(issue.path);
|
|
115
|
+
return location ? `${location}: ${issue.message}` : message(issue.message);
|
|
116
|
+
}
|
|
117
|
+
function message(value) {
|
|
118
|
+
if (value.startsWith("unexpected ")) {
|
|
119
|
+
let encoded = value.slice("unexpected ".length);
|
|
120
|
+
try {
|
|
121
|
+
let token = JSON.parse(encoded);
|
|
122
|
+
if (typeof token === "string") {
|
|
123
|
+
return `unexpected: \`${token.replaceAll("`", "\\`")}\``;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Keep non-JSON diagnostics intact.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
function address(path) {
|
|
133
|
+
if (!path || path.length === 0) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let result = "";
|
|
137
|
+
for (let segment of path) {
|
|
138
|
+
let key = typeof segment === "object" && segment !== null
|
|
139
|
+
? segment.key
|
|
140
|
+
: segment;
|
|
141
|
+
if (typeof key === "number") {
|
|
142
|
+
result += `[${key}]`;
|
|
143
|
+
}
|
|
144
|
+
else if (typeof key === "symbol") {
|
|
145
|
+
result += `[${String(key)}]`;
|
|
146
|
+
}
|
|
147
|
+
else if (/^[A-Za-z_$][\w$]*$/.test(key)) {
|
|
148
|
+
result += result ? `.${key}` : key;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
result += `[${JSON.stringify(key)}]`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
const width = 80;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Maybe } from "./maybe.js";
|
|
2
|
+
import type { AnyParam } from "./param.js";
|
|
3
|
+
import { type IdentityElement } from "./pipeline.js";
|
|
4
|
+
import type { Result } from "./result.js";
|
|
5
|
+
import type { Flag, Setter, Word } from "./tokenize.js";
|
|
6
|
+
import type { Claim, TokenInput } from "./tokenizer.js";
|
|
7
|
+
export type Symbol = Flag | Setter | Word;
|
|
8
|
+
export type ReadCLI = (tokens: TokenInput<Symbol>) => CLIRead;
|
|
9
|
+
export interface CLIBinding {
|
|
10
|
+
readonly read: ReadCLI;
|
|
11
|
+
readonly syntax?: CLISyntax;
|
|
12
|
+
}
|
|
13
|
+
export type CLISyntax = {
|
|
14
|
+
readonly type: "argument";
|
|
15
|
+
readonly label: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: "option";
|
|
18
|
+
readonly label: string;
|
|
19
|
+
};
|
|
20
|
+
export interface CLIRead {
|
|
21
|
+
result: Result<Maybe<string | boolean>>;
|
|
22
|
+
claim: Claim<Symbol>;
|
|
23
|
+
}
|
|
24
|
+
export interface CLIOptions {
|
|
25
|
+
switch?: true;
|
|
26
|
+
}
|
|
27
|
+
export declare function cli(names: readonly string[], options?: CLIOptions): IdentityElement<AnyParam>;
|
package/esm/lib/read.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { brand } from "./pipeline.js";
|
|
2
|
+
export function cli(names, options = {}) {
|
|
3
|
+
const read = (tokens) => {
|
|
4
|
+
if (options.switch) {
|
|
5
|
+
let s = tokens.claimOne((t) => {
|
|
6
|
+
return t.type === "flag" && names.includes(t.text);
|
|
7
|
+
});
|
|
8
|
+
let [flag] = s.tokens;
|
|
9
|
+
return flag
|
|
10
|
+
? {
|
|
11
|
+
result: {
|
|
12
|
+
ok: true,
|
|
13
|
+
value: { exists: true, value: true },
|
|
14
|
+
issues: [],
|
|
15
|
+
},
|
|
16
|
+
claim: s,
|
|
17
|
+
}
|
|
18
|
+
: nothing(tokens);
|
|
19
|
+
}
|
|
20
|
+
let setter = tokens.claimOne((token) => {
|
|
21
|
+
return (token.type === "setter" && names.includes(`--${token.nameText}`));
|
|
22
|
+
});
|
|
23
|
+
let [token] = setter.tokens;
|
|
24
|
+
if (token) {
|
|
25
|
+
return {
|
|
26
|
+
claim: setter,
|
|
27
|
+
result: {
|
|
28
|
+
ok: true,
|
|
29
|
+
value: {
|
|
30
|
+
exists: true,
|
|
31
|
+
value: token.valueText,
|
|
32
|
+
},
|
|
33
|
+
issues: [],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
let pair = tokens.claimPair((name, value) => {
|
|
38
|
+
return name.type === "flag" && names.includes(name.text) &&
|
|
39
|
+
value.type === "word";
|
|
40
|
+
});
|
|
41
|
+
let [, value] = pair.tokens;
|
|
42
|
+
if (value) {
|
|
43
|
+
return {
|
|
44
|
+
claim: pair,
|
|
45
|
+
result: {
|
|
46
|
+
ok: true,
|
|
47
|
+
value: {
|
|
48
|
+
exists: true,
|
|
49
|
+
value: value.text,
|
|
50
|
+
},
|
|
51
|
+
issues: [],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
let bare = tokens.claimOne((t) => {
|
|
56
|
+
return t.type === "flag" && names.includes(t.text);
|
|
57
|
+
});
|
|
58
|
+
let [incomplete] = bare.tokens;
|
|
59
|
+
if (incomplete) {
|
|
60
|
+
return {
|
|
61
|
+
claim: bare,
|
|
62
|
+
result: {
|
|
63
|
+
ok: false,
|
|
64
|
+
issues: [{
|
|
65
|
+
message: `${incomplete.text} requires a value`,
|
|
66
|
+
}],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return nothing(tokens);
|
|
71
|
+
};
|
|
72
|
+
return brand((param) => ({
|
|
73
|
+
...param,
|
|
74
|
+
cli: {
|
|
75
|
+
read,
|
|
76
|
+
syntax: {
|
|
77
|
+
type: "option",
|
|
78
|
+
label: options.switch
|
|
79
|
+
? names.join(", ")
|
|
80
|
+
: `${names.join(", ")} <VALUE>`,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
function nothing(tokenizer) {
|
|
86
|
+
let claim = tokenizer.claimAll(() => false);
|
|
87
|
+
return {
|
|
88
|
+
claim,
|
|
89
|
+
result: {
|
|
90
|
+
ok: true,
|
|
91
|
+
value: { exists: false },
|
|
92
|
+
issues: [],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Envs } from "./env.js";
|
|
2
|
+
import type { Symbol } from "./read.js";
|
|
3
|
+
import type { Tokenizer } from "./tokenizer.js";
|
|
4
|
+
import type { Values } from "./values.js";
|
|
5
|
+
export interface Rest {
|
|
6
|
+
readonly tokens: Tokenizer<Symbol>;
|
|
7
|
+
readonly values: Values;
|
|
8
|
+
readonly envs: Envs;
|
|
9
|
+
}
|
package/esm/lib/rest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Check, type Fold, type Materialize, type MethodElement, type RoutesElement, type Unary } from "./pipeline.js";
|
|
2
|
+
import type { AnyRoute, Definition, Done, Route } from "./types.js";
|
|
3
|
+
export type RouteZero<N extends string = string> = Route<N, "help", [
|
|
4
|
+
Done<{}, []>
|
|
5
|
+
]>;
|
|
6
|
+
export declare function route<const N extends string, const E extends readonly Unary[]>(start: Definition<N>, ...elements: E & Check<RouteZero<N>, E>): Materialize<Fold<RouteZero<N>, E>>;
|
|
7
|
+
export declare function version(semver: string): MethodElement<"version">;
|
|
8
|
+
export declare function executable(): MethodElement<"execute">;
|
|
9
|
+
export declare function routes<const C extends readonly AnyRoute[]>(...children: C): RoutesElement<C>;
|
package/esm/lib/route.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// deno-lint-ignore-file ban-types
|
|
2
|
+
import { brand, } from "./pipeline.js";
|
|
3
|
+
export function route(start, ...elements) {
|
|
4
|
+
let zero = {
|
|
5
|
+
...start,
|
|
6
|
+
methods: ["help"],
|
|
7
|
+
phases: [{
|
|
8
|
+
model: {
|
|
9
|
+
params: {},
|
|
10
|
+
steps: [],
|
|
11
|
+
},
|
|
12
|
+
routes: [],
|
|
13
|
+
values: [],
|
|
14
|
+
envs: [],
|
|
15
|
+
}],
|
|
16
|
+
};
|
|
17
|
+
return elements.reduce((value, element) => element(value), zero);
|
|
18
|
+
}
|
|
19
|
+
export function version(semver) {
|
|
20
|
+
return brand((route) => ({
|
|
21
|
+
...route,
|
|
22
|
+
methods: [...route.methods, "version"],
|
|
23
|
+
version: semver,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
export function executable() {
|
|
27
|
+
return brand((route) => ({
|
|
28
|
+
...route,
|
|
29
|
+
methods: [...route.methods, "execute"],
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
export function routes(...children) {
|
|
33
|
+
return brand((route) => {
|
|
34
|
+
let phases = [...route.phases];
|
|
35
|
+
let phase = phases.pop();
|
|
36
|
+
phases.push({
|
|
37
|
+
...phase,
|
|
38
|
+
routes: [...phase.routes, ...children],
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
...route,
|
|
42
|
+
phases,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Param, type ParamModel } from "./param.js";
|
|
2
|
+
import { type Check, type Fold, type ModelElement, type Unary } from "./pipeline.js";
|
|
3
|
+
import type { Definition } from "./types.js";
|
|
4
|
+
export declare function toggle<const N extends string, const E extends readonly Unary[]>(named: Definition<N>, ...elements: E & Check<Zero<N>, E>): ElementOf<N, Fold<Zero<N>, E>>;
|
|
5
|
+
type Zero<N extends string> = Param<N, boolean, "one">;
|
|
6
|
+
type ElementOf<N extends string, P> = P extends Param<N, infer Model, infer _Cardinality> ? ModelElement<ParamModel<N, Model>> : never;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { boolean as decode } from "./decode.js";
|
|
2
|
+
import { dasherize } from "./dasherize.js";
|
|
3
|
+
import { param, schema, } from "./param.js";
|
|
4
|
+
import { brand, } from "./pipeline.js";
|
|
5
|
+
export function toggle(named, ...elements) {
|
|
6
|
+
const added = elements.reduce((value, element) => element(value), {
|
|
7
|
+
...param(named, binding(named.name), schema(bool)),
|
|
8
|
+
decode,
|
|
9
|
+
});
|
|
10
|
+
return brand((route) => {
|
|
11
|
+
let phases = [...route.phases];
|
|
12
|
+
let phase = phases.pop();
|
|
13
|
+
phases.push({
|
|
14
|
+
...phase,
|
|
15
|
+
model: {
|
|
16
|
+
params: {
|
|
17
|
+
...phase.model.params,
|
|
18
|
+
[added.name]: added,
|
|
19
|
+
},
|
|
20
|
+
steps: phase.model.steps.concat((current, bindings) => ({
|
|
21
|
+
ok: true,
|
|
22
|
+
value: {
|
|
23
|
+
...current,
|
|
24
|
+
[added.name]: bindings[added.name],
|
|
25
|
+
},
|
|
26
|
+
})),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
...route,
|
|
31
|
+
phases,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function binding(name) {
|
|
36
|
+
const stem = dasherize(name);
|
|
37
|
+
const yes = `--${stem}`;
|
|
38
|
+
const no = `--no-${stem}`;
|
|
39
|
+
return (param) => ({
|
|
40
|
+
...param,
|
|
41
|
+
cli: {
|
|
42
|
+
read: reader(name),
|
|
43
|
+
syntax: {
|
|
44
|
+
type: "option",
|
|
45
|
+
label: `${yes}, ${no}`,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function reader(name) {
|
|
51
|
+
const stem = dasherize(name);
|
|
52
|
+
const yes = `--${stem}`;
|
|
53
|
+
const no = `--no-${stem}`;
|
|
54
|
+
return (tokens) => {
|
|
55
|
+
let claim = tokens.claimOne((token) => {
|
|
56
|
+
return token.type === "flag" &&
|
|
57
|
+
(token.text === yes || token.text === no);
|
|
58
|
+
});
|
|
59
|
+
let [flag] = claim.tokens;
|
|
60
|
+
return flag
|
|
61
|
+
? {
|
|
62
|
+
claim,
|
|
63
|
+
result: {
|
|
64
|
+
ok: true,
|
|
65
|
+
value: { exists: true, value: flag.text === yes },
|
|
66
|
+
issues: [],
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
: {
|
|
70
|
+
claim,
|
|
71
|
+
result: {
|
|
72
|
+
ok: true,
|
|
73
|
+
value: { exists: false },
|
|
74
|
+
issues: [],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const bool = {
|
|
80
|
+
"~standard": {
|
|
81
|
+
version: 1,
|
|
82
|
+
vendor: "@bomb.sh/router",
|
|
83
|
+
validate(value) {
|
|
84
|
+
return typeof value === "undefined"
|
|
85
|
+
? { value: false }
|
|
86
|
+
: typeof value === "boolean"
|
|
87
|
+
? { value }
|
|
88
|
+
: { issues: [{ message: "expected boolean" }] };
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Token<T extends string> {
|
|
2
|
+
type: T;
|
|
3
|
+
index: number;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export type AnyToken = Word | Flag | Setter | Separator | Literal;
|
|
7
|
+
export interface Setter extends Token<"setter"> {
|
|
8
|
+
nameText: string;
|
|
9
|
+
valueText: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Flag extends Token<"flag"> {
|
|
12
|
+
flagText: string;
|
|
13
|
+
flagType: "short" | "long";
|
|
14
|
+
}
|
|
15
|
+
export type Word = Token<"word">;
|
|
16
|
+
export interface Separator extends Token<"separator"> {
|
|
17
|
+
readonly text: "--";
|
|
18
|
+
}
|
|
19
|
+
export type Literal = Token<"literal">;
|
|
20
|
+
export declare function tokenize(argv: string[]): readonly AnyToken[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function tokenize(argv) {
|
|
2
|
+
let quote = false;
|
|
3
|
+
let tokens = [];
|
|
4
|
+
for (let i = 0; i < argv.length; i++) {
|
|
5
|
+
let index = i;
|
|
6
|
+
let text = argv[i];
|
|
7
|
+
if (quote) {
|
|
8
|
+
tokens.push({ type: "literal", index, text });
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (text === "--") {
|
|
12
|
+
tokens.push({ type: "separator", index, text });
|
|
13
|
+
quote = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
let matchSetter = setterMatch.exec(text);
|
|
17
|
+
if (matchSetter?.groups) {
|
|
18
|
+
let { nameText, valueText } = matchSetter.groups;
|
|
19
|
+
tokens.push({ type: "setter", index, text, nameText, valueText });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
let matchFlag = flagMatch.exec(text);
|
|
23
|
+
if (matchFlag?.groups) {
|
|
24
|
+
let { prefix, flagText } = matchFlag.groups;
|
|
25
|
+
tokens.push({
|
|
26
|
+
type: "flag",
|
|
27
|
+
index,
|
|
28
|
+
text,
|
|
29
|
+
flagText,
|
|
30
|
+
flagType: prefix === "-" ? "short" : "long",
|
|
31
|
+
});
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
tokens.push({ type: "word", index, text });
|
|
35
|
+
}
|
|
36
|
+
return tokens;
|
|
37
|
+
}
|
|
38
|
+
// deno-lint-ignore no-invalid-regexp
|
|
39
|
+
const flagMatch = /^(?<prefix>--?)(?<flagText>[^-=\s][^=\s]*)$/;
|
|
40
|
+
// deno-lint-ignore no-invalid-regexp
|
|
41
|
+
const setterMatch = /^--(?<nameText>[^-=\s][^=\s]*)=(?<valueText>[\s\S]*)$/;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AnyToken } from "./tokenize.js";
|
|
2
|
+
export interface TokenInput<T extends AnyToken> extends Iterable<T> {
|
|
3
|
+
claimNext(): Claim<T>;
|
|
4
|
+
claimOne<S extends T>(match: (token: T) => token is S): Claim<S, T>;
|
|
5
|
+
claimOne(match: (token: T) => boolean): Claim<T, T>;
|
|
6
|
+
claimPair(match: (a: T, b: T) => boolean): Claim<T, T>;
|
|
7
|
+
claimAll<S extends T>(match: (token: T) => token is S): Claim<S, T>;
|
|
8
|
+
claimAll(match: (token: T) => boolean): Claim<T, T>;
|
|
9
|
+
}
|
|
10
|
+
export interface TokenRange {
|
|
11
|
+
readonly start: number;
|
|
12
|
+
readonly end?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ViewOptions {
|
|
15
|
+
readonly range: TokenRange;
|
|
16
|
+
readonly through?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class Tokenizer<T extends AnyToken> implements TokenInput<T> {
|
|
19
|
+
readonly tokens: readonly T[];
|
|
20
|
+
readonly claimed: ReadonlySet<number>;
|
|
21
|
+
constructor(tokens: readonly T[], claimed?: ReadonlySet<number>);
|
|
22
|
+
claimNext(): Claim<T>;
|
|
23
|
+
claimOne<S extends T>(match: (token: T) => token is S): Claim<S, T>;
|
|
24
|
+
claimOne(match: (token: T) => boolean): Claim<T, T>;
|
|
25
|
+
claimPair(match: (a: T, b: T) => boolean): Claim<T, T>;
|
|
26
|
+
claimAll<S extends T>(match: (token: T) => token is S): Claim<S, T>;
|
|
27
|
+
claimAll(match: (token: T) => boolean): Claim<T, T>;
|
|
28
|
+
view(options: ViewOptions): TokenInput<T>;
|
|
29
|
+
[Symbol.iterator](): Generator<T, void, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export interface Claim<T extends AnyToken, R extends AnyToken = T> {
|
|
32
|
+
tokens: T[];
|
|
33
|
+
rest: Tokenizer<R>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export class Tokenizer {
|
|
2
|
+
tokens;
|
|
3
|
+
claimed;
|
|
4
|
+
constructor(tokens, claimed = new Set()) {
|
|
5
|
+
this.tokens = tokens;
|
|
6
|
+
this.claimed = new Set(claimed);
|
|
7
|
+
}
|
|
8
|
+
claimNext() {
|
|
9
|
+
return this.claimOne(() => true);
|
|
10
|
+
}
|
|
11
|
+
claimOne(match) {
|
|
12
|
+
for (let token of this) {
|
|
13
|
+
if (match(token)) {
|
|
14
|
+
return {
|
|
15
|
+
tokens: [token],
|
|
16
|
+
rest: remainder(this, [token.index]),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { tokens: [], rest: this };
|
|
21
|
+
}
|
|
22
|
+
claimPair(match) {
|
|
23
|
+
let previous;
|
|
24
|
+
for (let token of this) {
|
|
25
|
+
if (!previous) {
|
|
26
|
+
previous = token;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (token.index === previous.index + 1 && match(previous, token)) {
|
|
30
|
+
return {
|
|
31
|
+
tokens: [previous, token],
|
|
32
|
+
rest: remainder(this, [previous.index, token.index]),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
previous = token;
|
|
36
|
+
}
|
|
37
|
+
return { tokens: [], rest: this };
|
|
38
|
+
}
|
|
39
|
+
claimAll(match) {
|
|
40
|
+
let tokens = [];
|
|
41
|
+
let claimed = new Set();
|
|
42
|
+
for (let token of this) {
|
|
43
|
+
if (match(token)) {
|
|
44
|
+
claimed.add(token.index);
|
|
45
|
+
tokens.push(token);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
tokens,
|
|
50
|
+
rest: tokens.length > 0 ? remainder(this, claimed) : this,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
view(options) {
|
|
54
|
+
return new View(this, options);
|
|
55
|
+
}
|
|
56
|
+
*[Symbol.iterator]() {
|
|
57
|
+
for (let token of this.tokens) {
|
|
58
|
+
if (!this.claimed.has(token.index)) {
|
|
59
|
+
yield token;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
class View {
|
|
65
|
+
source;
|
|
66
|
+
options;
|
|
67
|
+
constructor(source, options) {
|
|
68
|
+
this.source = source;
|
|
69
|
+
this.options = options;
|
|
70
|
+
}
|
|
71
|
+
claimNext() {
|
|
72
|
+
return this.claimOne(() => true);
|
|
73
|
+
}
|
|
74
|
+
claimOne(match) {
|
|
75
|
+
for (let token of this) {
|
|
76
|
+
if (match(token)) {
|
|
77
|
+
return {
|
|
78
|
+
tokens: [token],
|
|
79
|
+
rest: remainder(this.source, [token.index]),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { tokens: [], rest: this.source };
|
|
84
|
+
}
|
|
85
|
+
claimPair(match) {
|
|
86
|
+
let previous;
|
|
87
|
+
for (let token of this) {
|
|
88
|
+
if (!previous) {
|
|
89
|
+
previous = token;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (token.index === previous.index + 1 && match(previous, token)) {
|
|
93
|
+
return {
|
|
94
|
+
tokens: [previous, token],
|
|
95
|
+
rest: remainder(this.source, [previous.index, token.index]),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
previous = token;
|
|
99
|
+
}
|
|
100
|
+
return { tokens: [], rest: this.source };
|
|
101
|
+
}
|
|
102
|
+
claimAll(match) {
|
|
103
|
+
let tokens = [];
|
|
104
|
+
let claimed = new Set();
|
|
105
|
+
for (let token of this) {
|
|
106
|
+
if (match(token)) {
|
|
107
|
+
claimed.add(token.index);
|
|
108
|
+
tokens.push(token);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
tokens,
|
|
113
|
+
rest: tokens.length > 0 ? remainder(this.source, claimed) : this.source,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
*[Symbol.iterator]() {
|
|
117
|
+
let { start, end } = this.options.range;
|
|
118
|
+
let { through } = this.options;
|
|
119
|
+
for (let token of this.source) {
|
|
120
|
+
if (token.index > start &&
|
|
121
|
+
(end === undefined || token.index < end) &&
|
|
122
|
+
(through === undefined || token.index <= through)) {
|
|
123
|
+
yield token;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function remainder(source, claimed) {
|
|
129
|
+
return new Tokenizer(source.tokens, new Set([...source.claimed, ...claimed]));
|
|
130
|
+
}
|