@optique/core 0.10.7 → 1.0.0-dev.1109
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 +4 -6
- package/dist/annotations.cjs +209 -1
- package/dist/annotations.d.cts +78 -1
- package/dist/annotations.d.ts +78 -1
- package/dist/annotations.js +201 -1
- package/dist/completion.cjs +186 -50
- package/dist/completion.js +186 -50
- package/dist/constructs.cjs +310 -78
- package/dist/constructs.d.cts +525 -644
- package/dist/constructs.d.ts +525 -644
- package/dist/constructs.js +311 -79
- package/dist/context.cjs +43 -3
- package/dist/context.d.cts +113 -5
- package/dist/context.d.ts +113 -5
- package/dist/context.js +41 -3
- package/dist/dependency.cjs +172 -66
- package/dist/dependency.d.cts +22 -2
- package/dist/dependency.d.ts +22 -2
- package/dist/dependency.js +172 -66
- package/dist/doc.cjs +46 -1
- package/dist/doc.d.cts +24 -0
- package/dist/doc.d.ts +24 -0
- package/dist/doc.js +46 -1
- package/dist/facade.cjs +702 -322
- package/dist/facade.d.cts +124 -190
- package/dist/facade.d.ts +124 -190
- package/dist/facade.js +703 -323
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/message.cjs +7 -4
- package/dist/message.js +7 -4
- package/dist/mode-dispatch.cjs +23 -1
- package/dist/mode-dispatch.d.cts +55 -0
- package/dist/mode-dispatch.d.ts +55 -0
- package/dist/mode-dispatch.js +21 -1
- package/dist/modifiers.cjs +210 -55
- package/dist/modifiers.js +211 -56
- package/dist/parser.cjs +80 -47
- package/dist/parser.d.cts +18 -3
- package/dist/parser.d.ts +18 -3
- package/dist/parser.js +82 -50
- package/dist/primitives.cjs +102 -37
- package/dist/primitives.d.cts +81 -24
- package/dist/primitives.d.ts +81 -24
- package/dist/primitives.js +103 -39
- package/dist/usage.cjs +88 -6
- package/dist/usage.d.cts +51 -13
- package/dist/usage.d.ts +51 -13
- package/dist/usage.js +85 -7
- package/dist/valueparser.cjs +371 -99
- package/dist/valueparser.d.cts +56 -7
- package/dist/valueparser.d.ts +56 -7
- package/dist/valueparser.js +371 -99
- package/package.json +10 -1
package/dist/valueparser.js
CHANGED
|
@@ -14,56 +14,129 @@ function isValueParser(object) {
|
|
|
14
14
|
* Implementation of the choice parser for both string and number types.
|
|
15
15
|
*/
|
|
16
16
|
function choice(choices, options = {}) {
|
|
17
|
+
if (choices.length < 1) throw new TypeError("Expected at least one choice, but got an empty array.");
|
|
18
|
+
if (choices.some((c) => c === "")) throw new TypeError("Empty strings are not allowed as choices.");
|
|
19
|
+
for (const c of choices) if (typeof c !== "string" && typeof c !== "number") throw new TypeError(`Expected every choice to be a string or number, but got ${typeof c}.`);
|
|
20
|
+
const isNumber = typeof choices[0] === "number";
|
|
21
|
+
for (const c of choices) if (isNumber ? typeof c !== "number" : typeof c !== "string") throw new TypeError(`Expected every choice to be the same type, but got both ${isNumber ? "number" : "string"} and ${typeof c}.`);
|
|
22
|
+
if (isNumber && choices.some((v) => Number.isNaN(v))) throw new TypeError("NaN is not allowed in number choices.");
|
|
17
23
|
const metavar = options.metavar ?? "TYPE";
|
|
18
24
|
ensureNonEmptyString(metavar);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
if (isNumber) {
|
|
26
|
+
const numberChoices = (() => {
|
|
27
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28
|
+
let hasPositiveZero = false;
|
|
29
|
+
let hasNegativeZero = false;
|
|
30
|
+
const result = [];
|
|
31
|
+
for (const v of choices) {
|
|
32
|
+
if (Object.is(v, -0)) {
|
|
33
|
+
if (hasNegativeZero) continue;
|
|
34
|
+
hasNegativeZero = true;
|
|
35
|
+
} else if (Object.is(v, 0)) {
|
|
36
|
+
if (hasPositiveZero) continue;
|
|
37
|
+
hasPositiveZero = true;
|
|
38
|
+
} else {
|
|
39
|
+
if (seen.has(v)) continue;
|
|
40
|
+
seen.add(v);
|
|
41
|
+
}
|
|
42
|
+
result.push(v);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
})();
|
|
46
|
+
const numberInvalidChoice = options.errors?.invalidChoice;
|
|
47
|
+
const numberStrings = numberChoices.map((v) => Object.is(v, -0) ? "-0" : String(v));
|
|
48
|
+
const frozenNumberChoices = Object.freeze(numberChoices);
|
|
23
49
|
return {
|
|
24
50
|
$mode: "sync",
|
|
25
51
|
metavar,
|
|
26
|
-
choices,
|
|
52
|
+
choices: frozenNumberChoices,
|
|
27
53
|
parse(input) {
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
success: false,
|
|
31
|
-
error: formatNumberChoiceError(input, numberChoices, numberOptions)
|
|
32
|
-
};
|
|
33
|
-
const index = numberChoices.indexOf(parsed);
|
|
34
|
-
if (index < 0) return {
|
|
35
|
-
success: false,
|
|
36
|
-
error: formatNumberChoiceError(input, numberChoices, numberOptions)
|
|
37
|
-
};
|
|
38
|
-
return {
|
|
54
|
+
const index = numberStrings.indexOf(input);
|
|
55
|
+
if (index >= 0) return {
|
|
39
56
|
success: true,
|
|
40
57
|
value: numberChoices[index]
|
|
41
58
|
};
|
|
59
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)$/.test(input)) {
|
|
60
|
+
const parsed = Number(input);
|
|
61
|
+
if (Number.isFinite(parsed)) {
|
|
62
|
+
const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
|
|
63
|
+
const normalizedInput = normalizeDecimal(input);
|
|
64
|
+
const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
|
|
65
|
+
if (normalizedInput === normalizedCanonical) {
|
|
66
|
+
const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
|
|
67
|
+
if (fallbackIndex >= 0) return {
|
|
68
|
+
success: true,
|
|
69
|
+
value: numberChoices[fallbackIndex]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (parsed === 0 && normalizedInput.replace(/^-/, "") === "0" && !numberChoices.some((v) => Object.is(v, -0))) {
|
|
73
|
+
const zeroIndex = numberChoices.indexOf(0);
|
|
74
|
+
if (zeroIndex >= 0) return {
|
|
75
|
+
success: true,
|
|
76
|
+
value: numberChoices[zeroIndex]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)[eE][+-]?\d+$/.test(input)) {
|
|
82
|
+
const parsed = Number(input);
|
|
83
|
+
if (Number.isFinite(parsed)) {
|
|
84
|
+
const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
|
|
85
|
+
if (/[eE]/.test(canonical)) {
|
|
86
|
+
const normalizedInput = normalizeDecimal(expandScientific(input));
|
|
87
|
+
const normalizedCanonical = normalizeDecimal(expandScientific(canonical));
|
|
88
|
+
if (normalizedInput === normalizedCanonical) {
|
|
89
|
+
const fallbackIndex = numberChoices.findIndex((v) => Object.is(v, parsed));
|
|
90
|
+
if (fallbackIndex >= 0) return {
|
|
91
|
+
success: true,
|
|
92
|
+
value: numberChoices[fallbackIndex]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
error: formatNumberChoiceError(input, numberChoices, numberChoices, numberInvalidChoice)
|
|
101
|
+
};
|
|
42
102
|
},
|
|
43
103
|
format(value) {
|
|
44
|
-
return String(value);
|
|
104
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
45
105
|
},
|
|
46
106
|
suggest(prefix) {
|
|
47
|
-
return
|
|
107
|
+
return numberStrings.filter((valueStr, i) => !Number.isNaN(numberChoices[i]) && valueStr.startsWith(prefix)).map((valueStr) => ({
|
|
48
108
|
kind: "literal",
|
|
49
109
|
text: valueStr
|
|
50
110
|
}));
|
|
51
111
|
}
|
|
52
112
|
};
|
|
53
113
|
}
|
|
54
|
-
const stringChoices = choices;
|
|
114
|
+
const stringChoices = Object.freeze([...new Set(choices)]);
|
|
55
115
|
const stringOptions = options;
|
|
56
|
-
|
|
116
|
+
if (stringOptions.caseInsensitive !== void 0 && typeof stringOptions.caseInsensitive !== "boolean") throw new TypeError(`Expected caseInsensitive to be a boolean, but got ${typeof stringOptions.caseInsensitive}: ${String(stringOptions.caseInsensitive)}.`);
|
|
117
|
+
const caseInsensitive = stringOptions.caseInsensitive ?? false;
|
|
118
|
+
const normalizedValues = caseInsensitive ? stringChoices.map((v) => v.toLowerCase()) : stringChoices;
|
|
119
|
+
if (caseInsensitive) {
|
|
120
|
+
const seen = /* @__PURE__ */ new Map();
|
|
121
|
+
for (let i = 0; i < stringChoices.length; i++) {
|
|
122
|
+
const nv = normalizedValues[i];
|
|
123
|
+
const original = stringChoices[i];
|
|
124
|
+
const prev = seen.get(nv);
|
|
125
|
+
if (prev !== void 0 && prev !== original) throw new TypeError(`Ambiguous choices for case-insensitive matching: ${JSON.stringify(prev)} and ${JSON.stringify(original)} both normalize to ${JSON.stringify(nv)}.`);
|
|
126
|
+
seen.set(nv, original);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const stringInvalidChoice = stringOptions.errors?.invalidChoice;
|
|
57
130
|
return {
|
|
58
131
|
$mode: "sync",
|
|
59
132
|
metavar,
|
|
60
|
-
choices,
|
|
133
|
+
choices: stringChoices,
|
|
61
134
|
parse(input) {
|
|
62
|
-
const normalizedInput =
|
|
135
|
+
const normalizedInput = caseInsensitive ? input.toLowerCase() : input;
|
|
63
136
|
const index = normalizedValues.indexOf(normalizedInput);
|
|
64
137
|
if (index < 0) return {
|
|
65
138
|
success: false,
|
|
66
|
-
error: formatStringChoiceError(input, stringChoices,
|
|
139
|
+
error: formatStringChoiceError(input, stringChoices, stringInvalidChoice)
|
|
67
140
|
};
|
|
68
141
|
return {
|
|
69
142
|
success: true,
|
|
@@ -74,9 +147,9 @@ function choice(choices, options = {}) {
|
|
|
74
147
|
return String(value);
|
|
75
148
|
},
|
|
76
149
|
suggest(prefix) {
|
|
77
|
-
const normalizedPrefix =
|
|
150
|
+
const normalizedPrefix = caseInsensitive ? prefix.toLowerCase() : prefix;
|
|
78
151
|
return stringChoices.filter((value) => {
|
|
79
|
-
const normalizedValue =
|
|
152
|
+
const normalizedValue = caseInsensitive ? value.toLowerCase() : value;
|
|
80
153
|
return normalizedValue.startsWith(normalizedPrefix);
|
|
81
154
|
}).map((value) => ({
|
|
82
155
|
kind: "literal",
|
|
@@ -86,24 +159,73 @@ function choice(choices, options = {}) {
|
|
|
86
159
|
};
|
|
87
160
|
}
|
|
88
161
|
/**
|
|
162
|
+
* Expands a numeric string in scientific notation (e.g., `"1e+21"`,
|
|
163
|
+
* `"1.5e-3"`, `".1e-6"`) into plain decimal form for normalization.
|
|
164
|
+
* Used for both canonical `String(number)` output and user input.
|
|
165
|
+
* Returns the input unchanged if it does not contain scientific notation.
|
|
166
|
+
*/
|
|
167
|
+
function expandScientific(s) {
|
|
168
|
+
const match = /^([+-]?)(\d+\.?\d*|\.\d+)[eE]([+-]?\d+)$/.exec(s);
|
|
169
|
+
if (!match) return s;
|
|
170
|
+
const [, rawSign, mantissa, expStr] = match;
|
|
171
|
+
const sign = rawSign === "-" ? "-" : "";
|
|
172
|
+
const exp = parseInt(expStr, 10);
|
|
173
|
+
const dotPos = mantissa.indexOf(".");
|
|
174
|
+
const digits = mantissa.replace(".", "");
|
|
175
|
+
const intLen = dotPos >= 0 ? dotPos : digits.length;
|
|
176
|
+
const newIntLen = intLen + exp;
|
|
177
|
+
let result;
|
|
178
|
+
if (newIntLen >= digits.length) result = digits + "0".repeat(newIntLen - digits.length);
|
|
179
|
+
else if (newIntLen <= 0) result = "0." + "0".repeat(-newIntLen) + digits;
|
|
180
|
+
else result = digits.slice(0, newIntLen) + "." + digits.slice(newIntLen);
|
|
181
|
+
return sign + result;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Normalizes a plain decimal string by stripping leading zeros from the
|
|
185
|
+
* integer part and trailing zeros from the fractional part, so that two
|
|
186
|
+
* strings representing the same mathematical value compare as equal.
|
|
187
|
+
*/
|
|
188
|
+
function normalizeDecimal(s) {
|
|
189
|
+
let sign = "";
|
|
190
|
+
let str = s;
|
|
191
|
+
if (str.startsWith("-") || str.startsWith("+")) {
|
|
192
|
+
if (str[0] === "-") sign = "-";
|
|
193
|
+
str = str.slice(1);
|
|
194
|
+
}
|
|
195
|
+
const dot = str.indexOf(".");
|
|
196
|
+
let int;
|
|
197
|
+
let frac;
|
|
198
|
+
if (dot >= 0) {
|
|
199
|
+
int = str.slice(0, dot);
|
|
200
|
+
frac = str.slice(dot + 1);
|
|
201
|
+
} else {
|
|
202
|
+
int = str;
|
|
203
|
+
frac = "";
|
|
204
|
+
}
|
|
205
|
+
int = int.replace(/^0+/, "") || "0";
|
|
206
|
+
frac = frac.replace(/0+$/, "");
|
|
207
|
+
return sign + (frac ? int + "." + frac : int);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
89
210
|
* Formats error message for string choice parser.
|
|
90
211
|
*/
|
|
91
|
-
function formatStringChoiceError(input, choices,
|
|
92
|
-
if (
|
|
212
|
+
function formatStringChoiceError(input, choices, invalidChoice) {
|
|
213
|
+
if (invalidChoice) return typeof invalidChoice === "function" ? invalidChoice(input, choices) : invalidChoice;
|
|
93
214
|
return formatDefaultChoiceError(input, choices);
|
|
94
215
|
}
|
|
95
216
|
/**
|
|
96
217
|
* Formats error message for number choice parser.
|
|
97
218
|
*/
|
|
98
|
-
function formatNumberChoiceError(input,
|
|
99
|
-
if (
|
|
100
|
-
return formatDefaultChoiceError(input,
|
|
219
|
+
function formatNumberChoiceError(input, validChoices, allChoices, invalidChoice) {
|
|
220
|
+
if (invalidChoice) return typeof invalidChoice === "function" ? invalidChoice(input, validChoices) : invalidChoice;
|
|
221
|
+
return formatDefaultChoiceError(input, allChoices);
|
|
101
222
|
}
|
|
102
223
|
/**
|
|
103
224
|
* Formats default error message for choice parser.
|
|
104
225
|
*/
|
|
105
226
|
function formatDefaultChoiceError(input, choices) {
|
|
106
|
-
const choiceStrings = choices.map((c) => String(c));
|
|
227
|
+
const choiceStrings = choices.filter((c) => typeof c === "string" || !Number.isNaN(c)).map((c) => Object.is(c, -0) ? "-0" : String(c));
|
|
228
|
+
if (choiceStrings.length === 0 && choices.length > 0) return message`No valid choices are configured, but got ${input}.`;
|
|
107
229
|
return message`Expected one of ${valueSet(choiceStrings, { locale: "en-US" })}, but got ${input}.`;
|
|
108
230
|
}
|
|
109
231
|
/**
|
|
@@ -119,18 +241,31 @@ function formatDefaultChoiceError(input, choices) {
|
|
|
119
241
|
* @param options Configuration options for the string parser.
|
|
120
242
|
* @returns A {@link ValueParser} that parses strings according to the
|
|
121
243
|
* specified options.
|
|
244
|
+
* @throws {TypeError} If `options.pattern` is provided but is not a
|
|
245
|
+
* `RegExp` instance.
|
|
122
246
|
*/
|
|
123
247
|
function string(options = {}) {
|
|
248
|
+
if (options.pattern != null && !(options.pattern instanceof RegExp)) throw new TypeError(`Expected pattern to be a RegExp, but got: ${Object.prototype.toString.call(options.pattern)}`);
|
|
124
249
|
const metavar = options.metavar ?? "STRING";
|
|
125
250
|
ensureNonEmptyString(metavar);
|
|
251
|
+
const patternSource = options.pattern?.source ?? null;
|
|
252
|
+
const patternFlags = options.pattern?.flags ?? null;
|
|
253
|
+
const patternMismatch = options.errors?.patternMismatch;
|
|
126
254
|
return {
|
|
127
255
|
$mode: "sync",
|
|
128
256
|
metavar,
|
|
129
257
|
parse(input) {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
258
|
+
if (patternSource != null && patternFlags != null) {
|
|
259
|
+
const pattern = new RegExp(patternSource, patternFlags);
|
|
260
|
+
if (pattern.test(input)) return {
|
|
261
|
+
success: true,
|
|
262
|
+
value: input
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
error: patternMismatch ? typeof patternMismatch === "function" ? patternMismatch(input, pattern) : patternMismatch : message`Expected a string matching pattern ${text(patternSource)}, but got ${input}.`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
134
269
|
return {
|
|
135
270
|
success: true,
|
|
136
271
|
value: input
|
|
@@ -173,8 +308,16 @@ function string(options = {}) {
|
|
|
173
308
|
* @param options Configuration options specifying the type and constraints.
|
|
174
309
|
* @returns A {@link ValueParser} that converts string input to the specified
|
|
175
310
|
* integer type.
|
|
311
|
+
* @throws {TypeError} If `options.type` is provided but is neither `"number"`
|
|
312
|
+
* nor `"bigint"`.
|
|
176
313
|
*/
|
|
177
314
|
function integer(options) {
|
|
315
|
+
if (options?.type !== void 0 && options.type !== "number" && options.type !== "bigint") throw new TypeError(`Expected type to be "number" or "bigint", but got: ${String(options.type)}.`);
|
|
316
|
+
if (options?.type !== "bigint") {
|
|
317
|
+
if (options?.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
|
|
318
|
+
if (options?.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
|
|
319
|
+
}
|
|
320
|
+
if (options?.min != null && options?.max != null && options.min > options.max) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${options.min} and max: ${options.max}.`);
|
|
178
321
|
if (options?.type === "bigint") {
|
|
179
322
|
const metavar$1 = options.metavar ?? "INTEGER";
|
|
180
323
|
ensureNonEmptyString(metavar$1);
|
|
@@ -182,16 +325,11 @@ function integer(options) {
|
|
|
182
325
|
$mode: "sync",
|
|
183
326
|
metavar: metavar$1,
|
|
184
327
|
parse(input) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
success: false,
|
|
191
|
-
error: options.errors?.invalidInteger ? typeof options.errors.invalidInteger === "function" ? options.errors.invalidInteger(input) : options.errors.invalidInteger : message`Expected a valid integer, but got ${input}.`
|
|
192
|
-
};
|
|
193
|
-
throw e;
|
|
194
|
-
}
|
|
328
|
+
if (!input.match(/^-?\d+$/)) return {
|
|
329
|
+
success: false,
|
|
330
|
+
error: options.errors?.invalidInteger ? typeof options.errors.invalidInteger === "function" ? options.errors.invalidInteger(input) : options.errors.invalidInteger : message`Expected a valid integer, but got ${input}.`
|
|
331
|
+
};
|
|
332
|
+
const value = BigInt(input);
|
|
195
333
|
if (options.min != null && value < options.min) return {
|
|
196
334
|
success: false,
|
|
197
335
|
error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, options.min) : options.errors.belowMinimum : message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
|
|
@@ -212,6 +350,15 @@ function integer(options) {
|
|
|
212
350
|
}
|
|
213
351
|
const metavar = options?.metavar ?? "INTEGER";
|
|
214
352
|
ensureNonEmptyString(metavar);
|
|
353
|
+
const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
|
|
354
|
+
const minSafe = BigInt(Number.MIN_SAFE_INTEGER);
|
|
355
|
+
const unsafeIntegerError = options?.errors?.unsafeInteger;
|
|
356
|
+
function makeUnsafeIntegerError(input) {
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
error: unsafeIntegerError ? typeof unsafeIntegerError === "function" ? unsafeIntegerError(input) : unsafeIntegerError : message`Expected a safe integer between ${text(Number.MIN_SAFE_INTEGER.toLocaleString("en"))} and ${text(Number.MAX_SAFE_INTEGER.toLocaleString("en"))}, but got ${input}. Use type: "bigint" for large values.`
|
|
360
|
+
};
|
|
361
|
+
}
|
|
215
362
|
return {
|
|
216
363
|
$mode: "sync",
|
|
217
364
|
metavar,
|
|
@@ -220,7 +367,14 @@ function integer(options) {
|
|
|
220
367
|
success: false,
|
|
221
368
|
error: options?.errors?.invalidInteger ? typeof options.errors.invalidInteger === "function" ? options.errors.invalidInteger(input) : options.errors.invalidInteger : message`Expected a valid integer, but got ${input}.`
|
|
222
369
|
};
|
|
223
|
-
|
|
370
|
+
let n;
|
|
371
|
+
try {
|
|
372
|
+
n = BigInt(input);
|
|
373
|
+
} catch {
|
|
374
|
+
return makeUnsafeIntegerError(input);
|
|
375
|
+
}
|
|
376
|
+
if (n > maxSafe || n < minSafe) return makeUnsafeIntegerError(input);
|
|
377
|
+
const value = Number(input);
|
|
224
378
|
if (options?.min != null && value < options.min) return {
|
|
225
379
|
success: false,
|
|
226
380
|
error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, options.min) : options.errors.belowMinimum : message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
|
|
@@ -249,6 +403,9 @@ function integer(options) {
|
|
|
249
403
|
* numbers.
|
|
250
404
|
*/
|
|
251
405
|
function float(options = {}) {
|
|
406
|
+
if (options.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
|
|
407
|
+
if (options.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
|
|
408
|
+
if (options.min != null && options.max != null && options.min > options.max) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${options.min} and max: ${options.max}.`);
|
|
252
409
|
const floatRegex = /^[+-]?(?:(?:\d+\.?\d*)|(?:\d*\.\d+))(?:[eE][+-]?\d+)?$/;
|
|
253
410
|
const metavar = options.metavar ?? "NUMBER";
|
|
254
411
|
ensureNonEmptyString(metavar);
|
|
@@ -256,6 +413,10 @@ function float(options = {}) {
|
|
|
256
413
|
$mode: "sync",
|
|
257
414
|
metavar,
|
|
258
415
|
parse(input) {
|
|
416
|
+
const invalidNumber = (i) => ({
|
|
417
|
+
success: false,
|
|
418
|
+
error: options.errors?.invalidNumber ? typeof options.errors.invalidNumber === "function" ? options.errors.invalidNumber(i) : options.errors.invalidNumber : message`Expected a valid number, but got ${i}.`
|
|
419
|
+
});
|
|
259
420
|
let value;
|
|
260
421
|
const lowerInput = input.toLowerCase();
|
|
261
422
|
if (lowerInput === "nan" && options.allowNaN) value = NaN;
|
|
@@ -263,14 +424,9 @@ function float(options = {}) {
|
|
|
263
424
|
else if (lowerInput === "-infinity" && options.allowInfinity) value = -Infinity;
|
|
264
425
|
else if (floatRegex.test(input)) {
|
|
265
426
|
value = Number(input);
|
|
266
|
-
if (Number.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
};
|
|
270
|
-
} else return {
|
|
271
|
-
success: false,
|
|
272
|
-
error: options.errors?.invalidNumber ? typeof options.errors.invalidNumber === "function" ? options.errors.invalidNumber(input) : options.errors.invalidNumber : message`Expected a valid number, but got ${input}.`
|
|
273
|
-
};
|
|
427
|
+
if (!Number.isFinite(value) && !options.allowInfinity) return invalidNumber(input);
|
|
428
|
+
if (Number.isNaN(value)) return invalidNumber(input);
|
|
429
|
+
} else return invalidNumber(input);
|
|
274
430
|
if (options.min != null && value < options.min) return {
|
|
275
431
|
success: false,
|
|
276
432
|
error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, options.min) : options.errors.belowMinimum : message`Expected a value greater than or equal to ${text(options.min.toLocaleString("en"))}, but got ${input}.`
|
|
@@ -299,21 +455,24 @@ function float(options = {}) {
|
|
|
299
455
|
* @returns A {@link ValueParser} that converts string input to `URL` objects.
|
|
300
456
|
*/
|
|
301
457
|
function url(options = {}) {
|
|
302
|
-
const
|
|
458
|
+
const originalProtocols = options.allowedProtocols != null ? Object.freeze([...options.allowedProtocols]) : void 0;
|
|
459
|
+
const allowedProtocols = options.allowedProtocols != null ? Object.freeze(options.allowedProtocols.map((p) => p.toLowerCase())) : void 0;
|
|
303
460
|
const metavar = options.metavar ?? "URL";
|
|
304
461
|
ensureNonEmptyString(metavar);
|
|
462
|
+
const invalidUrl = options.errors?.invalidUrl;
|
|
463
|
+
const disallowedProtocol = options.errors?.disallowedProtocol;
|
|
305
464
|
return {
|
|
306
465
|
$mode: "sync",
|
|
307
466
|
metavar,
|
|
308
467
|
parse(input) {
|
|
309
468
|
if (!URL.canParse(input)) return {
|
|
310
469
|
success: false,
|
|
311
|
-
error:
|
|
470
|
+
error: invalidUrl ? typeof invalidUrl === "function" ? invalidUrl(input) : invalidUrl : message`Invalid URL: ${input}.`
|
|
312
471
|
};
|
|
313
472
|
const url$1 = new URL(input);
|
|
314
473
|
if (allowedProtocols != null && !allowedProtocols.includes(url$1.protocol)) return {
|
|
315
474
|
success: false,
|
|
316
|
-
error:
|
|
475
|
+
error: disallowedProtocol ? typeof disallowedProtocol === "function" ? disallowedProtocol(url$1.protocol, originalProtocols) : disallowedProtocol : message`URL protocol ${url$1.protocol} is not allowed. Allowed protocols: ${allowedProtocols.join(", ")}.`
|
|
317
476
|
};
|
|
318
477
|
return {
|
|
319
478
|
success: true,
|
|
@@ -367,7 +526,7 @@ function locale(options = {}) {
|
|
|
367
526
|
};
|
|
368
527
|
},
|
|
369
528
|
format(value) {
|
|
370
|
-
return value.
|
|
529
|
+
return value.toString();
|
|
371
530
|
},
|
|
372
531
|
*suggest(prefix) {
|
|
373
532
|
const commonLocales = [
|
|
@@ -617,24 +776,27 @@ function uuid(options = {}) {
|
|
|
617
776
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
618
777
|
const metavar = options.metavar ?? "UUID";
|
|
619
778
|
ensureNonEmptyString(metavar);
|
|
779
|
+
const allowedVersions = options.allowedVersions != null ? Object.freeze([...options.allowedVersions]) : null;
|
|
780
|
+
const invalidUuid = options.errors?.invalidUuid;
|
|
781
|
+
const disallowedVersion = options.errors?.disallowedVersion;
|
|
620
782
|
return {
|
|
621
783
|
$mode: "sync",
|
|
622
784
|
metavar,
|
|
623
785
|
parse(input) {
|
|
624
786
|
if (!uuidRegex.test(input)) return {
|
|
625
787
|
success: false,
|
|
626
|
-
error:
|
|
788
|
+
error: invalidUuid ? typeof invalidUuid === "function" ? invalidUuid(input) : invalidUuid : message`Expected a valid UUID in format ${"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}, but got ${input}.`
|
|
627
789
|
};
|
|
628
|
-
if (
|
|
790
|
+
if (allowedVersions != null && allowedVersions.length > 0) {
|
|
629
791
|
const versionChar = input.charAt(14);
|
|
630
792
|
const version = parseInt(versionChar, 16);
|
|
631
|
-
if (!
|
|
793
|
+
if (!allowedVersions.includes(version)) return {
|
|
632
794
|
success: false,
|
|
633
|
-
error:
|
|
795
|
+
error: disallowedVersion ? typeof disallowedVersion === "function" ? disallowedVersion(version, allowedVersions) : disallowedVersion : (() => {
|
|
634
796
|
let expectedVersions = message``;
|
|
635
797
|
let i = 0;
|
|
636
|
-
for (const v of
|
|
637
|
-
expectedVersions = i < 1 ? message`${expectedVersions}${v.toLocaleString("en")}` : i + 1 >=
|
|
798
|
+
for (const v of allowedVersions) {
|
|
799
|
+
expectedVersions = i < 1 ? message`${expectedVersions}${v.toLocaleString("en")}` : i + 1 >= allowedVersions.length ? message`${expectedVersions}, or ${v.toLocaleString("en")}` : message`${expectedVersions}, ${v.toLocaleString("en")}`;
|
|
638
800
|
i++;
|
|
639
801
|
}
|
|
640
802
|
return message`Expected UUID version ${expectedVersions}, but got version ${version.toLocaleString("en")}.`;
|
|
@@ -686,28 +848,28 @@ function uuid(options = {}) {
|
|
|
686
848
|
*
|
|
687
849
|
* @param options Configuration options specifying the type and constraints.
|
|
688
850
|
* @returns A {@link ValueParser} that converts string input to port numbers.
|
|
851
|
+
* @throws {TypeError} If `options.type` is provided but is neither `"number"`
|
|
852
|
+
* nor `"bigint"`.
|
|
689
853
|
* @since 0.10.0
|
|
690
854
|
*/
|
|
691
855
|
function port(options) {
|
|
856
|
+
if (options?.disallowWellKnown !== void 0 && typeof options.disallowWellKnown !== "boolean") throw new TypeError(`Expected disallowWellKnown to be a boolean, but got ${typeof options.disallowWellKnown}: ${String(options.disallowWellKnown)}.`);
|
|
857
|
+
if (options?.type !== void 0 && options.type !== "number" && options.type !== "bigint") throw new TypeError(`Expected type to be "number" or "bigint", but got: ${String(options.type)}.`);
|
|
692
858
|
if (options?.type === "bigint") {
|
|
693
859
|
const metavar$1 = options.metavar ?? "PORT";
|
|
694
860
|
ensureNonEmptyString(metavar$1);
|
|
695
861
|
const min$1 = options.min ?? 1n;
|
|
696
862
|
const max$1 = options.max ?? 65535n;
|
|
863
|
+
if (min$1 > max$1) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${min$1} and max: ${max$1}.`);
|
|
697
864
|
return {
|
|
698
865
|
$mode: "sync",
|
|
699
866
|
metavar: metavar$1,
|
|
700
867
|
parse(input) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
success: false,
|
|
707
|
-
error: options.errors?.invalidPort ? typeof options.errors.invalidPort === "function" ? options.errors.invalidPort(input) : options.errors.invalidPort : message`Expected a valid port number, but got ${input}.`
|
|
708
|
-
};
|
|
709
|
-
throw e;
|
|
710
|
-
}
|
|
868
|
+
if (!input.match(/^-?\d+$/)) return {
|
|
869
|
+
success: false,
|
|
870
|
+
error: options.errors?.invalidPort ? typeof options.errors.invalidPort === "function" ? options.errors.invalidPort(input) : options.errors.invalidPort : message`Expected a valid port number, but got ${input}.`
|
|
871
|
+
};
|
|
872
|
+
const value = BigInt(input);
|
|
711
873
|
if (value < min$1) return {
|
|
712
874
|
success: false,
|
|
713
875
|
error: options.errors?.belowMinimum ? typeof options.errors.belowMinimum === "function" ? options.errors.belowMinimum(value, min$1) : options.errors.belowMinimum : message`Expected a port number greater than or equal to ${text(min$1.toLocaleString("en"))}, but got ${input}.`
|
|
@@ -730,10 +892,13 @@ function port(options) {
|
|
|
730
892
|
}
|
|
731
893
|
};
|
|
732
894
|
}
|
|
895
|
+
if (options?.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
|
|
896
|
+
if (options?.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
|
|
733
897
|
const metavar = options?.metavar ?? "PORT";
|
|
734
898
|
ensureNonEmptyString(metavar);
|
|
735
899
|
const min = options?.min ?? 1;
|
|
736
900
|
const max = options?.max ?? 65535;
|
|
901
|
+
if (min > max) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${min} and max: ${max}.`);
|
|
737
902
|
return {
|
|
738
903
|
$mode: "sync",
|
|
739
904
|
metavar,
|
|
@@ -948,6 +1113,7 @@ function ipv4(options) {
|
|
|
948
1113
|
*
|
|
949
1114
|
* @param options - Options for hostname validation.
|
|
950
1115
|
* @returns A value parser for hostnames.
|
|
1116
|
+
* @throws {RangeError} If `maxLength` is not a positive integer.
|
|
951
1117
|
* @since 0.10.0
|
|
952
1118
|
*
|
|
953
1119
|
* @example
|
|
@@ -971,6 +1137,7 @@ function hostname(options) {
|
|
|
971
1137
|
const allowUnderscore = options?.allowUnderscore ?? false;
|
|
972
1138
|
const allowLocalhost = options?.allowLocalhost ?? true;
|
|
973
1139
|
const maxLength = options?.maxLength ?? 253;
|
|
1140
|
+
if (!Number.isInteger(maxLength) || maxLength < 1) throw new RangeError("maxLength must be an integer greater than or equal to 1.");
|
|
974
1141
|
return {
|
|
975
1142
|
$mode: "sync",
|
|
976
1143
|
metavar,
|
|
@@ -1079,14 +1246,28 @@ function email(options) {
|
|
|
1079
1246
|
const allowMultiple = options?.allowMultiple ?? false;
|
|
1080
1247
|
const allowDisplayName = options?.allowDisplayName ?? false;
|
|
1081
1248
|
const lowercase = options?.lowercase ?? false;
|
|
1082
|
-
const allowedDomains = options?.allowedDomains;
|
|
1249
|
+
const allowedDomains = options?.allowedDomains != null ? Object.freeze([...options.allowedDomains]) : void 0;
|
|
1250
|
+
if (allowedDomains != null) for (let i = 0; i < allowedDomains.length; i++) {
|
|
1251
|
+
const entry = allowedDomains[i];
|
|
1252
|
+
if (typeof entry !== "string") throw new TypeError(`allowedDomains[${i}] must be a string, got ${typeof entry}.`);
|
|
1253
|
+
if (entry !== entry.trim()) throw new TypeError(`allowedDomains[${i}] must not have leading or trailing whitespace: ${JSON.stringify(entry)}`);
|
|
1254
|
+
if (entry.startsWith("@")) throw new TypeError(`allowedDomains[${i}] must not start with "@": ${JSON.stringify(entry)}`);
|
|
1255
|
+
if (entry === "" || !entry.includes(".")) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
|
|
1256
|
+
if (entry.startsWith(".") || entry.endsWith(".") || entry.startsWith("-") || entry.endsWith("-")) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
|
|
1257
|
+
const labels = entry.split(".");
|
|
1258
|
+
for (const label of labels) if (label.length === 0 || label.length > 63 || label.startsWith("-") || label.endsWith("-") || !/^[a-zA-Z0-9-]+$/.test(label)) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
|
|
1259
|
+
if (labels.length === 4 && labels.every((label) => /^[0-9]+$/.test(label))) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
|
|
1260
|
+
}
|
|
1261
|
+
const invalidEmail = options?.errors?.invalidEmail;
|
|
1262
|
+
const domainNotAllowed = options?.errors?.domainNotAllowed;
|
|
1083
1263
|
const atextRegex = /^[a-zA-Z0-9._+-]+$/;
|
|
1264
|
+
const encoder = new TextEncoder();
|
|
1084
1265
|
function validateEmail(input) {
|
|
1085
1266
|
const trimmed = input.trim();
|
|
1086
1267
|
let emailAddr = trimmed;
|
|
1087
|
-
if (allowDisplayName
|
|
1088
|
-
const
|
|
1089
|
-
if (
|
|
1268
|
+
if (allowDisplayName) {
|
|
1269
|
+
const displayNameMatch = trimmed.match(/^((?:"(?:[^"\\]|\\.)*"|[^<>"])+)\s*<([^<>]+)>$/);
|
|
1270
|
+
if (displayNameMatch && /\S/.test(displayNameMatch[1].replace(/"((?:[^"\\]|\\.)*)"/g, (_match, inner) => inner))) emailAddr = displayNameMatch[2].trim();
|
|
1090
1271
|
}
|
|
1091
1272
|
let atIndex = -1;
|
|
1092
1273
|
if (emailAddr.startsWith("\"")) {
|
|
@@ -1108,6 +1289,7 @@ function email(options) {
|
|
|
1108
1289
|
isValidLocal = localParts.length > 0 && localParts.every((part) => part.length > 0 && atextRegex.test(part));
|
|
1109
1290
|
}
|
|
1110
1291
|
if (!isValidLocal) return null;
|
|
1292
|
+
if (encoder.encode(localPart).length > 64) return null;
|
|
1111
1293
|
if (!domain$1 || domain$1.length === 0) return null;
|
|
1112
1294
|
if (!domain$1.includes(".")) return null;
|
|
1113
1295
|
if (domain$1.startsWith(".") || domain$1.endsWith(".") || domain$1.startsWith("-") || domain$1.endsWith("-")) return null;
|
|
@@ -1117,32 +1299,64 @@ function email(options) {
|
|
|
1117
1299
|
if (label.startsWith("-") || label.endsWith("-")) return null;
|
|
1118
1300
|
if (!/^[a-zA-Z0-9-]+$/.test(label)) return null;
|
|
1119
1301
|
}
|
|
1302
|
+
if (domainLabels.length === 4 && domainLabels.every((label) => /^[0-9]+$/.test(label))) return null;
|
|
1303
|
+
if (encoder.encode(emailAddr).length > 254) return null;
|
|
1120
1304
|
const resultEmail = emailAddr;
|
|
1121
|
-
|
|
1305
|
+
if (!lowercase) return resultEmail;
|
|
1306
|
+
const lastAt = resultEmail.lastIndexOf("@");
|
|
1307
|
+
return resultEmail.slice(0, lastAt) + resultEmail.slice(lastAt).toLowerCase();
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Splits an input string on commas, respecting quoted segments and
|
|
1311
|
+
* angle-bracket display-name syntax per RFC 5322.
|
|
1312
|
+
*/
|
|
1313
|
+
function splitEmails(input) {
|
|
1314
|
+
const result = [];
|
|
1315
|
+
let current = "";
|
|
1316
|
+
let inQuotes = false;
|
|
1317
|
+
let inAngleBrackets = false;
|
|
1318
|
+
let escaped = false;
|
|
1319
|
+
for (const char of input) {
|
|
1320
|
+
if (escaped) escaped = false;
|
|
1321
|
+
else if (char === "\\" && inQuotes) escaped = true;
|
|
1322
|
+
else if (char === "\"" && !inAngleBrackets) {
|
|
1323
|
+
if (inQuotes) inQuotes = false;
|
|
1324
|
+
else if (current.trim() === "") inQuotes = true;
|
|
1325
|
+
} else if (char === "<" && !inQuotes) inAngleBrackets = true;
|
|
1326
|
+
else if (char === ">" && !inQuotes) inAngleBrackets = false;
|
|
1327
|
+
else if (char === "," && !inQuotes && !inAngleBrackets) {
|
|
1328
|
+
result.push(current);
|
|
1329
|
+
current = "";
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
current += char;
|
|
1333
|
+
}
|
|
1334
|
+
result.push(current);
|
|
1335
|
+
return result;
|
|
1122
1336
|
}
|
|
1123
1337
|
return {
|
|
1124
1338
|
$mode: "sync",
|
|
1125
1339
|
metavar,
|
|
1126
1340
|
parse(input) {
|
|
1127
1341
|
if (allowMultiple) {
|
|
1128
|
-
const emails = input
|
|
1342
|
+
const emails = splitEmails(input).map((e) => e.trim());
|
|
1129
1343
|
const validatedEmails = [];
|
|
1130
1344
|
for (const email$1 of emails) {
|
|
1131
1345
|
const validated = validateEmail(email$1);
|
|
1132
1346
|
if (validated === null) {
|
|
1133
|
-
const errorMsg =
|
|
1347
|
+
const errorMsg = invalidEmail;
|
|
1134
1348
|
const msg = typeof errorMsg === "function" ? errorMsg(email$1) : errorMsg ?? message`Expected a valid email address, but got ${email$1}.`;
|
|
1135
1349
|
return {
|
|
1136
1350
|
success: false,
|
|
1137
1351
|
error: msg
|
|
1138
1352
|
};
|
|
1139
1353
|
}
|
|
1140
|
-
if (allowedDomains
|
|
1354
|
+
if (allowedDomains != null) {
|
|
1141
1355
|
const atIndex = validated.indexOf("@");
|
|
1142
1356
|
const domain$1 = validated.substring(atIndex + 1).toLowerCase();
|
|
1143
1357
|
const isAllowed = allowedDomains.some((allowed) => domain$1 === allowed.toLowerCase());
|
|
1144
1358
|
if (!isAllowed) {
|
|
1145
|
-
const errorMsg =
|
|
1359
|
+
const errorMsg = domainNotAllowed;
|
|
1146
1360
|
if (typeof errorMsg === "function") return {
|
|
1147
1361
|
success: false,
|
|
1148
1362
|
error: errorMsg(validated, allowedDomains)
|
|
@@ -1176,19 +1390,19 @@ function email(options) {
|
|
|
1176
1390
|
} else {
|
|
1177
1391
|
const validated = validateEmail(input);
|
|
1178
1392
|
if (validated === null) {
|
|
1179
|
-
const errorMsg =
|
|
1393
|
+
const errorMsg = invalidEmail;
|
|
1180
1394
|
const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? message`Expected a valid email address, but got ${input}.`;
|
|
1181
1395
|
return {
|
|
1182
1396
|
success: false,
|
|
1183
1397
|
error: msg
|
|
1184
1398
|
};
|
|
1185
1399
|
}
|
|
1186
|
-
if (allowedDomains
|
|
1400
|
+
if (allowedDomains != null) {
|
|
1187
1401
|
const atIndex = validated.indexOf("@");
|
|
1188
1402
|
const domain$1 = validated.substring(atIndex + 1).toLowerCase();
|
|
1189
1403
|
const isAllowed = allowedDomains.some((allowed) => domain$1 === allowed.toLowerCase());
|
|
1190
1404
|
if (!isAllowed) {
|
|
1191
|
-
const errorMsg =
|
|
1405
|
+
const errorMsg = domainNotAllowed;
|
|
1192
1406
|
if (typeof errorMsg === "function") return {
|
|
1193
1407
|
success: false,
|
|
1194
1408
|
error: errorMsg(validated, allowedDomains)
|
|
@@ -1220,7 +1434,7 @@ function email(options) {
|
|
|
1220
1434
|
}
|
|
1221
1435
|
},
|
|
1222
1436
|
format(value) {
|
|
1223
|
-
if (Array.isArray(value)) return value.join(",");
|
|
1437
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
1224
1438
|
return value;
|
|
1225
1439
|
}
|
|
1226
1440
|
};
|
|
@@ -1237,6 +1451,8 @@ function email(options) {
|
|
|
1237
1451
|
*
|
|
1238
1452
|
* @param options - Options for socket address validation.
|
|
1239
1453
|
* @returns A value parser for socket addresses.
|
|
1454
|
+
* @throws {TypeError} If `separator` contains digit characters, since digits
|
|
1455
|
+
* in the separator would cause ambiguous splitting of port input.
|
|
1240
1456
|
* @since 0.10.0
|
|
1241
1457
|
*
|
|
1242
1458
|
* @example
|
|
@@ -1257,9 +1473,10 @@ function email(options) {
|
|
|
1257
1473
|
* ```
|
|
1258
1474
|
*/
|
|
1259
1475
|
function socketAddress(options) {
|
|
1260
|
-
const metavar = options?.metavar ?? "HOST:PORT";
|
|
1261
|
-
ensureNonEmptyString(metavar);
|
|
1262
1476
|
const separator = options?.separator ?? ":";
|
|
1477
|
+
if (/\p{Nd}/u.test(separator)) throw new TypeError(`Expected separator to not contain digits, but got: ${JSON.stringify(separator)}.`);
|
|
1478
|
+
const metavar = options?.metavar ?? `HOST${separator}PORT`;
|
|
1479
|
+
ensureNonEmptyString(metavar);
|
|
1263
1480
|
const defaultPort = options?.defaultPort;
|
|
1264
1481
|
const requirePort = options?.requirePort ?? false;
|
|
1265
1482
|
const hostType = options?.host?.type ?? "both";
|
|
@@ -1361,9 +1578,13 @@ function socketAddress(options) {
|
|
|
1361
1578
|
};
|
|
1362
1579
|
}
|
|
1363
1580
|
function portRange(options) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1581
|
+
if (options?.disallowWellKnown !== void 0 && typeof options.disallowWellKnown !== "boolean") throw new TypeError(`Expected disallowWellKnown to be a boolean, but got ${typeof options.disallowWellKnown}: ${String(options.disallowWellKnown)}.`);
|
|
1582
|
+
if (options?.allowSingle !== void 0 && typeof options.allowSingle !== "boolean") throw new TypeError(`Expected allowSingle to be a boolean, but got ${typeof options.allowSingle}: ${String(options.allowSingle)}.`);
|
|
1583
|
+
if (options?.type !== void 0 && options.type !== "number" && options.type !== "bigint") throw new TypeError(`Expected type to be "number" or "bigint", but got: ${String(options.type)}.`);
|
|
1366
1584
|
const separator = options?.separator ?? "-";
|
|
1585
|
+
if (/\p{Nd}/u.test(separator)) throw new TypeError(`Expected separator to not contain digits, but got: ${JSON.stringify(separator)}.`);
|
|
1586
|
+
const metavar = options?.metavar ?? `PORT${separator}PORT`;
|
|
1587
|
+
ensureNonEmptyString(metavar);
|
|
1367
1588
|
const allowSingle = options?.allowSingle ?? false;
|
|
1368
1589
|
const isBigInt = options?.type === "bigint";
|
|
1369
1590
|
const portParser = isBigInt ? port({
|
|
@@ -1609,6 +1830,10 @@ function macAddress(options) {
|
|
|
1609
1830
|
*
|
|
1610
1831
|
* @param options Parser options for domain validation.
|
|
1611
1832
|
* @returns A parser that accepts valid domain names as strings.
|
|
1833
|
+
* @throws {RangeError} If `maxLength` is not a positive integer.
|
|
1834
|
+
* @throws {RangeError} If `minLabels` is not a positive integer.
|
|
1835
|
+
* @throws {TypeError} If `allowSubdomains` is `false` and `minLabels` is
|
|
1836
|
+
* greater than 2, since non-subdomain domains have exactly 2 labels.
|
|
1612
1837
|
*
|
|
1613
1838
|
* @example
|
|
1614
1839
|
* ``` typescript
|
|
@@ -1633,17 +1858,33 @@ function macAddress(options) {
|
|
|
1633
1858
|
function domain(options) {
|
|
1634
1859
|
const metavar = options?.metavar ?? "DOMAIN";
|
|
1635
1860
|
const allowSubdomains = options?.allowSubdomains ?? true;
|
|
1636
|
-
const allowedTLDs = options?.allowedTLDs;
|
|
1861
|
+
const allowedTLDs = options?.allowedTLDs != null ? Object.freeze([...options.allowedTLDs]) : void 0;
|
|
1637
1862
|
const minLabels = options?.minLabels ?? 2;
|
|
1863
|
+
const maxLength = options?.maxLength ?? 253;
|
|
1638
1864
|
const lowercase = options?.lowercase ?? false;
|
|
1639
|
-
|
|
1865
|
+
if (!Number.isInteger(maxLength) || maxLength < 1) throw new RangeError("maxLength must be an integer greater than or equal to 1.");
|
|
1866
|
+
if (!Number.isInteger(minLabels) || minLabels < 1) throw new RangeError("minLabels must be an integer greater than or equal to 1.");
|
|
1867
|
+
if (!allowSubdomains && minLabels > 2) throw new TypeError("allowSubdomains: false is incompatible with minLabels > 2, as non-subdomain domains have exactly 2 labels.");
|
|
1868
|
+
const invalidDomain = options?.errors?.invalidDomain;
|
|
1869
|
+
const tooLong = options?.errors?.tooLong;
|
|
1870
|
+
const tooFewLabels = options?.errors?.tooFewLabels;
|
|
1871
|
+
const subdomainsNotAllowed = options?.errors?.subdomainsNotAllowed;
|
|
1872
|
+
const tldNotAllowed = options?.errors?.tldNotAllowed;
|
|
1640
1873
|
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
|
1641
1874
|
return {
|
|
1642
1875
|
$mode: "sync",
|
|
1643
1876
|
metavar,
|
|
1644
1877
|
parse(input) {
|
|
1878
|
+
if (input.length > maxLength) {
|
|
1879
|
+
const errorMsg = tooLong;
|
|
1880
|
+
const msg = typeof errorMsg === "function" ? errorMsg(input, maxLength) : errorMsg ?? message`Domain ${input} is too long (maximum ${text(maxLength.toString())} characters).`;
|
|
1881
|
+
return {
|
|
1882
|
+
success: false,
|
|
1883
|
+
error: msg
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1645
1886
|
if (input.length === 0 || input.startsWith(".") || input.endsWith(".")) {
|
|
1646
|
-
const errorMsg =
|
|
1887
|
+
const errorMsg = invalidDomain;
|
|
1647
1888
|
if (typeof errorMsg === "function") return {
|
|
1648
1889
|
success: false,
|
|
1649
1890
|
error: errorMsg(input)
|
|
@@ -1668,7 +1909,7 @@ function domain(options) {
|
|
|
1668
1909
|
};
|
|
1669
1910
|
}
|
|
1670
1911
|
if (input.includes("..")) {
|
|
1671
|
-
const errorMsg =
|
|
1912
|
+
const errorMsg = invalidDomain;
|
|
1672
1913
|
if (typeof errorMsg === "function") return {
|
|
1673
1914
|
success: false,
|
|
1674
1915
|
error: errorMsg(input)
|
|
@@ -1694,7 +1935,32 @@ function domain(options) {
|
|
|
1694
1935
|
}
|
|
1695
1936
|
const labels = input.split(".");
|
|
1696
1937
|
for (const label of labels) if (!labelRegex.test(label)) {
|
|
1697
|
-
const errorMsg =
|
|
1938
|
+
const errorMsg = invalidDomain;
|
|
1939
|
+
if (typeof errorMsg === "function") return {
|
|
1940
|
+
success: false,
|
|
1941
|
+
error: errorMsg(input)
|
|
1942
|
+
};
|
|
1943
|
+
const msg = errorMsg ?? [
|
|
1944
|
+
{
|
|
1945
|
+
type: "text",
|
|
1946
|
+
text: "Expected a valid domain name, but got "
|
|
1947
|
+
},
|
|
1948
|
+
{
|
|
1949
|
+
type: "value",
|
|
1950
|
+
value: input
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
type: "text",
|
|
1954
|
+
text: "."
|
|
1955
|
+
}
|
|
1956
|
+
];
|
|
1957
|
+
return {
|
|
1958
|
+
success: false,
|
|
1959
|
+
error: msg
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
if (labels.length >= 2 && labels.every((l) => /^[0-9]+$/.test(l))) {
|
|
1963
|
+
const errorMsg = invalidDomain;
|
|
1698
1964
|
if (typeof errorMsg === "function") return {
|
|
1699
1965
|
success: false,
|
|
1700
1966
|
error: errorMsg(input)
|
|
@@ -1719,7 +1985,7 @@ function domain(options) {
|
|
|
1719
1985
|
};
|
|
1720
1986
|
}
|
|
1721
1987
|
if (labels.length < minLabels) {
|
|
1722
|
-
const errorMsg =
|
|
1988
|
+
const errorMsg = tooFewLabels;
|
|
1723
1989
|
if (typeof errorMsg === "function") return {
|
|
1724
1990
|
success: false,
|
|
1725
1991
|
error: errorMsg(input, minLabels)
|
|
@@ -1744,7 +2010,7 @@ function domain(options) {
|
|
|
1744
2010
|
};
|
|
1745
2011
|
}
|
|
1746
2012
|
if (!allowSubdomains && labels.length > 2) {
|
|
1747
|
-
const errorMsg =
|
|
2013
|
+
const errorMsg = subdomainsNotAllowed;
|
|
1748
2014
|
if (typeof errorMsg === "function") return {
|
|
1749
2015
|
success: false,
|
|
1750
2016
|
error: errorMsg(input)
|
|
@@ -1773,7 +2039,7 @@ function domain(options) {
|
|
|
1773
2039
|
const tldLower = tld.toLowerCase();
|
|
1774
2040
|
const allowedTLDsLower = allowedTLDs.map((t) => t.toLowerCase());
|
|
1775
2041
|
if (!allowedTLDsLower.includes(tldLower)) {
|
|
1776
|
-
const errorMsg =
|
|
2042
|
+
const errorMsg = tldNotAllowed;
|
|
1777
2043
|
if (typeof errorMsg === "function") return {
|
|
1778
2044
|
success: false,
|
|
1779
2045
|
error: errorMsg(tld, allowedTLDs)
|
|
@@ -2237,7 +2503,13 @@ function ip(options) {
|
|
|
2237
2503
|
* @since 0.10.0
|
|
2238
2504
|
*/
|
|
2239
2505
|
function cidr(options) {
|
|
2506
|
+
if (options?.minPrefix != null && !Number.isFinite(options.minPrefix)) throw new RangeError(`Expected minPrefix to be a finite number, but got: ${options.minPrefix}`);
|
|
2507
|
+
if (options?.maxPrefix != null && !Number.isFinite(options.maxPrefix)) throw new RangeError(`Expected maxPrefix to be a finite number, but got: ${options.maxPrefix}`);
|
|
2508
|
+
if (options?.minPrefix != null && options?.maxPrefix != null && options.minPrefix > options.maxPrefix) throw new RangeError(`Expected minPrefix to be less than or equal to maxPrefix, but got minPrefix: ${options.minPrefix} and maxPrefix: ${options.maxPrefix}.`);
|
|
2240
2509
|
const version = options?.version ?? "both";
|
|
2510
|
+
const maxPrefixForVersion = version === 4 ? 32 : version === 6 ? 128 : 128;
|
|
2511
|
+
if (options?.minPrefix != null && (options.minPrefix < 0 || options.minPrefix > maxPrefixForVersion)) throw new RangeError(`Expected minPrefix to be between 0 and ${maxPrefixForVersion} for IPv${version === "both" ? "4/6" : version}, but got minPrefix: ${options.minPrefix}.`);
|
|
2512
|
+
if (options?.maxPrefix != null && (options.maxPrefix < 0 || options.maxPrefix > maxPrefixForVersion)) throw new RangeError(`Expected maxPrefix to be between 0 and ${maxPrefixForVersion} for IPv${version === "both" ? "4/6" : version}, but got maxPrefix: ${options.maxPrefix}.`);
|
|
2241
2513
|
const minPrefix = options?.minPrefix;
|
|
2242
2514
|
const maxPrefix = options?.maxPrefix;
|
|
2243
2515
|
const errors = options?.errors;
|