@optique/core 1.0.0-dev.921 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/annotation-state.cjs +425 -0
  2. package/dist/annotation-state.d.cts +24 -0
  3. package/dist/annotation-state.d.ts +24 -0
  4. package/dist/annotation-state.js +414 -0
  5. package/dist/annotations.cjs +2 -248
  6. package/dist/annotations.d.cts +2 -137
  7. package/dist/annotations.d.ts +2 -137
  8. package/dist/annotations.js +2 -238
  9. package/dist/completion.cjs +611 -100
  10. package/dist/completion.d.cts +1 -1
  11. package/dist/completion.d.ts +1 -1
  12. package/dist/completion.js +611 -100
  13. package/dist/constructs.cjs +3338 -827
  14. package/dist/constructs.d.cts +48 -7
  15. package/dist/constructs.d.ts +48 -7
  16. package/dist/constructs.js +3338 -827
  17. package/dist/context.cjs +0 -23
  18. package/dist/context.d.cts +119 -53
  19. package/dist/context.d.ts +119 -53
  20. package/dist/context.js +0 -22
  21. package/dist/dependency-metadata.cjs +139 -0
  22. package/dist/dependency-metadata.d.cts +112 -0
  23. package/dist/dependency-metadata.d.ts +112 -0
  24. package/dist/dependency-metadata.js +138 -0
  25. package/dist/dependency-runtime.cjs +698 -0
  26. package/dist/dependency-runtime.d.cts +149 -0
  27. package/dist/dependency-runtime.d.ts +149 -0
  28. package/dist/dependency-runtime.js +687 -0
  29. package/dist/dependency.cjs +7 -928
  30. package/dist/dependency.d.cts +2 -794
  31. package/dist/dependency.d.ts +2 -794
  32. package/dist/dependency.js +2 -899
  33. package/dist/displaywidth.cjs +44 -0
  34. package/dist/displaywidth.js +43 -0
  35. package/dist/doc.cjs +285 -23
  36. package/dist/doc.d.cts +57 -2
  37. package/dist/doc.d.ts +57 -2
  38. package/dist/doc.js +283 -25
  39. package/dist/execution-context.cjs +56 -0
  40. package/dist/execution-context.js +53 -0
  41. package/dist/extension.cjs +87 -0
  42. package/dist/extension.d.cts +97 -0
  43. package/dist/extension.d.ts +97 -0
  44. package/dist/extension.js +76 -0
  45. package/dist/facade.cjs +718 -525
  46. package/dist/facade.d.cts +59 -15
  47. package/dist/facade.d.ts +59 -15
  48. package/dist/facade.js +718 -525
  49. package/dist/index.cjs +14 -29
  50. package/dist/index.d.cts +10 -10
  51. package/dist/index.d.ts +10 -10
  52. package/dist/index.js +7 -7
  53. package/dist/input-trace.cjs +56 -0
  54. package/dist/input-trace.d.cts +77 -0
  55. package/dist/input-trace.d.ts +77 -0
  56. package/dist/input-trace.js +55 -0
  57. package/dist/internal/annotations.cjs +316 -0
  58. package/dist/internal/annotations.d.cts +140 -0
  59. package/dist/internal/annotations.d.ts +140 -0
  60. package/dist/internal/annotations.js +306 -0
  61. package/dist/internal/dependency.cjs +984 -0
  62. package/dist/internal/dependency.d.cts +539 -0
  63. package/dist/internal/dependency.d.ts +539 -0
  64. package/dist/internal/dependency.js +964 -0
  65. package/dist/{mode-dispatch.cjs → internal/mode-dispatch.cjs} +1 -3
  66. package/dist/{mode-dispatch.d.cts → internal/mode-dispatch.d.cts} +3 -7
  67. package/dist/{mode-dispatch.d.ts → internal/mode-dispatch.d.ts} +3 -7
  68. package/dist/{mode-dispatch.js → internal/mode-dispatch.js} +1 -3
  69. package/dist/internal/parser.cjs +728 -0
  70. package/dist/internal/parser.d.cts +947 -0
  71. package/dist/internal/parser.d.ts +947 -0
  72. package/dist/internal/parser.js +711 -0
  73. package/dist/message.cjs +84 -26
  74. package/dist/message.d.cts +49 -9
  75. package/dist/message.d.ts +49 -9
  76. package/dist/message.js +84 -27
  77. package/dist/modifiers.cjs +1023 -240
  78. package/dist/modifiers.d.cts +42 -1
  79. package/dist/modifiers.d.ts +42 -1
  80. package/dist/modifiers.js +1023 -240
  81. package/dist/parser.cjs +11 -463
  82. package/dist/parser.d.cts +3 -537
  83. package/dist/parser.d.ts +3 -537
  84. package/dist/parser.js +2 -433
  85. package/dist/phase2-seed.cjs +59 -0
  86. package/dist/phase2-seed.js +56 -0
  87. package/dist/primitives.cjs +557 -208
  88. package/dist/primitives.d.cts +10 -14
  89. package/dist/primitives.d.ts +10 -14
  90. package/dist/primitives.js +557 -208
  91. package/dist/program.cjs +5 -1
  92. package/dist/program.d.cts +5 -3
  93. package/dist/program.d.ts +5 -3
  94. package/dist/program.js +6 -1
  95. package/dist/suggestion.cjs +22 -8
  96. package/dist/suggestion.js +22 -8
  97. package/dist/usage-internals.cjs +3 -2
  98. package/dist/usage-internals.js +4 -2
  99. package/dist/usage.cjs +195 -40
  100. package/dist/usage.d.cts +92 -11
  101. package/dist/usage.d.ts +92 -11
  102. package/dist/usage.js +194 -41
  103. package/dist/validate.cjs +170 -0
  104. package/dist/validate.js +164 -0
  105. package/dist/valueparser.cjs +1270 -187
  106. package/dist/valueparser.d.cts +320 -14
  107. package/dist/valueparser.d.ts +320 -14
  108. package/dist/valueparser.js +1269 -188
  109. package/package.json +9 -9
@@ -6,9 +6,18 @@ const require_nonempty = require('./nonempty.cjs');
6
6
  * A predicate function that checks if an object is a {@link ValueParser}.
7
7
  * @param object The object to check.
8
8
  * @return `true` if the object is a {@link ValueParser}, `false` otherwise.
9
+ * @throws {TypeError} If the object looks like a value parser (has `mode`,
10
+ * `metavar`, `parse`, and `format`) but is missing the required
11
+ * `placeholder` property.
9
12
  */
10
13
  function isValueParser(object) {
11
- return typeof object === "object" && object != null && "$mode" in object && (object.$mode === "sync" || object.$mode === "async") && "metavar" in object && typeof object.metavar === "string" && "parse" in object && typeof object.parse === "function" && "format" in object && typeof object.format === "function";
14
+ if (typeof object !== "object" || object == null || !("mode" in object) || object.mode !== "sync" && object.mode !== "async") return false;
15
+ const hasMetavar = "metavar" in object && typeof object.metavar === "string";
16
+ const hasParse = "parse" in object && typeof object.parse === "function";
17
+ const hasFormat = "format" in object && typeof object.format === "function";
18
+ const hasPlaceholder = "placeholder" in object;
19
+ if (hasMetavar && hasParse && hasFormat && !hasPlaceholder) throw new TypeError("Value parser is missing the required placeholder property. All value parsers must define a placeholder value.");
20
+ return hasMetavar && hasParse && hasFormat && hasPlaceholder;
12
21
  }
13
22
  /**
14
23
  * Implementation of the choice parser for both string and number types.
@@ -47,8 +56,9 @@ function choice(choices, options = {}) {
47
56
  const numberStrings = numberChoices.map((v) => Object.is(v, -0) ? "-0" : String(v));
48
57
  const frozenNumberChoices = Object.freeze(numberChoices);
49
58
  return {
50
- $mode: "sync",
59
+ mode: "sync",
51
60
  metavar,
61
+ placeholder: choices[0],
52
62
  choices: frozenNumberChoices,
53
63
  parse(input) {
54
64
  const index = numberStrings.indexOf(input);
@@ -113,7 +123,7 @@ function choice(choices, options = {}) {
113
123
  }
114
124
  const stringChoices = Object.freeze([...new Set(choices)]);
115
125
  const stringOptions = options;
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)}.`);
126
+ checkBooleanOption(stringOptions, "caseInsensitive");
117
127
  const caseInsensitive = stringOptions.caseInsensitive ?? false;
118
128
  const normalizedValues = caseInsensitive ? stringChoices.map((v) => v.toLowerCase()) : stringChoices;
119
129
  if (caseInsensitive) {
@@ -128,8 +138,9 @@ function choice(choices, options = {}) {
128
138
  }
129
139
  const stringInvalidChoice = stringOptions.errors?.invalidChoice;
130
140
  return {
131
- $mode: "sync",
141
+ mode: "sync",
132
142
  metavar,
143
+ placeholder: choices[0],
133
144
  choices: stringChoices,
134
145
  parse(input) {
135
146
  const normalizedInput = caseInsensitive ? input.toLowerCase() : input;
@@ -159,6 +170,40 @@ function choice(choices, options = {}) {
159
170
  };
160
171
  }
161
172
  /**
173
+ * Validates that an option value, if present, is a boolean.
174
+ * Throws a {@link TypeError} if the value is defined but not a boolean.
175
+ *
176
+ * @template T The type of the options object.
177
+ * @param options The options object to check.
178
+ * @param key The key of the option to validate.
179
+ * @throws {TypeError} If the option value is defined but not a boolean.
180
+ * @since 1.0.0
181
+ */
182
+ function checkBooleanOption(options, key) {
183
+ const value = options?.[key];
184
+ if (value !== void 0 && typeof value !== "boolean") throw new TypeError(`Expected ${String(key)} to be a boolean, but got ${typeof value}: ${String(value)}.`);
185
+ }
186
+ /**
187
+ * Validates that an option value, if present, is one of the allowed values.
188
+ * Throws a {@link TypeError} if the value is defined but not in the allowed
189
+ * list.
190
+ *
191
+ * @template T The type of the options object.
192
+ * @param options The options object to check.
193
+ * @param key The key of the option to validate.
194
+ * @param allowed The list of allowed values.
195
+ * @throws {TypeError} If the option value is defined but not in the allowed
196
+ * list.
197
+ * @since 1.0.0
198
+ */
199
+ function checkEnumOption(options, key, allowed) {
200
+ const value = options?.[key];
201
+ if (value !== void 0 && (typeof value !== "string" || !allowed.includes(value))) {
202
+ const rendered = typeof value === "string" ? JSON.stringify(value) : typeof value === "symbol" ? value.toString() : String(value);
203
+ throw new TypeError(`Expected ${String(key)} to be one of ${allowed.map((v) => JSON.stringify(v)).join(", ")}, but got ${typeof value}: ${rendered}.`);
204
+ }
205
+ }
206
+ /**
162
207
  * Expands a numeric string in scientific notation (e.g., `"1e+21"`,
163
208
  * `"1.5e-3"`, `".1e-6"`) into plain decimal form for normalization.
164
209
  * Used for both canonical `String(number)` output and user input.
@@ -226,7 +271,10 @@ function formatNumberChoiceError(input, validChoices, allChoices, invalidChoice)
226
271
  function formatDefaultChoiceError(input, choices) {
227
272
  const choiceStrings = choices.filter((c) => typeof c === "string" || !Number.isNaN(c)).map((c) => Object.is(c, -0) ? "-0" : String(c));
228
273
  if (choiceStrings.length === 0 && choices.length > 0) return require_message.message`No valid choices are configured, but got ${input}.`;
229
- return require_message.message`Expected one of ${require_message.valueSet(choiceStrings, { locale: "en-US" })}, but got ${input}.`;
274
+ return require_message.message`Expected one of ${require_message.valueSet(choiceStrings, {
275
+ fallback: "",
276
+ locale: "en-US"
277
+ })}, but got ${input}.`;
230
278
  }
231
279
  /**
232
280
  * Creates a {@link ValueParser} for strings.
@@ -252,8 +300,9 @@ function string(options = {}) {
252
300
  const patternFlags = options.pattern?.flags ?? null;
253
301
  const patternMismatch = options.errors?.patternMismatch;
254
302
  return {
255
- $mode: "sync",
303
+ mode: "sync",
256
304
  metavar,
305
+ placeholder: options.placeholder ?? "",
257
306
  parse(input) {
258
307
  if (patternSource != null && patternFlags != null) {
259
308
  const pattern = new RegExp(patternSource, patternFlags);
@@ -308,14 +357,25 @@ function string(options = {}) {
308
357
  * @param options Configuration options specifying the type and constraints.
309
358
  * @returns A {@link ValueParser} that converts string input to the specified
310
359
  * integer type.
360
+ * @throws {TypeError} If `options.type` is provided but is neither `"number"`
361
+ * nor `"bigint"`.
362
+ * @throws {RangeError} If the configured min/max range for number mode contains
363
+ * no safe integers.
311
364
  */
