@optique/core 0.8.0-dev.165 → 0.8.0-dev.166

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/modifiers.js CHANGED
@@ -2,6 +2,45 @@ import { formatMessage, message, text } from "./message.js";
2
2
 
3
3
  //#region src/modifiers.ts
4
4
  /**
5
+ * Internal helper for optional-style parsing logic shared by optional()
6
+ * and withDefault(). Handles the common pattern of:
7
+ * - Unwrapping optional state to inner parser state
8
+ * - Detecting if inner parser actually matched (state changed or no consumption)
9
+ * - Returning success with undefined state when inner parser fails without consuming
10
+ * @internal
11
+ */
12
+ function parseOptionalStyle(context, parser) {
13
+ const innerState = typeof context.state === "undefined" ? parser.initialState : context.state[0];
14
+ const result = parser.parse({
15
+ ...context,
16
+ state: innerState
17
+ });
18
+ if (result.success) {
19
+ if (result.next.state !== innerState || result.consumed.length === 0) return {
20
+ success: true,
21
+ next: {
22
+ ...result.next,
23
+ state: [result.next.state]
24
+ },
25
+ consumed: result.consumed
26
+ };
27
+ return {
28
+ success: true,
29
+ next: {
30
+ ...result.next,
31
+ state: context.state
32
+ },
33
+ consumed: result.consumed
34
+ };
35
+ }
36
+ if (result.consumed === 0) return {
37
+ success: true,
38
+ next: context,
39
+ consumed: []
40
+ };
41
+ return result;
42
+ }
43
+ /**
5
44
  * Creates a parser that makes another parser optional, allowing it to succeed
6
45
  * without consuming input if the wrapped parser fails to match.
7
46
  * If the wrapped parser succeeds, this returns its value.
@@ -23,35 +62,7 @@ function optional(parser) {
23
62
  }],
24
63
  initialState: void 0,
25
64
  parse(context) {
26
- const innerState = typeof context.state === "undefined" ? parser.initialState : context.state[0];
27
- const result = parser.parse({
28
- ...context,
29
- state: innerState
30
- });
31
- if (result.success) {
32
- if (result.next.state !== innerState || result.consumed.length === 0) return {
33
- success: true,
34
- next: {
35
- ...result.next,
36
- state: [result.next.state]
37
- },
38
- consumed: result.consumed
39
- };
40
- return {
41
- success: true,
42
- next: {
43
- ...result.next,
44
- state: context.state
45
- },
46
- consumed: result.consumed
47
- };
48
- }
49
- if (result.consumed === 0) return {
50
- success: true,
51
- next: context,
52
- consumed: []
53
- };
54
- return result;
65
+ return parseOptionalStyle(context, parser);
55
66
  },
56
67
  complete(state) {
57
68
  if (typeof state === "undefined") return {
@@ -121,35 +132,7 @@ function withDefault(parser, defaultValue, options) {
121
132
  }],
122
133
  initialState: void 0,
123
134
  parse(context) {
124
- const innerState = typeof context.state === "undefined" ? parser.initialState : context.state[0];
125
- const result = parser.parse({
126
- ...context,
127
- state: innerState
128
- });
129
- if (result.success) {
130
- if (result.next.state !== innerState || result.consumed.length === 0) return {
131
- success: true,
132
- next: {
133
- ...result.next,
134
- state: [result.next.state]
135
- },
136
- consumed: result.consumed
137
- };
138
- return {
139
- success: true,
140
- next: {
141
- ...result.next,
142
- state: context.state
143
- },
144
- consumed: result.consumed
145
- };
146
- }
147
- if (result.consumed === 0) return {
148
- success: true,
149
- next: context,
150
- consumed: []
151
- };
152
- return result;
135
+ return parseOptionalStyle(context, parser);
153
136
  },
154
137
  complete(state) {
155
138
  if (typeof state === "undefined") try {
package/dist/parser.cjs CHANGED
@@ -34,7 +34,7 @@ function parse(parser, args) {
34
34
  };
35
35
  const previousBuffer = context.buffer;
36
36
  context = result.next;
37
- if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer[0] === previousBuffer[0]) return {
37
+ if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer.every((item, i) => item === previousBuffer[i])) return {
38
38
  success: false,
39
39
  error: require_message.message`Unexpected option or argument: ${context.buffer[0]}.`
40
40
  };
@@ -90,7 +90,7 @@ function suggest(parser, args) {
90
90
  if (!result.success) return Array.from(parser.suggest(context, prefix));
91
91
  const previousBuffer = context.buffer;
92
92
  context = result.next;
93
- if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer[0] === previousBuffer[0]) return [];
93
+ if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer.every((item, i) => item === previousBuffer[i])) return [];
94
94
  }
95
95
  return Array.from(parser.suggest(context, prefix));
96
96
  }
@@ -154,14 +154,18 @@ function getDocPage(parser, args = []) {
154
154
  const usage = [...require_usage.normalizeUsage(parser.usage)];
155
155
  let i = 0;
156
156
  for (const arg of args) {
157
+ if (i >= usage.length) break;
157
158
  const term = usage[i];
158
- if (usage.length > i && term.type === "exclusive") for (const termGroup of term.terms) {
159
- const firstTerm = termGroup[0];
160
- if (firstTerm?.type !== "command" || firstTerm.name !== arg) continue;
161
- usage.splice(i, 1, ...termGroup);
162
- break;
163
- }
164
- i++;
159
+ if (term.type === "exclusive") {
160
+ for (const termGroup of term.terms) {
161
+ const firstTerm = termGroup[0];
162
+ if (firstTerm?.type !== "command" || firstTerm.name !== arg) continue;
163
+ usage.splice(i, 1, ...termGroup);
164
+ i += termGroup.length;
165
+ break;
166
+ }
167
+ if (usage[i] === term) i++;
168
+ } else i++;
165
169
  }
166
170
  return {
167
171
  usage,
@@ -172,6 +176,7 @@ function getDocPage(parser, args = []) {
172
176
  }
173
177
 
174
178
  //#endregion
179
+ exports.DuplicateOptionError = require_constructs.DuplicateOptionError;
175
180
  exports.WithDefaultError = require_modifiers.WithDefaultError;
176
181
  exports.argument = require_primitives.argument;
177
182
  exports.command = require_primitives.command;
package/dist/parser.d.cts CHANGED
@@ -4,7 +4,7 @@ import { DocFragments, DocPage } from "./doc.cjs";
4
4
  import { ValueParserResult } from "./valueparser.cjs";
5
5
  import { MultipleErrorOptions, MultipleOptions, WithDefaultError, WithDefaultOptions, map, multiple, optional, withDefault } from "./modifiers.cjs";
6
6
  import { ArgumentErrorOptions, ArgumentOptions, CommandErrorOptions, CommandOptions, FlagErrorOptions, FlagOptions, OptionErrorOptions, OptionOptions, PassThroughFormat, PassThroughOptions, argument, command, constant, flag, option, passThrough } from "./primitives.cjs";
7
- import { ConditionalErrorOptions, ConditionalOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.cjs";
7
+ import { ConditionalErrorOptions, ConditionalOptions, DuplicateOptionError, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.cjs";
8
8
 
9
9
  //#region src/parser.d.ts
10
10
 
@@ -323,4 +323,4 @@ declare function suggest<T>(parser: Parser<T, unknown>, args: readonly [string,
323
323
  */
