@optique/core 1.0.0-dev.1463 → 1.0.0-dev.1470

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/doc.cjs CHANGED
@@ -1,4 +1,5 @@
1
1
  const require_message = require('./message.cjs');
2
+ const require_validate = require('./validate.cjs');
2
3
  const require_usage = require('./usage.cjs');
3
4
 
4
5
  //#region src/doc.ts
@@ -182,9 +183,10 @@ function defaultSectionOrder(a, b) {
182
183
  * @param page The documentation page to format
183
184
  * @param options Formatting options to customize the output
184
185
  * @returns A formatted string representation of the documentation page
185
- * @throws {TypeError} If `programName` contains a CR or LF character, if
186
- * any non-empty section's title is empty, whitespace-only, or contains a CR
187
- * or LF character, or if `maxWidth` is not a finite integer.
186
+ * @throws {TypeError} If `programName` is not a string, is empty,
187
+ * whitespace-only, or contains control characters, if any non-empty
188
+ * section's title is empty, whitespace-only, or contains a CR or LF
189
+ * character, or if `maxWidth` is not a finite integer.
188
190
  * @throws {RangeError} If any entry needs a description column and `maxWidth`
189
191
  * is too small to fit the minimum layout (less than `termIndent + 4`), or if
190
192
  * `showChoices.maxItems` is less than `1`.
@@ -208,7 +210,7 @@ function defaultSectionOrder(a, b) {
208
210
  * ```
209
211
  */
210
212
  function formatDocPage(programName, page, options = {}) {
211
- if (/[\r\n]/.test(programName)) throw new TypeError("Program name must not contain newlines.");
213
+ require_validate.validateProgramName(programName);
212
214
  const termIndent = options.termIndent ?? 2;
213
215
  const termWidth = options.termWidth ?? 26;
214
216
  if (options.maxWidth != null && (!Number.isFinite(options.maxWidth) || !Number.isInteger(options.maxWidth))) throw new TypeError(`maxWidth must be a finite integer, got ${options.maxWidth}.`);
package/dist/doc.d.cts CHANGED
@@ -311,9 +311,10 @@ interface DocPageFormatOptions {
311
311
  * @param page The documentation page to format
312
312
  * @param options Formatting options to customize the output
313
313
  * @returns A formatted string representation of the documentation page
314
- * @throws {TypeError} If `programName` contains a CR or LF character, if
315
- * any non-empty section's title is empty, whitespace-only, or contains a CR
316
- * or LF character, or if `maxWidth` is not a finite integer.
314
+ * @throws {TypeError} If `programName` is not a string, is empty,
315
+ * whitespace-only, or contains control characters, if any non-empty
316
+ * section's title is empty, whitespace-only, or contains a CR or LF
317
+ * character, or if `maxWidth` is not a finite integer.
317
318
  * @throws {RangeError} If any entry needs a description column and `maxWidth`
318
319
  * is too small to fit the minimum layout (less than `termIndent + 4`), or if
319
320
  * `showChoices.maxItems` is less than `1`.
package/dist/doc.d.ts CHANGED
@@ -311,9 +311,10 @@ interface DocPageFormatOptions {
311
311
  * @param page The documentation page to format
312
312
  * @param options Formatting options to customize the output
313
313
  * @returns A formatted string representation of the documentation page
314
- * @throws {TypeError} If `programName` contains a CR or LF character, if
315
- * any non-empty section's title is empty, whitespace-only, or contains a CR
316
- * or LF character, or if `maxWidth` is not a finite integer.
314
+ * @throws {TypeError} If `programName` is not a string, is empty,
315
+ * whitespace-only, or contains control characters, if any non-empty
316
+ * section's title is empty, whitespace-only, or contains a CR or LF
317
+ * character, or if `maxWidth` is not a finite integer.
317
318
  * @throws {RangeError} If any entry needs a description column and `maxWidth`
318
319
  * is too small to fit the minimum layout (less than `termIndent + 4`), or if
319
320
  * `showChoices.maxItems` is less than `1`.
package/dist/doc.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { cloneMessage, formatMessage, text } from "./message.js";
2
+ import { validateProgramName } from "./validate.js";
2
3
  import { cloneUsageTerm, formatUsage, formatUsageTerm, isDocHidden } from "./usage.js";
3
4
 
4
5
  //#region src/doc.ts
@@ -182,9 +183,10 @@ function defaultSectionOrder(a, b) {
182
183
  * @param page The documentation page to format
183
184
  * @param options Formatting options to customize the output
184
185
  * @returns A formatted string representation of the documentation page
185
- * @throws {TypeError} If `programName` contains a CR or LF character, if
186
- * any non-empty section's title is empty, whitespace-only, or contains a CR
187
- * or LF character, or if `maxWidth` is not a finite integer.
186
+ * @throws {TypeError} If `programName` is not a string, is empty,
187
+ * whitespace-only, or contains control characters, if any non-empty
188
+ * section's title is empty, whitespace-only, or contains a CR or LF
189
+ * character, or if `maxWidth` is not a finite integer.
188
190
  * @throws {RangeError} If any entry needs a description column and `maxWidth`
189
191
  * is too small to fit the minimum layout (less than `termIndent + 4`), or if
190
192
  * `showChoices.maxItems` is less than `1`.
@@ -208,7 +210,7 @@ function defaultSectionOrder(a, b) {
208
210
  * ```
209
211
  */
210
212
  function formatDocPage(programName, page, options = {}) {
211
- if (/[\r\n]/.test(programName)) throw new TypeError("Program name must not contain newlines.");
213
+ validateProgramName(programName);
212
214
  const termIndent = options.termIndent ?? 2;
213
215
  const termWidth = options.termWidth ?? 26;
214
216
  if (options.maxWidth != null && (!Number.isFinite(options.maxWidth) || !Number.isInteger(options.maxWidth))) throw new TypeError(`maxWidth must be a finite integer, got ${options.maxWidth}.`);
package/dist/facade.cjs CHANGED
@@ -2,12 +2,12 @@ const require_annotations = require('./annotations.cjs');
2
2
  const require_message = require('./message.cjs');
3
3
  const require_completion = require('./completion.cjs');
4
4
  const require_mode_dispatch = require('./mode-dispatch.cjs');
5
+ const require_validate = require('./validate.cjs');
5
6
  const require_usage = require('./usage.cjs');
6
7
  const require_doc = require('./doc.cjs');
7
8
  const require_constructs = require('./constructs.cjs');
8
9
  const require_context = require('./context.cjs');
9
10
  const require_modifiers = require('./modifiers.cjs');
10
- const require_validate = require('./validate.cjs');
11
11
  const require_valueparser = require('./valueparser.cjs');
12
12
  const require_primitives = require('./primitives.cjs');
13
13
  const require_parser = require('./parser.cjs');
package/dist/facade.js CHANGED
@@ -2,12 +2,12 @@ import { injectAnnotations } from "./annotations.js";
2
2
  import { commandLine, formatMessage, lineBreak, message, optionName, text, value } from "./message.js";
3
3
  import { bash, fish, nu, pwsh, zsh } from "./completion.js";
4
4
  import { dispatchByMode } from "./mode-dispatch.js";
5
+ import { validateCommandNames, validateOptionNames } from "./validate.js";
5
6
  import { formatUsage } from "./usage.js";
6
7
  import { formatDocPage } from "./doc.js";
7
8
  import { group, longestMatch, object } from "./constructs.js";
8
9
  import { isPlaceholderValue } from "./context.js";
9
10
  import { multiple, optional, withDefault } from "./modifiers.js";
10
- import { validateCommandNames, validateOptionNames } from "./validate.js";
11
11
  import { string } from "./valueparser.js";
12
12
  import { argument, command, constant, flag, option } from "./primitives.js";
13
13
  import { getDocPage, parseAsync, parseSync, suggest, suggestAsync } from "./parser.js";
@@ -2,10 +2,10 @@ const require_annotations = require('./annotations.cjs');
2
2
  const require_message = require('./message.cjs');
3
3
  const require_dependency = require('./dependency.cjs');
4
4
  const require_mode_dispatch = require('./mode-dispatch.cjs');
5
+ const require_validate = require('./validate.cjs');
5
6
  const require_usage = require('./usage.cjs');
6
7
  const require_suggestion = require('./suggestion.cjs');
7
8
  const require_usage_internals = require('./usage-internals.cjs');
8
- const require_validate = require('./validate.cjs');
9
9
  const require_valueparser = require('./valueparser.cjs');
10
10
 
11
11
  //#region src/primitives.ts
@@ -2,10 +2,10 @@ import { annotateFreshArray, getAnnotations } from "./annotations.js";
2
2
  import { message, metavar, optionName, optionNames, text, valueSet } from "./message.js";
3
3
  import { createDeferredParseState, createDependencySourceState, createPendingDependencySourceState, dependencyId, getDefaultValuesFunction, getDependencyIds, isDeferredParseState, isDependencySource, isDependencySourceState, isDerivedValueParser, isPendingDependencySourceState, suggestWithDependency } from "./dependency.js";
4
4
  import { dispatchIterableByMode } from "./mode-dispatch.js";
5
+ import { validateOptionNames } from "./validate.js";
5
6
  import { extractOptionNames, isDocHidden, isSuggestionHidden } from "./usage.js";
6
7
  import { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, createSuggestionMessage, findSimilar } from "./suggestion.js";
7
8
  import { extractLeadingCommandNames } from "./usage-internals.js";
8
- import { validateOptionNames } from "./validate.js";
9
9
  import { isValueParser } from "./valueparser.js";
10
10
 
11
11
  //#region src/primitives.ts
package/dist/usage.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ const require_validate = require('./validate.cjs');
1
2
 
2
3
  //#region src/usage.ts
3
4
  /**
@@ -147,8 +148,11 @@ function extractArgumentMetavars(usage) {
147
148
  * @param options Optional formatting options to customize the output.
148
149
  * See {@link UsageFormatOptions} for available options.
149
150
  * @returns A formatted string representation of the usage description.
151
+ * @throws {TypeError} If `programName` is not a string, is empty,
152
+ * whitespace-only, or contains control characters.
150
153
  */
151
154
  function formatUsage(programName, usage, options = {}) {
155
+ require_validate.validateProgramName(programName);
152
156
  usage = normalizeUsage(filterUsageForDisplay(usage));
153
157
  if (options.expandCommands) {
154
158
  const lastTerm = usage.at(-1);
package/dist/usage.d.cts CHANGED
@@ -325,6 +325,8 @@ interface UsageFormatOptions {
325
325
  * @param options Optional formatting options to customize the output.
326
326
  * See {@link UsageFormatOptions} for available options.
327
327
  * @returns A formatted string representation of the usage description.
328
+ * @throws {TypeError} If `programName` is not a string, is empty,
329
+ * whitespace-only, or contains control characters.
328
330
  */
329
331
  declare function formatUsage(programName: string, usage: Usage, options?: UsageFormatOptions): string;
330
332
  /**
package/dist/usage.d.ts CHANGED
@@ -325,6 +325,8 @@ interface UsageFormatOptions {
325
325
  * @param options Optional formatting options to customize the output.
326
326
  * See {@link UsageFormatOptions} for available options.
327
327
  * @returns A formatted string representation of the usage description.
328
+ * @throws {TypeError} If `programName` is not a string, is empty,
329
+ * whitespace-only, or contains control characters.
328
330
  */
329
331
  declare function formatUsage(programName: string, usage: Usage, options?: UsageFormatOptions): string;
330
332
  /**
package/dist/usage.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { validateProgramName } from "./validate.js";
2
+
1
3
  //#region src/usage.ts
2
4
  /**
3
5
  * Returns whether the term should be hidden from usage output.
@@ -146,8 +148,11 @@ function extractArgumentMetavars(usage) {
146
148
  * @param options Optional formatting options to customize the output.
147
149
  * See {@link UsageFormatOptions} for available options.
148
150
  * @returns A formatted string representation of the usage description.
151
+ * @throws {TypeError} If `programName` is not a string, is empty,
152
+ * whitespace-only, or contains control characters.
149
153
  */
150
154
  function formatUsage(programName, usage, options = {}) {
155
+ validateProgramName(programName);
151
156
  usage = normalizeUsage(filterUsageForDisplay(usage));
152
157
  if (options.expandCommands) {
153
158
  const lastTerm = usage.at(-1);
package/dist/validate.cjs CHANGED
@@ -1,6 +1,12 @@
1
1
 
2
2
  //#region src/validate.ts
3
3
  /**
4
+ * Matches Unicode control characters: C0 (U+0000–U+001F), DEL (U+007F),
5
+ * C1 (U+0080–U+009F), and line separators (U+2028, U+2029).
6
+ */
7
+ const CONTROL_CHAR_RE = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/;
8
+ const CONTROL_CHAR_RE_GLOBAL = new RegExp(CONTROL_CHAR_RE.source, "g");
9
+ /**
4
10
  * Escapes control characters in a string for readable error messages.
5
11
  *
6
12
  * @param value The string to escape.
@@ -8,13 +14,13 @@
8
14
  * sequences.
9
15
  */
10
16
  function escapeControlChars(value) {
11
- return value.replace(/[\x00-\x1f\x7f]/g, (ch) => {
17
+ return value.replace(CONTROL_CHAR_RE_GLOBAL, (ch) => {
12
18
  const code = ch.charCodeAt(0);
13
19
  switch (code) {
14
20
  case 9: return "\\t";
15
21
  case 10: return "\\n";
16
22
  case 13: return "\\r";
17
- default: return `\\x${code.toString(16).padStart(2, "0")}`;
23
+ default: return code > 255 ? `\\u${code.toString(16).padStart(4, "0")}` : `\\x${code.toString(16).padStart(2, "0")}`;
18
24
  }
19
25
  });
20
26
  }
@@ -32,7 +38,7 @@ function validateOptionNames(names, label) {
32
38
  for (const name of names) {
33
39
  if (name === "") throw new TypeError(`${label} name must not be empty.`);
34
40
  if (/^\s+$/.test(name)) throw new TypeError(`${label} name must not be whitespace-only: "${escapeControlChars(name)}".`);
35
- if (/[\x00-\x1f\x7f]/.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
41
+ if (CONTROL_CHAR_RE.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
36
42
  if (/\s/.test(name)) throw new TypeError(`${label} name must not contain whitespace: "${escapeControlChars(name)}".`);
37
43
  if (!/^(--|[-/+])/.test(name)) throw new TypeError(`${label} name must start with "--", "-", "/", or "+": "${name}".`);
38
44
  if (name === "--") throw new TypeError(`${label} name must not be the options terminator "--".`);
@@ -52,11 +58,28 @@ function validateCommandNames(names, label) {
52
58
  for (const name of names) {
53
59
  if (name === "") throw new TypeError(`${label} name must not be empty.`);
54
60
  if (/^\s+$/.test(name)) throw new TypeError(`${label} name must not be whitespace-only: "${escapeControlChars(name)}".`);
55
- if (/[\x00-\x1f\x7f]/.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
61
+ if (CONTROL_CHAR_RE.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
56
62
  if (/\s/.test(name)) throw new TypeError(`${label} name must not contain whitespace: "${escapeControlChars(name)}".`);
57
63
  }
58
64
  }
65
+ /**
66
+ * Validates a program name at runtime.
67
+ *
68
+ * Program names may contain spaces (e.g., file paths), but must not be empty,
69
+ * whitespace-only, or contain control characters.
70
+ *
71
+ * @param programName The program name to validate.
72
+ * @throws {TypeError} If the value is not a string, is empty,
73
+ * whitespace-only, or contains control characters.
74
+ */
75
+ function validateProgramName(programName) {
76
+ if (typeof programName !== "string") throw new TypeError("Program name must be a string.");
77
+ if (programName === "") throw new TypeError("Program name must not be empty.");
78
+ if (/^\s+$/.test(programName)) throw new TypeError(`Program name must not be whitespace-only: "${escapeControlChars(programName)}".`);
79
+ if (CONTROL_CHAR_RE.test(programName)) throw new TypeError(`Program name must not contain control characters: "${escapeControlChars(programName)}".`);
80
+ }
59
81
 
60
82
  //#endregion
61
83
  exports.validateCommandNames = validateCommandNames;
62
- exports.validateOptionNames = validateOptionNames;
84
+ exports.validateOptionNames = validateOptionNames;
85
+ exports.validateProgramName = validateProgramName;
package/dist/validate.js CHANGED
@@ -1,5 +1,11 @@
1
1
  //#region src/validate.ts
2
2
  /**
3
+ * Matches Unicode control characters: C0 (U+0000–U+001F), DEL (U+007F),
4
+ * C1 (U+0080–U+009F), and line separators (U+2028, U+2029).
5
+ */
6
+ const CONTROL_CHAR_RE = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/;
7
+ const CONTROL_CHAR_RE_GLOBAL = new RegExp(CONTROL_CHAR_RE.source, "g");
8
+ /**
3
9
  * Escapes control characters in a string for readable error messages.
4
10
  *
5
11
  * @param value The string to escape.
@@ -7,13 +13,13 @@
7
13
  * sequences.
8
14
  */
9
15
  function escapeControlChars(value) {
10
- return value.replace(/[\x00-\x1f\x7f]/g, (ch) => {
16
+ return value.replace(CONTROL_CHAR_RE_GLOBAL, (ch) => {
11
17
  const code = ch.charCodeAt(0);
12
18
  switch (code) {
13
19
  case 9: return "\\t";
14
20
  case 10: return "\\n";
15
21
  case 13: return "\\r";
16
- default: return `\\x${code.toString(16).padStart(2, "0")}`;
22
+ default: return code > 255 ? `\\u${code.toString(16).padStart(4, "0")}` : `\\x${code.toString(16).padStart(2, "0")}`;
17
23
  }
18
24
  });
19
25
  }
@@ -31,7 +37,7 @@ function validateOptionNames(names, label) {
31
37
  for (const name of names) {
32
38
  if (name === "") throw new TypeError(`${label} name must not be empty.`);
33
39
  if (/^\s+$/.test(name)) throw new TypeError(`${label} name must not be whitespace-only: "${escapeControlChars(name)}".`);
34
- if (/[\x00-\x1f\x7f]/.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
40
+ if (CONTROL_CHAR_RE.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
35
41
  if (/\s/.test(name)) throw new TypeError(`${label} name must not contain whitespace: "${escapeControlChars(name)}".`);
36
42
  if (!/^(--|[-/+])/.test(name)) throw new TypeError(`${label} name must start with "--", "-", "/", or "+": "${name}".`);
37
43
  if (name === "--") throw new TypeError(`${label} name must not be the options terminator "--".`);
@@ -51,10 +57,26 @@ function validateCommandNames(names, label) {
51
57
  for (const name of names) {
52
58
  if (name === "") throw new TypeError(`${label} name must not be empty.`);
53
59
  if (/^\s+$/.test(name)) throw new TypeError(`${label} name must not be whitespace-only: "${escapeControlChars(name)}".`);
54
- if (/[\x00-\x1f\x7f]/.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
60
+ if (CONTROL_CHAR_RE.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
55
61
  if (/\s/.test(name)) throw new TypeError(`${label} name must not contain whitespace: "${escapeControlChars(name)}".`);
56
62
  }
57
63
  }
64
+ /**
65
+ * Validates a program name at runtime.
66
+ *
67
+ * Program names may contain spaces (e.g., file paths), but must not be empty,
68
+ * whitespace-only, or contain control characters.
69
+ *
70
+ * @param programName The program name to validate.
71
+ * @throws {TypeError} If the value is not a string, is empty,
72
+ * whitespace-only, or contains control characters.
73
+ */
74
+ function validateProgramName(programName) {
75
+ if (typeof programName !== "string") throw new TypeError("Program name must be a string.");
76
+ if (programName === "") throw new TypeError("Program name must not be empty.");
77
+ if (/^\s+$/.test(programName)) throw new TypeError(`Program name must not be whitespace-only: "${escapeControlChars(programName)}".`);
78
+ if (CONTROL_CHAR_RE.test(programName)) throw new TypeError(`Program name must not contain control characters: "${escapeControlChars(programName)}".`);
79
+ }
58
80
 
59
81
  //#endregion
60
- export { validateCommandNames, validateOptionNames };
82
+ export { validateCommandNames, validateOptionNames, validateProgramName };
@@ -1925,10 +1925,14 @@ function portRange(options) {
1925
1925
  * Creates a value parser for MAC (Media Access Control) addresses.
1926
1926
  *
1927
1927
  * Validates MAC-48 addresses (6 octets, 12 hex digits) in various formats:
1928
- * - Colon-separated: `00:1A:2B:3C:4D:5E`
1929
- * - Hyphen-separated: `00-1A-2B-3C-4D-5E`
1930
- * - Dot-separated (Cisco): `001A.2B3C.4D5E`
1931
- * - No separator: `001A2B3C4D5E`
1928
+ * - Colon-separated: `00:1A:2B:3C:4D:5E` (1–2 hex digits per octet)
1929
+ * - Hyphen-separated: `00-1A-2B-3C-4D-5E` (1–2 hex digits per octet)
1930
+ * - Dot-separated (Cisco): `001A.2B3C.4D5E` (exactly 4 hex digits per group)
1931
+ * - No separator: `001A2B3C4D5E` (exactly 12 hex digits)
1932
+ *
1933
+ * Colon-separated and hyphen-separated formats accept single-digit octets
1934
+ * (e.g., `0:1:2:3:4:5`), which are automatically zero-padded to canonical
1935
+ * two-digit form (e.g., `00:01:02:03:04:05`).
1932
1936
  *
1933
1937
  * Returns the MAC address as a formatted string according to `case` and
1934
1938
  * `outputSeparator` options.
@@ -1974,8 +1978,8 @@ function macAddress(options) {
1974
1978
  const caseOption = options?.case ?? "preserve";
1975
1979
  const outputSeparator = options?.outputSeparator;
1976
1980
  const metavar = options?.metavar ?? "MAC";
1977
- const colonRegex = /^([0-9a-fA-F]{2}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2})$/;
1978
- const hyphenRegex = /^([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})$/;
1981
+ const colonRegex = /^([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})$/;
1982
+ 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})$/;
1979
1983
  const dotRegex = /^([0-9a-fA-F]{4})\.([0-9a-fA-F]{4})\.([0-9a-fA-F]{4})$/;
1980
1984
  const noneRegex = /^([0-9a-fA-F]{12})$/;
1981
1985
  return {
@@ -2040,6 +2044,7 @@ function macAddress(options) {
2040
2044
  error: msg
2041
2045
  };
2042
2046
  }
2047
+ octets = octets.map((o) => o.padStart(2, "0"));
2043
2048
  let formattedOctets = octets;
2044
2049
  if (caseOption === "upper") formattedOctets = octets.map((octet) => octet.toUpperCase());
2045
2050
  else if (caseOption === "lower") formattedOctets = octets.map((octet) => octet.toLowerCase());
@@ -1507,10 +1507,14 @@ interface MacAddressOptions {
1507
1507
  * Creates a value parser for MAC (Media Access Control) addresses.
1508
1508
  *
1509
1509
  * Validates MAC-48 addresses (6 octets, 12 hex digits) in various formats:
1510
- * - Colon-separated: `00:1A:2B:3C:4D:5E`
1511
- * - Hyphen-separated: `00-1A-2B-3C-4D-5E`
1512
- * - Dot-separated (Cisco): `001A.2B3C.4D5E`
1513
- * - No separator: `001A2B3C4D5E`
1510
+ * - Colon-separated: `00:1A:2B:3C:4D:5E` (1–2 hex digits per octet)
1511
+ * - Hyphen-separated: `00-1A-2B-3C-4D-5E` (1–2 hex digits per octet)
1512
+ * - Dot-separated (Cisco): `001A.2B3C.4D5E` (exactly 4 hex digits per group)
1513
+ * - No separator: `001A2B3C4D5E` (exactly 12 hex digits)
1514
+ *
1515
+ * Colon-separated and hyphen-separated formats accept single-digit octets
1516
+ * (e.g., `0:1:2:3:4:5`), which are automatically zero-padded to canonical
1517
+ * two-digit form (e.g., `00:01:02:03:04:05`).
1514
1518
  *
1515
1519
  * Returns the MAC address as a formatted string according to `case` and
1516
1520
  * `outputSeparator` options.
@@ -1507,10 +1507,14 @@ interface MacAddressOptions {
1507
1507
  * Creates a value parser for MAC (Media Access Control) addresses.
1508
1508
  *
1509
1509
  * Validates MAC-48 addresses (6 octets, 12 hex digits) in various formats:
1510
- * - Colon-separated: `00:1A:2B:3C:4D:5E`
1511
- * - Hyphen-separated: `00-1A-2B-3C-4D-5E`
1512
- * - Dot-separated (Cisco): `001A.2B3C.4D5E`
1513
- * - No separator: `001A2B3C4D5E`
1510
+ * - Colon-separated: `00:1A:2B:3C:4D:5E` (1–2 hex digits per octet)
1511
+ * - Hyphen-separated: `00-1A-2B-3C-4D-5E` (1–2 hex digits per octet)
1512
+ * - Dot-separated (Cisco): `001A.2B3C.4D5E` (exactly 4 hex digits per group)
1513
+ * - No separator: `001A2B3C4D5E` (exactly 12 hex digits)
1514
+ *
1515
+ * Colon-separated and hyphen-separated formats accept single-digit octets
1516
+ * (e.g., `0:1:2:3:4:5`), which are automatically zero-padded to canonical
1517
+ * two-digit form (e.g., `00:01:02:03:04:05`).
1514
1518
  *
1515
1519
  * Returns the MAC address as a formatted string according to `case` and
1516
1520
  * `outputSeparator` options.
@@ -1925,10 +1925,14 @@ function portRange(options) {
1925
1925
  * Creates a value parser for MAC (Media Access Control) addresses.
1926
1926
  *
1927
1927
  * Validates MAC-48 addresses (6 octets, 12 hex digits) in various formats:
1928
- * - Colon-separated: `00:1A:2B:3C:4D:5E`
1929
- * - Hyphen-separated: `00-1A-2B-3C-4D-5E`
1930
- * - Dot-separated (Cisco): `001A.2B3C.4D5E`
1931
- * - No separator: `001A2B3C4D5E`
1928
+ * - Colon-separated: `00:1A:2B:3C:4D:5E` (1–2 hex digits per octet)
1929
+ * - Hyphen-separated: `00-1A-2B-3C-4D-5E` (1–2 hex digits per octet)
1930
+ * - Dot-separated (Cisco): `001A.2B3C.4D5E` (exactly 4 hex digits per group)
1931
+ * - No separator: `001A2B3C4D5E` (exactly 12 hex digits)
1932
+ *
1933
+ * Colon-separated and hyphen-separated formats accept single-digit octets
1934
+ * (e.g., `0:1:2:3:4:5`), which are automatically zero-padded to canonical
1935
+ * two-digit form (e.g., `00:01:02:03:04:05`).
1932
1936
  *
1933
1937
  * Returns the MAC address as a formatted string according to `case` and
1934
1938
  * `outputSeparator` options.
@@ -1974,8 +1978,8 @@ function macAddress(options) {
1974
1978
  const caseOption = options?.case ?? "preserve";
1975
1979
  const outputSeparator = options?.outputSeparator;
1976
1980
  const metavar = options?.metavar ?? "MAC";
1977
- const colonRegex = /^([0-9a-fA-F]{2}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2})$/;
1978
- const hyphenRegex = /^([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})$/;
1981
+ const colonRegex = /^([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})$/;
1982
+ 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})$/;
1979
1983
  const dotRegex = /^([0-9a-fA-F]{4})\.([0-9a-fA-F]{4})\.([0-9a-fA-F]{4})$/;
1980
1984
  const noneRegex = /^([0-9a-fA-F]{12})$/;
1981
1985
  return {
@@ -2040,6 +2044,7 @@ function macAddress(options) {
2040
2044
  error: msg
2041
2045
  };
2042
2046
  }
2047
+ octets = octets.map((o) => o.padStart(2, "0"));
2043
2048
  let formattedOctets = octets;
2044
2049
  if (caseOption === "upper") formattedOctets = octets.map((octet) => octet.toUpperCase());
2045
2050
  else if (caseOption === "lower") formattedOctets = octets.map((octet) => octet.toLowerCase());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1463+507712c0",
3
+ "version": "1.0.0-dev.1470+e7499369",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",