@optique/core 1.1.2 → 1.1.4

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.
@@ -581,6 +581,35 @@ function createExclusiveState(parentState, index, parser, result) {
581
581
  return require_internal_annotations.annotateFreshArray(parentState, [index, annotatedResult]);
582
582
  }
583
583
  /**
584
+ * Detects a branch result that handled only the global options terminator.
585
+ * Such a result advances the shared parser context, but does not identify
586
+ * which exclusive branch matched.
587
+ * @internal
588
+ */
589
+ function isOptionsTerminatorOnlyResult(context, result) {
590
+ return !context.optionsTerminated && context.buffer[0] === "--" && result.success && result.next.optionsTerminated && result.consumed.length === 1 && result.consumed[0] === "--";
591
+ }
592
+ function isUnselectedBooleanOptionState(state) {
593
+ return state != null && typeof state === "object" && "success" in state && state.success === true && "value" in state && state.value === false;
594
+ }
595
+ function preserveExclusiveStateAfterOptionsTerminator(context, result) {
596
+ const mergedExec = require_execution_context.mergeChildExec(context.exec, result.next.exec);
597
+ return {
598
+ success: true,
599
+ next: {
600
+ ...context,
601
+ buffer: result.next.buffer,
602
+ optionsTerminated: true,
603
+ state: context.state,
604
+ ...mergedExec != null ? {
605
+ exec: mergedExec,
606
+ dependencyRegistry: mergedExec.dependencyRegistry
607
+ } : {}
608
+ },
609
+ consumed: result.consumed
610
+ };
611
+ }
612
+ /**
584
613
  * Creates a complete() method shared by or() and longestMatch().
585
614
  * @internal
586
615
  */