312
365
  function integer(options) {
366
+ 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)}.`);
367
+ if (options?.type !== "bigint") {
368
+ if (options?.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
369
+ if (options?.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
370
+ }
371
+ 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}.`);
313
372
  if (options?.type === "bigint") {
314
373
  const metavar$1 = options.metavar ?? "INTEGER";
315
374
  require_nonempty.ensureNonEmptyString(metavar$1);
316
375
  return {
317
- $mode: "sync",
376
+ mode: "sync",
318
377
  metavar: metavar$1,
378
+ placeholder: options?.placeholder ?? (options?.min != null && options.min > 0n ? options.min : options?.max != null && options.max < 0n ? options.max : 0n),
319
379
  parse(input) {
320
380
  if (!input.match(/^-?\d+$/)) return {
321
381
  success: false,
@@ -344,6 +404,11 @@ function integer(options) {
344
404
  require_nonempty.ensureNonEmptyString(metavar);
345
405
  const maxSafe = BigInt(Number.MAX_SAFE_INTEGER);
346
406
  const minSafe = BigInt(Number.MIN_SAFE_INTEGER);
407
+ const safeMin = Math.max(options?.min ?? Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER);
408
+ const safeMax = Math.min(options?.max ?? Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
409
+ const firstAllowed = Math.ceil(safeMin);
410
+ const lastAllowed = Math.floor(safeMax);
411
+ if (firstAllowed > lastAllowed) throw new RangeError("The configured integer range contains no safe integers. Use type: \"bigint\" instead.");
347
412
  const unsafeIntegerError = options?.errors?.unsafeInteger;
348
413
  function makeUnsafeIntegerError(input) {
349
414
  return {
@@ -352,8 +417,9 @@ function integer(options) {
352
417
  };
353
418
  }
354
419
  return {
355
- $mode: "sync",
420
+ mode: "sync",
356
421
  metavar,
422
+ placeholder: options?.placeholder ?? (firstAllowed > 0 ? firstAllowed : lastAllowed < 0 ? lastAllowed : 0),
357
423
  parse(input) {
358
424
  if (!input.match(/^-?\d+$/)) return {
359
425
  success: false,
@@ -395,12 +461,16 @@ function integer(options) {
395
461
  * numbers.
396
462
  */
397
463
  function float(options = {}) {
464
+ if (options.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
465
+ if (options.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
466
+ 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}.`);
398
467
  const floatRegex = /^[+-]?(?:(?:\d+\.?\d*)|(?:\d*\.\d+))(?:[eE][+-]?\d+)?$/;
399
468
  const metavar = options.metavar ?? "NUMBER";
400
469
  require_nonempty.ensureNonEmptyString(metavar);
401
470
  return {
402
- $mode: "sync",
471
+ mode: "sync",
403
472
  metavar,
473
+ placeholder: options?.placeholder ?? (options?.min != null && options.min > 0 ? options.min : options?.max != null && options.max < 0 ? options.max : 0),
404
474
  parse(input) {
405
475
  const invalidNumber = (i) => ({
406
476
  success: false,
@@ -435,6 +505,19 @@ function float(options = {}) {
435
505
  };
436
506
  }
437
507
  /**
508
+ * The set of URL schemes that are considered "special" by the WHATWG URL
509
+ * Standard. These schemes always use the `://` authority syntax.
510
+ * Non-special schemes use only `:` (e.g., `mailto:`, `urn:`).
511
+ */
512
+ const SPECIAL_URL_SCHEMES = new Set([
513
+ "ftp",
514
+ "file",
515
+ "http",
516
+ "https",
517
+ "ws",
518
+ "wss"
519
+ ]);
520
+ /**
438
521
  * Creates a {@link ValueParser} for URL values.
439
522
  *
440
523
  * This parser validates that the input is a well-formed URL and optionally
@@ -442,17 +525,39 @@ function float(options = {}) {
442
525
  * object.
443
526
  * @param options Configuration options for the URL parser.
444
527
  * @returns A {@link ValueParser} that converts string input to `URL` objects.
528
+ * @throws {TypeError} If any `allowedProtocols` entry is not a valid protocol
529
+ * string ending with a colon (e.g., `"https:"`).
445
530
  */
446
531
  function url(options = {}) {
447
- const originalProtocols = options.allowedProtocols != null ? Object.freeze([...options.allowedProtocols]) : void 0;
448
- const allowedProtocols = options.allowedProtocols != null ? Object.freeze(options.allowedProtocols.map((p) => p.toLowerCase())) : void 0;
532
+ const originalProtocolsList = [];
533
+ const normalizedProtocolsList = [];
534
+ if (options.allowedProtocols != null) {
535
+ const seen = /* @__PURE__ */ new Set();
536
+ for (const protocol of options.allowedProtocols) {
537
+ if (typeof protocol !== "string" || !/^[a-z][a-z0-9+\-.]*:$/i.test(protocol)) {
538
+ const rendered = typeof protocol === "string" ? JSON.stringify(protocol) : String(protocol);
539
+ throw new TypeError(`Each allowed protocol must be a valid protocol ending with a colon (e.g., "https:"), got: ${rendered}.`);
540
+ }
541
+ const normalized = protocol.toLowerCase();
542
+ if (seen.has(normalized)) continue;
543
+ seen.add(normalized);
544
+ originalProtocolsList.push(protocol);
545
+ normalizedProtocolsList.push(normalized);
546
+ }
547
+ if (originalProtocolsList.length === 0) throw new TypeError("allowedProtocols must not be empty.");
548
+ }
549
+ const originalProtocols = options.allowedProtocols != null ? Object.freeze(originalProtocolsList) : void 0;
550
+ const allowedProtocols = options.allowedProtocols != null ? Object.freeze(normalizedProtocolsList) : void 0;
449
551
  const metavar = options.metavar ?? "URL";
450
552
  require_nonempty.ensureNonEmptyString(metavar);
451
553
  const invalidUrl = options.errors?.invalidUrl;
452
554
  const disallowedProtocol = options.errors?.disallowedProtocol;
453
555
  return {
454
- $mode: "sync",
556
+ mode: "sync",
455
557
  metavar,
558
+ get placeholder() {
559
+ return new URL(`${allowedProtocols?.[0] ?? "http:"}//0.invalid`);
560
+ },
456
561
  parse(input) {
457
562
  if (!URL.canParse(input)) return {
458
563
  success: false,
@@ -461,7 +566,28 @@ function url(options = {}) {
461
566
  const url$1 = new URL(input);
462
567
  if (allowedProtocols != null && !allowedProtocols.includes(url$1.protocol)) return {
463
568
  success: false,
464
- error: disallowedProtocol ? typeof disallowedProtocol === "function" ? disallowedProtocol(url$1.protocol, originalProtocols) : disallowedProtocol : require_message.message`URL protocol ${url$1.protocol} is not allowed. Allowed protocols: ${allowedProtocols.join(", ")}.`
569
+ error: disallowedProtocol ? typeof disallowedProtocol === "function" ? disallowedProtocol(url$1.protocol, originalProtocols) : disallowedProtocol : [
570
+ {
571
+ type: "text",
572
+ text: "URL protocol "
573
+ },
574
+ {
575
+ type: "value",
576
+ value: url$1.protocol
577
+ },
578
+ {
579
+ type: "text",
580
+ text: " is not allowed. Allowed protocols: "
581
+ },
582
+ ...require_message.valueSet(originalProtocols, {
583
+ fallback: "",
584
+ locale: "en-US"
585
+ }),
586
+ {
587
+ type: "text",
588
+ text: "."
589
+ }
590
+ ]
465
591
  };
466
592
  return {
467
593
  success: true,
@@ -472,12 +598,15 @@ function url(options = {}) {
472
598
  return value.href;
473
599
  },
474
600
  *suggest(prefix) {
475
- if (allowedProtocols && prefix.length > 0 && !prefix.includes("://")) for (const protocol of allowedProtocols) {
601
+ if (allowedProtocols && prefix.length > 0 && !prefix.includes(":")) for (const protocol of allowedProtocols) {
476
602
  const cleanProtocol = protocol.replace(/:+$/, "");
477
- if (cleanProtocol.startsWith(prefix.toLowerCase())) yield {
478
- kind: "literal",
479
- text: `${cleanProtocol}://`
480
- };
603
+ if (cleanProtocol.startsWith(prefix.toLowerCase())) {
604
+ const suffix = SPECIAL_URL_SCHEMES.has(cleanProtocol) ? "://" : ":";
605
+ yield {
606
+ kind: "literal",
607
+ text: `${cleanProtocol}${suffix}`
608
+ };
609
+ }
481
610
  }
482
611
  }
483
612
  };
@@ -496,8 +625,9 @@ function locale(options = {}) {
496
625
  const metavar = options.metavar ?? "LOCALE";
497
626
  require_nonempty.ensureNonEmptyString(metavar);
498
627
  return {
499
- $mode: "sync",
628
+ mode: "sync",
500
629
  metavar,
630
+ placeholder: new Intl.Locale("und"),
501
631
  parse(input) {
502
632
  let locale$1;
503
633
  try {
@@ -754,31 +884,59 @@ function locale(options = {}) {
754
884
  *
755
885
  * This parser validates that the input is a well-formed UUID string in the
756
886
  * standard format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` where each `x`
757
- * is a hexadecimal digit. The parser can optionally restrict to specific
758
- * UUID versions.
887
+ * is a hexadecimal digit.
759
888
  *
889
+ * By default, the parser enforces strict [RFC 9562] validation: it requires
890
+ * a standardized version digit (1 through 8) and the RFC 9562 variant bits
891
+ * (`10xx`). The nil and max UUIDs are accepted as special standard values.
892
+ * Set `strict: false` to disable the default RFC 9562 version/variant
893
+ * checks. An explicit {@link UuidOptions.allowedVersions} list still
894
+ * constrains the version nibble even in lenient mode.
895
+ *
896
+ * [RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562
760
897
  * @param options Configuration options for the UUID parser.
761
898
  * @returns A {@link ValueParser} that converts string input to {@link Uuid}
762
899
  * strings.
900
+ * @throws {TypeError} If any element of
901
+ * {@link UuidOptions.allowedVersions} is not an integer.
902
+ * @throws {RangeError} If any element of
903
+ * {@link UuidOptions.allowedVersions} is outside the range 1 to 8.
763
904
  */
764
905
  function uuid(options = {}) {
765
906
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
766
907
  const metavar = options.metavar ?? "UUID";
767
908
  require_nonempty.ensureNonEmptyString(metavar);
768
- const allowedVersions = options.allowedVersions != null ? Object.freeze([...options.allowedVersions]) : null;
909
+ checkBooleanOption(options, "strict");
910
+ const strict = options.strict !== false;
911
+ const allowedVersions = options.allowedVersions != null ? (() => {
912
+ const unique = /* @__PURE__ */ new Set();
913
+ for (const v of options.allowedVersions) {
914
+ if (!Number.isInteger(v)) throw new TypeError(`Expected every element of allowedVersions to be an integer, but got value "${typeof v === "symbol" ? v.toString() : String(v)}" of type "${Array.isArray(v) ? "array" : v === null ? "null" : typeof v}".`);
915
+ if (v < 1 || v > 8) throw new RangeError(`Expected every element of allowedVersions to be between 1 and 8, but got: ${v}.`);
916
+ unique.add(v);
917
+ }
918
+ return Object.freeze([...unique]);
919
+ })() : null;
769
920
  const invalidUuid = options.errors?.invalidUuid;
770
921
  const disallowedVersion = options.errors?.disallowedVersion;
922
+ const invalidVariant = options.errors?.invalidVariant;
771
923
  return {
772
- $mode: "sync",
924
+ mode: "sync",
773
925
  metavar,
926
+ placeholder: "00000000-0000-0000-0000-000000000000",
774
927
  parse(input) {
775
928
  if (!uuidRegex.test(input)) return {
776
929
  success: false,
777
930
  error: invalidUuid ? typeof invalidUuid === "function" ? invalidUuid(input) : invalidUuid : require_message.message`Expected a valid UUID in format ${"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}, but got ${input}.`
778
931
  };
932
+ const lower = input.toLowerCase();
933
+ if (lower === "00000000-0000-0000-0000-000000000000" || lower === "ffffffff-ffff-ffff-ffff-ffffffffffff") return {
934
+ success: true,
935
+ value: input
936
+ };
937
+ const versionChar = input.charAt(14);
938
+ const version = parseInt(versionChar, 16);
779
939
  if (allowedVersions != null && allowedVersions.length > 0) {
780
- const versionChar = input.charAt(14);
781
- const version = parseInt(versionChar, 16);
782
940
  if (!allowedVersions.includes(version)) return {
783
941
  success: false,
784
942
  error: disallowedVersion ? typeof disallowedVersion === "function" ? disallowedVersion(version, allowedVersions) : disallowedVersion : (() => {
@@ -791,6 +949,25 @@ function uuid(options = {}) {
791
949
  return require_message.message`Expected UUID version ${expectedVersions}, but got version ${version.toLocaleString("en")}.`;
792
950
  })()
793
951
  };
952
+ } else if (strict && (version < 1 || version > 8)) return {
953
+ success: false,
954
+ error: disallowedVersion ? typeof disallowedVersion === "function" ? disallowedVersion(version, [
955
+ 1,
956
+ 2,
957
+ 3,
958
+ 4,
959
+ 5,
960
+ 6,
961
+ 7,
962
+ 8
963
+ ]) : disallowedVersion : require_message.message`Expected UUID version 1 through 8, but got version ${version.toLocaleString("en")}.`
964
+ };
965
+ if (strict) {
966
+ const variantChar = input.charAt(19).toLowerCase();
967
+ if (variantChar !== "8" && variantChar !== "9" && variantChar !== "a" && variantChar !== "b") return {
968
+ success: false,
969
+ error: invalidVariant ? typeof invalidVariant === "function" ? invalidVariant(input) : invalidVariant : require_message.message`Expected RFC 9562 variant (8, 9, a, or b at position 20), but got ${variantChar} in ${input}.`
970
+ };
794
971
  }
795
972
  return {
796
973
  success: true,
@@ -837,18 +1014,24 @@ function uuid(options = {}) {
837
1014
  *
838
1015
  * @param options Configuration options specifying the type and constraints.
839
1016
  * @returns A {@link ValueParser} that converts string input to port numbers.
1017
+ * @throws {TypeError} If `options.type` is provided but is neither `"number"`
1018
+ * nor `"bigint"`.
840
1019
  * @since 0.10.0
841
1020
  */
842
1021
  function port(options) {
843
- 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)}.`);
1022
+ checkBooleanOption(options, "disallowWellKnown");
1023
+ 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)}.`);
844
1024
  if (options?.type === "bigint") {
845
1025
  const metavar$1 = options.metavar ?? "PORT";
846
1026
  require_nonempty.ensureNonEmptyString(metavar$1);
847
1027
  const min$1 = options.min ?? 1n;
848
1028
  const max$1 = options.max ?? 65535n;
1029
+ 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}.`);
1030
+ if (options.disallowWellKnown && min$1 < 1024n && max$1 < 1024n) throw new RangeError(`disallowWellKnown is incompatible with the configured port range: all ports ${min$1}..${max$1} are well-known.`);
849
1031
  return {
850
- $mode: "sync",
1032
+ mode: "sync",
851
1033
  metavar: metavar$1,
1034
+ placeholder: options.placeholder ?? (options.disallowWellKnown && min$1 < 1024n ? 1024n > min$1 ? 1024n : min$1 : min$1),
852
1035
  parse(input) {
853
1036
  if (!input.match(/^-?\d+$/)) return {
854
1037
  success: false,
@@ -877,13 +1060,18 @@ function port(options) {
877
1060
  }
878
1061
  };
879
1062
  }
1063
+ if (options?.min != null && !Number.isFinite(options.min)) throw new RangeError(`Expected min to be a finite number, but got: ${options.min}`);
1064
+ if (options?.max != null && !Number.isFinite(options.max)) throw new RangeError(`Expected max to be a finite number, but got: ${options.max}`);
880
1065
  const metavar = options?.metavar ?? "PORT";
881
1066
  require_nonempty.ensureNonEmptyString(metavar);
882
1067
  const min = options?.min ?? 1;
883
1068
  const max = options?.max ?? 65535;
1069
+ if (min > max) throw new RangeError(`Expected min to be less than or equal to max, but got min: ${min} and max: ${max}.`);
1070
+ if (options?.disallowWellKnown && min < 1024 && max < 1024) throw new RangeError(`disallowWellKnown is incompatible with the configured port range: all ports ${min}..${max} are well-known.`);
884
1071
  return {
885
- $mode: "sync",
1072
+ mode: "sync",
886
1073
  metavar,
1074
+ placeholder: options?.placeholder ?? (options?.disallowWellKnown && min < 1024 ? Math.max(1024, min) : min),
887
1075
  parse(input) {
888
1076
  if (!input.match(/^-?\d+$/)) return {
889
1077
  success: false,
@@ -975,11 +1163,12 @@ function ipv4(options) {
975
1163
  const allowBroadcast = options?.allowBroadcast ?? true;
976
1164
  const allowZero = options?.allowZero ?? true;
977
1165
  return {
978
- $mode: "sync",
1166
+ mode: "sync",
979
1167
  metavar,
1168
+ placeholder: allowZero ? "0.0.0.0" : allowLoopback ? "127.0.0.1" : "192.0.2.1",
980
1169
  parse(input) {
981
- const parts = input.split(".");
982
- if (parts.length !== 4) {
1170
+ const octets = parseIpv4Octets(input);
1171
+ if (octets === null) {
983
1172
  const errorMsg = options?.errors?.invalidIpv4;
984
1173
  const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
985
1174
  return {
@@ -987,43 +1176,6 @@ function ipv4(options) {
987
1176
  error: msg
988
1177
  };
989
1178
  }
990
- const octets = [];
991
- for (const part of parts) {
992
- if (part.length === 0) {
993
- const errorMsg = options?.errors?.invalidIpv4;
994
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
995
- return {
996
- success: false,
997
- error: msg
998
- };
999
- }
1000
- if (part.trim() !== part) {
1001
- const errorMsg = options?.errors?.invalidIpv4;
1002
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1003
- return {
1004
- success: false,
1005
- error: msg
1006
- };
1007
- }
1008
- if (part.length > 1 && part[0] === "0") {
1009
- const errorMsg = options?.errors?.invalidIpv4;
1010
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1011
- return {
1012
- success: false,
1013
- error: msg
1014
- };
1015
- }
1016
- const octet = Number(part);
1017
- if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
1018
- const errorMsg = options?.errors?.invalidIpv4;
1019
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid IPv4 address, but got ${input}.`;
1020
- return {
1021
- success: false,
1022
- error: msg
1023
- };
1024
- }
1025
- octets.push(octet);
1026
- }
1027
1179
  const ipAddress = octets.join(".");
1028
1180
  if (!allowPrivate && isPrivateIp(octets)) {
1029
1181
  const errorMsg = options?.errors?.privateNotAllowed;
@@ -1092,9 +1244,14 @@ function ipv4(options) {
1092
1244
  * - Labels can contain alphanumeric characters and hyphens
1093
1245
  * - Labels cannot start or end with a hyphen
1094
1246
  * - Total length ≤ 253 characters (default)
1247
+ * - Dotted all-numeric strings (e.g., `192.168.0.1`) are rejected as they
1248
+ * resemble IPv4 addresses rather than DNS hostnames
1095
1249
  *
1096
1250
  * @param options - Options for hostname validation.
1097
1251
  * @returns A value parser for hostnames.
1252
+ * @throws {TypeError} If `allowWildcard`, `allowUnderscore`, or
1253
+ * `allowLocalhost` is not a boolean.
1254
+ * @throws {RangeError} If `maxLength` is not a positive integer.
1098
1255
  * @since 0.10.0
1099
1256
  *
1100
1257
  * @example
@@ -1114,13 +1271,18 @@ function ipv4(options) {
1114
1271
  function hostname(options) {
1115
1272
  const metavar = options?.metavar ?? "HOST";
1116
1273
  require_nonempty.ensureNonEmptyString(metavar);
1274
+ checkBooleanOption(options, "allowWildcard");
1275
+ checkBooleanOption(options, "allowUnderscore");
1276
+ checkBooleanOption(options, "allowLocalhost");
1117
1277
  const allowWildcard = options?.allowWildcard ?? false;
1118
1278
  const allowUnderscore = options?.allowUnderscore ?? false;
1119
1279
  const allowLocalhost = options?.allowLocalhost ?? true;
1120
1280
  const maxLength = options?.maxLength ?? 253;
1281
+ if (!Number.isInteger(maxLength) || maxLength < 1) throw new RangeError("maxLength must be an integer greater than or equal to 1.");
1121
1282
  return {
1122
- $mode: "sync",
1283
+ mode: "sync",
1123
1284
  metavar,
1285
+ placeholder: options?.placeholder ?? (allowLocalhost ? maxLength >= 9 ? "localhost" : "a.bc" : maxLength >= 11 ? "example.com" : "a.bc"),
1124
1286
  parse(input) {
1125
1287
  if (input.length > maxLength) {
1126
1288
  const errorMsg = options?.errors?.tooLong;
@@ -1130,7 +1292,7 @@ function hostname(options) {
1130
1292
  error: msg
1131
1293
  };
1132
1294
  }
1133
- if (!allowLocalhost && input === "localhost") {
1295
+ if (!allowLocalhost && input.toLowerCase() === "localhost") {
1134
1296
  const errorMsg = options?.errors?.localhostNotAllowed;
1135
1297
  const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Hostname 'localhost' is not allowed.`;
1136
1298
  return {
@@ -1156,6 +1318,14 @@ function hostname(options) {
1156
1318
  error: msg
1157
1319
  };
1158
1320
  }
1321
+ if (!allowLocalhost && rest.toLowerCase() === "localhost") {
1322
+ const errorMsg = options?.errors?.localhostNotAllowed;
1323
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Hostname 'localhost' is not allowed.`;
1324
+ return {
1325
+ success: false,
1326
+ error: msg
1327
+ };
1328
+ }
1159
1329
  }
1160
1330
  if (!allowUnderscore && input.includes("_")) {
1161
1331
  const errorMsg = options?.errors?.underscoreNotAllowed;
@@ -1191,7 +1361,7 @@ function hostname(options) {
1191
1361
  error: msg
1192
1362
  };
1193
1363
  }
1194
- if (label === "*") continue;
1364
+ if (label === "*" && allowWildcard && input.startsWith("*.") && label === labels[0]) continue;
1195
1365
  if (label.startsWith("-") || label.endsWith("-")) {
1196
1366
  const errorMsg = options?.errors?.invalidHostname;
1197
1367
  const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid hostname, but got ${input}.`;
@@ -1210,6 +1380,14 @@ function hostname(options) {
1210
1380
  };
1211
1381
  }
1212
1382
  }
1383
+ if (labels.length >= 2 && labels.every((l) => /^[0-9]+$/.test(l))) {
1384
+ const errorMsg = options?.errors?.invalidHostname;
1385
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a valid hostname, but got ${input}.`;
1386
+ return {
1387
+ success: false,
1388
+ error: msg
1389
+ };
1390
+ }
1213
1391
  return {
1214
1392
  success: true,
1215
1393
  value: input
@@ -1224,18 +1402,37 @@ function email(options) {
1224
1402
  const metavar = options?.metavar ?? "EMAIL";
1225
1403
  require_nonempty.ensureNonEmptyString(metavar);
1226
1404
  const allowMultiple = options?.allowMultiple ?? false;
1405
+ if (options?.placeholder != null) {
1406
+ if (allowMultiple && !Array.isArray(options.placeholder)) throw new TypeError("email() placeholder must be an array when allowMultiple is true.");
1407
+ if (!allowMultiple && typeof options.placeholder !== "string") throw new TypeError("email() placeholder must be a string when allowMultiple is false.");
1408
+ }
1227
1409
  const allowDisplayName = options?.allowDisplayName ?? false;
1228
1410
  const lowercase = options?.lowercase ?? false;
1229
1411
  const allowedDomains = options?.allowedDomains != null ? Object.freeze([...options.allowedDomains]) : void 0;
1412
+ if (allowedDomains != null) {
1413
+ if (allowedDomains.length === 0) throw new TypeError("allowedDomains must not be empty.");
1414
+ for (let i = 0; i < allowedDomains.length; i++) {
1415
+ const entry = allowedDomains[i];
1416
+ if (typeof entry !== "string") throw new TypeError(`allowedDomains[${i}] must be a string, got ${typeof entry}.`);
1417
+ if (entry !== entry.trim()) throw new TypeError(`allowedDomains[${i}] must not have leading or trailing whitespace: ${JSON.stringify(entry)}`);
1418
+ if (entry.startsWith("@")) throw new TypeError(`allowedDomains[${i}] must not start with "@": ${JSON.stringify(entry)}`);
1419
+ if (entry === "" || !entry.includes(".")) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
1420
+ if (entry.startsWith(".") || entry.endsWith(".") || entry.startsWith("-") || entry.endsWith("-")) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
1421
+ const labels = entry.split(".");
1422
+ 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)}`);
1423
+ if (labels.length === 4 && labels.every((label) => /^[0-9]+$/.test(label))) throw new TypeError(`allowedDomains[${i}] is not a valid domain: ${JSON.stringify(entry)}`);
1424
+ }
1425
+ }
1230
1426
  const invalidEmail = options?.errors?.invalidEmail;
1231
1427
  const domainNotAllowed = options?.errors?.domainNotAllowed;
1232
1428
  const atextRegex = /^[a-zA-Z0-9._+-]+$/;
1429
+ const encoder = new TextEncoder();
1233
1430
  function validateEmail(input) {
1234
1431
  const trimmed = input.trim();
1235
1432
  let emailAddr = trimmed;
1236
- if (allowDisplayName && trimmed.includes("<") && trimmed.endsWith(">")) {
1237
- const match = trimmed.match(/<([^>]+)>$/);
1238
- if (match) emailAddr = match[1].trim();
1433
+ if (allowDisplayName) {
1434
+ const displayNameMatch = trimmed.match(/^((?:"(?:[^"\\]|\\.)*"|[^<>"])+)\s*<([^<>]+)>$/);
1435
+ if (displayNameMatch && /\S/.test(displayNameMatch[1].replace(/"((?:[^"\\]|\\.)*)"/g, (_match, inner) => inner))) emailAddr = displayNameMatch[2].trim();
1239
1436
  }
1240
1437
  let atIndex = -1;
1241
1438
  if (emailAddr.startsWith("\"")) {
@@ -1257,6 +1454,7 @@ function email(options) {
1257
1454
  isValidLocal = localParts.length > 0 && localParts.every((part) => part.length > 0 && atextRegex.test(part));
1258
1455
  }
1259
1456
  if (!isValidLocal) return null;
1457
+ if (encoder.encode(localPart).length > 64) return null;
1260
1458
  if (!domain$1 || domain$1.length === 0) return null;
1261
1459
  if (!domain$1.includes(".")) return null;
1262
1460
  if (domain$1.startsWith(".") || domain$1.endsWith(".") || domain$1.startsWith("-") || domain$1.endsWith("-")) return null;
@@ -1266,15 +1464,48 @@ function email(options) {
1266
1464
  if (label.startsWith("-") || label.endsWith("-")) return null;
1267
1465
  if (!/^[a-zA-Z0-9-]+$/.test(label)) return null;
1268
1466
  }
1467
+ if (domainLabels.length === 4 && domainLabels.every((label) => /^[0-9]+$/.test(label))) return null;
1468
+ if (encoder.encode(emailAddr).length > 254) return null;
1269
1469
  const resultEmail = emailAddr;
1270
- return lowercase ? resultEmail.toLowerCase() : resultEmail;
1470
+ if (!lowercase) return resultEmail;
1471
+ const lastAt = resultEmail.lastIndexOf("@");
1472
+ return resultEmail.slice(0, lastAt) + resultEmail.slice(lastAt).toLowerCase();
1473
+ }
1474
+ /**
1475
+ * Splits an input string on commas, respecting quoted segments and
1476
+ * angle-bracket display-name syntax per RFC 5322.
1477
+ */
1478
+ function splitEmails(input) {
1479
+ const result = [];
1480
+ let current = "";
1481
+ let inQuotes = false;
1482
+ let inAngleBrackets = false;
1483
+ let escaped = false;
1484
+ for (const char of input) {
1485
+ if (escaped) escaped = false;
1486
+ else if (char === "\\" && inQuotes) escaped = true;
1487
+ else if (char === "\"" && !inAngleBrackets) {
1488
+ if (inQuotes) inQuotes = false;
1489
+ else if (current.trim() === "") inQuotes = true;
1490
+ } else if (char === "<" && !inQuotes) inAngleBrackets = true;
1491
+ else if (char === ">" && !inQuotes) inAngleBrackets = false;
1492
+ else if (char === "," && !inQuotes && !inAngleBrackets) {
1493
+ result.push(current);
1494
+ current = "";
1495
+ continue;
1496
+ }
1497
+ current += char;
1498
+ }
1499
+ result.push(current);
1500
+ return result;
1271
1501
  }
1272
1502
  return {
1273
- $mode: "sync",
1503
+ mode: "sync",
1274
1504
  metavar,
1505
+ placeholder: options?.placeholder ?? (options?.allowMultiple ? [`user@${options?.allowedDomains?.[0] ?? "example.com"}`] : `user@${options?.allowedDomains?.[0] ?? "example.com"}`),
1275
1506
  parse(input) {
1276
1507
  if (allowMultiple) {
1277
- const emails = input.split(",").map((e) => e.trim());
1508
+ const emails = splitEmails(input).map((e) => e.trim());
1278
1509
  const validatedEmails = [];
1279
1510
  for (const email$1 of emails) {
1280
1511
  const validated = validateEmail(email$1);
@@ -1286,7 +1517,7 @@ function email(options) {
1286
1517
  error: msg
1287
1518
  };
1288
1519
  }
1289
- if (allowedDomains && allowedDomains.length > 0) {
1520
+ if (allowedDomains != null) {
1290
1521
  const atIndex = validated.indexOf("@");
1291
1522
  const domain$1 = validated.substring(atIndex + 1).toLowerCase();
1292
1523
  const isAllowed = allowedDomains.some((allowed) => domain$1 === allowed.toLowerCase());
@@ -1307,7 +1538,15 @@ function email(options) {
1307
1538
  },
1308
1539
  {
1309
1540
  type: "text",
1310
- text: ` is not allowed. Allowed domains: ${allowedDomains.join(", ")}.`
1541
+ text: " is not allowed. Allowed domains: "
1542
+ },
1543
+ ...require_message.valueSet(allowedDomains, {
1544
+ fallback: "",
1545
+ locale: "en-US"
1546
+ }),
1547
+ {
1548
+ type: "text",
1549
+ text: "."
1311
1550
  }
1312
1551
  ];
1313
1552
  return {
@@ -1332,7 +1571,7 @@ function email(options) {
1332
1571
  error: msg
1333
1572
  };
1334
1573
  }
1335
- if (allowedDomains && allowedDomains.length > 0) {
1574
+ if (allowedDomains != null) {
1336
1575
  const atIndex = validated.indexOf("@");
1337
1576
  const domain$1 = validated.substring(atIndex + 1).toLowerCase();
1338
1577
  const isAllowed = allowedDomains.some((allowed) => domain$1 === allowed.toLowerCase());
@@ -1353,7 +1592,15 @@ function email(options) {
1353
1592
  },
1354
1593
  {
1355
1594
  type: "text",
1356
- text: ` is not allowed. Allowed domains: ${allowedDomains.join(", ")}.`
1595
+ text: " is not allowed. Allowed domains: "
1596
+ },
1597
+ ...require_message.valueSet(allowedDomains, {
1598
+ fallback: "",
1599
+ locale: "en-US"
1600
+ }),
1601
+ {
1602
+ type: "text",
1603
+ text: "."
1357
1604
  }
1358
1605
  ];
1359
1606
  return {
@@ -1369,7 +1616,7 @@ function email(options) {
1369
1616
  }
1370
1617
  },
1371
1618
  format(value) {
1372
- if (Array.isArray(value)) return value.join(",");
1619
+ if (Array.isArray(value)) return value.join(", ");
1373
1620
  return value;
1374
1621
  }
1375
1622
  };
@@ -1386,6 +1633,7 @@ function email(options) {
1386
1633
  *
1387
1634
  * @param options - Options for socket address validation.
1388
1635
  * @returns A value parser for socket addresses.
1636
+ * @throws {TypeError} If `separator` is an empty string.
1389
1637
  * @throws {TypeError} If `separator` contains digit characters, since digits
1390
1638
  * in the separator would cause ambiguous splitting of port input.
1391
1639
  * @since 0.10.0
@@ -1409,7 +1657,9 @@ function email(options) {
1409
1657
  */
1410
1658
  function socketAddress(options) {
1411
1659
  const separator = options?.separator ?? ":";
1660
+ if (separator === "") throw new TypeError("Expected separator to not be empty.");
1412
1661
  if (/\p{Nd}/u.test(separator)) throw new TypeError(`Expected separator to not contain digits, but got: ${JSON.stringify(separator)}.`);
1662
+ const formatExample = `host${JSON.stringify(separator).slice(1, -1)}port`;
1413
1663
  const metavar = options?.metavar ?? `HOST${separator}PORT`;
1414
1664
  require_nonempty.ensureNonEmptyString(metavar);
1415
1665
  const defaultPort = options?.defaultPort;
@@ -1423,88 +1673,274 @@ function socketAddress(options) {
1423
1673
  ...options?.host?.ip,
1424
1674
  metavar: "HOST"
1425
1675
  });
1676
+ const disambiguationParser = hostType === "ip" ? hostname({
1677
+ metavar: "HOST",
1678
+ allowWildcard: true,
1679
+ allowUnderscore: true,
1680
+ maxLength: Math.max(253, options?.host?.hostname?.maxLength ?? 0)
1681
+ }) : hostnameParser;
1682
+ const separatorIsHostChar = /^[a-zA-Z0-9._-]+$/.test(separator);
1426
1683
  const portParser = port({
1427
1684
  ...options?.port,
1428
1685
  metavar: "PORT",
1429
1686
  type: "number"
1430
1687
  });
1688
+ function looksLikeIpv4(input) {
1689
+ return /^\d+\.\d+\.\d+\.\d+$/.test(input);
1690
+ }
1691
+ function looksLikeAltIpv4Literal(input) {
1692
+ if (/^0[xX][0-9a-fA-F]+$/.test(input)) {
1693
+ const n = parseInt(input.slice(2), 16);
1694
+ return n <= 4294967295;
1695
+ }
1696
+ if (/^0[0-7]+$/.test(input)) {
1697
+ const n = parseInt(input.slice(1), 8);
1698
+ return n <= 4294967295;
1699
+ }
1700
+ const parts = input.split(".");
1701
+ if (parts.length >= 2 && parts.length <= 4) {
1702
+ const numericOrHex = /^(?:[0-9]+|0[xX][0-9a-fA-F]+)$/;
1703
+ if (parts.every((p) => numericOrHex.test(p)) && parts.some((p) => /^0[xX]/i.test(p) || p.length > 1 && p[0] === "0")) {
1704
+ const values = [];
1705
+ for (const p of parts) if (/^0[xX]/i.test(p)) values.push(parseInt(p.slice(2), 16));
1706
+ else if (p.length > 1 && p[0] === "0") {
1707
+ if (/[89]/.test(p)) return false;
1708
+ values.push(parseInt(p, 8));
1709
+ } else values.push(Number(p));
1710
+ const lastMax = 256 ** (5 - parts.length);
1711
+ return values.slice(0, -1).every((v) => v <= 255) && values[values.length - 1] < lastMax;
1712
+ }
1713
+ }
1714
+ return false;
1715
+ }
1431
1716
  function parseHost(hostInput) {
1432
1717
  if (hostType === "hostname") {
1433
- const ipResult = ipParser.parse(hostInput);
1434
- if (ipResult.success) return null;
1435
- const result = hostnameParser.parse(hostInput);
1436
- return result.success ? result.value : null;
1437
- } else if (hostType === "ip") {
1438
- const result = ipParser.parse(hostInput);
1439
- return result.success ? result.value : null;
1440
- } else {
1441
- const ipResult = ipParser.parse(hostInput);
1442
- if (ipResult.success) return ipResult.value;
1443
- const hostnameResult = hostnameParser.parse(hostInput);
1444
- return hostnameResult.success ? hostnameResult.value : null;
1718
+ if (looksLikeAltIpv4Literal(hostInput)) return {
1719
+ success: false,
1720
+ error: require_message.message`${hostInput} appears to be a non-standard IPv4 address notation.`
1721
+ };
1722
+ if (looksLikeIpv4(hostInput)) return {
1723
+ success: false,
1724
+ error: require_message.message`Expected a valid hostname, but got ${hostInput}.`
1725
+ };
1726
+ return hostnameParser.parse(hostInput);
1727
+ } else if (hostType === "ip") return ipParser.parse(hostInput);
1728
+ else {
1729
+ if (looksLikeAltIpv4Literal(hostInput)) return {
1730
+ success: false,
1731
+ error: require_message.message`${hostInput} appears to be a non-standard IPv4 address notation.`
1732
+ };
1733
+ if (looksLikeIpv4(hostInput)) return ipParser.parse(hostInput);
1734
+ return hostnameParser.parse(hostInput);
1445
1735
  }
1446
1736
  }
1447
1737
  return {
1448
- $mode: "sync",
1738
+ mode: "sync",
1449
1739
  metavar,
1740
+ get placeholder() {
1741
+ return {
1742
+ host: hostType === "ip" ? ipParser.placeholder : hostnameParser.placeholder,
1743
+ port: defaultPort ?? portParser.placeholder
1744
+ };
1745
+ },
1450
1746
  parse(input) {
1451
1747
  const trimmed = input.trim();
1452
- const separatorIndex = trimmed.lastIndexOf(separator);
1453
- let hostPart;
1454
- let portPart;
1455
- if (separatorIndex === -1) {
1456
- hostPart = trimmed;
1457
- portPart = void 0;
1458
- } else {
1459
- hostPart = trimmed.substring(0, separatorIndex);
1460
- portPart = trimmed.substring(separatorIndex + separator.length);
1748
+ const searchInput = input.trimStart();
1749
+ const canOmitPort = defaultPort !== void 0 && !requirePort;
1750
+ let firstHostError;
1751
+ let validHostInvalidPortError;
1752
+ let trailingSepHost;
1753
+ let trailingSepHostError;
1754
+ let anySeparatorFound = false;
1755
+ let trailingSepInTrimmedRegion = false;
1756
+ let searchFrom = searchInput.length;
1757
+ while (searchFrom > 0) {
1758
+ const separatorIndex = searchInput.lastIndexOf(separator, searchFrom - 1);
1759
+ if (separatorIndex === -1) break;
1760
+ anySeparatorFound = true;
1761
+ const hostPart = searchInput.substring(0, separatorIndex).trim();
1762
+ const portPart = searchInput.substring(separatorIndex + separator.length).trim();
1763
+ if (portPart === "") {
1764
+ if (trailingSepHost === void 0 && trailingSepHostError === void 0) {
1765
+ if (separatorIndex + separator.length > trimmed.length) trailingSepInTrimmedRegion = true;
1766
+ const hostResult = parseHost(hostPart);
1767
+ if (hostResult.success) trailingSepHost = hostResult.value;
1768
+ else trailingSepHostError = {
1769
+ hostPart,
1770
+ error: hostResult.error
1771
+ };
1772
+ }
1773
+ } else {
1774
+ const portResult = portParser.parse(portPart);
1775
+ if (portResult.success) {
1776
+ const hostResult = parseHost(hostPart);
1777
+ if (hostResult.success) return {
1778
+ success: true,
1779
+ value: {
1780
+ host: hostResult.value,
1781
+ port: portResult.value
1782
+ }
1783
+ };
1784
+ if (firstHostError === void 0 || (looksLikeIpv4(hostPart) || looksLikeAltIpv4Literal(hostPart)) && !looksLikeIpv4(firstHostError.hostPart) && !looksLikeAltIpv4Literal(firstHostError.hostPart)) firstHostError = {
1785
+ hostPart,
1786
+ error: hostResult.error
1787
+ };
1788
+ } else if (validHostInvalidPortError === void 0 && hostPart !== "" && /^[0-9]+$/.test(portPart)) if (looksLikeIpv4(hostPart) || looksLikeAltIpv4Literal(hostPart)) {
1789
+ const hostResult = parseHost(hostPart);
1790
+ if (!hostResult.success) {
1791
+ if (firstHostError === void 0 || !looksLikeIpv4(firstHostError.hostPart) && !looksLikeAltIpv4Literal(firstHostError.hostPart)) firstHostError = {
1792
+ hostPart,
1793
+ error: hostResult.error
1794
+ };
1795
+ } else validHostInvalidPortError = portResult.error;
1796
+ } else {
1797
+ validHostInvalidPortError = portResult.error;
1798
+ const hostResult = parseHost(hostPart);
1799
+ if (!hostResult.success && firstHostError === void 0) firstHostError = {
1800
+ hostPart,
1801
+ error: hostResult.error
1802
+ };
1803
+ }
1804
+ else if ((firstHostError === void 0 || !looksLikeIpv4(firstHostError.hostPart) && !looksLikeAltIpv4Literal(firstHostError.hostPart)) && (looksLikeIpv4(hostPart) || looksLikeAltIpv4Literal(hostPart))) {
1805
+ const hostResult = parseHost(hostPart);
1806
+ if (!hostResult.success) firstHostError = {
1807
+ hostPart,
1808
+ error: hostResult.error
1809
+ };
1810
+ }
1811
+ }
1812
+ searchFrom = separatorIndex;
1461
1813
  }
1462
- const validatedHost = parseHost(hostPart);
1463
- if (validatedHost === null) {
1464
- const errorMsg = options?.errors?.invalidFormat;
1465
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a socket address in format host${separator}port, but got ${input}.`;
1814
+ if (validHostInvalidPortError !== void 0) {
1815
+ const errorMsg$1 = options?.errors?.invalidFormat;
1816
+ if (errorMsg$1) {
1817
+ const msg = typeof errorMsg$1 === "function" ? errorMsg$1(input) : errorMsg$1;
1818
+ return {
1819
+ success: false,
1820
+ error: msg
1821
+ };
1822
+ }
1823
+ if (firstHostError !== void 0 && firstHostError.hostPart !== "") {
1824
+ const portSplitHostIsDegenerate = separatorIsHostChar ? firstHostError.hostPart.replaceAll(separator, "") === "" : firstHostError.hostPart.includes(separator);
1825
+ if (portSplitHostIsDegenerate) return {
1826
+ success: false,
1827
+ error: require_message.message`Expected a socket address in format ${require_message.text(formatExample)}, but got ${input}.`
1828
+ };
1829
+ if (!disambiguationParser.parse(trimmed).success) return {
1830
+ success: false,
1831
+ error: firstHostError.error
1832
+ };
1833
+ }
1834
+ if (disambiguationParser.parse(trimmed).success) return {
1835
+ success: false,
1836
+ error: require_message.message`Expected a socket address in format ${require_message.text(formatExample)}, but got ${input}.`
1837
+ };
1838
+ return {
1839
+ success: false,
1840
+ error: validHostInvalidPortError
1841
+ };
1842
+ }
1843
+ if (firstHostError !== void 0) {
1844
+ if (looksLikeIpv4(firstHostError.hostPart) || looksLikeAltIpv4Literal(firstHostError.hostPart)) {
1845
+ const errorMsg$1 = options?.errors?.invalidFormat;
1846
+ if (errorMsg$1) {
1847
+ const msg = typeof errorMsg$1 === "function" ? errorMsg$1(input) : errorMsg$1;
1848
+ return {
1849
+ success: false,
1850
+ error: msg
1851
+ };
1852
+ }
1853
+ return {
1854
+ success: false,
1855
+ error: firstHostError.error
1856
+ };
1857
+ }
1858
+ }
1859
+ let hostOnlyResult;
1860
+ if (!requirePort || trailingSepHost !== void 0 || trailingSepHostError !== void 0) {
1861
+ hostOnlyResult = parseHost(trimmed);
1862
+ if (canOmitPort && hostOnlyResult.success && !trailingSepInTrimmedRegion) return {
1863
+ success: true,
1864
+ value: {
1865
+ host: hostOnlyResult.value,
1866
+ port: defaultPort
1867
+ }
1868
+ };
1869
+ }
1870
+ if (trailingSepHost !== void 0 && hostOnlyResult !== void 0 && (!hostOnlyResult.success || trailingSepInTrimmedRegion)) {
1871
+ const errorMsg$1 = options?.errors?.missingPort;
1872
+ const msg = typeof errorMsg$1 === "function" ? errorMsg$1(input) : errorMsg$1 ?? require_message.message`Port number is required but was not specified.`;
1466
1873
  return {
1467
1874
  success: false,
1468
1875
  error: msg
1469
1876
  };
1470
1877
  }
1471
- let validatedPort;
1472
- if (portPart === void 0 || portPart === "") {
1473
- if (requirePort) {
1474
- const errorMsg = options?.errors?.missingPort;
1475
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Port number is required but was not specified.`;
1878
+ if (trailingSepHostError !== void 0 && hostOnlyResult !== void 0 && (!hostOnlyResult.success || trailingSepInTrimmedRegion)) {
1879
+ const errorMsg$1 = options?.errors?.invalidFormat;
1880
+ if (errorMsg$1) {
1881
+ const msg = typeof errorMsg$1 === "function" ? errorMsg$1(input) : errorMsg$1;
1476
1882
  return {
1477
1883
  success: false,
1478
1884
  error: msg
1479
1885
  };
1480
1886
  }
1481
- if (defaultPort !== void 0) validatedPort = defaultPort;
1482
- else {
1483
- const errorMsg = options?.errors?.missingPort;
1484
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Port number is required but was not specified.`;
1887
+ const trailingHostIsDegenerate = separatorIsHostChar ? trailingSepHostError.hostPart.replaceAll(separator, "") === "" : trailingSepHostError.hostPart.includes(separator);
1888
+ if (trailingSepHostError.hostPart !== "" && !trailingHostIsDegenerate && !disambiguationParser.parse(trimmed).success) return {
1889
+ success: false,
1890
+ error: trailingSepHostError.error
1891
+ };
1892
+ }
1893
+ if (!canOmitPort && !requirePort && hostOnlyResult !== void 0 && hostOnlyResult.success) {
1894
+ const errorMsg$1 = options?.errors?.missingPort;
1895
+ const msg = typeof errorMsg$1 === "function" ? errorMsg$1(input) : errorMsg$1 ?? require_message.message`Port number is required but was not specified.`;
1896
+ return {
1897
+ success: false,
1898
+ error: msg
1899
+ };
1900
+ }
1901
+ if (!canOmitPort && !anySeparatorFound) {
1902
+ hostOnlyResult = hostOnlyResult ?? parseHost(trimmed);
1903
+ if (hostOnlyResult.success) {
1904
+ const errorMsg$1 = options?.errors?.missingPort;
1905
+ const msg = typeof errorMsg$1 === "function" ? errorMsg$1(input) : errorMsg$1 ?? require_message.message`Port number is required but was not specified.`;
1485
1906
  return {
1486
1907
  success: false,
1487
1908
  error: msg
1488
1909
  };
1489
1910
  }
1490
- } else {
1491
- const portResult = portParser.parse(portPart);
1492
- if (!portResult.success) {
1493
- const errorMsg = options?.errors?.invalidFormat;
1494
- const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg ?? require_message.message`Expected a socket address in format host${separator}port, but got ${input}.`;
1911
+ }
1912
+ if (firstHostError !== void 0) {
1913
+ const errorMsg$1 = options?.errors?.invalidFormat;
1914
+ if (errorMsg$1) {
1915
+ const msg = typeof errorMsg$1 === "function" ? errorMsg$1(input) : errorMsg$1;
1495
1916
  return {
1496
1917
  success: false,
1497
1918
  error: msg
1498
1919
  };
1499
1920
  }
1500
- validatedPort = portResult.value;
1921
+ const hostPartIsDegenerate = separatorIsHostChar ? firstHostError.hostPart.replaceAll(separator, "") === "" : firstHostError.hostPart.includes(separator);
1922
+ if (firstHostError.hostPart !== "" && !hostPartIsDegenerate && !disambiguationParser.parse(trimmed).success) return {
1923
+ success: false,
1924
+ error: firstHostError.error
1925
+ };
1926
+ }
1927
+ const errorMsg = options?.errors?.invalidFormat;
1928
+ if (errorMsg) {
1929
+ const msg = typeof errorMsg === "function" ? errorMsg(input) : errorMsg;
1930
+ return {
1931
+ success: false,
1932
+ error: msg
1933
+ };
1934
+ }
1935
+ if (hostOnlyResult !== void 0 && !hostOnlyResult.success) {
1936
+ if (looksLikeIpv4(trimmed) || looksLikeAltIpv4Literal(trimmed)) return {
1937
+ success: false,
1938
+ error: hostOnlyResult.error
1939
+ };
1501
1940
  }
1502
1941
  return {
1503
- success: true,
1504
- value: {
1505
- host: validatedHost,
1506
- port: validatedPort
1507
- }
1942
+ success: false,
1943
+ error: require_message.message`Expected a socket address in format ${require_message.text(formatExample)}, but got ${input}.`
1508
1944
  };
1509
1945
  },
1510
1946
  format(value) {
@@ -1513,9 +1949,11 @@ function socketAddress(options) {
1513
1949
  };
1514
1950
  }
1515
1951
  function portRange(options) {
1516
- 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)}.`);
1517
- 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)}.`);
1952
+ checkBooleanOption(options, "disallowWellKnown");
1953
+ checkBooleanOption(options, "allowSingle");
1954
+ 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)}.`);
1518
1955
  const separator = options?.separator ?? "-";
1956
+ if (separator === "") throw new TypeError("Expected separator to not be empty.");
1519
1957
  if (/\p{Nd}/u.test(separator)) throw new TypeError(`Expected separator to not contain digits, but got: ${JSON.stringify(separator)}.`);
1520
1958
  const metavar = options?.metavar ?? `PORT${separator}PORT`;
1521
1959
  require_nonempty.ensureNonEmptyString(metavar);
@@ -1535,8 +1973,17 @@ function portRange(options) {
1535
1973
  errors: options?.errors
1536
1974
  });
1537
1975
  return {
1538
- $mode: "sync",
1976
+ mode: "sync",
1539
1977
  metavar,
1978
+ get placeholder() {
1979
+ return isBigInt ? {
1980
+ start: portParser.placeholder,
1981
+ end: portParser.placeholder
1982
+ } : {
1983
+ start: portParser.placeholder,
1984
+ end: portParser.placeholder
1985
+ };
1986
+ },
1540
1987
  parse(input) {
1541
1988
  const trimmed = input.trim();
1542
1989
  const separatorIndex = trimmed.indexOf(separator);
@@ -1635,10 +2082,14 @@ function portRange(options) {
1635
2082
  * Creates a value parser for MAC (Media Access Control) addresses.
1636
2083
  *
1637
2084
  * Validates MAC-48 addresses (6 octets, 12 hex digits) in various formats:
1638
- * - Colon-separated: `00:1A:2B:3C:4D:5E`
1639
- * - Hyphen-separated: `00-1A-2B-3C-4D-5E`
1640
- * - Dot-separated (Cisco): `001A.2B3C.4D5E`
1641
- * - No separator: `001A2B3C4D5E`
2085
+ * - Colon-separated: `00:1A:2B:3C:4D:5E` (1–2 hex digits per octet)
2086
+ * - Hyphen-separated: `00-1A-2B-3C-4D-5E` (1–2 hex digits per octet)
2087
+ * - Dot-separated (Cisco): `001A.2B3C.4D5E` (exactly 4 hex digits per group)
2088
+ * - No separator: `001A2B3C4D5E` (exactly 12 hex digits)
2089
+ *
2090
+ * Colon-separated and hyphen-separated formats accept single-digit octets
2091
+ * (e.g., `0:1:2:3:4:5`), which are automatically zero-padded to canonical
2092
+ * two-digit form (e.g., `00:01:02:03:04:05`).
1642
2093
  *
1643
2094
  * Returns the MAC address as a formatted string according to `case` and
1644
2095
  * `outputSeparator` options.
@@ -1662,6 +2113,24 @@ function portRange(options) {
1662
2113
  * ```
1663
2114
  */
1664
2115
  function macAddress(options) {
2116
+ checkEnumOption(options, "separator", [
2117
+ ":",
2118
+ "-",
2119
+ ".",
2120
+ "none",
2121
+ "any"
2122
+ ]);
2123
+ checkEnumOption(options, "outputSeparator", [
2124
+ ":",
2125
+ "-",
2126
+ ".",
2127
+ "none"
2128
+ ]);
2129
+ checkEnumOption(options, "case", [
2130
+ "preserve",
2131
+ "upper",
2132
+ "lower"
2133
+ ]);
1665
2134
  const separator = options?.separator ?? "any";
1666
2135
  const caseOption = options?.case ?? "preserve";
1667
2136
  const outputSeparator = options?.outputSeparator;
@@ -1670,9 +2139,64 @@ function macAddress(options) {
1670
2139
  const hyphenRegex = /^([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})-([0-9a-fA-F]{1,2})$/;
1671
2140
  const dotRegex = /^([0-9a-fA-F]{4})\.([0-9a-fA-F]{4})\.([0-9a-fA-F]{4})$/;
1672
2141
  const noneRegex = /^([0-9a-fA-F]{12})$/;
1673
- return {
1674
- $mode: "sync",
2142
+ function joinOctets(octets, sep) {
2143
+ let formatted = octets;
2144
+ if (caseOption === "upper") formatted = octets.map((o) => o.toUpperCase());
2145
+ else if (caseOption === "lower") formatted = octets.map((o) => o.toLowerCase());
2146
+ if (sep === ".") return [
2147
+ formatted[0] + formatted[1],
2148
+ formatted[2] + formatted[3],
2149
+ formatted[4] + formatted[5]
2150
+ ].join(".");
2151
+ if (sep === "none") return formatted.join("");
2152
+ return formatted.join(sep);
2153
+ }
2154
+ function normalizeMac(value) {
2155
+ if (typeof value !== "string") return metavar;
2156
+ let octets;
2157
+ let detectedSep;
2158
+ if (value.includes(":")) {
2159
+ octets = value.split(":");
2160
+ detectedSep = ":";
2161
+ } else if (value.includes("-")) {
2162
+ octets = value.split("-");
2163
+ detectedSep = "-";
2164
+ } else if (value.includes(".")) {
2165
+ const groups = value.split(".");
2166
+ if (groups.length !== 3 || !groups.every((g) => /^[0-9a-fA-F]{4}$/.test(g))) return value;
2167
+ octets = groups.flatMap((g) => [g.slice(0, 2), g.slice(2)]);
2168
+ detectedSep = ".";
2169
+ } else {
2170
+ if (value.length !== 12) return value;
2171
+ octets = [];
2172
+ for (let i = 0; i < value.length; i += 2) octets.push(value.slice(i, i + 2));
2173
+ detectedSep = "none";
2174
+ }
2175
+ if (octets.length !== 6 || !octets.every((o) => /^[0-9a-fA-F]{1,2}$/.test(o))) return value;
2176
+ octets = octets.map((o) => o.padStart(2, "0"));
2177
+ let sep;
2178
+ if (outputSeparator != null) sep = outputSeparator;
2179
+ else if (separator !== "any") sep = separator;
2180
+ else sep = detectedSep;
2181
+ return joinOctets(octets, sep);
2182
+ }
2183
+ const parserObj = {
2184
+ mode: "sync",
1675
2185
  metavar,
2186
+ get placeholder() {
2187
+ const octets = [
2188
+ "00",
2189
+ "00",
2190
+ "00",
2191
+ "00",
2192
+ "00",
2193
+ "00"
2194
+ ];
2195
+ const sep = outputSeparator ?? (separator === "any" ? ":" : separator);
2196
+ if (sep === ".") return `${octets[0]}${octets[1]}.${octets[2]}${octets[3]}.${octets[4]}${octets[5]}`;
2197
+ if (sep === "none") return octets.join("");
2198
+ return octets.join(sep);
2199
+ },
1676
2200
  parse(input) {
1677
2201
  let octets = [];
1678
2202
  let inputSeparator;
@@ -1732,28 +2256,52 @@ function macAddress(options) {
1732
2256
  error: msg
1733
2257
  };
1734
2258
  }
1735
- let formattedOctets = octets;
1736
- if (caseOption === "upper") formattedOctets = octets.map((octet) => octet.toUpperCase());
1737
- else if (caseOption === "lower") formattedOctets = octets.map((octet) => octet.toLowerCase());
2259
+ octets = octets.map((o) => o.padStart(2, "0"));
1738
2260
  const finalSeparator = outputSeparator ?? inputSeparator ?? ":";
1739
- let result;
1740
- if (finalSeparator === ":") result = formattedOctets.join(":");
1741
- else if (finalSeparator === "-") result = formattedOctets.join("-");
1742
- else if (finalSeparator === ".") result = [
1743
- formattedOctets[0] + formattedOctets[1],
1744
- formattedOctets[2] + formattedOctets[3],
1745
- formattedOctets[4] + formattedOctets[5]
1746
- ].join(".");
1747
- else result = formattedOctets.join("");
1748
2261
  return {
1749
2262
  success: true,
1750
- value: result
2263
+ value: joinOctets(octets, finalSeparator)
1751
2264
  };
1752
2265
  },
1753
- format() {
1754
- return metavar;
1755
- }
2266
+ format: normalizeMac
1756
2267
  };
2268
+ const macParser = parserObj;
2269
+ let macParsing = false;
2270
+ Object.defineProperty(parserObj, "format", {
2271
+ value(v) {
2272
+ if (typeof v !== "string") return metavar;
2273
+ if (macParsing) return v;
2274
+ macParsing = true;
2275
+ try {
2276
+ const result = macParser.parse(v);
2277
+ return result.success ? result.value : v;
2278
+ } catch {
2279
+ return v;
2280
+ } finally {
2281
+ macParsing = false;
2282
+ }
2283
+ },
2284
+ configurable: true,
2285
+ enumerable: true
2286
+ });
2287
+ Object.defineProperty(parserObj, "normalize", {
2288
+ value(v) {
2289
+ if (typeof v !== "string") return v;
2290
+ if (macParsing) return v;
2291
+ macParsing = true;
2292
+ try {
2293
+ const result = macParser.parse(v);
2294
+ return result.success ? result.value : v;
2295
+ } catch {
2296
+ return v;
2297
+ } finally {
2298
+ macParsing = false;
2299
+ }
2300
+ },
2301
+ configurable: true,
2302
+ enumerable: true
2303
+ });
2304
+ return parserObj;
1757
2305
  }
1758
2306
  /**
1759
2307
  * Creates a value parser for domain names.
@@ -1764,6 +2312,14 @@ function macAddress(options) {
1764
2312
  *
1765
2313
  * @param options Parser options for domain validation.
1766
2314
  * @returns A parser that accepts valid domain names as strings.
2315
+ * @throws {RangeError} If `maxLength` is not a positive integer.
2316
+ * @throws {RangeError} If `minLabels` is not a positive integer.
2317
+ * @throws {TypeError} If `allowSubdomains` or `lowercase` is not a boolean.
2318
+ * @throws {TypeError} If any `allowedTlds` entry is not a string, is empty,
2319
+ * contains dots, has leading/trailing whitespace, or is not a valid DNS
2320
+ * label.
2321
+ * @throws {TypeError} If `allowSubdomains` is `false` and `minLabels` is
2322
+ * greater than 2, since non-subdomain domains have exactly 2 labels.
1767
2323
  *
1768
2324
  * @example
1769
2325
  * ``` typescript
@@ -1777,7 +2333,7 @@ function macAddress(options) {
1777
2333
  * option("--root", domain({ allowSubdomains: false }))
1778
2334
  *
1779
2335
  * // Restrict to specific TLDs
1780
- * option("--domain", domain({ allowedTLDs: ["com", "org", "net"] }))
2336
+ * option("--domain", domain({ allowedTlds: ["com", "org", "net"] }))
1781
2337
  *
1782
2338
  * // Normalize to lowercase
1783
2339
  * option("--domain", domain({ lowercase: true }))
@@ -1787,19 +2343,49 @@ function macAddress(options) {
1787
2343
  */
1788
2344
  function domain(options) {
1789
2345
  const metavar = options?.metavar ?? "DOMAIN";
2346
+ checkBooleanOption(options, "allowSubdomains");
2347
+ checkBooleanOption(options, "lowercase");
1790
2348
  const allowSubdomains = options?.allowSubdomains ?? true;
1791
- const allowedTLDs = options?.allowedTLDs != null ? Object.freeze([...options.allowedTLDs]) : void 0;
2349
+ const allowedTlds = options?.allowedTlds != null ? Object.freeze([...options.allowedTlds]) : void 0;
2350
+ const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
2351
+ if (allowedTlds !== void 0) {
2352
+ if (allowedTlds.length === 0) throw new TypeError("allowedTlds must not be empty.");
2353
+ for (const [i, tld] of allowedTlds.entries()) {
2354
+ if (typeof tld !== "string") {
2355
+ const actualType = Array.isArray(tld) ? "array" : typeof tld;
2356
+ throw new TypeError(`allowedTlds[${i}] must be a string, but got ${actualType}.`);
2357
+ }
2358
+ if (tld.length === 0) throw new TypeError(`allowedTlds[${i}] must not be an empty string.`);
2359
+ if (tld.includes(".")) throw new TypeError(`allowedTlds[${i}] must not contain dots: ${JSON.stringify(tld)}.`);
2360
+ if (tld !== tld.trim()) throw new TypeError(`allowedTlds[${i}] must not have leading or trailing whitespace: ${JSON.stringify(tld)}.`);
2361
+ if (!labelRegex.test(tld)) throw new TypeError(`allowedTlds[${i}] is not a valid DNS label: ${JSON.stringify(tld)}.`);
2362
+ }
2363
+ }
2364
+ const allowedTldsLower = allowedTlds != null ? Object.freeze(allowedTlds.map((t) => t.toLowerCase())) : void 0;
1792
2365
  const minLabels = options?.minLabels ?? 2;
2366
+ const maxLength = options?.maxLength ?? 253;
1793
2367
  const lowercase = options?.lowercase ?? false;
2368
+ if (!Number.isInteger(maxLength) || maxLength < 1) throw new RangeError("maxLength must be an integer greater than or equal to 1.");
2369
+ if (!Number.isInteger(minLabels) || minLabels < 1) throw new RangeError("minLabels must be an integer greater than or equal to 1.");
2370
+ if (!allowSubdomains && minLabels > 2) throw new TypeError("allowSubdomains: false is incompatible with minLabels > 2, as non-subdomain domains have exactly 2 labels.");
1794
2371
  const invalidDomain = options?.errors?.invalidDomain;
2372
+ const tooLong = options?.errors?.tooLong;
1795
2373
  const tooFewLabels = options?.errors?.tooFewLabels;
1796
2374
  const subdomainsNotAllowed = options?.errors?.subdomainsNotAllowed;
1797
2375
  const tldNotAllowed = options?.errors?.tldNotAllowed;
1798
- const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
1799
- return {
1800
- $mode: "sync",
2376
+ const domainParserObj = {
2377
+ mode: "sync",
1801
2378
  metavar,
2379
+ placeholder: options?.placeholder ?? `example.${allowedTldsLower?.[0] ?? "com"}`,
1802
2380
  parse(input) {
2381
+ if (input.length > maxLength) {
2382
+ const errorMsg = tooLong;
2383
+ const msg = typeof errorMsg === "function" ? errorMsg(input, maxLength) : errorMsg ?? require_message.message`Domain ${input} is too long (maximum ${require_message.text(maxLength.toString())} characters).`;
2384
+ return {
2385
+ success: false,
2386
+ error: msg
2387
+ };
2388
+ }
1803
2389
  if (input.length === 0 || input.startsWith(".") || input.endsWith(".")) {
1804
2390
  const errorMsg = invalidDomain;
1805
2391
  if (typeof errorMsg === "function") return {
@@ -1876,6 +2462,31 @@ function domain(options) {
1876
2462
  error: msg
1877
2463
  };
1878
2464
  }
2465
+ if (labels.length >= 2 && labels.every((l) => /^[0-9]+$/.test(l))) {
2466
+ const errorMsg = invalidDomain;
2467
+ if (typeof errorMsg === "function") return {
2468
+ success: false,
2469
+ error: errorMsg(input)
2470
+ };
2471
+ const msg = errorMsg ?? [
2472
+ {
2473
+ type: "text",
2474
+ text: "Expected a valid domain name, but got "
2475
+ },
2476
+ {
2477
+ type: "value",
2478
+ value: input
2479
+ },
2480
+ {
2481
+ type: "text",
2482
+ text: "."
2483
+ }
2484
+ ];
2485
+ return {
2486
+ success: false,
2487
+ error: msg
2488
+ };
2489
+ }
1879
2490
  if (labels.length < minLabels) {
1880
2491
  const errorMsg = tooFewLabels;
1881
2492
  if (typeof errorMsg === "function") return {
@@ -1926,15 +2537,14 @@ function domain(options) {
1926
2537
  error: msg
1927
2538
  };
1928
2539
  }
1929
- if (allowedTLDs !== void 0) {
2540
+ if (allowedTlds !== void 0 && allowedTldsLower !== void 0) {
1930
2541
  const tld = labels[labels.length - 1];
1931
2542
  const tldLower = tld.toLowerCase();
1932
- const allowedTLDsLower = allowedTLDs.map((t) => t.toLowerCase());
1933
- if (!allowedTLDsLower.includes(tldLower)) {
2543
+ if (!allowedTldsLower.includes(tldLower)) {
1934
2544
  const errorMsg = tldNotAllowed;
1935
2545
  if (typeof errorMsg === "function") return {
1936
2546
  success: false,
1937
- error: errorMsg(tld, allowedTLDs)
2547
+ error: errorMsg(tld, allowedTlds)
1938
2548
  };
1939
2549
  const msg = errorMsg ?? [
1940
2550
  {
@@ -1947,7 +2557,15 @@ function domain(options) {
1947
2557
  },
1948
2558
  {
1949
2559
  type: "text",
1950
- text: ` is not allowed. Allowed TLDs: ${allowedTLDs.join(", ")}.`
2560
+ text: " is not allowed. Allowed TLDs: "
2561
+ },
2562
+ ...require_message.valueSet(allowedTlds, {
2563
+ fallback: "",
2564
+ locale: "en-US"
2565
+ }),
2566
+ {
2567
+ type: "text",
2568
+ text: "."
1951
2569
  }
1952
2570
  ];
1953
2571
  return {
@@ -1962,10 +2580,51 @@ function domain(options) {
1962
2580
  value: result
1963
2581
  };
1964
2582
  },
1965
- format() {
1966
- return metavar;
2583
+ format(value) {
2584
+ if (typeof value !== "string") return metavar;
2585
+ if (!lowercase) return value;
2586
+ return value.split(".").length >= minLabels ? value.toLowerCase() : value;
1967
2587
  }
1968
2588
  };
2589
+ if (lowercase) {
2590
+ const domParser = domainParserObj;
2591
+ let domParsing = false;
2592
+ Object.defineProperty(domainParserObj, "format", {
2593
+ value(v) {
2594
+ if (typeof v !== "string") return metavar;
2595
+ if (domParsing) return v;
2596
+ domParsing = true;
2597
+ try {
2598
+ const result = domParser.parse(v);
2599
+ return result.success ? result.value : v;
2600
+ } catch {
2601
+ return v;
2602
+ } finally {
2603
+ domParsing = false;
2604
+ }
2605
+ },
2606
+ configurable: true,
2607
+ enumerable: true
2608
+ });
2609
+ Object.defineProperty(domainParserObj, "normalize", {
2610
+ value(v) {
2611
+ if (typeof v !== "string") return v;
2612
+ if (domParsing) return v;
2613
+ domParsing = true;
2614
+ try {
2615
+ const result = domParser.parse(v);
2616
+ return result.success ? result.value : v;
2617
+ } catch {
2618
+ return v;
2619
+ } finally {
2620
+ domParsing = false;
2621
+ }
2622
+ },
2623
+ configurable: true,
2624
+ enumerable: true
2625
+ });
2626
+ }
2627
+ return domainParserObj;
1969
2628
  }
1970
2629
  /**
1971
2630
  * Creates a value parser for IPv6 addresses.
@@ -1998,9 +2657,10 @@ function ipv6(options) {
1998
2657
  const allowZero = options?.allowZero ?? true;
1999
2658
  const errors = options?.errors;
2000
2659
  const metavar = options?.metavar ?? "IPV6";
2001
- return {
2002
- $mode: "sync",
2660
+ const ipv6ParserObj = {
2661
+ mode: "sync",
2003
2662
  metavar,
2663
+ placeholder: allowZero ? "::" : allowLoopback ? "::1" : "2001:db8::1",
2004
2664
  parse(input) {
2005
2665
  const normalized = parseAndNormalizeIpv6(input);
2006
2666
  if (normalized === null) {
@@ -2150,10 +2810,74 @@ function ipv6(options) {
2150
2810
  value: normalized
2151
2811
  };
2152
2812
  },
2153
- format() {
2154
- return metavar;
2813
+ format(value) {
2814
+ if (typeof value !== "string") return metavar;
2815
+ return parseAndNormalizeIpv6(value) ?? value;
2155
2816
  }
2156
2817
  };
2818
+ let ipv6Parsing = false;
2819
+ Object.defineProperty(ipv6ParserObj, "format", {
2820
+ value(v) {
2821
+ if (typeof v !== "string") return metavar;
2822
+ if (ipv6Parsing) return v;
2823
+ ipv6Parsing = true;
2824
+ try {
2825
+ const result = ipv6ParserObj.parse(v);
2826
+ return result.success ? result.value : v;
2827
+ } catch {
2828
+ return v;
2829
+ } finally {
2830
+ ipv6Parsing = false;
2831
+ }
2832
+ },
2833
+ configurable: true,
2834
+ enumerable: true
2835
+ });
2836
+ Object.defineProperty(ipv6ParserObj, "normalize", {
2837
+ value(v) {
2838
+ if (typeof v !== "string") return v;
2839
+ if (ipv6Parsing) return v;
2840
+ ipv6Parsing = true;
2841
+ try {
2842
+ const result = ipv6ParserObj.parse(v);
2843
+ return result.success ? result.value : v;
2844
+ } catch {
2845
+ return v;
2846
+ } finally {
2847
+ ipv6Parsing = false;
2848
+ }
2849
+ },
2850
+ configurable: true,
2851
+ enumerable: true
2852
+ });
2853
+ return ipv6ParserObj;
2854
+ }
2855
+ /**
2856
+ * Parses a dotted-decimal IPv4 string into four validated octets.
2857
+ * Returns null if the input is not a valid strict IPv4 address
2858
+ * (exactly four decimal octets 0–255, no leading zeros, no whitespace,
2859
+ * no non-decimal characters).
2860
+ */
2861
+ function parseIpv4Octets(input) {
2862
+ const parts = input.split(".");
2863
+ if (parts.length !== 4) return null;
2864
+ const octets = [
2865
+ 0,
2866
+ 0,
2867
+ 0,
2868
+ 0
2869
+ ];
2870
+ for (let i = 0; i < 4; i++) {
2871
+ const part = parts[i];
2872
+ if (part.length === 0) return null;
2873
+ if (part.trim() !== part) return null;
2874
+ if (part.length > 1 && part[0] === "0") return null;
2875
+ if (!/^[0-9]+$/.test(part)) return null;
2876
+ const octet = Number(part);
2877
+ if (!Number.isInteger(octet) || octet < 0 || octet > 255) return null;
2878
+ octets[i] = octet;
2879
+ }
2880
+ return octets;
2157
2881
  }
2158
2882
  /**
2159
2883
  * Parses and normalizes an IPv6 address to canonical form.
@@ -2165,10 +2889,8 @@ function parseAndNormalizeIpv6(input) {
2165
2889
  if (ipv4MappedMatch) {
2166
2890
  const ipv6Part = ipv4MappedMatch[1];
2167
2891
  const ipv4Part = ipv4MappedMatch[2];
2168
- const ipv4Octets = ipv4Part.split(".");
2169
- if (ipv4Octets.length !== 4) return null;
2170
- const octets = ipv4Octets.map((o) => parseInt(o, 10));
2171
- if (octets.some((o) => isNaN(o) || o < 0 || o > 255)) return null;
2892
+ const octets = parseIpv4Octets(ipv4Part);
2893
+ if (octets === null) return null;
2172
2894
  const group1 = octets[0] << 8 | octets[1];
2173
2895
  const group2 = octets[2] << 8 | octets[3];
2174
2896
  const fullAddress = `${ipv6Part}:${group1.toString(16)}:${group2.toString(16)}`;
@@ -2262,6 +2984,90 @@ function compressIpv6(groups) {
2262
2984
  else return before.join(":") + "::" + after.join(":");
2263
2985
  }
2264
2986
  /**
2987
+ * Extracts IPv4 octets from an IPv4-mapped IPv6 address.
2988
+ * Returns null if the address is not an IPv4-mapped address
2989
+ * (i.e., not in the `::ffff:x.x.x.x` range).
2990
+ */
2991
+ function extractIpv4FromMapped(normalizedIpv6) {
2992
+ const groups = expandIpv6(normalizedIpv6);
2993
+ if (groups === null) return null;
2994
+ for (let i = 0; i < 5; i++) if (parseInt(groups[i], 16) !== 0) return null;
2995
+ if (parseInt(groups[5], 16) !== 65535) return null;
2996
+ const high = parseInt(groups[6], 16);
2997
+ const low = parseInt(groups[7], 16);
2998
+ return [
2999
+ high >> 8 & 255,
3000
+ high & 255,
3001
+ low >> 8 & 255,
3002
+ low & 255
3003
+ ];
3004
+ }
3005
+ /**
3006
+ * Checks IPv4 restrictions against octets extracted from an IPv4-mapped
3007
+ * IPv6 address. The check uses the base address, consistent with how
3008
+ * the `ipv4()` parser validates the address part of a regular IPv4 CIDR.
3009
+ *
3010
+ * Returns an error result if a restriction is violated, or null if all
3011
+ * checks pass.
3012
+ */
3013
+ function checkIpv4MappedRestrictions(octets, normalizedIpv6, ipv4Opts, errors) {
3014
+ const allowPrivate = ipv4Opts?.allowPrivate ?? true;
3015
+ const allowLoopback = ipv4Opts?.allowLoopback ?? true;
3016
+ const allowLinkLocal = ipv4Opts?.allowLinkLocal ?? true;
3017
+ const allowMulticast = ipv4Opts?.allowMulticast ?? true;
3018
+ const allowBroadcast = ipv4Opts?.allowBroadcast ?? true;
3019
+ const allowZero = ipv4Opts?.allowZero ?? true;
3020
+ if (!allowPrivate && isPrivateIp(octets)) {
3021
+ const errorMsg = errors?.privateNotAllowed;
3022
+ const msg = typeof errorMsg === "function" ? errorMsg(normalizedIpv6) : errorMsg ?? require_message.message`${normalizedIpv6} is a private IP address.`;
3023
+ return {
3024
+ success: false,
3025
+ error: msg
3026
+ };
3027
+ }
3028
+ if (!allowLoopback && isLoopbackIp(octets)) {
3029
+ const errorMsg = errors?.loopbackNotAllowed;
3030
+ const msg = typeof errorMsg === "function" ? errorMsg(normalizedIpv6) : errorMsg ?? require_message.message`${normalizedIpv6} is a loopback address.`;
3031
+ return {
3032
+ success: false,
3033
+ error: msg
3034
+ };
3035
+ }
3036
+ if (!allowLinkLocal && isLinkLocalIp(octets)) {
3037
+ const errorMsg = errors?.linkLocalNotAllowed;
3038
+ const msg = typeof errorMsg === "function" ? errorMsg(normalizedIpv6) : errorMsg ?? require_message.message`${normalizedIpv6} is a link-local address.`;
3039
+ return {
3040
+ success: false,
3041
+ error: msg
3042
+ };
3043
+ }
3044
+ if (!allowMulticast && isMulticastIp(octets)) {
3045
+ const errorMsg = errors?.multicastNotAllowed;
3046
+ const msg = typeof errorMsg === "function" ? errorMsg(normalizedIpv6) : errorMsg ?? require_message.message`${normalizedIpv6} is a multicast address.`;
3047
+ return {
3048
+ success: false,
3049
+ error: msg
3050
+ };
3051
+ }
3052
+ if (!allowBroadcast && isBroadcastIp(octets)) {
3053
+ const errorMsg = errors?.broadcastNotAllowed;
3054
+ const msg = typeof errorMsg === "function" ? errorMsg(normalizedIpv6) : errorMsg ?? require_message.message`${normalizedIpv6} is the broadcast address.`;
3055
+ return {
3056
+ success: false,
3057
+ error: msg
3058
+ };
3059
+ }
3060
+ if (!allowZero && isZeroIp(octets)) {
3061
+ const errorMsg = errors?.zeroNotAllowed;
3062
+ const msg = typeof errorMsg === "function" ? errorMsg(normalizedIpv6) : errorMsg ?? require_message.message`${normalizedIpv6} is the zero address.`;
3063
+ return {
3064
+ success: false,
3065
+ error: msg
3066
+ };
3067
+ }
3068
+ return null;
3069
+ }
3070
+ /**
2265
3071
  * Creates a value parser that accepts both IPv4 and IPv6 addresses.
2266
3072
  *
2267
3073
  * By default, accepts both IPv4 and IPv6 addresses. Use the `version` option
@@ -2314,9 +3120,26 @@ function ip(options) {
2314
3120
  zeroNotAllowed: errors?.zeroNotAllowed
2315
3121
  }
2316
3122
  }) : null;
2317
- return {
2318
- $mode: "sync",
3123
+ const mappedIpv4Opts = version === "both" ? {
3124
+ allowPrivate: options?.ipv4?.allowPrivate,
3125
+ allowLoopback: options?.ipv4?.allowLoopback,
3126
+ allowLinkLocal: options?.ipv4?.allowLinkLocal,
3127
+ allowMulticast: options?.ipv4?.allowMulticast,
3128
+ allowBroadcast: options?.ipv4?.allowBroadcast,
3129
+ allowZero: options?.ipv4?.allowZero
3130
+ } : void 0;
3131
+ const mappedIpv4Errors = version === "both" ? {
3132
+ privateNotAllowed: errors?.privateNotAllowed,
3133
+ loopbackNotAllowed: errors?.loopbackNotAllowed,
3134
+ linkLocalNotAllowed: errors?.linkLocalNotAllowed,
3135
+ multicastNotAllowed: errors?.multicastNotAllowed,
3136
+ broadcastNotAllowed: errors?.broadcastNotAllowed,
3137
+ zeroNotAllowed: errors?.zeroNotAllowed
3138
+ } : void 0;
3139
+ const ipParserObj = {
3140
+ mode: "sync",
2319
3141
  metavar,
3142
+ placeholder: version === 6 ? ipv6Parser.placeholder : ipv4Parser.placeholder,
2320
3143
  parse(input) {
2321
3144
  let ipv4Error = null;
2322
3145
  let ipv6Error = null;
@@ -2328,7 +3151,16 @@ function ip(options) {
2328
3151
  }
2329
3152
  if (ipv6Parser !== null) {
2330
3153
  const result = ipv6Parser.parse(input);
2331
- if (result.success) return result;
3154
+ if (result.success) {
3155
+ if (version === "both") {
3156
+ const mappedOctets = extractIpv4FromMapped(result.value);
3157
+ if (mappedOctets !== null) {
3158
+ const restrictionError = checkIpv4MappedRestrictions(mappedOctets, result.value, mappedIpv4Opts, mappedIpv4Errors);
3159
+ if (restrictionError !== null) return restrictionError;
3160
+ }
3161
+ }
3162
+ return result;
3163
+ }
2332
3164
  ipv6Error = result;
2333
3165
  if (version === 6) return result;
2334
3166
  }
@@ -2364,10 +3196,47 @@ function ip(options) {
2364
3196
  error: msg
2365
3197
  };
2366
3198
  },
2367
- format() {
2368
- return metavar;
3199
+ format(value) {
3200
+ if (typeof value !== "string") return metavar;
3201
+ return parseAndNormalizeIpv6(value) ?? value;
2369
3202
  }
2370
3203
  };
3204
+ let ipParsing = false;
3205
+ Object.defineProperty(ipParserObj, "format", {
3206
+ value(v) {
3207
+ if (typeof v !== "string") return metavar;
3208
+ if (ipParsing) return v;
3209
+ ipParsing = true;
3210
+ try {
3211
+ const result = ipParserObj.parse(v);
3212
+ return result.success ? result.value : v;
3213
+ } catch {
3214
+ return v;
3215
+ } finally {
3216
+ ipParsing = false;
3217
+ }
3218
+ },
3219
+ configurable: true,
3220
+ enumerable: true
3221
+ });
3222
+ Object.defineProperty(ipParserObj, "normalize", {
3223
+ value(v) {
3224
+ if (typeof v !== "string") return v;
3225
+ if (ipParsing) return v;
3226
+ ipParsing = true;
3227
+ try {
3228
+ const result = ipParserObj.parse(v);
3229
+ return result.success ? result.value : v;
3230
+ } catch {
3231
+ return v;
3232
+ } finally {
3233
+ ipParsing = false;
3234
+ }
3235
+ },
3236
+ configurable: true,
3237
+ enumerable: true
3238
+ });
3239
+ return ipParserObj;
2371
3240
  }
2372
3241
  /**
2373
3242
  * Creates a value parser for CIDR notation (IP address with prefix length).
@@ -2395,16 +3264,71 @@ function ip(options) {
2395
3264
  * @since 0.10.0
2396
3265
  */
2397
3266
  function cidr(options) {
3267
+ if (options?.minPrefix != null && !Number.isFinite(options.minPrefix)) throw new RangeError(`Expected minPrefix to be a finite number, but got: ${options.minPrefix}`);
3268
+ if (options?.maxPrefix != null && !Number.isFinite(options.maxPrefix)) throw new RangeError(`Expected maxPrefix to be a finite number, but got: ${options.maxPrefix}`);
3269
+ 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}.`);
2398
3270
  const version = options?.version ?? "both";
3271
+ const maxPrefixForVersion = version === 4 ? 32 : version === 6 ? 128 : 128;
3272
+ 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}.`);
3273
+ 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}.`);
2399
3274
  const minPrefix = options?.minPrefix;
2400
3275
  const maxPrefix = options?.maxPrefix;
2401
3276
  const errors = options?.errors;
2402
3277
  const metavar = options?.metavar ?? "CIDR";
2403
- const ipv4Parser = version === 4 || version === "both" ? ipv4(options?.ipv4) : null;
2404
- const ipv6Parser = version === 6 || version === "both" ? ipv6(options?.ipv6) : null;
2405
- return {
2406
- $mode: "sync",
3278
+ const genericIpSentinel = [];
3279
+ const ipv4Parser = version === 4 || version === "both" ? ipv4({
3280
+ ...options?.ipv4,
3281
+ errors: {
3282
+ invalidIpv4: genericIpSentinel,
3283
+ privateNotAllowed: errors?.privateNotAllowed,
3284
+ loopbackNotAllowed: errors?.loopbackNotAllowed,
3285
+ linkLocalNotAllowed: errors?.linkLocalNotAllowed,
3286
+ multicastNotAllowed: errors?.multicastNotAllowed,
3287
+ broadcastNotAllowed: errors?.broadcastNotAllowed,
3288
+ zeroNotAllowed: errors?.zeroNotAllowed
3289
+ }
3290
+ }) : null;
3291
+ const ipv6Parser = version === 6 || version === "both" ? ipv6({
3292
+ ...options?.ipv6,
3293
+ errors: {
3294
+ invalidIpv6: genericIpSentinel,
3295
+ loopbackNotAllowed: errors?.loopbackNotAllowed,
3296
+ linkLocalNotAllowed: errors?.linkLocalNotAllowed,
3297
+ multicastNotAllowed: errors?.multicastNotAllowed,
3298
+ zeroNotAllowed: errors?.zeroNotAllowed,
3299
+ uniqueLocalNotAllowed: errors?.uniqueLocalNotAllowed
3300
+ }
3301
+ }) : null;
3302
+ const mappedIpv4Opts = version === "both" ? {
3303
+ allowPrivate: options?.ipv4?.allowPrivate,
3304
+ allowLoopback: options?.ipv4?.allowLoopback,
3305
+ allowLinkLocal: options?.ipv4?.allowLinkLocal,
3306
+ allowMulticast: options?.ipv4?.allowMulticast,
3307
+ allowBroadcast: options?.ipv4?.allowBroadcast,
3308
+ allowZero: options?.ipv4?.allowZero
3309
+ } : void 0;
3310
+ const mappedIpv4Errors = version === "both" ? {
3311
+ privateNotAllowed: errors?.privateNotAllowed,
3312
+ loopbackNotAllowed: errors?.loopbackNotAllowed,
3313
+ linkLocalNotAllowed: errors?.linkLocalNotAllowed,
3314
+ multicastNotAllowed: errors?.multicastNotAllowed,
3315
+ broadcastNotAllowed: errors?.broadcastNotAllowed,
3316
+ zeroNotAllowed: errors?.zeroNotAllowed
3317
+ } : void 0;
3318
+ const cidrParserObj = {
3319
+ mode: "sync",
2407
3320
  metavar,
3321
+ get placeholder() {
3322
+ return version === 6 || version === "both" && (minPrefix ?? 0) > 32 ? {
3323
+ address: ipv6Parser.placeholder,
3324
+ prefix: minPrefix ?? 0,
3325
+ version: 6
3326
+ } : {
3327
+ address: ipv4Parser.placeholder,
3328
+ prefix: minPrefix ?? 0,
3329
+ version: 4
3330
+ };
3331
+ },
2408
3332
  parse(input) {
2409
3333
  const slashIndex = input.lastIndexOf("/");
2410
3334
  if (slashIndex === -1) {
@@ -2462,6 +3386,8 @@ function cidr(options) {
2462
3386
  }
2463
3387
  let ipVersion = null;
2464
3388
  let normalizedIp = null;
3389
+ let ipv4Error = null;
3390
+ let ipv6Error = null;
2465
3391
  if (ipv4Parser !== null) {
2466
3392
  const result = ipv4Parser.parse(ipPart);
2467
3393
  if (result.success) {
@@ -2500,7 +3426,7 @@ function cidr(options) {
2500
3426
  error: msg
2501
3427
  };
2502
3428
  }
2503
- }
3429
+ } else ipv4Error = result;
2504
3430
  }
2505
3431
  if (ipVersion === null && ipv6Parser !== null) {
2506
3432
  const result = ipv6Parser.parse(ipPart);
@@ -2540,9 +3466,120 @@ function cidr(options) {
2540
3466
  error: msg
2541
3467
  };
2542
3468
  }
2543
- }
3469
+ } else ipv6Error = result;
2544
3470
  }
2545
3471
  if (ipVersion === null || normalizedIp === null) {
3472
+ const candidates = [[
3473
+ ipv4Error,
3474
+ 4,
3475
+ 32
3476
+ ], [
3477
+ ipv6Error,
3478
+ 6,
3479
+ 128
3480
+ ]];
3481
+ for (const [err, ver, maxPfx] of candidates) if (err !== null && !err.success && err.error !== genericIpSentinel) {
3482
+ if (prefix > maxPfx) {
3483
+ const errorMsg$1 = errors?.invalidPrefix;
3484
+ if (typeof errorMsg$1 === "function") return {
3485
+ success: false,
3486
+ error: errorMsg$1(prefix, ver)
3487
+ };
3488
+ const msg$1 = errorMsg$1 ?? [
3489
+ {
3490
+ type: "text",
3491
+ text: "Expected a prefix length between 0 and "
3492
+ },
3493
+ {
3494
+ type: "text",
3495
+ text: maxPfx.toString()
3496
+ },
3497
+ {
3498
+ type: "text",
3499
+ text: ` for IPv${ver}, but got `
3500
+ },
3501
+ {
3502
+ type: "text",
3503
+ text: prefix.toString()
3504
+ },
3505
+ {
3506
+ type: "text",
3507
+ text: "."
3508
+ }
3509
+ ];
3510
+ return {
3511
+ success: false,
3512
+ error: msg$1
3513
+ };
3514
+ }
3515
+ if (minPrefix !== void 0 && prefix < minPrefix) {
3516
+ const errorMsg$1 = errors?.prefixBelowMinimum;
3517
+ if (typeof errorMsg$1 === "function") return {
3518
+ success: false,
3519
+ error: errorMsg$1(prefix, minPrefix)
3520
+ };
3521
+ const msg$1 = errorMsg$1 ?? [
3522
+ {
3523
+ type: "text",
3524
+ text: "Expected a prefix length greater than or equal to "
3525
+ },
3526
+ {
3527
+ type: "text",
3528
+ text: minPrefix.toString()
3529
+ },
3530
+ {
3531
+ type: "text",
3532
+ text: ", but got "
3533
+ },
3534
+ {
3535
+ type: "text",
3536
+ text: prefix.toString()
3537
+ },
3538
+ {
3539
+ type: "text",
3540
+ text: "."
3541
+ }
3542
+ ];
3543
+ return {
3544
+ success: false,
3545
+ error: msg$1
3546
+ };
3547
+ }
3548
+ if (maxPrefix !== void 0 && prefix > maxPrefix) {
3549
+ const errorMsg$1 = errors?.prefixAboveMaximum;
3550
+ if (typeof errorMsg$1 === "function") return {
3551
+ success: false,
3552
+ error: errorMsg$1(prefix, maxPrefix)
3553
+ };
3554
+ const msg$1 = errorMsg$1 ?? [
3555
+ {
3556
+ type: "text",
3557
+ text: "Expected a prefix length less than or equal to "
3558
+ },
3559
+ {
3560
+ type: "text",
3561
+ text: maxPrefix.toString()
3562
+ },
3563
+ {
3564
+ type: "text",
3565
+ text: ", but got "
3566
+ },
3567
+ {
3568
+ type: "text",
3569
+ text: prefix.toString()
3570
+ },
3571
+ {
3572
+ type: "text",
3573
+ text: "."
3574
+ }
3575
+ ];
3576
+ return {
3577
+ success: false,
3578
+ error: msg$1
3579
+ };
3580
+ }
3581
+ return err;
3582
+ }
2546
3583
  const errorMsg = errors?.invalidCidr;
2547
3584
  if (typeof errorMsg === "function") return {
2548
3585
  success: false,
@@ -2633,6 +3670,13 @@ function cidr(options) {
2633
3670
  error: msg
2634
3671
  };
2635
3672
  }
3673
+ if (version === "both" && ipVersion === 6 && normalizedIp !== null) {
3674
+ const mappedOctets = extractIpv4FromMapped(normalizedIp);
3675
+ if (mappedOctets !== null) {
3676
+ const restrictionError = checkIpv4MappedRestrictions(mappedOctets, normalizedIp, mappedIpv4Opts, mappedIpv4Errors);
3677
+ if (restrictionError !== null) return restrictionError;
3678
+ }
3679
+ }
2636
3680
  return {
2637
3681
  success: true,
2638
3682
  value: {
@@ -2642,13 +3686,52 @@ function cidr(options) {
2642
3686
  }
2643
3687
  };
2644
3688
  },
2645
- format() {
2646
- return metavar;
2647
- }
3689
+ format: ((_) => metavar)
2648
3690
  };
3691
+ let cidrParsing = false;
3692
+ Object.defineProperty(cidrParserObj, "format", {
3693
+ value(value) {
3694
+ if (typeof value !== "object" || value == null || !("address" in value) || !("prefix" in value) || !("version" in value)) return metavar;
3695
+ if (cidrParsing) return `${value.address}/${value.prefix}`;
3696
+ cidrParsing = true;
3697
+ try {
3698
+ const raw = `${value.address}/${value.prefix}`;
3699
+ const result = cidrParserObj.parse(raw);
3700
+ return result.success && result.value.version === value.version ? `${result.value.address}/${result.value.prefix}` : raw;
3701
+ } catch {
3702
+ return `${value.address}/${value.prefix}`;
3703
+ } finally {
3704
+ cidrParsing = false;
3705
+ }
3706
+ },
3707
+ configurable: true,
3708
+ enumerable: true
3709
+ });
3710
+ Object.defineProperty(cidrParserObj, "normalize", {
3711
+ value(v) {
3712
+ if (typeof v !== "object" || v == null || !("address" in v) || !("prefix" in v) || !("version" in v)) return v;
3713
+ if (cidrParsing) return v;
3714
+ cidrParsing = true;
3715
+ const formatted = `${v.address}/${v.prefix}`;
3716
+ try {
3717
+ const result = cidrParserObj.parse(formatted);
3718
+ if (result.success && result.value.version === v.version) return result.value;
3719
+ return v;
3720
+ } catch {
3721
+ return v;
3722
+ } finally {
3723
+ cidrParsing = false;
3724
+ }
3725
+ },
3726
+ configurable: true,
3727
+ enumerable: true
3728
+ });
3729
+ return cidrParserObj;
2649
3730
  }
2650
3731
 
2651
3732
  //#endregion
3733
+ exports.checkBooleanOption = checkBooleanOption;
3734
+ exports.checkEnumOption = checkEnumOption;
2652
3735
  exports.choice = choice;
2653
3736
  exports.cidr = cidr;
2654
3737
  exports.domain = domain;