@optique/core 1.0.0-dev.1136 → 1.0.0-dev.1155

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.
@@ -40,6 +40,9 @@ function validateProgramName(programName) {
40
40
  function encodePattern(pattern) {
41
41
  return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
42
42
  }
43
+ function encodeExtensions(extensions) {
44
+ return extensions?.map((ext) => ext.replace(/^\./, "")).join(",") ?? "";
45
+ }
43
46
  /**
44
47
  * Replaces control characters that would corrupt shell completion protocols.
45
48
  * Shell completion formats use tabs as field delimiters and newlines as record
@@ -211,7 +214,7 @@ complete -F _${programName} -- ${programName}
211
214
  if (i > 0) yield "\n";
212
215
  if (suggestion.kind === "literal") yield `${suggestion.text}`;
213
216
  else {
214
- const extensions = suggestion.extensions?.join(",") || "";
217
+ const extensions = encodeExtensions(suggestion.extensions);
215
218
  const hidden = suggestion.includeHidden ? "1" : "0";
216
219
  const pattern = encodePattern(suggestion.pattern ?? "");
217
220
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}`;
@@ -336,7 +339,7 @@ compdef _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
336
339
  const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
337
340
  yield `${suggestion.text}\0${description}\0`;
338
341
  } else {
339
- const extensions = suggestion.extensions?.join(",") || "";
342
+ const extensions = encodeExtensions(suggestion.extensions);
340
343
  const hidden = suggestion.includeHidden ? "1" : "0";
341
344
  const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
342
345
  const pattern = encodePattern(suggestion.pattern ?? "");
@@ -508,7 +511,7 @@ complete -c ${programName} -f -a '(${functionName})'
508
511
  const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
509
512
  yield `${suggestion.text}\t${description}`;
510
513
  } else {
511
- const extensions = suggestion.extensions?.join(",") || "";
514
+ const extensions = encodeExtensions(suggestion.extensions);
512
515
  const hidden = suggestion.includeHidden ? "1" : "0";
513
516
  const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
514
517
  const pattern = encodePattern(suggestion.pattern ?? "");
@@ -756,7 +759,7 @@ ${functionName}-external
756
759
  const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
757
760
  yield `${suggestion.text}\t${description}`;
758
761
  } else {
759
- const extensions = suggestion.extensions?.join(",") || "";
762
+ const extensions = encodeExtensions(suggestion.extensions);
760
763
  const hidden = suggestion.includeHidden ? "1" : "0";
761
764
  const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
762
765
  const pattern = encodePattern(suggestion.pattern ?? "");
@@ -926,7 +929,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
926
929
  const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
927
930
  yield `${suggestion.text}\t${suggestion.text}\t${description}`;
928
931
  } else {
929
- const extensions = suggestion.extensions?.join(",") || "";
932
+ const extensions = encodeExtensions(suggestion.extensions);
930
933
  const hidden = suggestion.includeHidden ? "1" : "0";
931
934
  const description = suggestion.description == null ? "" : sanitizeDescription(require_message.formatMessage(suggestion.description, { colors: false }));
932
935
  const pattern = encodePattern(suggestion.pattern ?? "");
@@ -40,6 +40,9 @@ function validateProgramName(programName) {
40
40
  function encodePattern(pattern) {
41
41
  return pattern.replace(/%/g, "%25").replace(/:/g, "%3A");
42
42
  }
43
+ function encodeExtensions(extensions) {
44
+ return extensions?.map((ext) => ext.replace(/^\./, "")).join(",") ?? "";
45
+ }
43
46
  /**
44
47
  * Replaces control characters that would corrupt shell completion protocols.
45
48
  * Shell completion formats use tabs as field delimiters and newlines as record
@@ -211,7 +214,7 @@ complete -F _${programName} -- ${programName}
211
214
  if (i > 0) yield "\n";
212
215
  if (suggestion.kind === "literal") yield `${suggestion.text}`;
213
216
  else {
214
- const extensions = suggestion.extensions?.join(",") || "";
217
+ const extensions = encodeExtensions(suggestion.extensions);
215
218
  const hidden = suggestion.includeHidden ? "1" : "0";
216
219
  const pattern = encodePattern(suggestion.pattern ?? "");
217
220
  yield `__FILE__:${suggestion.type}:${extensions}:${pattern}:${hidden}`;
@@ -336,7 +339,7 @@ compdef _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
336
339
  const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
337
340
  yield `${suggestion.text}\0${description}\0`;
338
341
  } else {
339
- const extensions = suggestion.extensions?.join(",") || "";
342
+ const extensions = encodeExtensions(suggestion.extensions);
340
343
  const hidden = suggestion.includeHidden ? "1" : "0";
341
344
  const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
342
345
  const pattern = encodePattern(suggestion.pattern ?? "");
@@ -508,7 +511,7 @@ complete -c ${programName} -f -a '(${functionName})'
508
511
  const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
509
512
  yield `${suggestion.text}\t${description}`;
510
513
  } else {
511
- const extensions = suggestion.extensions?.join(",") || "";
514
+ const extensions = encodeExtensions(suggestion.extensions);
512
515
  const hidden = suggestion.includeHidden ? "1" : "0";
513
516
  const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
514
517
  const pattern = encodePattern(suggestion.pattern ?? "");
@@ -756,7 +759,7 @@ ${functionName}-external
756
759
  const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
757
760
  yield `${suggestion.text}\t${description}`;
758
761
  } else {
759
- const extensions = suggestion.extensions?.join(",") || "";
762
+ const extensions = encodeExtensions(suggestion.extensions);
760
763
  const hidden = suggestion.includeHidden ? "1" : "0";
761
764
  const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
762
765
  const pattern = encodePattern(suggestion.pattern ?? "");
@@ -926,7 +929,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
926
929
  const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
927
930
  yield `${suggestion.text}\t${suggestion.text}\t${description}`;
928
931
  } else {
929
- const extensions = suggestion.extensions?.join(",") || "";
932
+ const extensions = encodeExtensions(suggestion.extensions);
930
933
  const hidden = suggestion.includeHidden ? "1" : "0";
931
934
  const description = suggestion.description == null ? "" : sanitizeDescription(formatMessage(suggestion.description, { colors: false }));
932
935
  const pattern = encodePattern(suggestion.pattern ?? "");
package/dist/facade.cjs CHANGED
@@ -727,6 +727,61 @@ function validateVersionValue(value$1) {
727
727
  if (/[\x00-\x1f\x7f]/.test(value$1)) throw new TypeError("Version value must not contain control characters.");
728
728
  return value$1;
729
729
  }
730
+ /**
731
+ * Escapes control characters in a string for display in error messages.
732
+ *
733
+ * @param value The string to escape.
734
+ * @returns The escaped string with control characters replaced by escape
735
+ * sequences.
736
+ */
737
+ function escapeControlChars(value$1) {
738
+ return value$1.replace(/[\x00-\x1f\x7f]/g, (ch) => {
739
+ const code = ch.charCodeAt(0);
740
+ switch (code) {
741
+ case 9: return "\\t";
742
+ case 10: return "\\n";
743
+ case 13: return "\\r";
744
+ default: return `\\x${code.toString(16).padStart(2, "0")}`;
745
+ }
746
+ });
747
+ }
748
+ /**
749
+ * Validates meta option names at runtime.
750
+ *
751
+ * @param names The option names to validate.
752
+ * @param label A human-readable label for error messages (e.g.,
753
+ * `"Help option"`).
754
+ * @throws {TypeError} If the names array is empty, or any name is empty,
755
+ * lacks a valid prefix, or contains whitespace or control characters.
756
+ */
757
+ function validateOptionNames(names, label) {
758
+ if (names.length === 0) throw new TypeError(`Expected at least one ${label.toLowerCase()} name.`);
759
+ for (const name of names) {
760
+ if (name === "") throw new TypeError(`${label} name must not be empty.`);
761
+ if (/^\s+$/.test(name)) throw new TypeError(`${label} name must not be whitespace-only: "${escapeControlChars(name)}".`);
762
+ if (/[\x00-\x1f\x7f]/.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
763
+ if (/\s/.test(name)) throw new TypeError(`${label} name must not contain whitespace: "${escapeControlChars(name)}".`);
764
+ if (!/^(--|[-/+])/.test(name)) throw new TypeError(`${label} name must start with "--", "-", "/", or "+": "${name}".`);
765
+ }
766
+ }
767
+ /**
768
+ * Validates meta command names at runtime.
769
+ *
770
+ * @param names The command names to validate.
771
+ * @param label A human-readable label for error messages (e.g.,
772
+ * `"Help command"`).
773
+ * @throws {TypeError} If the names array is empty, or any name is empty,
774
+ * whitespace-only, or contains whitespace or control characters.
775
+ */
776
+ function validateCommandNames(names, label) {
777
+ if (names.length === 0) throw new TypeError(`Expected at least one ${label.toLowerCase()} name.`);
778
+ for (const name of names) {
779
+ if (name === "") throw new TypeError(`${label} name must not be empty.`);
780
+ if (/^\s+$/.test(name)) throw new TypeError(`${label} name must not be whitespace-only: "${escapeControlChars(name)}".`);
781
+ if (/[\x00-\x1f\x7f]/.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
782
+ if (/\s/.test(name)) throw new TypeError(`${label} name must not contain whitespace: "${escapeControlChars(name)}".`);
783
+ }
784
+ }
730
785
  function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsParam) {
731
786
  const isProgram = typeof programNameOrArgs !== "string";
732
787
  let parser;
@@ -770,6 +825,12 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
770
825
  const onCompletion = options.completion?.onShow ?? (() => ({}));
771
826
  const onCompletionResult = (code) => onCompletion(code);
772
827
  const onErrorResult = (code) => onError(code);
828
+ if (helpOptionConfig?.names) validateOptionNames(helpOptionConfig.names, "Help option");
829
+ if (helpCommandConfig?.names) validateCommandNames(helpCommandConfig.names, "Help command");
830
+ if (versionOptionConfig?.names) validateOptionNames(versionOptionConfig.names, "Version option");
831
+ if (versionCommandConfig?.names) validateCommandNames(versionCommandConfig.names, "Version command");
832
+ if (completionOptionConfig?.names) validateOptionNames(completionOptionConfig.names, "Completion option");
833
+ if (completionCommandConfig?.names) validateCommandNames(completionCommandConfig.names, "Completion command");
773
834
  const helpOptionNames = helpOptionConfig?.names ?? ["--help"];
774
835
  const helpCommandNames = helpCommandConfig?.names ?? ["help"];
775
836
  const versionOptionNames = versionOptionConfig?.names ?? ["--version"];
package/dist/facade.d.cts CHANGED
@@ -273,7 +273,10 @@ interface RunOptions<THelp, TError> {
273
273
  * @returns The parsed result value, or the return value of `onHelp`/`onError`
274
274
  * callbacks.
275
275
  * @throws {TypeError} If `options.version.value` is not a non-empty string
276
- * without ASCII control characters.
276
+ * without ASCII control characters, or if any meta command/option
277
+ * name is empty, whitespace-only, contains whitespace or control
278
+ * characters, or (for option names) lacks a valid prefix (`--`,
279
+ * `-`, `/`, or `+`).
277
280
  * @throws {RunParserError} When parsing fails and no `onError` callback is
278
281
  * provided.
279
282
  * @since 0.10.0 Added support for {@link Program} objects.
package/dist/facade.d.ts CHANGED
@@ -273,7 +273,10 @@ interface RunOptions<THelp, TError> {
273
273
  * @returns The parsed result value, or the return value of `onHelp`/`onError`
274
274
  * callbacks.
275
275
  * @throws {TypeError} If `options.version.value` is not a non-empty string
276
- * without ASCII control characters.
276
+ * without ASCII control characters, or if any meta command/option
277
+ * name is empty, whitespace-only, contains whitespace or control
278
+ * characters, or (for option names) lacks a valid prefix (`--`,
279
+ * `-`, `/`, or `+`).
277
280
  * @throws {RunParserError} When parsing fails and no `onError` callback is
278
281
  * provided.
279
282
  * @since 0.10.0 Added support for {@link Program} objects.
package/dist/facade.js CHANGED
@@ -727,6 +727,61 @@ function validateVersionValue(value$1) {
727
727
  if (/[\x00-\x1f\x7f]/.test(value$1)) throw new TypeError("Version value must not contain control characters.");
728
728
  return value$1;
729
729
  }
730
+ /**
731
+ * Escapes control characters in a string for display in error messages.
732
+ *
733
+ * @param value The string to escape.
734
+ * @returns The escaped string with control characters replaced by escape
735
+ * sequences.
736
+ */
737
+ function escapeControlChars(value$1) {
738
+ return value$1.replace(/[\x00-\x1f\x7f]/g, (ch) => {
739
+ const code = ch.charCodeAt(0);
740
+ switch (code) {
741
+ case 9: return "\\t";
742
+ case 10: return "\\n";
743
+ case 13: return "\\r";
744
+ default: return `\\x${code.toString(16).padStart(2, "0")}`;
745
+ }
746
+ });
747
+ }
748
+ /**
749
+ * Validates meta option names at runtime.
750
+ *
751
+ * @param names The option names to validate.
752
+ * @param label A human-readable label for error messages (e.g.,
753
+ * `"Help option"`).
754
+ * @throws {TypeError} If the names array is empty, or any name is empty,
755
+ * lacks a valid prefix, or contains whitespace or control characters.
756
+ */
757
+ function validateOptionNames(names, label) {
758
+ if (names.length === 0) throw new TypeError(`Expected at least one ${label.toLowerCase()} name.`);
759
+ for (const name of names) {
760
+ if (name === "") throw new TypeError(`${label} name must not be empty.`);
761
+ if (/^\s+$/.test(name)) throw new TypeError(`${label} name must not be whitespace-only: "${escapeControlChars(name)}".`);
762
+ if (/[\x00-\x1f\x7f]/.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
763
+ if (/\s/.test(name)) throw new TypeError(`${label} name must not contain whitespace: "${escapeControlChars(name)}".`);
764
+ if (!/^(--|[-/+])/.test(name)) throw new TypeError(`${label} name must start with "--", "-", "/", or "+": "${name}".`);
765
+ }
766
+ }
767
+ /**
768
+ * Validates meta command names at runtime.
769
+ *
770
+ * @param names The command names to validate.
771
+ * @param label A human-readable label for error messages (e.g.,
772
+ * `"Help command"`).
773
+ * @throws {TypeError} If the names array is empty, or any name is empty,
774
+ * whitespace-only, or contains whitespace or control characters.
775
+ */
776
+ function validateCommandNames(names, label) {
777
+ if (names.length === 0) throw new TypeError(`Expected at least one ${label.toLowerCase()} name.`);
778
+ for (const name of names) {
779
+ if (name === "") throw new TypeError(`${label} name must not be empty.`);
780
+ if (/^\s+$/.test(name)) throw new TypeError(`${label} name must not be whitespace-only: "${escapeControlChars(name)}".`);
781
+ if (/[\x00-\x1f\x7f]/.test(name)) throw new TypeError(`${label} name must not contain control characters: "${escapeControlChars(name)}".`);
782
+ if (/\s/.test(name)) throw new TypeError(`${label} name must not contain whitespace: "${escapeControlChars(name)}".`);
783
+ }
784
+ }
730
785
  function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsParam) {
731
786
  const isProgram = typeof programNameOrArgs !== "string";
732
787
  let parser;
@@ -770,6 +825,12 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
770
825
  const onCompletion = options.completion?.onShow ?? (() => ({}));
771
826
  const onCompletionResult = (code) => onCompletion(code);
772
827
  const onErrorResult = (code) => onError(code);
828
+ if (helpOptionConfig?.names) validateOptionNames(helpOptionConfig.names, "Help option");
829
+ if (helpCommandConfig?.names) validateCommandNames(helpCommandConfig.names, "Help command");
830
+ if (versionOptionConfig?.names) validateOptionNames(versionOptionConfig.names, "Version option");
831
+ if (versionCommandConfig?.names) validateCommandNames(versionCommandConfig.names, "Version command");
832
+ if (completionOptionConfig?.names) validateOptionNames(completionOptionConfig.names, "Completion option");
833
+ if (completionCommandConfig?.names) validateCommandNames(completionCommandConfig.names, "Completion command");
773
834
  const helpOptionNames = helpOptionConfig?.names ?? ["--help"];
774
835
  const helpCommandNames = helpCommandConfig?.names ?? ["help"];
775
836
  const versionOptionNames = versionOptionConfig?.names ?? ["--version"];
@@ -453,10 +453,28 @@ function float(options = {}) {
453
453
  * object.
454
454
  * @param options Configuration options for the URL parser.
455
455
  * @returns A {@link ValueParser} that converts string input to `URL` objects.
456
+ * @throws {TypeError} If any `allowedProtocols` entry is not a valid protocol
457
+ * string ending with a colon (e.g., `"https:"`).
456
458
  */
457
459
  function url(options = {}) {
458
- const originalProtocols = options.allowedProtocols != null ? Object.freeze([...options.allowedProtocols]) : void 0;
459
- const allowedProtocols = options.allowedProtocols != null ? Object.freeze(options.allowedProtocols.map((p) => p.toLowerCase())) : void 0;
460
+ const originalProtocolsList = [];
461
+ const normalizedProtocolsList = [];
462
+ if (options.allowedProtocols != null) {
463
+ const seen = /* @__PURE__ */ new Set();
464
+ for (const protocol of options.allowedProtocols) {
465
+ if (typeof protocol !== "string" || !/^[a-z][a-z0-9+\-.]*:$/i.test(protocol)) {
466
+ const rendered = typeof protocol === "string" ? JSON.stringify(protocol) : String(protocol);
467
+ throw new TypeError(`Each allowed protocol must be a valid protocol ending with a colon (e.g., "https:"), got: ${rendered}.`);
468
+ }
469
+ const normalized = protocol.toLowerCase();
470
+ if (seen.has(normalized)) continue;
471
+ seen.add(normalized);
472
+ originalProtocolsList.push(protocol);
473
+ normalizedProtocolsList.push(normalized);
474
+ }
475
+ }
476
+ const originalProtocols = options.allowedProtocols != null ? Object.freeze(originalProtocolsList) : void 0;
477
+ const allowedProtocols = options.allowedProtocols != null ? Object.freeze(normalizedProtocolsList) : void 0;
460
478
  const metavar = options.metavar ?? "URL";
461
479
  require_nonempty.ensureNonEmptyString(metavar);
462
480
  const invalidUrl = options.errors?.invalidUrl;
@@ -491,6 +491,8 @@ interface UrlOptions {
491
491
  * object.
492
492
  * @param options Configuration options for the URL parser.
493
493
  * @returns A {@link ValueParser} that converts string input to `URL` objects.
494
+ * @throws {TypeError} If any `allowedProtocols` entry is not a valid protocol
495
+ * string ending with a colon (e.g., `"https:"`).
494
496
  */
495
497
  declare function url(options?: UrlOptions): ValueParser<"sync", URL>;
496
498
  /**
@@ -491,6 +491,8 @@ interface UrlOptions {
491
491
  * object.
492
492
  * @param options Configuration options for the URL parser.
493
493
  * @returns A {@link ValueParser} that converts string input to `URL` objects.
494
+ * @throws {TypeError} If any `allowedProtocols` entry is not a valid protocol
495
+ * string ending with a colon (e.g., `"https:"`).
494
496
  */
495
497
  declare function url(options?: UrlOptions): ValueParser<"sync", URL>;
496
498
  /**
@@ -453,10 +453,28 @@ function float(options = {}) {
453
453
  * object.
454
454
  * @param options Configuration options for the URL parser.
455
455
  * @returns A {@link ValueParser} that converts string input to `URL` objects.
456
+ * @throws {TypeError} If any `allowedProtocols` entry is not a valid protocol
457
+ * string ending with a colon (e.g., `"https:"`).
456
458
  */
457
459
  function url(options = {}) {
458
- const originalProtocols = options.allowedProtocols != null ? Object.freeze([...options.allowedProtocols]) : void 0;
459
- const allowedProtocols = options.allowedProtocols != null ? Object.freeze(options.allowedProtocols.map((p) => p.toLowerCase())) : void 0;
460
+ const originalProtocolsList = [];
461
+ const normalizedProtocolsList = [];
462
+ if (options.allowedProtocols != null) {
463
+ const seen = /* @__PURE__ */ new Set();
464
+ for (const protocol of options.allowedProtocols) {
465
+ if (typeof protocol !== "string" || !/^[a-z][a-z0-9+\-.]*:$/i.test(protocol)) {
466
+ const rendered = typeof protocol === "string" ? JSON.stringify(protocol) : String(protocol);
467
+ throw new TypeError(`Each allowed protocol must be a valid protocol ending with a colon (e.g., "https:"), got: ${rendered}.`);
468
+ }
469
+ const normalized = protocol.toLowerCase();
470
+ if (seen.has(normalized)) continue;
471
+ seen.add(normalized);
472
+ originalProtocolsList.push(protocol);
473
+ normalizedProtocolsList.push(normalized);
474
+ }
475
+ }
476
+ const originalProtocols = options.allowedProtocols != null ? Object.freeze(originalProtocolsList) : void 0;
477
+ const allowedProtocols = options.allowedProtocols != null ? Object.freeze(normalizedProtocolsList) : void 0;
460
478
  const metavar = options.metavar ?? "URL";
461
479
  ensureNonEmptyString(metavar);
462
480
  const invalidUrl = options.errors?.invalidUrl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1136+00dc9aa5",
3
+ "version": "1.0.0-dev.1155+7532f28d",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",