@optique/core 0.1.0-dev.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.
@@ -0,0 +1,325 @@
1
+ import { message, text } from "./message.js";
2
+
3
+ //#region src/valueparser.ts
4
+ /**
5
+ * A predicate function that checks if an object is a {@link ValueParser}.
6
+ * @param object The object to check.
7
+ * @return `true` if the object is a {@link ValueParser}, `false` otherwise.
8
+ */
9
+ function isValueParser(object) {
10
+ return typeof object === "object" && object != null && "metavar" in object && typeof object.metavar === "string" && "parse" in object && typeof object.parse === "function" && "format" in object && typeof object.format === "function";
11
+ }
12
+ /**
13
+ * Creates a {@link ValueParser} that accepts one of multiple
14
+ * string values, so-called enumerated values.
15
+ *
16
+ * This parser validates that the input string matches one of
17
+ * the specified values. If the input does not match any of the values,
18
+ * it returns an error message indicating the valid options.
19
+ * @param values An array of valid string values that this parser can accept.
20
+ * @param options Configuration options for the choice parser.
21
+ * @returns A {@link ValueParser} that checks if the input matches one of the
22
+ * specified values.
23
+ */
24
+ function choice(values, options = {}) {
25
+ const normalizedValues = options.caseInsensitive ? values.map((v) => v.toLowerCase()) : values;
26
+ return {
27
+ metavar: options.metavar ?? "TYPE",
28
+ parse(input) {
29
+ const normalizedInput = options.caseInsensitive ? input.toLowerCase() : input;
30
+ const index = normalizedValues.indexOf(normalizedInput);
31
+ if (index < 0) return {
32
+ success: false,
33
+ error: message`Expected one of ${values.join(", ")}, but got ${input}.`
34
+ };
35
+ return {
36
+ success: true,
37
+ value: values[index]
38
+ };
39
+ },
40
+ format(value) {
41
+ return value;
42
+ }
43
+ };
44
+ }
45
+ /**
46
+ * Creates a {@link ValueParser} for strings.
47
+ *
48
+ * This parser validates that the input is a string and optionally checks
49
+ * if it matches a specified regular expression pattern.
50
+ * @param options Configuration options for the string parser.
51
+ * @returns A {@link ValueParser} that parses strings according to the
52
+ * specified options.
53
+ */
54
+ function string(options = {}) {
55
+ return {
56
+ metavar: options.metavar ?? "STRING",
57
+ parse(input) {
58
+ if (options.pattern != null && !options.pattern.test(input)) return {
59
+ success: false,
60
+ error: message`Expected a string matching pattern ${text(options.pattern.source)}, but got ${input}.`
61
+ };
62
+ return {
63
+ success: true,
64
+ value: input
65
+ };
66
+ },
67
+ format(value) {
68
+ return value;
69
+ }
70
+ };
71
+ }
72
+ /**
73
+ * Creates a ValueParser for parsing integer values from strings.
74
+ *
75
+ * This function provides two modes of operation:
76
+ *
77
+ * - Regular mode: Returns JavaScript numbers
78
+ * (safe up to `Number.MAX_SAFE_INTEGER`)
79
+ * - `bigint` mode: Returns `bigint` values for arbitrarily large integers
80
+ *
81
+ * The parser validates that the input is a valid integer and optionally
82
+ * enforces minimum and maximum value constraints.
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * // Create a parser for regular integers
87
+ * const portParser = integer({ min: 1, max: 65535 });
88
+ *
89
+ * // Create a parser for BigInt values
90
+ * const bigIntParser = integer({ type: "bigint", min: 0n });
91
+ *
92
+ * // Use the parser
93
+ * const result = portParser.parse("8080");
94
+ * if (result.success) {
95
+ * console.log(`Port: ${result.value}`);
96
+ * } else {
97
+ * console.error(result.error);
98
+ * }
99
+ * ```
100
+ *
101
+ * @param options Configuration options specifying the type and constraints.
102
+ * @returns A {@link ValueParser} that converts string input to the specified
103
+ * integer type.
104
+ */
105
+ function integer(options) {
106
+ if (options?.type === "bigint") return {
107
+ metavar: options.metavar ?? "INTEGER",
108
+ parse(input) {
109
+ let value;
110
+ try {
111
+ value = BigInt(input);
112
+ } catch (e) {
113
+ if (e instanceof SyntaxError) return {
114
+ success: false,
115
+ error: message`Expected a valid integer, but got ${input}.`
116
+ };
117
+ throw e;
118
+ }
119
+ if (options.min != null && value < options.min) return {
120
+ success: false,
121
+ error: message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
122
+ };
123
+ else if (options.max != null && value > options.max) return {
124
+ success: false,
125
+ error: message`Expected a value less than or equal to ${text(options.max.toLocaleString("en"))}, but got ${input}.`
126
+ };
127
+ return {
128
+ success: true,
129
+ value
130
+ };
131
+ },
132
+ format(value) {
133
+ return value.toString();
134
+ }
135
+ };
136
+ return {
137
+ metavar: options?.metavar ?? "INTEGER",
138
+ parse(input) {
139
+ if (!input.match(/^\d+$/)) return {
140
+ success: false,
141
+ error: message`Expected a valid integer, but got ${input}.`
142
+ };
143
+ const value = Number.parseInt(input);
144
+ if (options?.min != null && value < options.min) return {
145
+ success: false,
146
+ error: message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
147
+ };
148
+ else if (options?.max != null && value > options.max) return {
149
+ success: false,
150
+ error: message`Expected a value less than or equal to ${text(options.max.toLocaleString("en"))}, but got ${input}.`
151
+ };
152
+ return {
153
+ success: true,
154
+ value
155
+ };
156
+ },
157
+ format(value) {
158
+ return value.toString();
159
+ }
160
+ };
161
+ }
162
+ /**
163
+ * Creates a {@link ValueParser} for floating-point numbers.
164
+ *
165
+ * This parser validates that the input is a valid floating-point number
166
+ * and optionally enforces minimum and maximum value constraints.
167
+ * @param options Configuration options for the float parser.
168
+ * @returns A {@link ValueParser} that parses strings into floating-point
169
+ * numbers.
170
+ */
171
+ function float(options = {}) {
172
+ const floatRegex = /^[+-]?(?:(?:\d+\.?\d*)|(?:\d*\.\d+))(?:[eE][+-]?\d+)?$/;
173
+ return {
174
+ metavar: options.metavar ?? "NUMBER",
175
+ parse(input) {
176
+ let value;
177
+ const lowerInput = input.toLowerCase();
178
+ if (lowerInput === "nan" && options.allowNaN) value = NaN;
179
+ else if ((lowerInput === "infinity" || lowerInput === "+infinity") && options.allowInfinity) value = Infinity;
180
+ else if (lowerInput === "-infinity" && options.allowInfinity) value = -Infinity;
181
+ else if (floatRegex.test(input)) {
182
+ value = Number(input);
183
+ if (Number.isNaN(value)) return {
184
+ success: false,
185
+ error: message`Expected a valid number, but got ${input}.`
186
+ };
187
+ } else return {
188
+ success: false,
189
+ error: message`Expected a valid number, but got ${input}.`
190
+ };
191
+ if (options.min != null && value < options.min) return {
192
+ success: false,
193
+ error: message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
194
+ };
195
+ else if (options.max != null && value > options.max) return {
196
+ success: false,
197
+ error: message`Expected a value less than or equal to ${text(options.max.toLocaleString("en"))}, but got ${input}.`
198
+ };
199
+ return {
200
+ success: true,
201
+ value
202
+ };
203
+ },
204
+ format(value) {
205
+ return value.toString();
206
+ }
207
+ };
208
+ }
209
+ /**
210
+ * Creates a {@link ValueParser} for URL values.
211
+ *
212
+ * This parser validates that the input is a well-formed URL and optionally
213
+ * restricts the allowed protocols. The parsed result is a JavaScript `URL`
214
+ * object.
215
+ * @param options Configuration options for the URL parser.
216
+ * @returns A {@link ValueParser} that converts string input to `URL` objects.
217
+ */
218
+ function url(options = {}) {
219
+ const allowedProtocols = options.allowedProtocols?.map((p) => p.toLowerCase());
220
+ return {
221
+ metavar: options.metavar ?? "URL",
222
+ parse(input) {
223
+ if (!URL.canParse(input)) return {
224
+ success: false,
225
+ error: message`Invalid URL: ${input}.`
226
+ };
227
+ const url$1 = new URL(input);
228
+ if (allowedProtocols != null && !allowedProtocols.includes(url$1.protocol)) return {
229
+ success: false,
230
+ error: message`URL protocol ${url$1.protocol} is not allowed. Allowed protocols: ${allowedProtocols.join(", ")}.`
231
+ };
232
+ return {
233
+ success: true,
234
+ value: url$1
235
+ };
236
+ },
237
+ format(value) {
238
+ return value.href;
239
+ }
240
+ };
241
+ }
242
+ /**
243
+ * Creates a {@link ValueParser} for locale values.
244
+ *
245
+ * This parser validates that the input is a well-formed locale identifier
246
+ * according to the Unicode Locale Identifier standard (BCP 47).
247
+ * The parsed result is a JavaScript `Intl.Locale` object.
248
+ * @param options Configuration options for the locale parser.
249
+ * @returns A {@link ValueParser} that converts string input to `Intl.Locale`
250
+ * objects.
251
+ */
252
+ function locale(options = {}) {
253
+ return {
254
+ metavar: options.metavar ?? "LOCALE",
255
+ parse(input) {
256
+ let locale$1;
257
+ try {
258
+ locale$1 = new Intl.Locale(input);
259
+ } catch (e) {
260
+ if (e instanceof RangeError) return {
261
+ success: false,
262
+ error: message`Invalid locale: ${input}.`
263
+ };
264
+ throw e;
265
+ }
266
+ return {
267
+ success: true,
268
+ value: locale$1
269
+ };
270
+ },
271
+ format(value) {
272
+ return value.baseName;
273
+ }
274
+ };
275
+ }
276
+ /**
277
+ * Creates a {@link ValueParser} for UUID values.
278
+ *
279
+ * This parser validates that the input is a well-formed UUID string in the
280
+ * standard format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` where each `x`
281
+ * is a hexadecimal digit. The parser can optionally restrict to specific
282
+ * UUID versions.
283
+ *
284
+ * @param options Configuration options for the UUID parser.
285
+ * @returns A {@link ValueParser} that converts string input to {@link Uuid}
286
+ * strings.
287
+ */
288
+ function uuid(options = {}) {
289
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
290
+ return {
291
+ metavar: options.metavar ?? "UUID",
292
+ parse(input) {
293
+ if (!uuidRegex.test(input)) return {
294
+ success: false,
295
+ error: message`Expected a valid UUID in format ${"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}, but got ${input}.`
296
+ };
297
+ if (options.allowedVersions != null && options.allowedVersions.length > 0) {
298
+ const versionChar = input.charAt(14);
299
+ const version = parseInt(versionChar, 16);
300
+ if (!options.allowedVersions.includes(version)) {
301
+ let expectedVersions = message``;
302
+ let i = 0;
303
+ for (const v of options.allowedVersions) {
304
+ expectedVersions = i < 1 ? message`${expectedVersions}${v.toLocaleString("en")}` : i + 1 >= options.allowedVersions.length ? message`${expectedVersions}, or ${v.toLocaleString("en")}` : message`${expectedVersions}, ${v.toLocaleString("en")}`;
305
+ i++;
306
+ }
307
+ return {
308
+ success: false,
309
+ error: message`Expected UUID version ${expectedVersions}, but got version ${version.toLocaleString("en")}.`
310
+ };
311
+ }
312
+ }
313
+ return {
314
+ success: true,
315
+ value: input
316
+ };
317
+ },
318
+ format(value) {
319
+ return value;
320
+ }
321
+ };
322
+ }
323
+
324
+ //#endregion
325
+ export { choice, float, integer, isValueParser, locale, string, url, uuid };
package/package.json ADDED
@@ -0,0 +1,117 @@
1
+ {
2
+ "name": "@optique/core",
3
+ "version": "0.1.0-dev.1+4d43eddd",
4
+ "description": "Type-safe combinatorial command-line interface parser",
5
+ "keywords": [
6
+ "CLI",
7
+ "command-line",
8
+ "commandline",
9
+ "parser",
10
+ "getopt",
11
+ "optparse"
12
+ ],
13
+ "license": "MIT",
14
+ "author": {
15
+ "name": "Hong Minhee",
16
+ "email": "hong@minhee.org",
17
+ "url": "https://hongminhee.org/"
18
+ },
19
+ "homepage": "https://optique.dev/",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/dahlia/optique.git",
23
+ "directory": "packages/core/"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/dahlia/optique/issues"
27
+ },
28
+ "funding": [
29
+ "https://github.com/sponsors/dahlia"
30
+ ],
31
+ "engines": {
32
+ "node": ">=20.0.0",
33
+ "bun": ">=1.2.0",
34
+ "deno": ">=2.3.0"
35
+ },
36
+ "files": [
37
+ "dist/",
38
+ "package.json",
39
+ "README.md"
40
+ ],
41
+ "type": "module",
42
+ "module": "./dist/index.js",
43
+ "main": "./dist/index.cjs",
44
+ "types": "./dist/index.d.ts",
45
+ "exports": {
46
+ ".": {
47
+ "types": {
48
+ "import": "./dist/index.d.ts",
49
+ "require": "./dist/index.d.cts"
50
+ },
51
+ "import": "./dist/index.js",
52
+ "require": "./dist/index.cjs"
53
+ },
54
+ "./doc": {
55
+ "types": {
56
+ "import": "./dist/doc.d.ts",
57
+ "require": "./dist/doc.cts"
58
+ },
59
+ "import": "./dist/doc.js",
60
+ "require": "./dist/doc.cjs"
61
+ },
62
+ "./facade": {
63
+ "types": {
64
+ "import": "./dist/facade.d.ts",
65
+ "require": "./dist/facade.cts"
66
+ },
67
+ "import": "./dist/facade.js",
68
+ "require": "./dist/facade.cjs"
69
+ },
70
+ "./message": {
71
+ "types": {
72
+ "import": "./dist/message.d.ts",
73
+ "require": "./dist/message.cts"
74
+ },
75
+ "import": "./dist/message.js",
76
+ "require": "./dist/message.cjs"
77
+ },
78
+ "./parser": {
79
+ "types": {
80
+ "import": "./dist/parser.d.ts",
81
+ "require": "./dist/parser.cts"
82
+ },
83
+ "import": "./dist/parser.js",
84
+ "require": "./dist/parser.cjs"
85
+ },
86
+ "./usage": {
87
+ "types": {
88
+ "import": "./dist/usage.d.ts",
89
+ "require": "./dist/usage.cts"
90
+ },
91
+ "import": "./dist/usage.js",
92
+ "require": "./dist/usage.cjs"
93
+ },
94
+ "./valueparser": {
95
+ "types": {
96
+ "import": "./dist/valueparser.d.ts",
97
+ "require": "./dist/valueparser.cts"
98
+ },
99
+ "import": "./dist/valueparser.js",
100
+ "require": "./dist/valueparser.cjs"
101
+ }
102
+ },
103
+ "sideEffects": false,
104
+ "devDependencies": {
105
+ "@types/node": "^20.19.9",
106
+ "tsdown": "^0.13.0",
107
+ "typescript": "^5.8.3"
108
+ },
109
+ "scripts": {
110
+ "build": "tsdown",
111
+ "prepublish": "tsdown",
112
+ "test": "tsdown && node --experimental-transform-types --test",
113
+ "test:bun": "tsdown && bun test",
114
+ "test:deno": "deno test",
115
+ "test-all": "tsdown && node --experimental-transform-types --test && bun test && deno test"
116
+ }
117
+ }