@@ -818,9 +847,15 @@ function or(...args) {
818
847
  let zeroConsumedBranch = null;
819
848
  let zeroConsumedCount = 0;
820
849
  let provisionalConsuming = null;
850
+ let optionsTerminatorResult = null;
821
851
  let provisionalAmbiguous = false;
822
852
  for (const [parser, i] of orderedParsers) {
823
853
  const result = parser.parse(withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
854
+ if (isOptionsTerminatorOnlyResult(context, result)) {
855
+ if (activeState?.[0] === i && activeState[1].success && activeState[1].consumed.length > 0 && result.next.buffer.length < 1) return preserveExclusiveStateAfterOptionsTerminator(context, result);
856
+ optionsTerminatorResult ??= result;
857
+ continue;
858
+ }
824
859
  if (result.success && result.consumed.length > 0) {
825
860
  if (result.provisional) {
826
861
  const activeBranchLocked = activeState != null && activeState[1].success && activeState[1].consumed.length > 0;
@@ -1001,6 +1036,7 @@ function or(...args) {
1001
1036
  }
1002
1037
  }
1003
1038
  }
1039
+ if (optionsTerminatorResult != null) return preserveExclusiveStateAfterOptionsTerminator(context, optionsTerminatorResult);
1004
1040
  return {
1005
1041
  ...error,
1006
1042
  success: false
@@ -1014,10 +1050,16 @@ function or(...args) {
1014
1050
  let zeroConsumedBranch = null;
1015
1051
  let zeroConsumedCount = 0;
1016
1052
  let provisionalConsuming = null;
1053
+ let optionsTerminatorResult = null;
1017
1054
  let provisionalAmbiguous = false;
1018
1055
  for (const [parser, i] of orderedParsers) {
1019
1056
  const resultOrPromise = parser.parse(withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1020
1057
  const result = await resultOrPromise;
1058
+ if (isOptionsTerminatorOnlyResult(context, result)) {
1059
+ if (activeState?.[0] === i && activeState[1].success && activeState[1].consumed.length > 0 && result.next.buffer.length < 1) return preserveExclusiveStateAfterOptionsTerminator(context, result);
1060
+ optionsTerminatorResult ??= result;
1061
+ continue;
1062
+ }
1021
1063
  if (result.success && result.consumed.length > 0) {
1022
1064
  if (result.provisional) {
1023
1065
  const activeBranchLocked = activeState != null && activeState[1].success && activeState[1].consumed.length > 0;
@@ -1200,6 +1242,7 @@ function or(...args) {
1200
1242
  }
1201
1243
  }
1202
1244
  }
1245
+ if (optionsTerminatorResult != null) return preserveExclusiveStateAfterOptionsTerminator(context, optionsTerminatorResult);
1203
1246
  return {
1204
1247
  ...error,
1205
1248
  success: false
@@ -1301,82 +1344,122 @@ function createLongestMatch(...args) {
1301
1344
  return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
1302
1345
  })()
1303
1346
  });
1304
- const parseSync = (context) => {
1347
+ const commitLongestMatch = (context, match, optionsTerminatorResult) => {
1348
+ const selectedResult = optionsTerminatorResult == null ? match.result : {
1349
+ ...match.result,
1350
+ next: {
1351
+ ...match.result.next,
1352
+ buffer: optionsTerminatorResult.next.buffer,
1353
+ optionsTerminated: true
1354
+ },
1355
+ consumed: [...optionsTerminatorResult.consumed, ...match.result.consumed]
1356
+ };
1357
+ const parser = parsers[match.index];
1358
+ const mergedExec = require_execution_context.mergeChildExec(context.exec, selectedResult.next.exec);
1359
+ return {
1360
+ success: true,
1361
+ next: {
1362
+ ...context,
1363
+ buffer: selectedResult.next.buffer,
1364
+ optionsTerminated: selectedResult.next.optionsTerminated,
1365
+ state: createExclusiveState(context.state, match.index, parser, selectedResult),
1366
+ ...mergedExec != null ? {
1367
+ exec: mergedExec,
1368
+ dependencyRegistry: mergedExec.dependencyRegistry
1369
+ } : {}
1370
+ },
1371
+ consumed: selectedResult.consumed
1372
+ };
1373
+ };
1374
+ const resolveLongestMatchOptionsTerminator = (context, bestMatch, optionsTerminatorMatches) => {
1375
+ if (optionsTerminatorMatches.length < 1) return null;
1376
+ if (bestMatch != null && bestMatch.consumed > 0) return null;
1377
+ const activeState = normalizeExclusiveState(context.state);
1378
+ if (activeState != null && activeState[1].success && activeState[1].consumed.length > 0) return preserveExclusiveStateAfterOptionsTerminator(context, optionsTerminatorMatches[0].result);
1379
+ let selected = bestMatch == null ? null : {
1380
+ match: bestMatch,
1381
+ fromTerminator: false
1382
+ };
1383
+ for (const match of optionsTerminatorMatches) {
1384
+ if (!match.selectedAfterTerminator && (extractRequiredUsage(parsers[match.index].usage).length > 0 || isUnselectedBooleanOptionState(match.result.next.state))) continue;
1385
+ const selectedIsProvisional = selected != null && !!selected.match.result.provisional;
1386
+ const matchIsProvisional = !!match.result.provisional;
1387
+ if (selected == null || selectedIsProvisional && !matchIsProvisional || selectedIsProvisional === matchIsProvisional && match.index < selected.match.index) selected = {
1388
+ match,
1389
+ fromTerminator: true
1390
+ };
1391
+ }
1392
+ if (selected != null) return commitLongestMatch(context, selected.match, selected.fromTerminator ? void 0 : optionsTerminatorMatches[0].result);
1393
+ return preserveExclusiveStateAfterOptionsTerminator(context, optionsTerminatorMatches[0].result);
1394
+ };
1395
+ const parseSyncBranches = (context) => {
1305
1396
  let bestMatch = null;
1397
+ const optionsTerminatorMatches = [];
1306
1398
  let error = getInitialError(context);
1307
1399
  const activeState = normalizeExclusiveState(context.state);
1308
1400
  for (let i = 0; i < syncParsers.length; i++) {
1309
1401
  const parser = syncParsers[i];
1310
- const result = parser.parse(withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1402
+ const childContext = withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser);
1403
+ const result = parser.parse(childContext);
1404
+ if (isOptionsTerminatorOnlyResult(context, result)) {
1405
+ optionsTerminatorMatches.push({
1406
+ index: i,
1407
+ result,
1408
+ consumed: 1,
1409
+ ...normalizeExclusiveState(result.next.state) != null ? { selectedAfterTerminator: true } : {}
1410
+ });
1411
+ continue;
1412
+ }
1311
1413
  if (result.success) {
1312
1414
  const consumed = context.buffer.length - result.next.buffer.length;
1313
1415
  const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1314
1416
  if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1315
1417
  index: i,
1316
- parser,
1317
1418
  result,
1318
1419
  consumed
1319
1420
  };
1320
1421
  } else if (error.consumed < result.consumed) error = result;
1321
1422
  }
1322
- if (bestMatch && bestMatch.result.success) {
1323
- const mergedExec = require_execution_context.mergeChildExec(context.exec, bestMatch.result.next.exec);
1324
- return {
1325
- success: true,
1326
- next: {
1327
- ...context,
1328
- buffer: bestMatch.result.next.buffer,
1329
- optionsTerminated: bestMatch.result.next.optionsTerminated,
1330
- state: createExclusiveState(context.state, bestMatch.index, bestMatch.parser, bestMatch.result),
1331
- ...mergedExec != null ? {
1332
- exec: mergedExec,
1333
- dependencyRegistry: mergedExec.dependencyRegistry
1334
- } : {}
1335
- },
1336
- consumed: bestMatch.result.consumed
1337
- };
1338
- }
1423
+ const optionsTerminatorResolution = resolveLongestMatchOptionsTerminator(context, bestMatch, optionsTerminatorMatches);
1424
+ if (optionsTerminatorResolution != null) return optionsTerminatorResolution;
1425
+ if (bestMatch != null) return commitLongestMatch(context, bestMatch);
1339
1426
  return {
1340
1427
  ...error,
1341
1428
  success: false
1342
1429
  };
1343
1430
  };
1344
- const parseAsync = async (context) => {
1431
+ const parseAsyncBranches = async (context) => {
1345
1432
  let bestMatch = null;
1433
+ const optionsTerminatorMatches = [];
1346
1434
  let error = getInitialError(context);
1347
1435
  const activeState = normalizeExclusiveState(context.state);
1348
1436
  for (let i = 0; i < parsers.length; i++) {
1349
1437
  const parser = parsers[i];
1350
- const resultOrPromise = parser.parse(withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1438
+ const childContext = withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser);
1439
+ const resultOrPromise = parser.parse(childContext);
1351
1440
  const result = await resultOrPromise;
1441
+ if (isOptionsTerminatorOnlyResult(context, result)) {
1442
+ optionsTerminatorMatches.push({
1443
+ index: i,
1444
+ result,
1445
+ consumed: 1,
1446
+ ...normalizeExclusiveState(result.next.state) != null ? { selectedAfterTerminator: true } : {}
1447
+ });
1448
+ continue;
1449
+ }
1352
1450
  if (result.success) {
1353
1451
  const consumed = context.buffer.length - result.next.buffer.length;
1354
1452
  const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1355
1453
  if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1356
1454
  index: i,
1357
- parser,
1358
1455
  result,
1359
1456
  consumed
1360
1457
  };
1361
1458
  } else if (error.consumed < result.consumed) error = result;
1362
1459
  }
1363
- if (bestMatch && bestMatch.result.success) {
1364
- const mergedExec = require_execution_context.mergeChildExec(context.exec, bestMatch.result.next.exec);
1365
- return {
1366
- success: true,
1367
- next: {
1368
- ...context,
1369
- buffer: bestMatch.result.next.buffer,
1370
- optionsTerminated: bestMatch.result.next.optionsTerminated,
1371
- state: createExclusiveState(context.state, bestMatch.index, bestMatch.parser, bestMatch.result),
1372
- ...mergedExec != null ? {
1373
- exec: mergedExec,
1374
- dependencyRegistry: mergedExec.dependencyRegistry
1375
- } : {}
1376
- },
1377
- consumed: bestMatch.result.consumed
1378
- };
1379
- }
1460
+ const optionsTerminatorResolution = resolveLongestMatchOptionsTerminator(context, bestMatch, optionsTerminatorMatches);
1461
+ if (optionsTerminatorResolution != null) return optionsTerminatorResolution;
1462
+ if (bestMatch != null) return commitLongestMatch(context, bestMatch);
1380
1463
  return {
1381
1464
  ...error,
1382
1465
  success: false
@@ -1399,7 +1482,7 @@ function createLongestMatch(...args) {
1399
1482
  return extractExclusivePhase2Seed(parsers, state, exec, combinedMode);
1400
1483
  },
1401
1484
  parse(context) {
1402
- return require_mode_dispatch.dispatchByMode(combinedMode, () => parseSync(context), () => parseAsync(context));
1485
+ return require_mode_dispatch.dispatchByMode(combinedMode, () => parseSyncBranches(context), () => parseAsyncBranches(context));
1403
1486
  },
1404
1487
  getSuggestRuntimeNodes(state, path) {
1405
1488
  return getExclusiveSuggestRuntimeNodes(parsers, state, path);
@@ -1445,6 +1528,33 @@ function createLongestMatch(...args) {
1445
1528
  return multiResult;
1446
1529
  }
1447
1530
  /**
1531
+ * Creates the initial parse error shared by object-like combinators.
1532
+ * @param context The current parser context.
1533
+ * @param noMatchContext The kinds of input accepted by the combinator.
1534
+ * @param errors Optional custom error formatters.
1535
+ * @returns A zero-consumption parse error.
1536
+ */
1537
+ function createObjectLikeInitialError(context, noMatchContext, errors) {
1538
+ if (context.buffer.length < 1) {
1539
+ const customEndOfInput = errors?.endOfInput;
1540
+ return {
1541
+ consumed: 0,
1542
+ error: customEndOfInput ? typeof customEndOfInput === "function" ? customEndOfInput(noMatchContext) : customEndOfInput : generateNoMatchError(noMatchContext)
1543
+ };
1544
+ }
1545
+ const token = context.buffer[0];
1546
+ const customMessage = errors?.unexpectedInput;
1547
+ if (customMessage) return {
1548
+ consumed: 0,
1549
+ error: typeof customMessage === "function" ? customMessage(token) : customMessage
1550
+ };
1551
+ const baseError = require_message.message`Unexpected option or argument: ${token}.`;
1552
+ return {
1553
+ consumed: 0,
1554
+ error: require_suggestion.createErrorWithSuggestions(baseError, token, context.usage, "both", errors?.suggestions)
1555
+ };
1556
+ }
1557
+ /**
1448
1558
  * Internal sync helper for object suggest functionality.
1449
1559
  * @internal
1450
1560
  */
@@ -1837,19 +1947,7 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
1837
1947
  checkDuplicateReachableLeadingCommandNames(parserPairs.map(([field, parser]) => [field, parser]));
1838
1948
  const noMatchContext = analyzeNoMatchContext(parserKeys.map((k) => parsers[k]));
1839
1949
  const combinedMode = parserKeys.some((k) => parsers[k].mode === "async") ? "async" : "sync";
1840
- const getInitialError = (context) => ({
1841
- consumed: 0,
1842
- error: context.buffer.length > 0 ? (() => {
1843
- const token = context.buffer[0];
1844
- const customMessage = options.errors?.unexpectedInput;
1845
- if (customMessage) return typeof customMessage === "function" ? customMessage(token) : customMessage;
1846
- const baseError = require_message.message`Unexpected option or argument: ${token}.`;
1847
- return require_suggestion.createErrorWithSuggestions(baseError, token, context.usage, "both", options.errors?.suggestions);
1848
- })() : (() => {
1849
- const customEndOfInput = options.errors?.endOfInput;
1850
- return customEndOfInput ? typeof customEndOfInput === "function" ? customEndOfInput(noMatchContext) : customEndOfInput : generateNoMatchError(noMatchContext);
1851
- })()
1852
- });
1950
+ const getInitialError = (context) => createObjectLikeInitialError(context, noMatchContext, options.errors);
1853
1951
  const parseSync = (context) => {
1854
1952
  let error = getInitialError(context);
1855
1953
  let currentContext = context;
@@ -3719,6 +3817,7 @@ function merge(...args) {
3719
3817
  const syncParsers = syncSorted.map(([p]) => p);
3720
3818
  if (!options.allowDuplicates) checkDuplicateOptionNames(sorted.map(([parser, originalIndex]) => [String(originalIndex), parser.usage]));
3721
3819
  checkDuplicateReachableLeadingCommandNames(sorted.map(([parser, originalIndex]) => [String(originalIndex), parser]));
3820
+ const noMatchContext = analyzeNoMatchContext(rawParsers);
3722
3821
  const mergedFieldParsers = collectChildFieldParsers(parsers);
3723
3822
  const duplicateOutputFieldNames = collectDuplicateFieldNames(mergedFieldParsers);
3724
3823
  const parserStateKey = (index) => `__parser_${index}`;
@@ -3812,8 +3911,7 @@ function merge(...args) {
3812
3911
  };
3813
3912
  return {
3814
3913
  success: false,
3815
- consumed: 0,
3816
- error: require_message.message`No matching option or argument found.`
3914
+ ...createObjectLikeInitialError(context, noMatchContext)
3817
3915
  };
3818
3916
  };
3819
3917
  const parseAsync = async (context) => {
@@ -3858,8 +3956,7 @@ function merge(...args) {
3858
3956
  };
3859
3957
  return {
3860
3958
  success: false,
3861
- consumed: 0,
3862
- error: require_message.message`No matching option or argument found.`
3959
+ ...createObjectLikeInitialError(context, noMatchContext)
3863
3960
  };
3864
3961
  };
3865
3962
  const mergeParser = {
@@ -581,6 +581,35 @@ function createExclusiveState(parentState, index, parser, result) {
581
581
  return annotateFreshArray(parentState, [index, annotatedResult]);
582
582
  }
583
583
  /**
584
+ * Detects a branch result that handled only the global options terminator.
585
+ * Such a result advances the shared parser context, but does not identify
586
+ * which exclusive branch matched.
587
+ * @internal
588
+ */
589
+ function isOptionsTerminatorOnlyResult(context, result) {
590
+ return !context.optionsTerminated && context.buffer[0] === "--" && result.success && result.next.optionsTerminated && result.consumed.length === 1 && result.consumed[0] === "--";
591
+ }
592
+ function isUnselectedBooleanOptionState(state) {
593
+ return state != null && typeof state === "object" && "success" in state && state.success === true && "value" in state && state.value === false;
594
+ }
595
+ function preserveExclusiveStateAfterOptionsTerminator(context, result) {
596
+ const mergedExec = mergeChildExec(context.exec, result.next.exec);
597
+ return {
598
+ success: true,
599
+ next: {
600
+ ...context,
601
+ buffer: result.next.buffer,
602
+ optionsTerminated: true,
603
+ state: context.state,
604
+ ...mergedExec != null ? {
605
+ exec: mergedExec,
606
+ dependencyRegistry: mergedExec.dependencyRegistry
607
+ } : {}
608
+ },
609
+ consumed: result.consumed
610
+ };
611
+ }
612
+ /**
584
613
  * Creates a complete() method shared by or() and longestMatch().
585
614
  * @internal
586
615
  */
@@ -818,9 +847,15 @@ function or(...args) {
818
847
  let zeroConsumedBranch = null;
819
848
  let zeroConsumedCount = 0;
820
849
  let provisionalConsuming = null;
850
+ let optionsTerminatorResult = null;
821
851
  let provisionalAmbiguous = false;
822
852
  for (const [parser, i] of orderedParsers) {
823
853
  const result = parser.parse(withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
854
+ if (isOptionsTerminatorOnlyResult(context, result)) {
855
+ if (activeState?.[0] === i && activeState[1].success && activeState[1].consumed.length > 0 && result.next.buffer.length < 1) return preserveExclusiveStateAfterOptionsTerminator(context, result);
856
+ optionsTerminatorResult ??= result;
857
+ continue;
858
+ }
824
859
  if (result.success && result.consumed.length > 0) {
825
860
  if (result.provisional) {
826
861
  const activeBranchLocked = activeState != null && activeState[1].success && activeState[1].consumed.length > 0;
@@ -1001,6 +1036,7 @@ function or(...args) {
1001
1036
  }
1002
1037
  }
1003
1038
  }
1039
+ if (optionsTerminatorResult != null) return preserveExclusiveStateAfterOptionsTerminator(context, optionsTerminatorResult);
1004
1040
  return {
1005
1041
  ...error,
1006
1042
  success: false
@@ -1014,10 +1050,16 @@ function or(...args) {
1014
1050
  let zeroConsumedBranch = null;
1015
1051
  let zeroConsumedCount = 0;
1016
1052
  let provisionalConsuming = null;
1053
+ let optionsTerminatorResult = null;
1017
1054
  let provisionalAmbiguous = false;
1018
1055
  for (const [parser, i] of orderedParsers) {
1019
1056
  const resultOrPromise = parser.parse(withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1020
1057
  const result = await resultOrPromise;
1058
+ if (isOptionsTerminatorOnlyResult(context, result)) {
1059
+ if (activeState?.[0] === i && activeState[1].success && activeState[1].consumed.length > 0 && result.next.buffer.length < 1) return preserveExclusiveStateAfterOptionsTerminator(context, result);
1060
+ optionsTerminatorResult ??= result;
1061
+ continue;
1062
+ }
1021
1063
  if (result.success && result.consumed.length > 0) {
1022
1064
  if (result.provisional) {
1023
1065
  const activeBranchLocked = activeState != null && activeState[1].success && activeState[1].consumed.length > 0;
@@ -1200,6 +1242,7 @@ function or(...args) {
1200
1242
  }
1201
1243
  }
1202
1244
  }
1245
+ if (optionsTerminatorResult != null) return preserveExclusiveStateAfterOptionsTerminator(context, optionsTerminatorResult);
1203
1246
  return {
1204
1247
  ...error,
1205
1248
  success: false
@@ -1301,82 +1344,122 @@ function createLongestMatch(...args) {
1301
1344
  return createUnexpectedInputErrorWithScopedSuggestions(defaultMsg, token, parsers, options?.errors?.suggestions);
1302
1345
  })()
1303
1346
  });
1304
- const parseSync = (context) => {
1347
+ const commitLongestMatch = (context, match, optionsTerminatorResult) => {
1348
+ const selectedResult = optionsTerminatorResult == null ? match.result : {
1349
+ ...match.result,
1350
+ next: {
1351
+ ...match.result.next,
1352
+ buffer: optionsTerminatorResult.next.buffer,
1353
+ optionsTerminated: true
1354
+ },
1355
+ consumed: [...optionsTerminatorResult.consumed, ...match.result.consumed]
1356
+ };
1357
+ const parser = parsers[match.index];
1358
+ const mergedExec = mergeChildExec(context.exec, selectedResult.next.exec);
1359
+ return {
1360
+ success: true,
1361
+ next: {
1362
+ ...context,
1363
+ buffer: selectedResult.next.buffer,
1364
+ optionsTerminated: selectedResult.next.optionsTerminated,
1365
+ state: createExclusiveState(context.state, match.index, parser, selectedResult),
1366
+ ...mergedExec != null ? {
1367
+ exec: mergedExec,
1368
+ dependencyRegistry: mergedExec.dependencyRegistry
1369
+ } : {}
1370
+ },
1371
+ consumed: selectedResult.consumed
1372
+ };
1373
+ };
1374
+ const resolveLongestMatchOptionsTerminator = (context, bestMatch, optionsTerminatorMatches) => {
1375
+ if (optionsTerminatorMatches.length < 1) return null;
1376
+ if (bestMatch != null && bestMatch.consumed > 0) return null;
1377
+ const activeState = normalizeExclusiveState(context.state);
1378
+ if (activeState != null && activeState[1].success && activeState[1].consumed.length > 0) return preserveExclusiveStateAfterOptionsTerminator(context, optionsTerminatorMatches[0].result);
1379
+ let selected = bestMatch == null ? null : {
1380
+ match: bestMatch,
1381
+ fromTerminator: false
1382
+ };
1383
+ for (const match of optionsTerminatorMatches) {
1384
+ if (!match.selectedAfterTerminator && (extractRequiredUsage(parsers[match.index].usage).length > 0 || isUnselectedBooleanOptionState(match.result.next.state))) continue;
1385
+ const selectedIsProvisional = selected != null && !!selected.match.result.provisional;
1386
+ const matchIsProvisional = !!match.result.provisional;
1387
+ if (selected == null || selectedIsProvisional && !matchIsProvisional || selectedIsProvisional === matchIsProvisional && match.index < selected.match.index) selected = {
1388
+ match,
1389
+ fromTerminator: true
1390
+ };
1391
+ }
1392
+ if (selected != null) return commitLongestMatch(context, selected.match, selected.fromTerminator ? void 0 : optionsTerminatorMatches[0].result);
1393
+ return preserveExclusiveStateAfterOptionsTerminator(context, optionsTerminatorMatches[0].result);
1394
+ };
1395
+ const parseSyncBranches = (context) => {
1305
1396
  let bestMatch = null;
1397
+ const optionsTerminatorMatches = [];
1306
1398
  let error = getInitialError(context);
1307
1399
  const activeState = normalizeExclusiveState(context.state);
1308
1400
  for (let i = 0; i < syncParsers.length; i++) {
1309
1401
  const parser = syncParsers[i];
1310
- const result = parser.parse(withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1402
+ const childContext = withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser);
1403
+ const result = parser.parse(childContext);
1404
+ if (isOptionsTerminatorOnlyResult(context, result)) {
1405
+ optionsTerminatorMatches.push({
1406
+ index: i,
1407
+ result,
1408
+ consumed: 1,
1409
+ ...normalizeExclusiveState(result.next.state) != null ? { selectedAfterTerminator: true } : {}
1410
+ });
1411
+ continue;
1412
+ }
1311
1413
  if (result.success) {
1312
1414
  const consumed = context.buffer.length - result.next.buffer.length;
1313
1415
  const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1314
1416
  if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1315
1417
  index: i,
1316
- parser,
1317
1418
  result,
1318
1419
  consumed
1319
1420
  };
1320
1421
  } else if (error.consumed < result.consumed) error = result;
1321
1422
  }
1322
- if (bestMatch && bestMatch.result.success) {
1323
- const mergedExec = mergeChildExec(context.exec, bestMatch.result.next.exec);
1324
- return {
1325
- success: true,
1326
- next: {
1327
- ...context,
1328
- buffer: bestMatch.result.next.buffer,
1329
- optionsTerminated: bestMatch.result.next.optionsTerminated,
1330
- state: createExclusiveState(context.state, bestMatch.index, bestMatch.parser, bestMatch.result),
1331
- ...mergedExec != null ? {
1332
- exec: mergedExec,
1333
- dependencyRegistry: mergedExec.dependencyRegistry
1334
- } : {}
1335
- },
1336
- consumed: bestMatch.result.consumed
1337
- };
1338
- }
1423
+ const optionsTerminatorResolution = resolveLongestMatchOptionsTerminator(context, bestMatch, optionsTerminatorMatches);
1424
+ if (optionsTerminatorResolution != null) return optionsTerminatorResolution;
1425
+ if (bestMatch != null) return commitLongestMatch(context, bestMatch);
1339
1426
  return {
1340
1427
  ...error,
1341
1428
  success: false
1342
1429
  };
1343
1430
  };
1344
- const parseAsync = async (context) => {
1431
+ const parseAsyncBranches = async (context) => {
1345
1432
  let bestMatch = null;
1433
+ const optionsTerminatorMatches = [];
1346
1434
  let error = getInitialError(context);
1347
1435
  const activeState = normalizeExclusiveState(context.state);
1348
1436
  for (let i = 0; i < parsers.length; i++) {
1349
1437
  const parser = parsers[i];
1350
- const resultOrPromise = parser.parse(withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1438
+ const childContext = withChildContext$1(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser);
1439
+ const resultOrPromise = parser.parse(childContext);
1351
1440
  const result = await resultOrPromise;
1441
+ if (isOptionsTerminatorOnlyResult(context, result)) {
1442
+ optionsTerminatorMatches.push({
1443
+ index: i,
1444
+ result,
1445
+ consumed: 1,
1446
+ ...normalizeExclusiveState(result.next.state) != null ? { selectedAfterTerminator: true } : {}
1447
+ });
1448
+ continue;
1449
+ }
1352
1450
  if (result.success) {
1353
1451
  const consumed = context.buffer.length - result.next.buffer.length;
1354
1452
  const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1355
1453
  if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1356
1454
  index: i,
1357
- parser,
1358
1455
  result,
1359
1456
  consumed
1360
1457
  };
1361
1458
  } else if (error.consumed < result.consumed) error = result;
1362
1459
  }
1363
- if (bestMatch && bestMatch.result.success) {
1364
- const mergedExec = mergeChildExec(context.exec, bestMatch.result.next.exec);
1365
- return {
1366
- success: true,
1367
- next: {
1368
- ...context,
1369
- buffer: bestMatch.result.next.buffer,
1370
- optionsTerminated: bestMatch.result.next.optionsTerminated,
1371
- state: createExclusiveState(context.state, bestMatch.index, bestMatch.parser, bestMatch.result),
1372
- ...mergedExec != null ? {
1373
- exec: mergedExec,
1374
- dependencyRegistry: mergedExec.dependencyRegistry
1375
- } : {}
1376
- },
1377
- consumed: bestMatch.result.consumed
1378
- };
1379
- }
1460
+ const optionsTerminatorResolution = resolveLongestMatchOptionsTerminator(context, bestMatch, optionsTerminatorMatches);
1461
+ if (optionsTerminatorResolution != null) return optionsTerminatorResolution;
1462
+ if (bestMatch != null) return commitLongestMatch(context, bestMatch);
1380
1463
  return {
1381
1464
  ...error,
1382
1465
  success: false
@@ -1399,7 +1482,7 @@ function createLongestMatch(...args) {
1399
1482
  return extractExclusivePhase2Seed(parsers, state, exec, combinedMode);
1400
1483
  },
1401
1484
  parse(context) {
1402
- return dispatchByMode(combinedMode, () => parseSync(context), () => parseAsync(context));
1485
+ return dispatchByMode(combinedMode, () => parseSyncBranches(context), () => parseAsyncBranches(context));
1403
1486
  },
1404
1487
  getSuggestRuntimeNodes(state, path) {
1405
1488
  return getExclusiveSuggestRuntimeNodes(parsers, state, path);
@@ -1445,6 +1528,33 @@ function createLongestMatch(...args) {
1445
1528
  return multiResult;
1446
1529
  }
1447
1530
  /**
1531
+ * Creates the initial parse error shared by object-like combinators.
1532
+ * @param context The current parser context.
1533
+ * @param noMatchContext The kinds of input accepted by the combinator.
1534
+ * @param errors Optional custom error formatters.
1535
+ * @returns A zero-consumption parse error.
1536
+ */
1537
+ function createObjectLikeInitialError(context, noMatchContext, errors) {
1538
+ if (context.buffer.length < 1) {
1539
+ const customEndOfInput = errors?.endOfInput;
1540
+ return {
1541
+ consumed: 0,
1542
+ error: customEndOfInput ? typeof customEndOfInput === "function" ? customEndOfInput(noMatchContext) : customEndOfInput : generateNoMatchError(noMatchContext)
1543
+ };
1544
+ }
1545
+ const token = context.buffer[0];
1546
+ const customMessage = errors?.unexpectedInput;
1547
+ if (customMessage) return {
1548
+ consumed: 0,
1549
+ error: typeof customMessage === "function" ? customMessage(token) : customMessage
1550
+ };
1551
+ const baseError = message`Unexpected option or argument: ${token}.`;
1552
+ return {
1553
+ consumed: 0,
1554
+ error: createErrorWithSuggestions(baseError, token, context.usage, "both", errors?.suggestions)
1555
+ };
1556
+ }
1557
+ /**
1448
1558
  * Internal sync helper for object suggest functionality.
1449
1559
  * @internal
1450
1560
  */
@@ -1837,19 +1947,7 @@ function object(labelOrParsers, maybeParsersOrOptions, maybeOptions) {
1837
1947
  checkDuplicateReachableLeadingCommandNames(parserPairs.map(([field, parser]) => [field, parser]));
1838
1948
  const noMatchContext = analyzeNoMatchContext(parserKeys.map((k) => parsers[k]));
1839
1949
  const combinedMode = parserKeys.some((k) => parsers[k].mode === "async") ? "async" : "sync";
1840
- const getInitialError = (context) => ({
1841
- consumed: 0,
1842
- error: context.buffer.length > 0 ? (() => {
1843
- const token = context.buffer[0];
1844
- const customMessage = options.errors?.unexpectedInput;
1845
- if (customMessage) return typeof customMessage === "function" ? customMessage(token) : customMessage;
1846
- const baseError = message`Unexpected option or argument: ${token}.`;
1847
- return createErrorWithSuggestions(baseError, token, context.usage, "both", options.errors?.suggestions);
1848
- })() : (() => {
1849
- const customEndOfInput = options.errors?.endOfInput;
1850
- return customEndOfInput ? typeof customEndOfInput === "function" ? customEndOfInput(noMatchContext) : customEndOfInput : generateNoMatchError(noMatchContext);
1851
- })()
1852
- });
1950
+ const getInitialError = (context) => createObjectLikeInitialError(context, noMatchContext, options.errors);
1853
1951
  const parseSync = (context) => {
1854
1952
  let error = getInitialError(context);
1855
1953
  let currentContext = context;
@@ -3719,6 +3817,7 @@ function merge(...args) {
3719
3817
  const syncParsers = syncSorted.map(([p]) => p);
3720
3818
  if (!options.allowDuplicates) checkDuplicateOptionNames(sorted.map(([parser, originalIndex]) => [String(originalIndex), parser.usage]));
3721
3819
  checkDuplicateReachableLeadingCommandNames(sorted.map(([parser, originalIndex]) => [String(originalIndex), parser]));
3820
+ const noMatchContext = analyzeNoMatchContext(rawParsers);
3722
3821
  const mergedFieldParsers = collectChildFieldParsers(parsers);
3723
3822
  const duplicateOutputFieldNames = collectDuplicateFieldNames(mergedFieldParsers);
3724
3823
  const parserStateKey = (index) => `__parser_${index}`;
@@ -3812,8 +3911,7 @@ function merge(...args) {
3812
3911
  };
3813
3912
  return {
3814
3913
  success: false,
3815
- consumed: 0,
3816
- error: message`No matching option or argument found.`
3914
+ ...createObjectLikeInitialError(context, noMatchContext)
3817
3915
  };
3818
3916
  };
3819
3917
  const parseAsync = async (context) => {
@@ -3858,8 +3956,7 @@ function merge(...args) {
3858
3956
  };
3859
3957
  return {
3860
3958
  success: false,
3861
- consumed: 0,
3862
- error: message`No matching option or argument found.`
3959
+ ...createObjectLikeInitialError(context, noMatchContext)
3863
3960
  };
3864
3961
  };
3865
3962
  const mergeParser = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -200,7 +200,7 @@
200
200
  "fast-check": "^4.7.0",
201
201
  "tsdown": "^0.13.0",
202
202
  "typescript": "^5.8.3",
203
- "@optique/env": "1.1.2"
203
+ "@optique/env": "1.1.4"
204
204
  },
205
205
  "scripts": {
206
206
  "build": "tsdown",