324
324
  declare function getDocPage(parser: Parser<unknown, unknown>, args?: readonly string[]): DocPage | undefined;
325
325
  //#endregion
326
- export { ArgumentErrorOptions, ArgumentOptions, CommandErrorOptions, CommandOptions, ConditionalErrorOptions, ConditionalOptions, DocState, FlagErrorOptions, FlagOptions, InferValue, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, MultipleErrorOptions, MultipleOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, PassThroughFormat, PassThroughOptions, Result, Suggestion, TupleOptions, WithDefaultError, WithDefaultOptions, argument, command, concat, conditional, constant, flag, getDocPage, group, longestMatch, map, merge, multiple, object, option, optional, or, parse, passThrough, suggest, tuple, withDefault };
326
+ export { ArgumentErrorOptions, ArgumentOptions, CommandErrorOptions, CommandOptions, ConditionalErrorOptions, ConditionalOptions, DocState, DuplicateOptionError, FlagErrorOptions, FlagOptions, InferValue, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, MultipleErrorOptions, MultipleOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, PassThroughFormat, PassThroughOptions, Result, Suggestion, TupleOptions, WithDefaultError, WithDefaultOptions, argument, command, concat, conditional, constant, flag, getDocPage, group, longestMatch, map, merge, multiple, object, option, optional, or, parse, passThrough, suggest, tuple, withDefault };
package/dist/parser.d.ts CHANGED
@@ -4,7 +4,7 @@ import { DocFragments, DocPage } from "./doc.js";
4
4
  import { ValueParserResult } from "./valueparser.js";
