@optique/core 1.1.0-dev.2086 → 1.1.0-dev.2096

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
@@ -12,6 +12,11 @@ function isTerminalMultipleItemState(state) {
12
12
  const unwrapped = unwrapMultipleItemState(state).value;
13
13
  return unwrapped != null && typeof unwrapped === "object" && Object.hasOwn(unwrapped, "success") && typeof unwrapped.success === "boolean";
14
14
  }
15
+ function isMatchedCommandParserState(parser, state) {
16
+ if (Reflect.get(parser, Symbol.for("@optique/core/commandParser")) !== true) return false;
17
+ const unwrapped = unwrapMultipleItemState(state).value;
18
+ return Array.isArray(unwrapped) && unwrapped.length === 2 && unwrapped[0] === "matched" && typeof unwrapped[1] === "string";
19
+ }
15
20
  function isUnstartedMultipleItemState(state, originalState) {
16
21
  const unwrappedState = unwrapMultipleItemState(state);
17
22
  if (unwrappedState.value == null) return true;
@@ -320,6 +325,11 @@ function optional(parser) {
320
325
  leadingNames: parser.leadingNames,
321
326
  acceptingAnyToken: false,
322
327
  initialState: void 0,
328
+ canSkip(state, exec) {
329
+ if (!Array.isArray(state)) return true;
330
+ const innerState = normalizeOptionalLikeInnerState(state, parser.initialState, parser);
331
+ return parser.canSkip?.(innerState, exec) === true;
332
+ },
323
333
  ...typeof parser.shouldDeferCompletion === "function" ? { shouldDeferCompletion: adaptShouldDeferCompletion(parser.shouldDeferCompletion.bind(parser), parser) } : {},
324
334
  getSuggestRuntimeNodes(state, path) {
325
335
  if (optionalParser.dependencyMetadata?.source != null) return [{
@@ -488,6 +498,11 @@ function withDefault(parser, defaultValue, options) {
488
498
  leadingNames: parser.leadingNames,
489
499
  acceptingAnyToken: false,
490
500
  initialState: void 0,
501
+ canSkip(state, exec) {
502
+ if (!Array.isArray(state)) return true;
503
+ const innerState = normalizeOptionalLikeInnerState(state, parser.initialState, parser);
504
+ return parser.canSkip?.(innerState, exec) === true;
505
+ },
491
506
  ...typeof parser.shouldDeferCompletion === "function" ? { shouldDeferCompletion: adaptShouldDeferCompletion(parser.shouldDeferCompletion.bind(parser), parser) } : {},
492
507
  getSuggestRuntimeNodes(state, path) {
493
508
  if (withDefaultParser.dependencyMetadata?.source != null) return [{
@@ -926,9 +941,11 @@ function multiple(parser, options = {}) {
926
941
  parser,
927
942
  state
928
943
  }] : []);
944
+ const canExtendMultipleItem = (state, itemIndex, exec) => state != null && !isTerminalMultipleItemState(state) && (isMatchedCommandParserState(parser, state) || parser.canSkip?.(state, withChildExecPath(exec, itemIndex)) !== true);
929
945
  const parseSync = (context) => {
930
946
  const currentItemState = context.state.at(-1);
931
- const canExtendCurrent = currentItemState != null && !isTerminalMultipleItemState(currentItemState);
947
+ const currentItemIndex = context.state.length - 1;
948
+ const canExtendCurrent = canExtendMultipleItem(currentItemState, currentItemIndex, context.exec);
932
949
  const canOpenFreshItem = context.state.length < max;
933
950
  if (!canExtendCurrent && !canOpenFreshItem) return {
934
951
  success: true,
@@ -1008,7 +1025,8 @@ function multiple(parser, options = {}) {
1008
1025
  };
1009
1026
  const parseAsync = async (context) => {
1010
1027
  const currentItemState = context.state.at(-1);
1011
- const canExtendCurrent = currentItemState != null && !isTerminalMultipleItemState(currentItemState);
1028
+ const currentItemIndex = context.state.length - 1;
1029
+ const canExtendCurrent = canExtendMultipleItem(currentItemState, currentItemIndex, context.exec);
1012
1030
  const canOpenFreshItem = context.state.length < max;
1013
1031
  if (!canExtendCurrent && !canOpenFreshItem) return {
1014
1032
  success: true,
@@ -1099,6 +1117,11 @@ function multiple(parser, options = {}) {
1099
1117
  leadingNames: parser.leadingNames,
1100
1118
  acceptingAnyToken: min > 0 && (parser.acceptingAnyToken ?? false),
1101
1119
  initialState: [],
1120
+ canSkip(state, exec) {
1121
+ if (state.length < min) return false;
1122
+ const currentItemState = state.at(-1);
1123
+ return !canExtendMultipleItem(currentItemState, state.length - 1, exec);
1124
+ },
1102
1125
  getSuggestRuntimeNodes(state, path) {
1103
1126
  const innerNodes = state.flatMap((item, i) => [...getInnerSuggestRuntimeNodes(item, [...path, i])]);
1104
1127
  return resultParser.dependencyMetadata?.source != null ? [{
@@ -1198,7 +1221,8 @@ function multiple(parser, options = {}) {
1198
1221
  },
1199
1222
  suggest(context, prefix) {
1200
1223
  const currentItemState = context.state.at(-1);
1201
- const canExtendCurrent = currentItemState != null && !isTerminalMultipleItemState(currentItemState);
1224
+ const currentItemIndex = context.state.length - 1;
1225
+ const canExtendCurrent = canExtendMultipleItem(currentItemState, currentItemIndex, context.exec);
1202
1226
  const canOpenNew = context.state.length < max;
1203
1227
  if (!canExtendCurrent && !canOpenNew) return dispatchIterableByMode(parser.mode, function* () {}, async function* () {});
1204
1228
  const itemIndex = canExtendCurrent ? context.state.length - 1 : context.state.length;
@@ -1484,6 +1508,7 @@ function multiple(parser, options = {}) {
1484
1508
  */
1485
1509
  function nonEmpty(parser) {
1486
1510
  const syncParser = parser;
1511
+ const initialState = parser.initialState;
1487
1512
  const processNonEmptyResult = (result) => {
1488
1513
  if (!result.success) return result;
1489
1514
  if (result.consumed.length === 0) return {
@@ -1509,7 +1534,12 @@ function nonEmpty(parser) {
1509
1534
  usage: parser.usage,
1510
1535
  leadingNames: parser.leadingNames,
1511
1536
  acceptingAnyToken: parser.acceptingAnyToken,
1512
- initialState: parser.initialState,
1537
+ initialState,
1538
+ ...typeof parser.canSkip === "function" ? { canSkip(state, exec) {
1539
+ const unwrappedState = unwrapInjectedAnnotationWrapper(state);
1540
+ if (unwrappedState === initialState) return false;
1541
+ return parser.canSkip?.(unwrappedState, exec) === true;
1542
+ } } : {},
1513
1543
  ...typeof parser.shouldDeferCompletion === "function" ? { shouldDeferCompletion: parser.shouldDeferCompletion.bind(parser) } : {},
1514
1544
  getSuggestRuntimeNodes(state, path) {
1515
1545
  return parser.getSuggestRuntimeNodes?.(state, path) ?? (parser.dependencyMetadata?.source != null ? [{
@@ -23,6 +23,9 @@ function hasParsedOptionValue(state, valueParser) {
23
23
  if (valueParser != null) return state != null && typeof state === "object" && "success" in state && typeof state.success === "boolean";
24
24
  return state != null && "success" in state && state.success && state.value === true;
25
25
  }
26
+ function isTerminalValueState(state) {
27
+ return state != null && typeof state === "object" && "success" in state && typeof state.success === "boolean";
28
+ }
26
29
  /**
27
30
  * Helper function to create the stored state for an option or argument value.
28
31
  *
@@ -117,6 +120,9 @@ function constant(value) {
117
120
  leadingNames: EMPTY_LEADING_NAMES,
118
121
  acceptingAnyToken: false,
119
122
  initialState: value,
123
+ canSkip() {
124
+ return true;
125
+ },
120
126
  parse(context) {
121
127
  return {
122
128
  success: true,
@@ -174,6 +180,9 @@ function fail() {
174
180
  leadingNames: EMPTY_LEADING_NAMES,
175
181
  acceptingAnyToken: false,
176
182
  initialState: void 0,
183
+ canSkip() {
184
+ return false;
185
+ },
177
186
  parse(_context) {
178
187
  return {
179
188
  success: false,
@@ -431,6 +440,9 @@ function option(...args) {
431
440
  success: true,
432
441
  value: false
433
442
  } : void 0,
443
+ canSkip(state) {
444
+ return valueParser == null || isTerminalValueState(state);
445
+ },
434
446
  parse(context) {
435
447
  if (context.optionsTerminated) return {
436
448
  success: false,
@@ -782,6 +794,9 @@ function flag(...args) {
782
794
  leadingNames: new Set(optionNames$1),
783
795
  acceptingAnyToken: false,
784
796
  initialState: void 0,
797
+ canSkip(state) {
798
+ return isTerminalValueState(state);
799
+ },
785
800
  parse(context) {
786
801
  if (context.optionsTerminated) return {
787
802
  success: false,
@@ -1013,6 +1028,9 @@ function negatableFlag(names, options = {}) {
1013
1028
  leadingNames: new Set(optionNames$1),
1014
1029
  acceptingAnyToken: false,
1015
1030
  initialState: void 0,
1031
+ canSkip(state) {
1032
+ return state != null;
1033
+ },
1016
1034
  parse(context) {
1017
1035
  if (context.optionsTerminated) return {
1018
1036
  success: false,
@@ -1177,6 +1195,9 @@ function argument(valueParser, options = {}) {
1177
1195
  leadingNames: EMPTY_LEADING_NAMES,
1178
1196
  acceptingAnyToken: true,
1179
1197
  initialState: void 0,
1198
+ canSkip(state) {
1199
+ return isTerminalValueState(state);
1200
+ },
1180
1201
  parse(context) {
1181
1202
  const localState = require_annotation_state.normalizeInjectedAnnotationState(context.state);
1182
1203
  if (context.buffer.length < 1) return {
@@ -1433,6 +1454,12 @@ function command(name, parser, options = {}) {
1433
1454
  leadingNames: new Set([name]),
1434
1455
  acceptingAnyToken: false,
1435
1456
  initialState: void 0,
1457
+ canSkip(state, exec) {
1458
+ const normalizedState = normalizeCommandState(state);
1459
+ if (normalizedState === void 0) return false;
1460
+ if (normalizedState[0] === "matched") return parser.canSkip?.(getCommandChildState(state, parser.initialState, parser), require_execution_context.withChildExecPath(exec, name)) === true;
1461
+ return parser.canSkip?.(getCommandChildState(state, normalizedState[1], parser), require_execution_context.withChildExecPath(exec, name)) === true;
1462
+ },
1436
1463
  getSuggestRuntimeNodes(state, path) {
1437
1464
  const normalizedState = normalizeCommandState(state);
1438
1465
  if (normalizedState === void 0) return [];
@@ -1696,6 +1723,9 @@ function passThrough(options = {}) {
1696
1723
  leadingNames: EMPTY_LEADING_NAMES,
1697
1724
  acceptingAnyToken: false,
1698
1725
  initialState: [],
1726
+ canSkip() {
1727
+ return true;
1728
+ },
1699
1729
  parse(context) {
1700
1730
  if (context.buffer.length < 1) return {
1701
1731
  success: false,
@@ -23,6 +23,9 @@ function hasParsedOptionValue(state, valueParser) {
23
23
  if (valueParser != null) return state != null && typeof state === "object" && "success" in state && typeof state.success === "boolean";
24
24
  return state != null && "success" in state && state.success && state.value === true;
25
25
  }
26
+ function isTerminalValueState(state) {
27
+ return state != null && typeof state === "object" && "success" in state && typeof state.success === "boolean";
28
+ }
26
29
  /**
27
30
  * Helper function to create the stored state for an option or argument value.
28
31
  *
@@ -117,6 +120,9 @@ function constant(value) {
117
120
  leadingNames: EMPTY_LEADING_NAMES,
118
121
  acceptingAnyToken: false,
119
122
  initialState: value,
123
+ canSkip() {
124
+ return true;
125
+ },
120
126
  parse(context) {
121
127
  return {
122
128
  success: true,
@@ -174,6 +180,9 @@ function fail() {
174
180
  leadingNames: EMPTY_LEADING_NAMES,
175
181
  acceptingAnyToken: false,
176
182
  initialState: void 0,
183
+ canSkip() {
184
+ return false;
185
+ },
177
186
  parse(_context) {
178
187
  return {
179
188
  success: false,
@@ -431,6 +440,9 @@ function option(...args) {
431
440
  success: true,
432
441
  value: false
433
442
  } : void 0,
443
+ canSkip(state) {
444
+ return valueParser == null || isTerminalValueState(state);
445
+ },
434
446
  parse(context) {
435
447
  if (context.optionsTerminated) return {
436
448
  success: false,
@@ -782,6 +794,9 @@ function flag(...args) {
782
794
  leadingNames: new Set(optionNames$1),
783
795
  acceptingAnyToken: false,
784
796
  initialState: void 0,
797
+ canSkip(state) {
798
+ return isTerminalValueState(state);
799
+ },
785
800
  parse(context) {
786
801
  if (context.optionsTerminated) return {
787
802
  success: false,
@@ -1013,6 +1028,9 @@ function negatableFlag(names, options = {}) {
1013
1028
  leadingNames: new Set(optionNames$1),
1014
1029
  acceptingAnyToken: false,
1015
1030
  initialState: void 0,
1031
+ canSkip(state) {
1032
+ return state != null;
1033
+ },
1016
1034
  parse(context) {
1017
1035
  if (context.optionsTerminated) return {
1018
1036
  success: false,
@@ -1177,6 +1195,9 @@ function argument(valueParser, options = {}) {
1177
1195
  leadingNames: EMPTY_LEADING_NAMES,
1178
1196
  acceptingAnyToken: true,
1179
1197
  initialState: void 0,
1198
+ canSkip(state) {
1199
+ return isTerminalValueState(state);
1200
+ },
1180
1201
  parse(context) {
1181
1202
  const localState = normalizeInjectedAnnotationState(context.state);
1182
1203
  if (context.buffer.length < 1) return {
@@ -1433,6 +1454,12 @@ function command(name, parser, options = {}) {
1433
1454
  leadingNames: new Set([name]),
1434
1455
  acceptingAnyToken: false,
1435
1456
  initialState: void 0,
1457
+ canSkip(state, exec) {
1458
+ const normalizedState = normalizeCommandState(state);
1459
+ if (normalizedState === void 0) return false;
1460
+ if (normalizedState[0] === "matched") return parser.canSkip?.(getCommandChildState(state, parser.initialState, parser), withChildExecPath(exec, name)) === true;
1461
+ return parser.canSkip?.(getCommandChildState(state, normalizedState[1], parser), withChildExecPath(exec, name)) === true;
1462
+ },
1436
1463
  getSuggestRuntimeNodes(state, path) {
1437
1464
  const normalizedState = normalizeCommandState(state);
1438
1465
  if (normalizedState === void 0) return [];
@@ -1696,6 +1723,9 @@ function passThrough(options = {}) {
1696
1723
  leadingNames: EMPTY_LEADING_NAMES,
1697
1724
  acceptingAnyToken: false,
1698
1725
  initialState: [],
1726
+ canSkip() {
1727
+ return true;
1728
+ },
1699
1729
  parse(context) {
1700
1730
  if (context.buffer.length < 1) return {
1701
1731
  success: false,
@@ -16,31 +16,35 @@ const require_usage = require('./usage.cjs');
16
16
  * @returns `true` if every term in `terms` is skippable (i.e., the caller
17
17
  * may continue scanning the next sibling term), `false` otherwise.
18
18
  */
19
- function collectLeadingCandidates(terms, optionNames, commandNames) {
19
+ function collectLeadingCandidates(terms, optionNames, commandNames, includeHidden = false) {
20
20
  if (!terms || !Array.isArray(terms)) return true;
21
21
  for (const term of terms) {
22
22
  if (term.type === "option") {
23
- if (!require_usage.isSuggestionHidden(term.hidden)) for (const name of term.names) optionNames.add(name);
23
+ if (includeHidden || !require_usage.isSuggestionHidden(term.hidden)) for (const name of term.names) optionNames.add(name);
24
24
  return false;
25
25
  }
26
26
  if (term.type === "command") {
27
- if (!require_usage.isSuggestionHidden(term.hidden)) commandNames.add(term.name);
27
+ if (includeHidden || !require_usage.isSuggestionHidden(term.hidden)) commandNames.add(term.name);
28
28
  return false;
29
29
  }
30
30
  if (term.type === "argument") return false;
31
31
  if (term.type === "optional") {
32
- collectLeadingCandidates(term.terms, optionNames, commandNames);
32
+ collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden);
33
33
  continue;
34
34
  }
35
35
  if (term.type === "multiple") {
36
- collectLeadingCandidates(term.terms, optionNames, commandNames);
36
+ collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden);
37
37
  if (term.min === 0) continue;
38
38
  return false;
39
39
  }
40
+ if (term.type === "sequence") {
41
+ if (collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden)) continue;
42
+ return false;
43
+ }
40
44
  if (term.type === "exclusive") {
41
45
  let allSkippable = true;
42
46
  for (const branch of term.terms) {
43
- const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames);
47
+ const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames, includeHidden);
44
48
  allSkippable = allSkippable && branchSkippable;
45
49
  }
46
50
  if (allSkippable) continue;
@@ -16,31 +16,35 @@ import { isSuggestionHidden } from "./usage.js";
16
16
  * @returns `true` if every term in `terms` is skippable (i.e., the caller
17
17
  * may continue scanning the next sibling term), `false` otherwise.
18
18
  */
19
- function collectLeadingCandidates(terms, optionNames, commandNames) {
19
+ function collectLeadingCandidates(terms, optionNames, commandNames, includeHidden = false) {
20
20
  if (!terms || !Array.isArray(terms)) return true;
21
21
  for (const term of terms) {
22
22
  if (term.type === "option") {
23
- if (!isSuggestionHidden(term.hidden)) for (const name of term.names) optionNames.add(name);
23
+ if (includeHidden || !isSuggestionHidden(term.hidden)) for (const name of term.names) optionNames.add(name);
24
24
  return false;
25
25
  }
26
26
  if (term.type === "command") {
27
- if (!isSuggestionHidden(term.hidden)) commandNames.add(term.name);
27
+ if (includeHidden || !isSuggestionHidden(term.hidden)) commandNames.add(term.name);
28
28
  return false;
29
29
  }
30
30
  if (term.type === "argument") return false;
31
31
  if (term.type === "optional") {
32
- collectLeadingCandidates(term.terms, optionNames, commandNames);
32
+ collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden);
33
33
  continue;
34
34
  }
35
35
  if (term.type === "multiple") {
36
- collectLeadingCandidates(term.terms, optionNames, commandNames);
36
+ collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden);
37
37
  if (term.min === 0) continue;
38
38
  return false;
39
39
  }
40
+ if (term.type === "sequence") {
41
+ if (collectLeadingCandidates(term.terms, optionNames, commandNames, includeHidden)) continue;
42
+ return false;
43
+ }
40
44
  if (term.type === "exclusive") {
41
45
  let allSkippable = true;
42
46
  for (const branch of term.terms) {
43
- const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames);
47
+ const branchSkippable = collectLeadingCandidates(branch, optionNames, commandNames, includeHidden);
44
48
  allSkippable = allSkippable && branchSkippable;
45
49
  }
46
50
  if (allSkippable) continue;
package/dist/usage.cjs CHANGED
@@ -63,7 +63,7 @@ function extractOptionNames(usage, includeHidden) {
63
63
  for (const term of terms) if (term.type === "option") {
64
64
  if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
65
65
  for (const name of term.names) names.add(name);
66
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
66
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
67
67
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
68
68
  }
69
69
  traverseUsage(usage);
@@ -98,7 +98,7 @@ function extractCommandNames(usage, includeHidden) {
98
98
  for (const term of terms) if (term.type === "command") {
99
99
  if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
100
100
  names.add(term.name);
101
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
101
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
102
102
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
103
103
  }
104
104
  traverseUsage(usage);
@@ -121,7 +121,7 @@ function extractLiteralValues(usage) {
121
121
  function traverseUsage(terms) {
122
122
  if (!terms || !Array.isArray(terms)) return;
123
123
  for (const term of terms) if (term.type === "literal") values.add(term.value);
124
- else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
124
+ else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
125
125
  else if (term.type === "exclusive") for (const branch of term.terms) traverseUsage(branch);
126
126
  }
127
127
  traverseUsage(usage);
@@ -155,7 +155,7 @@ function extractArgumentMetavars(usage) {
155
155
  for (const term of terms) if (term.type === "argument") {
156
156
  if (isSuggestionHidden(term.hidden)) continue;
157
157
  metavars.add(term.metavar);
158
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
158
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
159
159
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
160
160
  }
161
161
  traverseUsage(usage);
@@ -282,6 +282,10 @@ function normalizeUsageTerm(term) {
282
282
  terms: normalizeUsage(term.terms),
283
283
  min: term.min
284
284
  };
285
+ else if (term.type === "sequence") return {
286
+ type: "sequence",
287
+ terms: term.terms.map(normalizeUsageTerm).filter(isNonDegenerateTerm)
288
+ };
285
289
  else if (term.type === "exclusive") {
286
290
  const terms = [];
287
291
  for (const usage of term.terms) {
@@ -314,7 +318,7 @@ function isNonDegenerateTerm(term) {
314
318
  if (term.type === "option") return term.names.length > 0;
315
319
  if (term.type === "command") return term.name !== "";
316
320
  if (term.type === "argument") return term.metavar.length > 0;
317
- if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive") return term.terms.length > 0;
321
+ if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive" || term.type === "sequence") return term.terms.length > 0;
318
322
  return true;
319
323
  }
320
324
  function containsMalformedLeaf(usage) {
@@ -322,7 +326,7 @@ function containsMalformedLeaf(usage) {
322
326
  if (term.type === "option" && term.names.length === 0) return true;
323
327
  if (term.type === "command" && term.name === "") return true;
324
328
  if (term.type === "argument" && term.metavar.length === 0) return true;
325
- if (term.type === "optional" || term.type === "multiple") {
329
+ if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") {
326
330
  if (containsMalformedLeaf(term.terms)) return true;
327
331
  }
328
332
  if (term.type === "exclusive") {
@@ -369,6 +373,10 @@ function cloneUsageTerm(term) {
369
373
  type: "exclusive",
370
374
  terms: term.terms.map((u) => u.map(cloneUsageTerm))
371
375
  };
376
+ case "sequence": return {
377
+ type: "sequence",
378
+ terms: term.terms.map(cloneUsageTerm)
379
+ };
372
380
  case "literal":
373
381
  case "passthrough":
374
382
  case "ellipsis": return { ...term };
@@ -421,6 +429,14 @@ function filterUsageForDisplay(usage, isHidden = isUsageHidden) {
421
429
  });
422
430
  continue;
423
431
  }
432
+ if (term.type === "sequence") {
433
+ const filtered = filterUsageForDisplay(term.terms, isHidden);
434
+ if (filtered.length > 0) terms.push({
435
+ type: "sequence",
436
+ terms: filtered
437
+ });
438
+ continue;
439
+ }
424
440
  terms.push(term);
425
441
  }
426
442
  return terms;
@@ -541,7 +557,8 @@ function* formatUsageTermInternal(term, options) {
541
557
  text: options?.colors ? `\x1b[2m)\x1b[0m` : ")",
542
558
  width: 1
543
559
  };
544
- } else if (term.type === "multiple") {
560
+ } else if (term.type === "sequence") yield* formatUsageTerms(term.terms, options);
561
+ else if (term.type === "multiple") {
545
562
  if (term.min < 1) yield {
546
563
  text: options?.colors ? `\x1b[2m[\x1b[0m` : "[",
547
564
  width: 1
package/dist/usage.d.cts CHANGED
@@ -164,6 +164,23 @@ type UsageTerm =
164
164
  */
165
165
  readonly terms: readonly Usage[];
166
166
  }
167
+ /**
168
+ * A sequence term, which preserves the declaration order of its child
169
+ * terms through usage normalization.
170
+ *
171
+ * This is used by ordered parser combinators where argument/command/option
172
+ * order is part of the accepted grammar.
173
+ * @since 1.1.0
174
+ */ | {
175
+ /**
176
+ * The type of the term, which is always `"sequence"` for this term.
177
+ */
178
+ readonly type: "sequence";
179
+ /**
180
+ * Terms that must be displayed in the given order.
181
+ */
182
+ readonly terms: Usage;
183
+ }
167
184
  /**
168
185
  * A literal term, which represents a fixed string value in the command-line
169
186
  * usage. Unlike metavars which are placeholders for user-provided values,
package/dist/usage.d.ts CHANGED
@@ -164,6 +164,23 @@ type UsageTerm =
164
164
  */
165
165
  readonly terms: readonly Usage[];
166
166
  }
167
+ /**
168
+ * A sequence term, which preserves the declaration order of its child
169
+ * terms through usage normalization.
170
+ *
171
+ * This is used by ordered parser combinators where argument/command/option
172
+ * order is part of the accepted grammar.
173
+ * @since 1.1.0
174
+ */ | {
175
+ /**
176
+ * The type of the term, which is always `"sequence"` for this term.
177
+ */
178
+ readonly type: "sequence";
179
+ /**
180
+ * Terms that must be displayed in the given order.
181
+ */
182
+ readonly terms: Usage;
183
+ }
167
184
  /**
168
185
  * A literal term, which represents a fixed string value in the command-line
169
186
  * usage. Unlike metavars which are placeholders for user-provided values,
package/dist/usage.js CHANGED
@@ -63,7 +63,7 @@ function extractOptionNames(usage, includeHidden) {
63
63
  for (const term of terms) if (term.type === "option") {
64
64
  if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
65
65
  for (const name of term.names) names.add(name);
66
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
66
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
67
67
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
68
68
  }
69
69
  traverseUsage(usage);
@@ -98,7 +98,7 @@ function extractCommandNames(usage, includeHidden) {
98
98
  for (const term of terms) if (term.type === "command") {
99
99
  if (!includeHidden && isSuggestionHidden(term.hidden)) continue;
100
100
  names.add(term.name);
101
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
101
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
102
102
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
103
103
  }
104
104
  traverseUsage(usage);
@@ -121,7 +121,7 @@ function extractLiteralValues(usage) {
121
121
  function traverseUsage(terms) {
122
122
  if (!terms || !Array.isArray(terms)) return;
123
123
  for (const term of terms) if (term.type === "literal") values.add(term.value);
124
- else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
124
+ else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
125
125
  else if (term.type === "exclusive") for (const branch of term.terms) traverseUsage(branch);
126
126
  }
127
127
  traverseUsage(usage);
@@ -155,7 +155,7 @@ function extractArgumentMetavars(usage) {
155
155
  for (const term of terms) if (term.type === "argument") {
156
156
  if (isSuggestionHidden(term.hidden)) continue;
157
157
  metavars.add(term.metavar);
158
- } else if (term.type === "optional" || term.type === "multiple") traverseUsage(term.terms);
158
+ } else if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") traverseUsage(term.terms);
159
159
  else if (term.type === "exclusive") for (const exclusiveUsage of term.terms) traverseUsage(exclusiveUsage);
160
160
  }
161
161
  traverseUsage(usage);
@@ -282,6 +282,10 @@ function normalizeUsageTerm(term) {
282
282
  terms: normalizeUsage(term.terms),
283
283
  min: term.min
284
284
  };
285
+ else if (term.type === "sequence") return {
286
+ type: "sequence",
287
+ terms: term.terms.map(normalizeUsageTerm).filter(isNonDegenerateTerm)
288
+ };
285
289
  else if (term.type === "exclusive") {
286
290
  const terms = [];
287
291
  for (const usage of term.terms) {
@@ -314,7 +318,7 @@ function isNonDegenerateTerm(term) {
314
318
  if (term.type === "option") return term.names.length > 0;
315
319
  if (term.type === "command") return term.name !== "";
316
320
  if (term.type === "argument") return term.metavar.length > 0;
317
- if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive") return term.terms.length > 0;
321
+ if (term.type === "optional" || term.type === "multiple" || term.type === "exclusive" || term.type === "sequence") return term.terms.length > 0;
318
322
  return true;
319
323
  }
320
324
  function containsMalformedLeaf(usage) {
@@ -322,7 +326,7 @@ function containsMalformedLeaf(usage) {
322
326
  if (term.type === "option" && term.names.length === 0) return true;
323
327
  if (term.type === "command" && term.name === "") return true;
324
328
  if (term.type === "argument" && term.metavar.length === 0) return true;
325
- if (term.type === "optional" || term.type === "multiple") {
329
+ if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") {
326
330
  if (containsMalformedLeaf(term.terms)) return true;
327
331
  }
328
332
  if (term.type === "exclusive") {
@@ -369,6 +373,10 @@ function cloneUsageTerm(term) {
369
373
  type: "exclusive",
370
374
  terms: term.terms.map((u) => u.map(cloneUsageTerm))
371
375
  };
376
+ case "sequence": return {
377
+ type: "sequence",
378
+ terms: term.terms.map(cloneUsageTerm)
379
+ };
372
380
  case "literal":
373
381
  case "passthrough":
374
382
  case "ellipsis": return { ...term };
@@ -421,6 +429,14 @@ function filterUsageForDisplay(usage, isHidden = isUsageHidden) {
421
429
  });
422
430
  continue;
423
431
  }
432
+ if (term.type === "sequence") {
433
+ const filtered = filterUsageForDisplay(term.terms, isHidden);
434
+ if (filtered.length > 0) terms.push({
435
+ type: "sequence",
436
+ terms: filtered
437
+ });
438
+ continue;
439
+ }
424
440
  terms.push(term);
425
441
  }
426
442
  return terms;
@@ -541,7 +557,8 @@ function* formatUsageTermInternal(term, options) {
541
557
  text: options?.colors ? `\x1b[2m)\x1b[0m` : ")",
542
558
  width: 1
543
559
  };
544
- } else if (term.type === "multiple") {
560
+ } else if (term.type === "sequence") yield* formatUsageTerms(term.terms, options);
561
+ else if (term.type === "multiple") {
545
562
  if (term.min < 1) yield {
546
563
  text: options?.colors ? `\x1b[2m[\x1b[0m` : "[",
547
564
  width: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.1.0-dev.2086",
3
+ "version": "1.1.0-dev.2096",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",