parseman 0.39.0 → 0.40.0

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 };
@@ -2589,12 +2764,30 @@ function isCstChild(value) {
2589
2764
  function missingInferredType() {
2590
2765
  throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()');
2591
2766
  }
2767
+ function normalizeProject(project) {
2768
+ if (project === void 0) return void 0;
2769
+ if (!Number.isInteger(project) || project < 0) {
2770
+ throw new Error("node() project child must be a non-negative integer");
2771
+ }
2772
+ return project;
2773
+ }
2774
+ function projectChild(children, project, type) {
2775
+ if (!(project in children)) {
2776
+ throw new Error(`node(${JSON.stringify(type)}) project child ${project} was not captured`);
2777
+ }
2778
+ return unwrapChild(children[project]);
2779
+ }
2592
2780
  function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2593
2781
  const hasExplicitType = typeof typeOrCombinator === "string";
2594
2782
  const type = hasExplicitType ? typeOrCombinator : void 0;
2595
2783
  const combinator = hasExplicitType ? combinatorOrBuild : typeOrCombinator;
2596
- const build = hasExplicitType ? buildOrOpts : combinatorOrBuild;
2597
- const opts = hasExplicitType ? maybeOpts : buildOrOpts;
2784
+ const buildArg = hasExplicitType ? buildOrOpts : combinatorOrBuild;
2785
+ const build = typeof buildArg === "function" ? buildArg : void 0;
2786
+ const opts = typeof buildArg === "function" ? maybeOpts : buildArg ?? maybeOpts;
2787
+ const project = normalizeProject(opts?.project);
2788
+ if (project !== void 0 && build !== void 0) {
2789
+ throw new Error("node() options cannot combine project with a build callback");
2790
+ }
2598
2791
  const baseDef = { tag: "node", parser: combinator, ...type === void 0 ? {} : { type }, ...build === void 0 ? {} : { build } };
2599
2792
  const meta = {
2600
2793
  firstSet: combinator._meta.firstSet,
@@ -2608,11 +2801,14 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2608
2801
  if (unwrap && collapse) {
2609
2802
  throw new Error("node() options cannot set both unwrap and collapse");
2610
2803
  }
2611
- const def = unwrap || collapse || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
2612
- const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : true);
2613
- const clonesState = build ? buildReadsState(def) : true;
2804
+ if (project !== void 0 && (unwrap || collapse)) {
2805
+ throw new Error("node() options cannot combine project with unwrap or collapse");
2806
+ }
2807
+ const def = unwrap || collapse || project !== void 0 || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...project !== void 0 ? { project } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
2808
+ const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : project === void 0);
2809
+ const clonesState = build ? buildReadsState(def) : project === void 0;
2614
2810
  const hasOwnFields = parserHasOwnFields(combinator);
2615
- const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : true);
2811
+ const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : project === void 0);
2616
2812
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
2617
2813
  let failExpected;
2618
2814
  return {
@@ -2627,14 +2823,15 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2627
2823
  }
2628
2824
  return { ok: false, expected: failExpected, span: { start: pos, end: pos } };
2629
2825
  }
2630
- const hostCst = build !== void 0 && cstOutputHost(ctx.build);
2826
+ const hasDirectValue = build !== void 0 || project !== void 0;
2827
+ const hostCst = hasDirectValue && cstOutputHost(ctx.build);
2631
2828
  const effTrivia = capturesTrivia || hostCst;
2632
2829
  const effFields = capturesFields || hostCst && hasOwnFields;
2633
2830
  const saved = beginCstNodeCapture(ctx);
2634
2831
  const savedFields = ctx._fields;
2635
2832
  ctx._fields = effFields ? [] : void 0;
2636
2833
  const savedMask = ctx._triviaCaptureMask;
