parseman 0.39.0 → 0.39.1

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.
@@ -82,6 +82,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
82
82
  return d.value.length === 0;
83
83
  case "keywords":
84
84
  return false;
85
+ case "routed":
86
+ return false;
85
87
  case "regex":
86
88
  try {
87
89
  const m = new RegExp(d.source).exec("");
@@ -320,6 +322,7 @@ function markUnusedValues(root) {
320
322
  case "dispatch":
321
323
  visit(def.selector, consumed);
322
324
  for (const entry of def.cases) visit(entry.parser, consumed);
325
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed);
323
326
  if (def.otherwise) visit(def.otherwise, consumed);
324
327
  return;
325
328
  case "node":
@@ -773,6 +776,8 @@ function derive(c, seen) {
773
776
  case "not":
774
777
  case "peek":
775
778
  return deriveExpected2(def.parser);
779
+ case "routed":
780
+ return ["routed()"];
776
781
  case "lazy": {
777
782
  const name = c._ruleName;
778
783
  if (seen.has(c)) return name ? [name] : [];
@@ -2321,7 +2326,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2321
2326
  case "choice":
2322
2327
  return d.parsers.some((x) => parserHasOwnFields(x, seen));
2323
2328
  case "dispatch":
2324
- return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2329
+ return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.matchers ? d.matchers.some((entry) => parserHasOwnFields(entry.parser, seen)) : false) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2325
2330
  case "sepBy":
2326
2331
  return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen);
2327
2332
  case "skip":
@@ -2437,42 +2442,195 @@ function keywords(words, opts = {}) {
2437
2442
  }
2438
2443
 
2439
2444
  // src/combinators/dispatch.ts
2440
- function when(key, parser2) {
2445
+ function parserUsesRouted(parser2, seen = /* @__PURE__ */ new Set()) {
2446
+ if (seen.has(parser2)) return false;
2447
+ seen.add(parser2);
2448
+ const def = parser2._def;
2449
+ switch (def.tag) {
2450
+ case "routed":
2451
+ return true;
2452
+ case "sequence":
2453
+ case "choice":
2454
+ return def.parsers.some((entry) => parserUsesRouted(entry, seen));
2455
+ case "dispatch":
2456
+ return false;
2457
+ case "sepBy":
2458
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen);
2459
+ case "skip":
2460
+ return parserUsesRouted(def.main, seen) || parserUsesRouted(def.skipped, seen);
2461
+ case "grammar":
2462
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false);
2463
+ case "scanTo":
2464
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some((entry) => parserUsesRouted(entry, seen));
2465
+ case "recover":
2466
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen);
2467
+ case "lazy":
2468
+ try {
2469
+ return parserUsesRouted(def.thunk(), seen);
2470
+ } catch {
2471
+ return false;
2472
+ }
2473
+ case "many":
2474
+ case "oneOrMore":
2475
+ case "optional":
2476
+ case "attempt":
2477
+ case "transform":
2478
+ case "trivia":
2479
+ case "token":
2480
+ case "leaf":
2481
+ case "label":
2482
+ case "field":
2483
+ case "not":
2484
+ case "peek":
2485
+ case "node":
2486
+ case "withCtx":
2487
+ case "expect":
2488
+ return parserUsesRouted(def.parser, seen);
2489
+ default:
2490
+ return false;
2491
+ }
2492
+ }
2493
+ function branchUsesRouted(branch) {
2494
+ return branch.usesRouted === true || parserUsesRouted(branch.parser);
2495
+ }
2496
+ function asciiFoldKey(key) {
2497
+ let out = "";
2498
+ for (let i = 0; i < key.length; i++) {
2499
+ const c = key.charCodeAt(i);
2500
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c);
2501
+ }
2502
+ return out;
2503
+ }
2504
+ function matchesDispatchMatcher(value, matcher) {
2505
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value;
2506
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value;
2507
+ switch (matcher.kind) {
2508
+ case "startsWith":
2509
+ return candidate.startsWith(expected);
2510
+ case "endsWith":
2511
+ return candidate.endsWith(expected);
2512
+ case "matches": {
2513
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
2514
+ return new RegExp(matcher.value, flags).test(value);
2515
+ }
2516
+ }
2517
+ }
2518
+ function isStringMatcher(value) {
2519
+ if (typeof value !== "object" || value === null) return false;
2520
+ const kind = value.kind;
2521
+ return kind === "startsWith" || kind === "endsWith" || kind === "matches";
2522
+ }
2523
+ function fixedMatcher(kind, value) {
2524
+ if (typeof value !== "string") throw new TypeError(`parseman: ${kind}() value must be a string`);
2525
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`);
2526
+ return { kind, value };
2527
+ }
2528
+ function startsWith(prefix) {
2529
+ return fixedMatcher("startsWith", prefix);
2530
+ }
2531
+ function endsWith(suffix) {
2532
+ return fixedMatcher("endsWith", suffix);
2533
+ }
2534
+ function matches(pattern) {
2535
+ if (!(pattern instanceof RegExp)) throw new TypeError("parseman: matches() requires a RegExp");
2536
+ if (pattern.flags.includes("g") || pattern.flags.includes("y")) {
2537
+ throw new TypeError("parseman: matches() does not accept global or sticky regex flags");
2538
+ }
2539
+ return { kind: "matches", value: pattern.source, flags: pattern.flags };
2540
+ }
2541
+ function when(key, parser2, opts = {}) {
2542
+ const usesRouted = parserUsesRouted(parser2);
2543
+ if (isStringMatcher(key)) {
2544
+ return { kind: "whenMatcher", matcher: key, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2545
+ }
2441
2546
  const keys = Array.isArray(key) ? [...key] : [key];
2442
2547
  if (keys.length === 0) throw new RangeError("parseman: when() requires at least one key");
2443
2548
  for (const item of keys) {
2444
2549
  if (typeof item !== "string") throw new TypeError("parseman: when() keys must be strings");
2445
2550
  }
2446
- return { kind: "when", keys, parser: parser2 };
2551
+ return { kind: "when", keys, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2447
2552
  }
2448
2553
  function otherwise(parser2) {
2449
- return { kind: "otherwise", parser: parser2 };
2554
+ return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2555
+ }
2556
+ function routed() {
2557
+ return {
2558
+ _tag: "routed",
2559
+ _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2560
+ _def: { tag: "routed" },
2561
+ parse(_input, pos, ctx) {
2562
+ const item = ctx._routed;
2563
+ if (item === void 0) {
2564
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2565
+ }
2566
+ if (pos !== item.span.start) {
2567
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2568
+ }
2569
+ if (cstCaptureActive(ctx)) {
2570
+ pushCstLeaf(ctx, { _tag: "leaf", value: item.value, span: item.span });
2571
+ }
2572
+ return { ok: true, value: item.value, span: item.span };
2573
+ }
2574
+ };
2450
2575
  }
2451
2576
  function dispatch(selector, ...arms) {
2577
+ if (parserUsesRouted(selector)) {
2578
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2579
+ }
2452
2580
  let fallback;
2581
+ let fallbackUsesRouted = false;
2582
+ const matchers = [];
2453
2583
  const cases = [];
2454
- const seen = /* @__PURE__ */ new Set();
2584
+ const seen = [];
2455
2585
  for (let i = 0; i < arms.length; i++) {
2456
2586
  const arm = arms[i];
2457
2587
  if (arm.kind === "otherwise") {
2458
2588
  if (fallback !== void 0) throw new Error("parseman: dispatch() accepts at most one otherwise() arm");
2459
2589
  if (i !== arms.length - 1) throw new Error("parseman: otherwise() must be the last dispatch() arm");
2460
2590
  fallback = arm.parser;
2591
+ fallbackUsesRouted = arm.usesRouted === true;
2592
+ continue;
2593
+ }
2594
+ if (arm.kind === "whenMatcher") {
2595
+ const matcher = arm.matcher;
2596
+ matchers.push({
2597
+ kind: matcher.kind,
2598
+ value: matcher.value,
2599
+ ...matcher.kind === "matches" ? { flags: matcher.flags } : {},
2600
+ parser: arm.parser,
2601
+ caseInsensitive: arm.caseInsensitive,
2602
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2603
+ });
2461
2604
  continue;
2462
2605
  }
2463
2606
  for (const key of arm.keys) {
2464
- if (seen.has(key)) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2465
- seen.add(key);
2607
+ const folded = asciiFoldKey(key);
2608
+ for (const prior of seen) {
2609
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive ? folded === prior.folded : key === prior.raw;
2610
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2611
+ }
2612
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive });
2466
2613
  }
2467
- cases.push({ keys: arm.keys, parser: arm.parser });
2614
+ cases.push({
2615
+ keys: arm.keys,
2616
+ parser: arm.parser,
2617
+ caseInsensitive: arm.caseInsensitive,
2618
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2619
+ });
2468
2620
  }
2469
2621
  const meta = {
2470
2622
  firstSet: selector._meta.firstSet,
2471
- canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2623
+ canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || matchers.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2472
2624
  isTrivia: false
2473
2625
  };
2474
2626
  const byKey = /* @__PURE__ */ new Map();
2475
- for (const entry of cases) for (const key of entry.keys) byKey.set(key, entry.parser);
2627
+ const byFoldedKey = /* @__PURE__ */ new Map();
2628
+ for (const entry of cases) {
2629
+ for (const key of entry.keys) {
2630
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry);
2631
+ else byKey.set(key, entry);
2632
+ }
2633
+ }
2476
2634
  return {
2477
2635
  _tag: "dispatch",
2478
2636
  _meta: meta,
@@ -2480,18 +2638,35 @@ function dispatch(selector, ...arms) {
2480
2638
  tag: "dispatch",
2481
2639
  selector,
2482
2640
  cases,
2483
- ...fallback === void 0 ? {} : { otherwise: fallback }
2641
+ ...matchers.length === 0 ? {} : { matchers },
2642
+ ...fallback === void 0 ? {} : { otherwise: fallback, ...fallbackUsesRouted ? { otherwiseUsesRouted: true } : {} }
2484
2643
  },
2485
2644
  parse(input, pos, ctx) {
2645
+ if (parserUsesRouted(selector)) {
2646
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2647
+ }
2648
+ const selectorMark = saveTriviaMark(ctx);
2486
2649
  const selected = selector.parse(input, pos, ctx);
2487
2650
  if (!selected.ok) return selected;
2488
- const tail = byKey.get(selected.value) ?? fallback;
2489
- if (tail === void 0) {
2651
+ const branch = byKey.get(selected.value) ?? (byFoldedKey.size === 0 ? void 0 : byFoldedKey.get(asciiFoldKey(selected.value))) ?? matchers.find((matcher) => matchesDispatchMatcher(selected.value, matcher)) ?? (fallback === void 0 ? void 0 : { keys: [], parser: fallback, caseInsensitive: false, usesRouted: fallbackUsesRouted });
2652
+ if (branch === void 0) {
2490
2653
  const expected = cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key)));
2491
2654
  return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } };
2492
2655
  }
2493
- const mark = saveTriviaMark(ctx);
2494
- const result = tail.parse(input, selected.span.end, ctx);
2656
+ const savedRouted = ctx._routed;
2657
+ const usesRouted = branchUsesRouted(branch);
2658
+ let mark = saveTriviaMark(ctx);
2659
+ if (usesRouted) {
2660
+ rollbackTrivia(ctx, selectorMark);
2661
+ mark = saveTriviaMark(ctx);
2662
+ ctx._routed = { value: selected.value, span: selected.span };
2663
+ }
2664
+ let result;
2665
+ try {
2666
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx);
2667
+ } finally {
2668
+ if (usesRouted) ctx._routed = savedRouted;
2669
+ }
2495
2670
  if (!result.ok) {
2496
2671
  rollbackTrivia(ctx, mark);
2497
2672
  return { ...result, committed: true };
@@ -2939,7 +3114,11 @@ function evalRuleMapIR(ir) {
2939
3114
  "choice",
2940
3115
  "dispatch",
2941
3116
  "when",
3117
+ "startsWith",
3118
+ "endsWith",
3119
+ "matches",
2942
3120
  "otherwise",
3121
+ "routed",
2943
3122
  "attempt",
2944
3123
  "many",
2945
3124
  "oneOrMore",
@@ -2975,7 +3154,11 @@ function evalRuleMapIR(ir) {
2975
3154
  choice,
2976
3155
  dispatch,
2977
3156
  when,
3157
+ startsWith,
3158
+ endsWith,
3159
+ matches,
2978
3160
  otherwise,
3161
+ routed,
2979
3162
  attempt,
2980
3163
  many,
2981
3164
  oneOrMore,
@@ -3174,8 +3357,9 @@ var Builder = class {
3174
3357
  const selector = this.walk(def.selector);
3175
3358
  const tails = [
3176
3359
  ...def.cases.map((entry) => this.walk(entry.parser)),
3360
+ ...def.matchers ? def.matchers.map((entry) => this.walk(entry.parser)) : [],
3177
3361
  ...def.otherwise ? [this.walk(def.otherwise)] : []
3178
- ].filter(nonEmpty);
3362
+ ];
3179
3363
  return flattenSeq([selector, flattenChoice(tails)]);
3180
3364
  }
3181
3365
  // `max`/`min` are carried through so a BOUNDED repeat does not render as an
@@ -3207,6 +3391,8 @@ var Builder = class {
3207
3391
  return { kind: "not", item: this.walk(def.parser) };
3208
3392
  case "peek":
3209
3393
  return { kind: "peek", item: this.walk(def.parser) };
3394
+ case "routed":
3395
+ return { kind: "empty" };
3210
3396
  // Transparent semantic wrappers — render the inner syntax.
3211
3397
  case "transform":
3212
3398
  case "token":
@@ -3364,7 +3550,7 @@ function render(node2) {
3364
3550
  case "choice": {
3365
3551
  const text = node2.items.map((it) => {
3366
3552
  const r = render(it);
3367
- return wrap(r.text, r.level, ALT);
3553
+ return r.text.length === 0 ? "/* empty */" : wrap(r.text, r.level, ALT);
3368
3554
  }).join(" | ");
3369
3555
  return { text, level: ALT };
3370
3556
  }