@optique/core 1.4.0-dev.2670 → 1.4.0-dev.2671

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.
@@ -161,15 +161,24 @@ function choice(choices, options = {}) {
161
161
  const stringChoices = Object.freeze([...new Set(choices)]);
162
162
  const stringOptions = options;
163
163
  checkBooleanOption(stringOptions, "caseInsensitive");
164
+ const customKey = stringOptions.key;
165
+ if (customKey !== void 0 && typeof customKey !== "function") throw new TypeError("Expected key to be a function.");
166
+ if (customKey !== void 0 && stringOptions.caseInsensitive !== void 0) throw new TypeError("Cannot use key with caseInsensitive.");
164
167
  const caseInsensitive = stringOptions.caseInsensitive ?? false;
165
- const normalizedValues = caseInsensitive ? stringChoices.map((v) => v.toLowerCase()) : stringChoices;
166
- if (caseInsensitive) {
168
+ const matchingKey = customKey ?? (caseInsensitive ? (text$1) => text$1.toLowerCase() : (text$1) => text$1);
169
+ function getMatchingKey(text$1) {
170
+ const result = matchingKey(text$1);
171
+ if (typeof result !== "string") throw new TypeError("Expected key to return a string.");
172
+ return result;
173
+ }
174
+ const matchingKeys = stringChoices.map(getMatchingKey);
175
+ if (caseInsensitive || customKey !== void 0) {
167
176
  const seen = /* @__PURE__ */ new Map();
168
177
  for (let i = 0; i < stringChoices.length; i++) {
169
- const nv = normalizedValues[i];
178
+ const nv = matchingKeys[i];
170
179
  const original = stringChoices[i];
171
180
  const prev = seen.get(nv);
172
- if (prev !== void 0 && prev !== original) throw new TypeError(`Ambiguous choices for case-insensitive matching: ${JSON.stringify(prev)} and ${JSON.stringify(original)} both normalize to ${JSON.stringify(nv)}.`);
181
+ if (prev !== void 0 && prev !== original) throw new TypeError(`Ambiguous choices for ${caseInsensitive ? "case-insensitive" : "custom key"} matching: ${JSON.stringify(prev)} and ${JSON.stringify(original)} both normalize to ${JSON.stringify(nv)}.`);
173
182
  seen.set(nv, original);
174
183
  }
175
184
  }
@@ -200,8 +209,7 @@ function choice(choices, options = {}) {
200
209
  placeholder: choices[0],
201
210
  choices: stringChoices,
202
211
  parse(input) {
203
- const normalizedInput = caseInsensitive ? input.toLowerCase() : input;
204
- const index = normalizedValues.indexOf(normalizedInput);
212
+ const index = matchingKeys.indexOf(getMatchingKey(input));
205
213
  if (index < 0) return {
206
214
  success: false,
207
215
  error: formatStringChoiceError(input, stringChoices, stringInvalidChoice, snapshotSuggest)
@@ -215,11 +223,8 @@ function choice(choices, options = {}) {
215
223
  return String(value);
216
224
  },
217
225
  suggest(prefix) {
218
- const normalizedPrefix = caseInsensitive ? prefix.toLowerCase() : prefix;
219
- return stringChoices.filter((value) => {
220
- const normalizedValue = caseInsensitive ? value.toLowerCase() : value;
221
- return normalizedValue.startsWith(normalizedPrefix);
222
- }).map((value) => ({
226
+ const matchingPrefix = getMatchingKey(prefix);
227
+ return stringChoices.filter((_value, index) => matchingKeys[index].startsWith(matchingPrefix)).map((value) => ({
223
228
  kind: "literal",
224
229
  text: value
225
230
  }));
@@ -270,6 +270,15 @@ interface ChoiceOptionsString extends ChoiceOptionsBase {
270
270
  * @default `false`
271
271
  */
272
272
  readonly caseInsensitive?: boolean;
273
+ /**
274
+ * Maps both the input and declared choices to keys for matching. Parsing
275
+ * and completion still return the declared spelling. Cannot be combined
276
+ * with `caseInsensitive`; include case folding in this function instead.
277
+ *
278
+ * The function should preserve prefixes when completion is needed.
279
+ * @since 1.4.0
280
+ */
281
+ readonly key?: (text: string) => string;
273
282
  /**
274
283
  * Controls whether the error message for an invalid value includes a
275
284
  * "Did you mean …?" hint based on Levenshtein distance.
@@ -384,6 +393,9 @@ declare function isValueParser<M extends Mode, T>(object: unknown): object is Va
384
393
  * @throws {TypeError} If `caseInsensitive` is not a boolean.
385
394
  * @throws {TypeError} If `caseInsensitive` is `true` and multiple choices
386
395
  * normalize to the same lowercase value.
396
+ * @throws {TypeError} If `key` is not a function, is combined with
397
+ * `caseInsensitive`, returns a non-string key, or gives distinct
398
+ * choices the same key.
387
399
  */
388
400
  declare function choice<const T extends string>(choices: readonly T[], options?: ChoiceOptionsString): ValueParser<"sync", T>;
389
401
  /**
@@ -270,6 +270,15 @@ interface ChoiceOptionsString extends ChoiceOptionsBase {
270
270
  * @default `false`
271
271
  */
272
272
  readonly caseInsensitive?: boolean;
273
+ /**
274
+ * Maps both the input and declared choices to keys for matching. Parsing
275
+ * and completion still return the declared spelling. Cannot be combined
276
+ * with `caseInsensitive`; include case folding in this function instead.
277
+ *
278
+ * The function should preserve prefixes when completion is needed.
279
+ * @since 1.4.0
280
+ */
281
+ readonly key?: (text: string) => string;
273
282
  /**
274
283
  * Controls whether the error message for an invalid value includes a
275
284
  * "Did you mean …?" hint based on Levenshtein distance.
@@ -384,6 +393,9 @@ declare function isValueParser<M extends Mode, T>(object: unknown): object is Va
384
393
  * @throws {TypeError} If `caseInsensitive` is not a boolean.
385
394
  * @throws {TypeError} If `caseInsensitive` is `true` and multiple choices
386
395
  * normalize to the same lowercase value.
396
+ * @throws {TypeError} If `key` is not a function, is combined with
397
+ * `caseInsensitive`, returns a non-string key, or gives distinct
398
+ * choices the same key.
387
399
  */
388
400
  declare function choice<const T extends string>(choices: readonly T[], options?: ChoiceOptionsString): ValueParser<"sync", T>;
389
401
  /**
@@ -161,15 +161,24 @@ function choice(choices, options = {}) {
161
161
  const stringChoices = Object.freeze([...new Set(choices)]);
162
162
  const stringOptions = options;
163
163
  checkBooleanOption(stringOptions, "caseInsensitive");
164
+ const customKey = stringOptions.key;
165
+ if (customKey !== void 0 && typeof customKey !== "function") throw new TypeError("Expected key to be a function.");
166
+ if (customKey !== void 0 && stringOptions.caseInsensitive !== void 0) throw new TypeError("Cannot use key with caseInsensitive.");
164
167
  const caseInsensitive = stringOptions.caseInsensitive ?? false;
165
- const normalizedValues = caseInsensitive ? stringChoices.map((v) => v.toLowerCase()) : stringChoices;
166
- if (caseInsensitive) {
168
+ const matchingKey = customKey ?? (caseInsensitive ? (text$1) => text$1.toLowerCase() : (text$1) => text$1);
169
+ function getMatchingKey(text$1) {
170
+ const result = matchingKey(text$1);
171
+ if (typeof result !== "string") throw new TypeError("Expected key to return a string.");
172
+ return result;
173
+ }
174
+ const matchingKeys = stringChoices.map(getMatchingKey);
175
+ if (caseInsensitive || customKey !== void 0) {
167
176
  const seen = /* @__PURE__ */ new Map();
168
177
  for (let i = 0; i < stringChoices.length; i++) {
169
- const nv = normalizedValues[i];
178
+ const nv = matchingKeys[i];
170
179
  const original = stringChoices[i];
171
180
  const prev = seen.get(nv);
172
- if (prev !== void 0 && prev !== original) throw new TypeError(`Ambiguous choices for case-insensitive matching: ${JSON.stringify(prev)} and ${JSON.stringify(original)} both normalize to ${JSON.stringify(nv)}.`);
181
+ if (prev !== void 0 && prev !== original) throw new TypeError(`Ambiguous choices for ${caseInsensitive ? "case-insensitive" : "custom key"} matching: ${JSON.stringify(prev)} and ${JSON.stringify(original)} both normalize to ${JSON.stringify(nv)}.`);
173
182
  seen.set(nv, original);
174
183
  }
175
184
  }
@@ -200,8 +209,7 @@ function choice(choices, options = {}) {
200
209
  placeholder: choices[0],
201
210
  choices: stringChoices,
202
211
  parse(input) {
203
- const normalizedInput = caseInsensitive ? input.toLowerCase() : input;
204
- const index = normalizedValues.indexOf(normalizedInput);
212
+ const index = matchingKeys.indexOf(getMatchingKey(input));
205
213
  if (index < 0) return {
206
214
  success: false,
207
215
  error: formatStringChoiceError(input, stringChoices, stringInvalidChoice, snapshotSuggest)
@@ -215,11 +223,8 @@ function choice(choices, options = {}) {
215
223
  return String(value);
216
224
  },
217
225
  suggest(prefix) {
218
- const normalizedPrefix = caseInsensitive ? prefix.toLowerCase() : prefix;
219
- return stringChoices.filter((value) => {
220
- const normalizedValue = caseInsensitive ? value.toLowerCase() : value;
221
- return normalizedValue.startsWith(normalizedPrefix);
222
- }).map((value) => ({
226
+ const matchingPrefix = getMatchingKey(prefix);
227
+ return stringChoices.filter((_value, index) => matchingKeys[index].startsWith(matchingPrefix)).map((value) => ({
223
228
  kind: "literal",
224
229
  text: value
225
230
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.4.0-dev.2670",
3
+ "version": "1.4.0-dev.2671",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -237,7 +237,7 @@
237
237
  },
238
238
  "sideEffects": false,
239
239
  "devDependencies": {
240
- "@optique/env": "1.4.0-dev.2670+3fbd83fc",
240
+ "@optique/env": "1.4.0-dev.2671+ac89a35b",
241
241
  "@types/node": "^24.0.0",
242
242
  "fast-check": "^4.7.0",
243
243
  "tsdown": "^0.13.0",
@@ -43,9 +43,9 @@ Core rules
43
43
  - Use value parsers such as `integer()`, `choice()`, `biject()`, `regExp()`,
44
44
  `url()`, `origin()`, and `uuid()` instead of validating raw strings after
45
45
  parsing. Use `regExp({ flags })` for user-supplied sources, `biject()` for
46
- one-to-one string-to-value choices, and `transform()` when an existing
47
- value parser accepts the spelling but your app needs a different result
48
- type. Use `normalizeInput()` for raw-string cleanup before validation and
46
+ one-to-one mappings, `transform()` for mapped results, and
47
+ `choice(values, { key })` for custom string matching that returns the
48
+ declared spelling. Use `normalizeInput()` for raw-string cleanup and
49
49
  `path()` from `@optique/run/valueparser` for file-system paths. Write a
50
50
  custom value parser only when these tools do not cover the domain.
51
51
  - Since 1.4.0, `-p8080`/`-vp8080` accept attached values. Values consume