2637
- if (build === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
2834
+ if (build === void 0 && project === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
2638
2835
  ctx._triviaCaptureMask = ctx.build._parsemanTriviaKinds(def.type);
2639
2836
  }
2640
2837
  if (!effTrivia) ctx.captureTrivia = false;
@@ -2652,7 +2849,7 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2652
2849
  const nodeType = def.type ?? missingInferredType();
2653
2850
  const cstOutput = cstOutputHost(ctx.build);
2654
2851
  const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
2655
- const built = unwrap && children.length === 1 ? unwrapChild(children[0]) : collapse && children.length === 1 ? children[0] : !build && ctx.build?._parsemanCstCollapse && children.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children[0], children, rawChildren) ? children[0] : build ? cstOutput && ctx.build ? ctx.build(nodeType, children, fields, r.span, rawChildren, triviaLog, hostState) : build(children, fields, r.span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children, fields, r.span, rawChildren, triviaLog, st) : { _tag: "node", type: nodeType, span: { start: r.span.start, end: r.span.end }, state: st ?? null, children };
2852
+ const built = unwrap && children.length === 1 ? unwrapChild(children[0]) : collapse && children.length === 1 ? children[0] : !build && project === void 0 && ctx.build?._parsemanCstCollapse && children.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children[0], children, rawChildren) ? children[0] : project !== void 0 ? cstOutput && ctx.build ? ctx.build(nodeType, children, fields, r.span, rawChildren, triviaLog, hostState) : projectChild(children, project, nodeType) : build ? cstOutput && ctx.build ? ctx.build(nodeType, children, fields, r.span, rawChildren, triviaLog, hostState) : build(children, fields, r.span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children, fields, r.span, rawChildren, triviaLog, st) : { _tag: "node", type: nodeType, span: { start: r.span.start, end: r.span.end }, state: st ?? null, children };
2656
2853
  const rawEntry = isCstChild(built) ? built : { _tag: "leaf", value: typeof built === "string" ? built : typeof built === "object" && built !== null ? input.slice(r.span.start, r.span.end) : "", span: r.span };
2657
2854
  if (saved.buf !== void 0 || saved.ch !== void 0) {
2658
2855
  pushCstChild(ctx, built, rawEntry);
@@ -2939,7 +3136,11 @@ function evalRuleMapIR(ir) {
2939
3136
  "choice",
2940
3137
  "dispatch",
2941
3138
  "when",
3139
+ "startsWith",
3140
+ "endsWith",
3141
+ "matches",
2942
3142
  "otherwise",
3143
+ "routed",
2943
3144
  "attempt",
2944
3145
  "many",
2945
3146
  "oneOrMore",
@@ -2975,7 +3176,11 @@ function evalRuleMapIR(ir) {
2975
3176
  choice,
2976
3177
  dispatch,
2977
3178
  when,
3179
+ startsWith,
3180
+ endsWith,
3181
+ matches,
2978
3182
  otherwise,
3183
+ routed,
2979
3184
  attempt,
2980
3185
  many,
2981
3186
  oneOrMore,
@@ -3174,8 +3379,9 @@ var Builder = class {
3174
3379
  const selector = this.walk(def.selector);
3175
3380
  const tails = [
3176
3381
  ...def.cases.map((entry) => this.walk(entry.parser)),
3382
+ ...def.matchers ? def.matchers.map((entry) => this.walk(entry.parser)) : [],
3177
3383
  ...def.otherwise ? [this.walk(def.otherwise)] : []
3178
- ].filter(nonEmpty);
3384
+ ];
3179
3385
  return flattenSeq([selector, flattenChoice(tails)]);
3180
3386
  }
3181
3387
  // `max`/`min` are carried through so a BOUNDED repeat does not render as an
@@ -3207,6 +3413,8 @@ var Builder = class {
3207
3413
  return { kind: "not", item: this.walk(def.parser) };
3208
3414
  case "peek":
3209
3415
  return { kind: "peek", item: this.walk(def.parser) };
3416
+ case "routed":
3417
+ return { kind: "empty" };
3210
3418
  // Transparent semantic wrappers — render the inner syntax.
3211
3419
  case "transform":
3212
3420
  case "token":
@@ -3364,7 +3572,7 @@ function render(node2) {
3364
3572
  case "choice": {
3365
3573
  const text = node2.items.map((it) => {
3366
3574
  const r = render(it);
3367
- return wrap(r.text, r.level, ALT);
3575
+ return r.text.length === 0 ? "/* empty */" : wrap(r.text, r.level, ALT);
3368
3576
  }).join(" | ");
3369
3577
  return { text, level: ALT };
3370
3578
  }