@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.
package/dist/usage.js ADDED
@@ -0,0 +1,239 @@
1
+ //#region src/usage.ts
2
+ /**
3
+ * Formats a usage description into a human-readable string representation
4
+ * suitable for command-line help text.
5
+ *
6
+ * This function converts a structured {@link Usage} description into a
7
+ * formatted string that follows common CLI conventions. It supports various
8
+ * formatting options including colors and compact option display.
9
+ * @param programName The name of the program or command for which the usage
10
+ * description is being formatted. This is typically the
11
+ * name of the executable or script that the user will run.
12
+ * @param usage The usage description to format, consisting of an array
13
+ * of usage terms representing the command-line structure.
14
+ * @param options Optional formatting options to customize the output.
15
+ * See {@link UsageFormatOptions} for available options.
16
+ * @returns A formatted string representation of the usage description.
17
+ */
18
+ function formatUsage(programName, usage, options = {}) {
19
+ usage = normalizeUsage(usage);
20
+ if (options.expandCommands) {
21
+ const lastTerm = usage.at(-1);
22
+ if (usage.length > 0 && usage.slice(0, -1).every((t) => t.type === "command") && lastTerm.type === "exclusive" && lastTerm.terms.every((t) => t.length > 0 && (t[0].type === "command" || t[0].type === "option" || t[0].type === "argument" || t[0].type === "optional" && t[0].terms.length === 1 && (t[0].terms[0].type === "command" || t[0].terms[0].type === "option" || t[0].terms[0].type === "argument")))) {
23
+ const lines = [];
24
+ for (let command of lastTerm.terms) {
25
+ if (usage.length > 1) command = [...usage.slice(0, -1), ...command];
26
+ lines.push(formatUsage(programName, command, options));
27
+ }
28
+ return lines.join("\n");
29
+ }
30
+ }
31
+ let output = options.colors ? `\x1b[1m${programName}\x1b[0m ` : `${programName} `;
32
+ let lineWidth = programName.length + 1;
33
+ for (const { text, width } of formatUsageTerms(usage, options)) {
34
+ if (options.maxWidth != null && lineWidth + width > options.maxWidth) {
35
+ output += "\n";
36
+ lineWidth = 0;
37
+ if (text === " ") continue;
38
+ }
39
+ output += text;
40
+ lineWidth += width;
41
+ }
42
+ return output;
43
+ }
44
+ /**
45
+ * Normalizes a usage description by flattening nested exclusive terms,
46
+ * sorting terms for better readability, and ensuring consistent structure
47
+ * throughout the usage tree.
48
+ *
49
+ * This function performs two main operations:
50
+ *
51
+ * 1. *Flattening*: Recursively processes all usage terms and merges any
52
+ * nested exclusive terms into their parent exclusive term to avoid
53
+ * redundant nesting. For example, an exclusive term containing another
54
+ * exclusive term will have its nested terms flattened into the parent.
55
+ *
56
+ * 2. *Sorting*: Reorders terms to improve readability by placing:
57
+ * - Commands (subcommands) first
58
+ * - Options and other terms in the middle
59
+ * - Positional arguments last (including optional/multiple wrappers around
60
+ * arguments)
61
+ *
62
+ * The sorting logic also recognizes when optional or multiple terms contain
63
+ * positional arguments and treats them as arguments for sorting purposes.
64
+ *
65
+ * @param usage The usage description to normalize.
66
+ * @returns A normalized usage description with flattened exclusive terms
67
+ * and terms sorted for optimal readability.
68
+ */
69
+ function normalizeUsage(usage) {
70
+ const terms = usage.map(normalizeUsageTerm);
71
+ terms.sort((a, b) => {
72
+ const aCmd = a.type === "command";
73
+ const bCmd = b.type === "command";
74
+ const aArg = a.type === "argument" || (a.type === "optional" || a.type === "multiple") && a.terms.at(-1)?.type === "argument";
75
+ const bArg = b.type === "argument" || (b.type === "optional" || b.type === "multiple") && b.terms.at(-1)?.type === "argument";
76
+ return aCmd === bCmd ? aArg === bArg ? 0 : aArg ? 1 : -1 : aCmd ? -1 : 1;
77
+ });
78
+ return terms;
79
+ }
80
+ function normalizeUsageTerm(term) {
81
+ if (term.type === "optional") return {
82
+ type: "optional",
83
+ terms: normalizeUsage(term.terms)
84
+ };
85
+ else if (term.type === "multiple") return {
86
+ type: "multiple",
87
+ terms: normalizeUsage(term.terms),
88
+ min: term.min
89
+ };
90
+ else if (term.type === "exclusive") {
91
+ const terms = [];
92
+ for (const usage of term.terms) {
93
+ const normalized = normalizeUsage(usage);
94
+ if (normalized.length === 1 && normalized[0].type === "exclusive") for (const subUsage of normalized[0].terms) terms.push(subUsage);
95
+ else terms.push(normalized);
96
+ }
97
+ return {
98
+ type: "exclusive",
99
+ terms
100
+ };
101
+ } else return term;
102
+ }
103
+ function* formatUsageTerms(terms, options) {
104
+ let i = 0;
105
+ for (const t of terms) {
106
+ if (i > 0) yield {
107
+ text: " ",
108
+ width: 1
109
+ };
110
+ yield* formatUsageTermInternal(t, options);
111
+ i++;
112
+ }
113
+ }
114
+ /**
115
+ * Formats a single {@link UsageTerm} into a string representation
116
+ * suitable for command-line help text.
117
+ * @param term The usage term to format, which can be an argument,
118
+ * option, command, optional term, exclusive term, or multiple term.
119
+ * @param options Optional formatting options to customize the output.
120
+ * See {@link UsageTermFormatOptions} for available options.
121
+ * @returns A formatted string representation of the usage term.
122
+ */
123
+ function formatUsageTerm(term, options = {}) {
124
+ let lineWidth = 0;
125
+ let output = "";
126
+ for (const { text, width } of formatUsageTermInternal(term, options)) {
127
+ if (options.maxWidth != null && lineWidth + width > options.maxWidth) {
128
+ output += "\n";
129
+ lineWidth = 0;
130
+ if (text === " ") continue;
131
+ }
132
+ output += text;
133
+ lineWidth += width;
134
+ }
135
+ return output;
136
+ }
137
+ function* formatUsageTermInternal(term, options) {
138
+ const optionsSeparator = options.optionsSeparator ?? "/";
139
+ if (term.type === "argument") yield {
140
+ text: options?.colors ? `\x1b[4m${term.metavar}\x1b[0m` : term.metavar,
141
+ width: term.metavar.length
142
+ };
143
+ else if (term.type === "option") if (options?.onlyShortestOptions) {
144
+ const shortestName = term.names.reduce((a, b) => a.length <= b.length ? a : b);
145
+ yield {
146
+ text: options?.colors ? `\x1b[3m${shortestName}\x1b[0m` : shortestName,
147
+ width: shortestName.length
148
+ };
149
+ } else {
150
+ let i = 0;
151
+ for (const optionName of term.names) {
152
+ if (i > 0) yield {
153
+ text: options?.colors ? `\x1b[2m${optionsSeparator}\x1b[0m` : optionsSeparator,
154
+ width: optionsSeparator.length
155
+ };
156
+ yield {
157
+ text: options?.colors ? `\x1b[3m${optionName}\x1b[0m` : optionName,
158
+ width: optionName.length
159
+ };
160
+ i++;
161
+ }
162
+ if (term.metavar != null) {
163
+ yield {
164
+ text: " ",
165
+ width: 1
166
+ };
167
+ yield {
168
+ text: options?.colors ? `\x1b[4m\x1b[2m${term.metavar}\x1b[0m` : term.metavar,
169
+ width: term.metavar.length
170
+ };
171
+ }
172
+ }
173
+ else if (term.type === "command") yield {
174
+ text: options?.colors ? `\x1b[1m${term.name}\x1b[0m` : term.name,
175
+ width: term.name.length
176
+ };
177
+ else if (term.type === "optional") {
178
+ yield {
179
+ text: options?.colors ? `\x1b[2m[\x1b[0m` : "[",
180
+ width: 1
181
+ };
182
+ yield* formatUsageTerms(term.terms, options);
183
+ yield {
184
+ text: options?.colors ? `\x1b[2m]\x1b[0m` : "]",
185
+ width: 1
186
+ };
187
+ } else if (term.type === "exclusive") {
188
+ yield {
189
+ text: options?.colors ? `\x1b[2m(\x1b[0m` : "(",
190
+ width: 1
191
+ };
192
+ let i = 0;
193
+ for (const termGroup of term.terms) {
194
+ if (i > 0) {
195
+ yield {
196
+ text: " ",
197
+ width: 1
198
+ };
199
+ yield {
200
+ text: "|",
201
+ width: 1
202
+ };
203
+ yield {
204
+ text: " ",
205
+ width: 1
206
+ };
207
+ }
208
+ yield* formatUsageTerms(termGroup, options);
209
+ i++;
210
+ }
211
+ yield {
212
+ text: options?.colors ? `\x1b[2m)\x1b[0m` : ")",
213
+ width: 1
214
+ };
215
+ } else if (term.type === "multiple") {
216
+ if (term.min < 1) yield {
217
+ text: options?.colors ? `\x1b[2m[\x1b[0m` : "[",
218
+ width: 1
219
+ };
220
+ for (let i = 0; i < Math.max(1, term.min); i++) {
221
+ if (i > 0) yield {
222
+ text: " ",
223
+ width: 1
224
+ };
225
+ yield* formatUsageTerms(term.terms, options);
226
+ }
227
+ yield {
228
+ text: options?.colors ? `\x1b[2m...\x1b[0m` : "...",
229
+ width: 3
230
+ };
231
+ if (term.min < 1) yield {
232
+ text: options?.colors ? `\x1b[2m]\x1b[0m` : "]",
233
+ width: 1
234
+ };
235
+ } else throw new TypeError(`Unknown usage term type: ${term["type"]}.`);
236
+ }
237
+
238
+ //#endregion
239
+ export { formatUsage, formatUsageTerm, normalizeUsage };
@@ -0,0 +1,332 @@
1
+ const require_message = require('./message.cjs');
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: require_message.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: require_message.message`Expected a string matching pattern ${require_message.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: require_message.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: require_message.message`Expected a value greater than or equal to ${require_message.text(options.min.toLocaleString("en"))}, but got ${input}.`
122
+ };
123
+ else if (options.max != null && value > options.max) return {
124
+ success: false,
125
+ error: require_message.message`Expected a value less than or equal to ${require_message.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: require_message.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: require_message.message`Expected a value greater than or equal to ${require_message.text(options.min.toLocaleString("en"))}, but got ${input}.`
147
+ };
148
+ else if (options?.max != null && value > options.max) return {
149
+ success: false,
150
+ error: require_message.message`Expected a value less than or equal to ${require_message.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: require_message.message`Expected a valid number, but got ${input}.`
186
+ };
187
+ } else return {
188
+ success: false,
189
+ error: require_message.message`Expected a valid number, but got ${input}.`
190
+ };
191
+ if (options.min != null && value < options.min) return {
192
+ success: false,
193
+ error: require_message.message`Expected a value greater than or equal to ${require_message.text(options.min.toLocaleString("en"))}, but got ${input}.`
194
+ };
195
+ else if (options.max != null && value > options.max) return {
196
+ success: false,
197
+ error: require_message.message`Expected a value less than or equal to ${require_message.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: require_message.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: require_message.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: require_message.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: require_message.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 = require_message.message``;
302
+ let i = 0;
303
+ for (const v of options.allowedVersions) {
304
+ expectedVersions = i < 1 ? require_message.message`${expectedVersions}${v.toLocaleString("en")}` : i + 1 >= options.allowedVersions.length ? require_message.message`${expectedVersions}, or ${v.toLocaleString("en")}` : require_message.message`${expectedVersions}, ${v.toLocaleString("en")}`;
305
+ i++;
306
+ }
307
+ return {
308
+ success: false,
309
+ error: require_message.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
+ exports.choice = choice;
326
+ exports.float = float;
327
+ exports.integer = integer;
328
+ exports.isValueParser = isValueParser;
329
+ exports.locale = locale;
330
+ exports.string = string;
331
+ exports.url = url;
332
+ exports.uuid = uuid;