5
5
  import { MultipleErrorOptions, MultipleOptions, WithDefaultError, WithDefaultOptions, map, multiple, optional, withDefault } from "./modifiers.js";
6
6
  import { ArgumentErrorOptions, ArgumentOptions, CommandErrorOptions, CommandOptions, FlagErrorOptions, FlagOptions, OptionErrorOptions, OptionOptions, PassThroughFormat, PassThroughOptions, argument, command, constant, flag, option, passThrough } from "./primitives.js";
7
- import { ConditionalErrorOptions, ConditionalOptions, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
7
+ import { ConditionalErrorOptions, ConditionalOptions, DuplicateOptionError, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OrErrorOptions, OrOptions, TupleOptions, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
8
8
 
9
9
  //#region src/parser.d.ts
10
10
 
@@ -323,4 +323,4 @@ declare function suggest<T>(parser: Parser<T, unknown>, args: readonly [string,
323
323
  */
324
324
  declare function getDocPage(parser: Parser<unknown, unknown>, args?: readonly string[]): DocPage | undefined;
325
325
  //#endregion
326
- export { ArgumentErrorOptions, ArgumentOptions, CommandErrorOptions, CommandOptions, ConditionalErrorOptions, ConditionalOptions, DocState, FlagErrorOptions, FlagOptions, InferValue, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, MultipleErrorOptions, MultipleOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, PassThroughFormat, PassThroughOptions, Result, Suggestion, TupleOptions, WithDefaultError, WithDefaultOptions, argument, command, concat, conditional, constant, flag, getDocPage, group, longestMatch, map, merge, multiple, object, option, optional, or, parse, passThrough, suggest, tuple, withDefault };
326
+ export { ArgumentErrorOptions, ArgumentOptions, CommandErrorOptions, CommandOptions, ConditionalErrorOptions, ConditionalOptions, DocState, DuplicateOptionError, FlagErrorOptions, FlagOptions, InferValue, LongestMatchErrorOptions, LongestMatchOptions, MergeOptions, MultipleErrorOptions, MultipleOptions, NoMatchContext, ObjectErrorOptions, ObjectOptions, OptionErrorOptions, OptionOptions, OrErrorOptions, OrOptions, Parser, ParserContext, ParserResult, PassThroughFormat, PassThroughOptions, Result, Suggestion, TupleOptions, WithDefaultError, WithDefaultOptions, argument, command, concat, conditional, constant, flag, getDocPage, group, longestMatch, map, merge, multiple, object, option, optional, or, parse, passThrough, suggest, tuple, withDefault };
package/dist/parser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { message } from "./message.js";
2
2
  import { normalizeUsage } from "./usage.js";
3
- import { concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
3
+ import { DuplicateOptionError, concat, conditional, group, longestMatch, merge, object, or, tuple } from "./constructs.js";
4
4
  import { WithDefaultError, map, multiple, optional, withDefault } from "./modifiers.js";
5
5
  import { argument, command, constant, flag, option, passThrough } from "./primitives.js";
6
6
 
@@ -34,7 +34,7 @@ function parse(parser, args) {
34
34
  };
35
35
  const previousBuffer = context.buffer;
36
36
  context = result.next;
37
- if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer[0] === previousBuffer[0]) return {
37
+ if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer.every((item, i) => item === previousBuffer[i])) return {
38
38
  success: false,
39
39
  error: message`Unexpected option or argument: ${context.buffer[0]}.`
40
40
  };
@@ -90,7 +90,7 @@ function suggest(parser, args) {
90
90
  if (!result.success) return Array.from(parser.suggest(context, prefix));
91
91
  const previousBuffer = context.buffer;
92
92
  context = result.next;
93
- if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer[0] === previousBuffer[0]) return [];
93
+ if (context.buffer.length > 0 && context.buffer.length === previousBuffer.length && context.buffer.every((item, i) => item === previousBuffer[i])) return [];
94
94
  }
95
95
  return Array.from(parser.suggest(context, prefix));
96
96
  }
@@ -154,14 +154,18 @@ function getDocPage(parser, args = []) {
154
154
  const usage = [...normalizeUsage(parser.usage)];
155
155
  let i = 0;
156
156
  for (const arg of args) {
157
+ if (i >= usage.length) break;
157
158
  const term = usage[i];
158
- if (usage.length > i && term.type === "exclusive") for (const termGroup of term.terms) {
159
- const firstTerm = termGroup[0];
160
- if (firstTerm?.type !== "command" || firstTerm.name !== arg) continue;
161
- usage.splice(i, 1, ...termGroup);
162
- break;
163
- }
164
- i++;
159
+ if (term.type === "exclusive") {
160
+ for (const termGroup of term.terms) {
161
+ const firstTerm = termGroup[0];
162
+ if (firstTerm?.type !== "command" || firstTerm.name !== arg) continue;
163
+ usage.splice(i, 1, ...termGroup);
164
+ i += termGroup.length;
165
+ break;
166
+ }
167
+ if (usage[i] === term) i++;
168
+ } else i++;
165
169
  }
166
170
  return {
167
171
  usage,
@@ -172,4 +176,4 @@ function getDocPage(parser, args = []) {
172
176
  }
173
177
 
174
178
  //#endregion
175
- export { WithDefaultError, argument, command, concat, conditional, constant, flag, getDocPage, group, longestMatch, map, merge, multiple, object, option, optional, or, parse, passThrough, suggest, tuple, withDefault };
179
+ export { DuplicateOptionError, WithDefaultError, argument, command, concat, conditional, constant, flag, getDocPage, group, longestMatch, map, merge, multiple, object, option, optional, or, parse, passThrough, suggest, tuple, withDefault };
@@ -179,8 +179,58 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
179
179
  ...suggestionMsg
180
180
  ] : baseError;
181
181
  }
182
+ /**
183
+ * Creates a unique key for a suggestion to enable deduplication.
184
+ *
185
+ * For literal suggestions, the text itself is used as the key.
186
+ * For file suggestions, a composite key is created from the type,
187
+ * extensions, and pattern.
188
+ *
189
+ * @param suggestion The suggestion to create a key for
190
+ * @returns A string key that uniquely identifies this suggestion
191
+ * @internal
192
+ */
193
+ function getSuggestionKey(suggestion) {
194
+ if (suggestion.kind === "literal") return suggestion.text;
195
+ return `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",") ?? ""}:${suggestion.pattern ?? ""}`;
196
+ }
197
+ /**
198
+ * Removes duplicate suggestions from an array while preserving order.
199
+ *
200
+ * Suggestions are considered duplicates if they have the same key:
201
+ * - Literal suggestions: same text
202
+ * - File suggestions: same type, extensions, and pattern
203
+ *
204
+ * The first occurrence of each unique suggestion is kept.
205
+ *
206
+ * @param suggestions Array of suggestions that may contain duplicates
207
+ * @returns A new array with duplicates removed, preserving order of first occurrences
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * const suggestions = [
212
+ * { kind: "literal", text: "--verbose" },
213
+ * { kind: "literal", text: "--help" },
214
+ * { kind: "literal", text: "--verbose" }, // duplicate
215
+ * ];
216
+ * deduplicateSuggestions(suggestions);
217
+ * // returns [{ kind: "literal", text: "--verbose" }, { kind: "literal", text: "--help" }]
218
+ * ```
219
+ *
220
+ * @since 0.9.0
221
+ */
222
+ function deduplicateSuggestions(suggestions) {
223
+ const seen = /* @__PURE__ */ new Set();
224
+ return suggestions.filter((suggestion) => {
225
+ const key = getSuggestionKey(suggestion);
226
+ if (seen.has(key)) return false;
227
+ seen.add(key);
228
+ return true;
229
+ });
230
+ }
182
231
 
183
232
  //#endregion
184
233
  exports.DEFAULT_FIND_SIMILAR_OPTIONS = DEFAULT_FIND_SIMILAR_OPTIONS;
185
234
  exports.createErrorWithSuggestions = createErrorWithSuggestions;
235
+ exports.deduplicateSuggestions = deduplicateSuggestions;
186
236
  exports.findSimilar = findSimilar;
@@ -179,6 +179,55 @@ function createErrorWithSuggestions(baseError, invalidInput, usage, type = "both
179
179
  ...suggestionMsg
180
180
  ] : baseError;
181
181
  }
182
+ /**
183
+ * Creates a unique key for a suggestion to enable deduplication.
184
+ *
185
+ * For literal suggestions, the text itself is used as the key.
186
+ * For file suggestions, a composite key is created from the type,
187
+ * extensions, and pattern.
188
+ *
189
+ * @param suggestion The suggestion to create a key for
190
+ * @returns A string key that uniquely identifies this suggestion
191
+ * @internal
192
+ */
193
+ function getSuggestionKey(suggestion) {
194
+ if (suggestion.kind === "literal") return suggestion.text;
195
+ return `__FILE__:${suggestion.type}:${suggestion.extensions?.join(",") ?? ""}:${suggestion.pattern ?? ""}`;
196
+ }
197
+ /**
198
+ * Removes duplicate suggestions from an array while preserving order.
199
+ *
200
+ * Suggestions are considered duplicates if they have the same key:
201
+ * - Literal suggestions: same text
202
+ * - File suggestions: same type, extensions, and pattern
203
+ *
204
+ * The first occurrence of each unique suggestion is kept.
205
+ *
206
+ * @param suggestions Array of suggestions that may contain duplicates
207
+ * @returns A new array with duplicates removed, preserving order of first occurrences
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * const suggestions = [
212
+ * { kind: "literal", text: "--verbose" },
213
+ * { kind: "literal", text: "--help" },
214
+ * { kind: "literal", text: "--verbose" }, // duplicate
215
+ * ];
216
+ * deduplicateSuggestions(suggestions);
217
+ * // returns [{ kind: "literal", text: "--verbose" }, { kind: "literal", text: "--help" }]
218
+ * ```
219
+ *
220
+ * @since 0.9.0
221
+ */
222
+ function deduplicateSuggestions(suggestions) {
223
+ const seen = /* @__PURE__ */ new Set();
224
+ return suggestions.filter((suggestion) => {
225
+ const key = getSuggestionKey(suggestion);
226
+ if (seen.has(key)) return false;
227
+ seen.add(key);
228
+ return true;
229
+ });
230
+ }
182
231
 
183
232
  //#endregion
184
- export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, findSimilar };
233
+ export { DEFAULT_FIND_SIMILAR_OPTIONS, createErrorWithSuggestions, deduplicateSuggestions, findSimilar };
@@ -153,7 +153,7 @@ function integer(options) {
153
153
  return {
154
154
  metavar: options?.metavar ?? "INTEGER",
155
155
  parse(input) {
156
- if (!input.match(/^\d+$/)) return {
156
+ if (!input.match(/^-?\d+$/)) return {
157
157
  success: false,
158
158
  error: options?.errors?.invalidInteger ? typeof options.errors.invalidInteger === "function" ? options.errors.invalidInteger(input) : options.errors.invalidInteger : require_message.message`Expected a valid integer, but got ${input}.`
159
159
  };
@@ -153,7 +153,7 @@ function integer(options) {
153
153
  return {
154
154
  metavar: options?.metavar ?? "INTEGER",
155
155
  parse(input) {
156
- if (!input.match(/^\d+$/)) return {
156
+ if (!input.match(/^-?\d+$/)) return {
157
157
  success: false,
158
158
  error: options?.errors?.invalidInteger ? typeof options.errors.invalidInteger === "function" ? options.errors.invalidInteger(input) : options.errors.invalidInteger : message`Expected a valid integer, but got ${input}.`
159
159
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "0.8.0-dev.165+f4b6fb65",
3
+ "version": "0.8.0-dev.166+141d0c79",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",