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.
@@ -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 };
@@ -2555,12 +2730,30 @@ function isCstChild(value) {
2555
2730
  function missingInferredType() {
2556
2731
  throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()');
2557
2732
  }
2733
+ function normalizeProject(project) {
2734
+ if (project === void 0) return void 0;
2735
+ if (!Number.isInteger(project) || project < 0) {
2736
+ throw new Error("node() project child must be a non-negative integer");
2737
+ }
2738
+ return project;
2739
+ }
2740
+ function projectChild(children, project, type) {
2741
+ if (!(project in children)) {
2742
+ throw new Error(`node(${JSON.stringify(type)}) project child ${project} was not captured`);
2743
+ }
2744
+ return unwrapChild(children[project]);
2745
+ }
2558
2746
  function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2559
2747
  const hasExplicitType = typeof typeOrCombinator === "string";
2560
2748
  const type = hasExplicitType ? typeOrCombinator : void 0;
2561
2749
  const combinator = hasExplicitType ? combinatorOrBuild : typeOrCombinator;
2562
- const build = hasExplicitType ? buildOrOpts : combinatorOrBuild;
2563
- const opts = hasExplicitType ? maybeOpts : buildOrOpts;
2750
+ const buildArg = hasExplicitType ? buildOrOpts : combinatorOrBuild;
2751
+ const build = typeof buildArg === "function" ? buildArg : void 0;
2752
+ const opts = typeof buildArg === "function" ? maybeOpts : buildArg ?? maybeOpts;
2753
+ const project = normalizeProject(opts?.project);
2754
+ if (project !== void 0 && build !== void 0) {
2755
+ throw new Error("node() options cannot combine project with a build callback");
2756
+ }
2564
2757
  const baseDef = { tag: "node", parser: combinator, ...type === void 0 ? {} : { type }, ...build === void 0 ? {} : { build } };
2565
2758
  const meta = {
2566
2759
  firstSet: combinator._meta.firstSet,
@@ -2574,11 +2767,14 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2574
2767
  if (unwrap && collapse) {
2575
2768
  throw new Error("node() options cannot set both unwrap and collapse");
2576
2769
  }
2577
- const def = unwrap || collapse || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
2578
- const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : true);
2579
- const clonesState = build ? buildReadsState(def) : true;
2770
+ if (project !== void 0 && (unwrap || collapse)) {
2771
+ throw new Error("node() options cannot combine project with unwrap or collapse");
2772
+ }
2773
+ 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;
2774
+ const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : project === void 0);
2775
+ const clonesState = build ? buildReadsState(def) : project === void 0;
2580
2776
  const hasOwnFields = parserHasOwnFields(combinator);
2581
- const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : true);
2777
+ const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : project === void 0);
2582
2778
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
2583
2779
  let failExpected;
2584
2780
  return {
@@ -2593,14 +2789,15 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2593
2789
  }
2594
2790
  return { ok: false, expected: failExpected, span: { start: pos, end: pos } };
2595
2791
  }
2596
- const hostCst = build !== void 0 && cstOutputHost(ctx.build);
2792
+ const hasDirectValue = build !== void 0 || project !== void 0;
2793
+ const hostCst = hasDirectValue && cstOutputHost(ctx.build);
2597
2794
  const effTrivia = capturesTrivia || hostCst;
2598
2795
  const effFields = capturesFields || hostCst && hasOwnFields;
2599
2796
  const saved = beginCstNodeCapture(ctx);
2600
2797
  const savedFields = ctx._fields;
2601
2798
  ctx._fields = effFields ? [] : void 0;
2602
2799
  const savedMask = ctx._triviaCaptureMask;
2603
- if (build === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
2800
+ if (build === void 0 && project === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
2604
2801
  ctx._triviaCaptureMask = ctx.build._parsemanTriviaKinds(def.type);
2605
2802
  }
2606
2803
  if (!effTrivia) ctx.captureTrivia = false;
@@ -2618,7 +2815,7 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2618
2815
  const nodeType = def.type ?? missingInferredType();
2619
2816
  const cstOutput = cstOutputHost(ctx.build);
2620
2817
  const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
2621
- 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 };
2818
+ 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 };
2622
2819
  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 };
2623
2820
  if (saved.buf !== void 0 || saved.ch !== void 0) {
2624
2821
  pushCstChild(ctx, built, rawEntry);
@@ -2905,7 +3102,11 @@ function evalRuleMapIR(ir) {
2905
3102
  "choice",
2906
3103
  "dispatch",
2907
3104
  "when",
3105
+ "startsWith",
3106
+ "endsWith",
3107
+ "matches",
2908
3108
  "otherwise",
3109
+ "routed",
2909
3110
  "attempt",
2910
3111
  "many",
2911
3112
  "oneOrMore",
@@ -2941,7 +3142,11 @@ function evalRuleMapIR(ir) {
2941
3142
  choice,
2942
3143
  dispatch,
2943
3144
  when,
3145
+ startsWith,
3146
+ endsWith,
3147
+ matches,
2944
3148
  otherwise,
3149
+ routed,
2945
3150
  attempt,
2946
3151
  many,
2947
3152
  oneOrMore,
@@ -3140,8 +3345,9 @@ var Builder = class {
3140
3345
  const selector = this.walk(def.selector);
3141
3346
  const tails = [
3142
3347
  ...def.cases.map((entry) => this.walk(entry.parser)),
3348
+ ...def.matchers ? def.matchers.map((entry) => this.walk(entry.parser)) : [],
3143
3349
  ...def.otherwise ? [this.walk(def.otherwise)] : []
3144
- ].filter(nonEmpty);
3350
+ ];
3145
3351
  return flattenSeq([selector, flattenChoice(tails)]);
3146
3352
  }
3147
3353
  // `max`/`min` are carried through so a BOUNDED repeat does not render as an
@@ -3173,6 +3379,8 @@ var Builder = class {
3173
3379
  return { kind: "not", item: this.walk(def.parser) };
3174
3380
  case "peek":
3175
3381
  return { kind: "peek", item: this.walk(def.parser) };
3382
+ case "routed":
3383
+ return { kind: "empty" };
3176
3384
  // Transparent semantic wrappers — render the inner syntax.
3177
3385
  case "transform":
3178
3386
  case "token":
@@ -3330,7 +3538,7 @@ function render(node2) {
3330
3538
  case "choice": {
3331
3539
  const text = node2.items.map((it) => {
3332
3540
  const r = render(it);
3333
- return wrap(r.text, r.level, ALT);
3541
+ return r.text.length === 0 ? "/* empty */" : wrap(r.text, r.level, ALT);
3334
3542
  }).join(" | ");
3335
3543
  return { text, level: ALT };
3336
3544
  }