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.
@@ -48,6 +48,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
48
48
  return d.value.length === 0;
49
49
  case "keywords":
50
50
  return false;
51
+ case "routed":
52
+ return false;
51
53
  case "regex":
52
54
  try {
53
55
  const m = new RegExp(d.source).exec("");
@@ -286,6 +288,7 @@ function markUnusedValues(root) {
286
288
  case "dispatch":
287
289
  visit(def.selector, consumed);
288
290
  for (const entry of def.cases) visit(entry.parser, consumed);
291
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed);
289
292
  if (def.otherwise) visit(def.otherwise, consumed);
290
293
  return;
291
294
  case "node":
@@ -739,6 +742,8 @@ function derive(c, seen) {
739
742
  case "not":
740
743
  case "peek":
741
744
  return deriveExpected2(def.parser);
745
+ case "routed":
746
+ return ["routed()"];
742
747
  case "lazy": {
743
748
  const name = c._ruleName;
744
749
  if (seen.has(c)) return name ? [name] : [];
@@ -2287,7 +2292,7 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2287
2292
  case "choice":
2288
2293
  return d.parsers.some((x) => parserHasOwnFields(x, seen));
2289
2294
  case "dispatch":
2290
- return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2295
+ 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);
2291
2296
  case "sepBy":
2292
2297
  return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen);
2293
2298
  case "skip":
@@ -2403,42 +2408,195 @@ function keywords(words, opts = {}) {
2403
2408
  }
2404
2409
 
2405
2410
  // src/combinators/dispatch.ts
2406
- function when(key, parser2) {
2411
+ function parserUsesRouted(parser2, seen = /* @__PURE__ */ new Set()) {
2412
+ if (seen.has(parser2)) return false;
2413
+ seen.add(parser2);
2414
+ const def = parser2._def;
2415
+ switch (def.tag) {
2416
+ case "routed":
2417
+ return true;
2418
+ case "sequence":
2419
+ case "choice":
2420
+ return def.parsers.some((entry) => parserUsesRouted(entry, seen));
2421
+ case "dispatch":
2422
+ return false;
2423
+ case "sepBy":
2424
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen);
2425
+ case "skip":
2426
+ return parserUsesRouted(def.main, seen) || parserUsesRouted(def.skipped, seen);
2427
+ case "grammar":
2428
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false);
2429
+ case "scanTo":
2430
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some((entry) => parserUsesRouted(entry, seen));
2431
+ case "recover":
2432
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen);
2433
+ case "lazy":
2434
+ try {
2435
+ return parserUsesRouted(def.thunk(), seen);
2436
+ } catch {
2437
+ return false;
2438
+ }
2439
+ case "many":
2440
+ case "oneOrMore":
2441
+ case "optional":
2442
+ case "attempt":
2443
+ case "transform":
2444
+ case "trivia":
2445
+ case "token":
2446
+ case "leaf":
2447
+ case "label":
2448
+ case "field":
2449
+ case "not":
2450
+ case "peek":
2451
+ case "node":
2452
+ case "withCtx":
2453
+ case "expect":
2454
+ return parserUsesRouted(def.parser, seen);
2455
+ default:
2456
+ return false;
2457
+ }
2458
+ }
2459
+ function branchUsesRouted(branch) {
2460
+ return branch.usesRouted === true || parserUsesRouted(branch.parser);
2461
+ }
2462
+ function asciiFoldKey(key) {
2463
+ let out = "";
2464
+ for (let i = 0; i < key.length; i++) {
2465
+ const c = key.charCodeAt(i);
2466
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c);
2467
+ }
2468
+ return out;
2469
+ }
2470
+ function matchesDispatchMatcher(value, matcher) {
2471
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value;
2472
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value;
2473
+ switch (matcher.kind) {
2474
+ case "startsWith":
2475
+ return candidate.startsWith(expected);
2476
+ case "endsWith":
2477
+ return candidate.endsWith(expected);
2478
+ case "matches": {
2479
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
2480
+ return new RegExp(matcher.value, flags).test(value);
2481
+ }
2482
+ }
2483
+ }
2484
+ function isStringMatcher(value) {
2485
+ if (typeof value !== "object" || value === null) return false;
2486
+ const kind = value.kind;
2487
+ return kind === "startsWith" || kind === "endsWith" || kind === "matches";
2488
+ }
2489
+ function fixedMatcher(kind, value) {
2490
+ if (typeof value !== "string") throw new TypeError(`parseman: ${kind}() value must be a string`);
2491
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`);
2492
+ return { kind, value };
2493
+ }
2494
+ function startsWith(prefix) {
2495
+ return fixedMatcher("startsWith", prefix);
2496
+ }
2497
+ function endsWith(suffix) {
2498
+ return fixedMatcher("endsWith", suffix);
2499
+ }
2500
+ function matches(pattern) {
2501
+ if (!(pattern instanceof RegExp)) throw new TypeError("parseman: matches() requires a RegExp");
2502
+ if (pattern.flags.includes("g") || pattern.flags.includes("y")) {
2503
+ throw new TypeError("parseman: matches() does not accept global or sticky regex flags");
2504
+ }
2505
+ return { kind: "matches", value: pattern.source, flags: pattern.flags };
2506
+ }
2507
+ function when(key, parser2, opts = {}) {
2508
+ const usesRouted = parserUsesRouted(parser2);
2509
+ if (isStringMatcher(key)) {
2510
+ return { kind: "whenMatcher", matcher: key, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2511
+ }
2407
2512
  const keys = Array.isArray(key) ? [...key] : [key];
2408
2513
  if (keys.length === 0) throw new RangeError("parseman: when() requires at least one key");
2409
2514
  for (const item of keys) {
2410
2515
  if (typeof item !== "string") throw new TypeError("parseman: when() keys must be strings");
2411
2516
  }
2412
- return { kind: "when", keys, parser: parser2 };
2517
+ return { kind: "when", keys, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2413
2518
  }
2414
2519
  function otherwise(parser2) {
2415
- return { kind: "otherwise", parser: parser2 };
2520
+ return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2521
+ }
2522
+ function routed() {
2523
+ return {
2524
+ _tag: "routed",
2525
+ _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2526
+ _def: { tag: "routed" },
2527
+ parse(_input, pos, ctx) {
2528
+ const item = ctx._routed;
2529
+ if (item === void 0) {
2530
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2531
+ }
2532
+ if (pos !== item.span.start) {
2533
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2534
+ }
2535
+ if (cstCaptureActive(ctx)) {
2536
+ pushCstLeaf(ctx, { _tag: "leaf", value: item.value, span: item.span });
2537
+ }
2538
+ return { ok: true, value: item.value, span: item.span };
2539
+ }
2540
+ };
2416
2541
  }
2417
2542
  function dispatch(selector, ...arms) {
2543
+ if (parserUsesRouted(selector)) {
2544
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2545
+ }
2418
2546
  let fallback;
2547
+ let fallbackUsesRouted = false;
2548
+ const matchers = [];
2419
2549
  const cases = [];
2420
- const seen = /* @__PURE__ */ new Set();
2550
+ const seen = [];
2421
2551
  for (let i = 0; i < arms.length; i++) {
2422
2552
  const arm = arms[i];
2423
2553
  if (arm.kind === "otherwise") {
2424
2554
  if (fallback !== void 0) throw new Error("parseman: dispatch() accepts at most one otherwise() arm");
2425
2555
  if (i !== arms.length - 1) throw new Error("parseman: otherwise() must be the last dispatch() arm");
2426
2556
  fallback = arm.parser;
2557
+ fallbackUsesRouted = arm.usesRouted === true;
2558
+ continue;
2559
+ }
2560
+ if (arm.kind === "whenMatcher") {
2561
+ const matcher = arm.matcher;
2562
+ matchers.push({
2563
+ kind: matcher.kind,
2564
+ value: matcher.value,
2565
+ ...matcher.kind === "matches" ? { flags: matcher.flags } : {},
2566
+ parser: arm.parser,
2567
+ caseInsensitive: arm.caseInsensitive,
2568
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2569
+ });
2427
2570
  continue;
2428
2571
  }
2429
2572
  for (const key of arm.keys) {
2430
- if (seen.has(key)) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2431
- seen.add(key);
2573
+ const folded = asciiFoldKey(key);
2574
+ for (const prior of seen) {
2575
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive ? folded === prior.folded : key === prior.raw;
2576
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2577
+ }
2578
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive });
2432
2579
  }
2433
- cases.push({ keys: arm.keys, parser: arm.parser });
2580
+ cases.push({
2581
+ keys: arm.keys,
2582
+ parser: arm.parser,
2583
+ caseInsensitive: arm.caseInsensitive,
2584
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2585
+ });
2434
2586
  }
2435
2587
  const meta = {
2436
2588
  firstSet: selector._meta.firstSet,
2437
- canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2589
+ canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || matchers.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2438
2590
  isTrivia: false
2439
2591
  };
2440
2592
  const byKey = /* @__PURE__ */ new Map();
2441
- for (const entry of cases) for (const key of entry.keys) byKey.set(key, entry.parser);
2593
+ const byFoldedKey = /* @__PURE__ */ new Map();
2594
+ for (const entry of cases) {
2595
+ for (const key of entry.keys) {
2596
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry);
2597
+ else byKey.set(key, entry);
2598
+ }
2599
+ }
2442
2600
  return {
2443
2601
  _tag: "dispatch",
2444
2602
  _meta: meta,
@@ -2446,18 +2604,35 @@ function dispatch(selector, ...arms) {
2446
2604
  tag: "dispatch",
2447
2605
  selector,
2448
2606
  cases,
2449
- ...fallback === void 0 ? {} : { otherwise: fallback }
2607
+ ...matchers.length === 0 ? {} : { matchers },
2608
+ ...fallback === void 0 ? {} : { otherwise: fallback, ...fallbackUsesRouted ? { otherwiseUsesRouted: true } : {} }
2450
2609
  },
2451
2610
  parse(input, pos, ctx) {
2611
+ if (parserUsesRouted(selector)) {
2612
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2613
+ }
2614
+ const selectorMark = saveTriviaMark(ctx);
2452
2615
  const selected = selector.parse(input, pos, ctx);
2453
2616
  if (!selected.ok) return selected;
2454
- const tail = byKey.get(selected.value) ?? fallback;
2455
- if (tail === void 0) {
2617
+ 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 });
2618
+ if (branch === void 0) {
2456
2619
  const expected = cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key)));
2457
2620
  return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } };
2458
2621
  }
2459
- const mark = saveTriviaMark(ctx);
2460
- const result = tail.parse(input, selected.span.end, ctx);
2622
+ const savedRouted = ctx._routed;
2623
+ const usesRouted = branchUsesRouted(branch);
2624
+ let mark = saveTriviaMark(ctx);
2625
+ if (usesRouted) {
2626
+ rollbackTrivia(ctx, selectorMark);
2627
+ mark = saveTriviaMark(ctx);
2628
+ ctx._routed = { value: selected.value, span: selected.span };
2629
+ }
2630
+ let result;
2631
+ try {
2632
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx);
2633
+ } finally {
2634
+ if (usesRouted) ctx._routed = savedRouted;
2635
+ }
2461
2636
  if (!result.ok) {
2462
2637
  rollbackTrivia(ctx, mark);
2463
2638
  return { ...result, committed: true };
@@ -2905,7 +3080,11 @@ function evalRuleMapIR(ir) {
2905
3080
  "choice",
2906
3081
  "dispatch",
2907
3082
  "when",
3083
+ "startsWith",
3084
+ "endsWith",
3085
+ "matches",
2908
3086
  "otherwise",
3087
+ "routed",
2909
3088
  "attempt",
2910
3089
  "many",
2911
3090
  "oneOrMore",
@@ -2941,7 +3120,11 @@ function evalRuleMapIR(ir) {
2941
3120
  choice,
2942
3121
  dispatch,
2943
3122
  when,
3123
+ startsWith,
3124
+ endsWith,
3125
+ matches,
2944
3126
  otherwise,
3127
+ routed,
2945
3128
  attempt,
2946
3129
  many,
2947
3130
  oneOrMore,
@@ -3140,8 +3323,9 @@ var Builder = class {
3140
3323
  const selector = this.walk(def.selector);
3141
3324
  const tails = [
3142
3325
  ...def.cases.map((entry) => this.walk(entry.parser)),
3326
+ ...def.matchers ? def.matchers.map((entry) => this.walk(entry.parser)) : [],
3143
3327
  ...def.otherwise ? [this.walk(def.otherwise)] : []
3144
- ].filter(nonEmpty);
3328
+ ];
3145
3329
  return flattenSeq([selector, flattenChoice(tails)]);
3146
3330
  }
3147
3331
  // `max`/`min` are carried through so a BOUNDED repeat does not render as an
@@ -3173,6 +3357,8 @@ var Builder = class {
3173
3357
  return { kind: "not", item: this.walk(def.parser) };
3174
3358
  case "peek":
3175
3359
  return { kind: "peek", item: this.walk(def.parser) };
3360
+ case "routed":
3361
+ return { kind: "empty" };
3176
3362
  // Transparent semantic wrappers — render the inner syntax.
3177
3363
  case "transform":
3178
3364
  case "token":
@@ -3330,7 +3516,7 @@ function render(node2) {
3330
3516
  case "choice": {
3331
3517
  const text = node2.items.map((it) => {
3332
3518
  const r = render(it);
3333
- return wrap(r.text, r.level, ALT);
3519
+ return r.text.length === 0 ? "/* empty */" : wrap(r.text, r.level, ALT);
3334
3520
  }).join(" | ");
3335
3521
  return { text, level: ALT };
3336
3522
  }