@gmb/bitmark-parser-generator 5.23.0 → 5.25.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.
@@ -34,6 +34,8 @@ __export(index_exports, {
34
34
  BoundingBoxIndex: () => BoundingBoxIndex,
35
35
  Builder: () => Builder,
36
36
  CardSetVersion: () => CardSetVersion,
37
+ HtmlTableGenerator: () => HtmlTableGenerator2,
38
+ HtmlTableParser: () => HtmlTableParser2,
37
39
  InfoFormat: () => InfoFormat,
38
40
  InfoType: () => InfoType,
39
41
  Input: () => Input,
@@ -279,6 +281,7 @@ var BitType2 = {
279
281
  dangerAlt: "danger-alt",
280
282
  dangerCollapsible: "danger-collapsible",
281
283
  definitionList: "definition-list",
284
+ definitionListCollapsible: "definition-list-collapsible",
282
285
  definitionTerm: "definition-term",
283
286
  deleted: "deleted",
284
287
  details: "details",
@@ -559,6 +562,8 @@ var BitType2 = {
559
562
  smartStandardArticleNonNormativeCollapsible: "smart-standard-article-non-normative-collapsible",
560
563
  smartStandardArticleNormative: "smart-standard-article-normative",
561
564
  smartStandardArticleNormativeCollapsible: "smart-standard-article-normative-collapsible",
565
+ smartStandardDefinitionListNonNormative: "smart-standard-definition-list-non-normative",
566
+ smartStandardDefinitionListNormative: "smart-standard-definition-list-normative",
562
567
  smartStandardExampleNonNormative: "smart-standard-example-non-normative",
563
568
  smartStandardExampleNonNormativeCollapsible: "smart-standard-example-non-normative-collapsible",
564
569
  smartStandardExampleNormative: "smart-standard-example-normative",
@@ -609,6 +614,8 @@ var BitType2 = {
609
614
  smartStandardTableNormativeCollapsible: "smart-standard-table-normative-collapsible",
610
615
  standardArticleNonNormative: "standard-article-non-normative",
611
616
  standardArticleNormative: "standard-article-normative",
617
+ standardDefinitionListNonNormative: "standard-definition-list-non-normative",
618
+ standardDefinitionListNormative: "standard-definition-list-normative",
612
619
  standardExampleNonNormative: "standard-example-non-normative",
613
620
  standardExampleNormative: "standard-example-normative",
614
621
  standardImageFigureNonNormative: "standard-image-figure-non-normative",
@@ -778,10 +785,13 @@ var TextFormat2 = {
778
785
  var import_superenum7 = require("@ncoderz/superenum");
779
786
  var CardSetConfigKey = {
780
787
  flashcard: "flashcard",
788
+ flashcard1: "flashcard1",
781
789
  definitionList: "definitionList",
790
+ definitionListPlain: "definitionListPlain",
782
791
  elements: "elements",
783
792
  statements: "statements",
784
793
  quiz: "quiz",
794
+ quizResponses: "quizResponses",
785
795
  feedback: "feedback",
786
796
  questions: "questions",
787
797
  matchPairs: "matchPairs",
@@ -793,6 +803,7 @@ var CardSetConfigKey = {
793
803
  pronunciationTable: "pronunciationTable",
794
804
  botActionResponses: "botActionResponses",
795
805
  exampleBitList: "exampleBitList",
806
+ pageFooterSections: "pageFooterSections",
796
807
  clozeList: "clozeList",
797
808
  ingredients: "ingredients",
798
809
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -1209,12 +1220,17 @@ var groupKeys = {
1209
1220
  group_standardAllBits: "group_standardAllBits",
1210
1221
  group_standardItemLead: "group_standardItemLead",
1211
1222
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1223
+ group_standardInstructionHint: "group_standardInstructionHint",
1212
1224
  group_standardTags: "group_standardTags",
1225
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1213
1226
  group_imageSource: "group_imageSource",
1214
1227
  group_technicalTerm: "group_technicalTerm",
1215
1228
  group_person: "group_person",
1216
1229
  group_gap: "group_gap",
1217
1230
  group_trueFalse: "group_trueFalse",
1231
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1232
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1233
+ group_trueFalseResponses: "group_trueFalseResponses",
1218
1234
  group_markConfig: "group_markConfig",
1219
1235
  group_mark: "group_mark",
1220
1236
  group_bookCommon: "group_bookCommon",
@@ -1222,14 +1238,18 @@ var groupKeys = {
1222
1238
  group_advertisingCommon: "group_advertisingCommon",
1223
1239
  group_platformStylesCommon: "group_platformStylesCommon",
1224
1240
  group_quizCommon: "group_quizCommon",
1241
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1242
+ group_imageNoZoom: "group_imageNoZoom",
1225
1243
  group_resourceBitTags: "group_resourceBitTags",
1226
1244
  group_resourceCommon: "group_resourceCommon",
1227
1245
  group_resourceImageCommon: "group_resourceImageCommon",
1246
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1228
1247
  group_resourceAudioCommon: "group_resourceAudioCommon",
1229
1248
  group_resourceVideoCommon: "group_resourceVideoCommon",
1230
1249
  group_resourceIcon: "group_resourceIcon",
1231
1250
  // Alias for image
1232
1251
  group_resourceImage: "group_resourceImage",
1252
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1233
1253
  group_resourceImageEmbed: "group_resourceImageEmbed",
1234
1254
  group_resourceImageLink: "group_resourceImageLink",
1235
1255
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1381,6 +1401,7 @@ var propertyKeys = {
1381
1401
  property_enforceUpdateOverUserInput: "@enforceUpdateOverUserInput",
1382
1402
  property_extractorExtractionTimestamp: "@extractorExtractionTimestamp",
1383
1403
  property_isCaseSensitive: "@isCaseSensitive",
1404
+ property_isCollapsible: "@isCollapsible",
1384
1405
  property_isInfoOnly: "@isInfoOnly",
1385
1406
  property_isPublic: "@isPublic",
1386
1407
  property_isTemplate: "@isTemplate",
@@ -1506,6 +1527,7 @@ var propertyKeys = {
1506
1527
  property_src4x: "@src4x",
1507
1528
  property_srcAlt: "@srcAlt",
1508
1529
  property_stripePricingTableId: "@stripePricingTableId",
1530
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1509
1531
  property_stripePublishableKey: "@stripePublishableKey",
1510
1532
  property_subject: "@subject",
1511
1533
  property_subtype: "@subtype",
@@ -2257,7 +2279,7 @@ var CARDSETS = {
2257
2279
  variants: [
2258
2280
  {
2259
2281
  jsonKey: "question.text",
2260
- exportJsonKey: { question: { text: "$" } },
2282
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2261
2283
  tags: [
2262
2284
  {
2263
2285
  key: ConfigKey.group_standardTags,
@@ -2273,6 +2295,136 @@ var CARDSETS = {
2273
2295
  description: "Icon resource for the flashcard.",
2274
2296
  jsonKey: "question.icon|resource(type=image, key=image)",
2275
2297
  exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2298
+ },
2299
+ {
2300
+ key: ConfigKey.property_example,
2301
+ exportJsonKey: [
2302
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2303
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2304
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2305
+ ],
2306
+ description: "Example marker for the flashcard question.",
2307
+ format: TagFormat.bitmarkText,
2308
+ nullable: true
2309
+ }
2310
+ ]
2311
+ }
2312
+ ]
2313
+ },
2314
+ {
2315
+ name: "answer",
2316
+ variants: [
2317
+ {
2318
+ jsonKey: "answer.text",
2319
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2320
+ tags: [
2321
+ {
2322
+ key: ConfigKey.group_standardTags,
2323
+ description: "Standard tags for the flashcard."
2324
+ },
2325
+ {
2326
+ exportJsonKey: { title: "$" },
2327
+ key: ConfigKey.tag_title,
2328
+ description: "Title of the flashcard."
2329
+ },
2330
+ {
2331
+ key: ConfigKey.group_resourceIcon,
2332
+ description: "Icon resource for the flashcard.",
2333
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2334
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2335
+ },
2336
+ {
2337
+ key: ConfigKey.property_example,
2338
+ exportJsonKey: [
2339
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2340
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2341
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2342
+ ],
2343
+ description: "Example marker for the flashcard answer.",
2344
+ format: TagFormat.bitmarkText,
2345
+ nullable: true
2346
+ }
2347
+ ]
2348
+ },
2349
+ {
2350
+ jsonKey: "alternativeAnswers[].text",
2351
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2352
+ tags: [
2353
+ {
2354
+ key: ConfigKey.group_standardTags,
2355
+ description: "Standard tags for the flashcard."
2356
+ },
2357
+ {
2358
+ exportJsonKey: { title: "$" },
2359
+ key: ConfigKey.tag_title,
2360
+ description: "Title of the flashcard."
2361
+ },
2362
+ {
2363
+ key: ConfigKey.group_resourceIcon,
2364
+ description: "Icon resource for the flashcard.",
2365
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2366
+ exportJsonKey: {
2367
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2368
+ }
2369
+ }
2370
+ ],
2371
+ repeatCount: Count.infinity
2372
+ }
2373
+ ]
2374
+ }
2375
+ ]
2376
+ },
2377
+ //
2378
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2379
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2380
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2381
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2382
+ // cardset.
2383
+ //
2384
+ [CardSetConfigKey.flashcard1]: {
2385
+ jsonKey: "cards",
2386
+ exportJsonKey: { cards: "$" },
2387
+ sections: {
2388
+ default: {
2389
+ jsonKey: "cards",
2390
+ exportJsonKey: { cards: "$" },
2391
+ isDefault: true,
2392
+ maxCount: 1
2393
+ }
2394
+ },
2395
+ sides: [
2396
+ {
2397
+ name: "question",
2398
+ variants: [
2399
+ {
2400
+ jsonKey: "question.text",
2401
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2402
+ tags: [
2403
+ {
2404
+ key: ConfigKey.group_standardTags,
2405
+ description: "Standard tags for the flashcard."
2406
+ },
2407
+ {
2408
+ exportJsonKey: { title: "$" },
2409
+ key: ConfigKey.tag_title,
2410
+ description: "Title of the flashcard."
2411
+ },
2412
+ {
2413
+ key: ConfigKey.group_resourceIcon,
2414
+ description: "Icon resource for the flashcard.",
2415
+ jsonKey: "question.icon|resource(type=image, key=image)",
2416
+ exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2417
+ },
2418
+ {
2419
+ key: ConfigKey.property_example,
2420
+ exportJsonKey: [
2421
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2422
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2423
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2424
+ ],
2425
+ description: "Example marker for the flashcard question.",
2426
+ format: TagFormat.bitmarkText,
2427
+ nullable: true
2276
2428
  }
2277
2429
  ]
2278
2430
  }
@@ -2283,7 +2435,7 @@ var CARDSETS = {
2283
2435
  variants: [
2284
2436
  {
2285
2437
  jsonKey: "answer.text",
2286
- exportJsonKey: { answer: { text: "$" } },
2438
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2287
2439
  tags: [
2288
2440
  {
2289
2441
  key: ConfigKey.group_standardTags,
@@ -2299,6 +2451,17 @@ var CARDSETS = {
2299
2451
  description: "Icon resource for the flashcard.",
2300
2452
  jsonKey: "answer.icon|resource(type=image, key=image)",
2301
2453
  exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2454
+ },
2455
+ {
2456
+ key: ConfigKey.property_example,
2457
+ exportJsonKey: [
2458
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2459
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2460
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2461
+ ],
2462
+ description: "Example marker for the flashcard answer.",
2463
+ format: TagFormat.bitmarkText,
2464
+ nullable: true
2302
2465
  }
2303
2466
  ]
2304
2467
  },
@@ -2342,7 +2505,7 @@ var CARDSETS = {
2342
2505
  variants: [
2343
2506
  {
2344
2507
  jsonKey: "term.text",
2345
- exportJsonKey: { term: { text: "$" } },
2508
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2346
2509
  tags: [
2347
2510
  {
2348
2511
  key: ConfigKey.group_standardTags,
@@ -2361,6 +2524,17 @@ var CARDSETS = {
2361
2524
  format: TagFormat.plainText,
2362
2525
  jsonKey: "term.icon|resource(type=image, key=image)",
2363
2526
  exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2527
+ },
2528
+ {
2529
+ key: ConfigKey.property_example,
2530
+ exportJsonKey: [
2531
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2532
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2533
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2534
+ ],
2535
+ description: "Example marker for the definition term.",
2536
+ format: TagFormat.bitmarkText,
2537
+ nullable: true
2364
2538
  }
2365
2539
  ]
2366
2540
  }
@@ -2371,7 +2545,7 @@ var CARDSETS = {
2371
2545
  variants: [
2372
2546
  {
2373
2547
  jsonKey: "definition.text",
2374
- exportJsonKey: { definition: { text: "$" } },
2548
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2375
2549
  tags: [
2376
2550
  {
2377
2551
  key: ConfigKey.group_standardTags,
@@ -2389,6 +2563,17 @@ var CARDSETS = {
2389
2563
  description: "Icon resource for the definition.",
2390
2564
  jsonKey: "definition.icon|resource(type=image, key=image)",
2391
2565
  exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2566
+ },
2567
+ {
2568
+ key: ConfigKey.property_example,
2569
+ exportJsonKey: [
2570
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2571
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2572
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2573
+ ],
2574
+ description: "Example marker for the definition.",
2575
+ format: TagFormat.bitmarkText,
2576
+ nullable: true
2392
2577
  }
2393
2578
  ]
2394
2579
  },
@@ -2421,6 +2606,126 @@ var CARDSETS = {
2421
2606
  ]
2422
2607
  },
2423
2608
  //
2609
+ // definition-list-plain
2610
+ //
2611
+ // Same shape as `definitionList`, but every variant body is rendered as
2612
+ // plain text (string), not bitmark+ AST. Used by
2613
+ // `meta-search-default-terms` / `meta-search-default-topics`, which need
2614
+ // term/definition text emitted verbatim instead of as a ProseMirror tree.
2615
+ //
2616
+ [CardSetConfigKey.definitionListPlain]: {
2617
+ jsonKey: "definitions",
2618
+ exportJsonKey: { definitions: "$" },
2619
+ sides: [
2620
+ {
2621
+ name: "term",
2622
+ variants: [
2623
+ {
2624
+ jsonKey: "term.text",
2625
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2626
+ format: TextFormat2.plainText,
2627
+ tags: [
2628
+ {
2629
+ key: ConfigKey.group_standardTags,
2630
+ description: "Standard tags for the definition."
2631
+ },
2632
+ {
2633
+ key: ConfigKey.tag_title,
2634
+ description: "Title of the definition.",
2635
+ format: TagFormat.plainText,
2636
+ jsonKey: "^heading.forKeys",
2637
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2638
+ },
2639
+ {
2640
+ key: ConfigKey.group_resourceIcon,
2641
+ description: "Icon resource for the definition.",
2642
+ format: TagFormat.plainText,
2643
+ jsonKey: "term.icon|resource(type=image, key=image)",
2644
+ exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2645
+ },
2646
+ {
2647
+ key: ConfigKey.property_example,
2648
+ exportJsonKey: [
2649
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2650
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2651
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2652
+ ],
2653
+ description: "Example marker for the definition term.",
2654
+ format: TagFormat.bitmarkText,
2655
+ nullable: true
2656
+ }
2657
+ ]
2658
+ }
2659
+ ]
2660
+ },
2661
+ {
2662
+ name: "definition",
2663
+ variants: [
2664
+ {
2665
+ jsonKey: "definition.text",
2666
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2667
+ format: TextFormat2.plainText,
2668
+ tags: [
2669
+ {
2670
+ key: ConfigKey.group_standardTags,
2671
+ description: "Standard tags for the definition."
2672
+ },
2673
+ {
2674
+ key: ConfigKey.tag_title,
2675
+ description: "Title of the definition.",
2676
+ format: TagFormat.plainText,
2677
+ jsonKey: "^heading.forValues",
2678
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2679
+ },
2680
+ {
2681
+ key: ConfigKey.group_resourceIcon,
2682
+ description: "Icon resource for the definition.",
2683
+ jsonKey: "definition.icon|resource(type=image, key=image)",
2684
+ exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2685
+ },
2686
+ {
2687
+ key: ConfigKey.property_example,
2688
+ exportJsonKey: [
2689
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2690
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2691
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2692
+ ],
2693
+ description: "Example marker for the definition.",
2694
+ format: TagFormat.bitmarkText,
2695
+ nullable: true
2696
+ }
2697
+ ]
2698
+ },
2699
+ {
2700
+ jsonKey: "alternativeDefinitions[].text",
2701
+ exportJsonKey: { alternativeDefinitions: [{ text: "$" }] },
2702
+ format: TextFormat2.plainText,
2703
+ tags: [
2704
+ {
2705
+ key: ConfigKey.group_standardTags,
2706
+ description: "Standard tags for the definition."
2707
+ },
2708
+ {
2709
+ exportJsonKey: { title: "$" },
2710
+ key: ConfigKey.tag_title,
2711
+ description: "Title of the definition."
2712
+ },
2713
+ {
2714
+ key: ConfigKey.group_resourceIcon,
2715
+ description: "Icon resource for the definition.",
2716
+ jsonKey: "alternativeDefinitions[].icon|resource(type=image, key=image)",
2717
+ exportJsonKey: {
2718
+ alternativeDefinitions: [{ icon: { type: "image", image: { src: "$" } } }]
2719
+ }
2720
+ }
2721
+ ],
2722
+ repeatCount: Count.infinity
2723
+ }
2724
+ ]
2725
+ }
2726
+ ]
2727
+ },
2728
+ //
2424
2729
  // match-pairs
2425
2730
  //
2426
2731
  [CardSetConfigKey.matchPairs]: {
@@ -2433,11 +2738,51 @@ var CARDSETS = {
2433
2738
  {
2434
2739
  jsonKey: "key",
2435
2740
  exportJsonKey: { key: "$" },
2741
+ format: TextFormat2.plainText,
2436
2742
  tags: [
2437
2743
  {
2438
2744
  key: ConfigKey.group_standardTags,
2439
2745
  description: "Standard tags for the match pair."
2440
2746
  },
2747
+ {
2748
+ // Pair-level example: emit `isExample` alongside `example`
2749
+ // (matches OLD parser's per-pair allow-list behaviour).
2750
+ //
2751
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2752
+ // in the cardset) fills each pair's `example` with `V`
2753
+ // once per pair from the keys-side cascade fire (single
2754
+ // parsed_variant per pair — no per-value duplication).
2755
+ //
2756
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2757
+ // at the pair-object level. BPG `parseMatchPairs`
2758
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2759
+ // per PAIR (the `exampleCard` accumulator), placed at the
2760
+ // pair object. Unlike matchMatrix (per-cell example),
2761
+ // matchPairs has no nested per-cell layer, so the pair-key
2762
+ // `[@example:V]` must double-emit as the visible field.
2763
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2764
+ // expects `example: "packen"` at pair-object level from
2765
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2766
+ // draft applied to both keys-side tags; the matchPairs
2767
+ // half was reverted after fixture parity testing.
2768
+ key: ConfigKey.property_example,
2769
+ exportJsonKey: [
2770
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2771
+ {
2772
+ predicates: ["@absent", { $cascade: "*" }],
2773
+ rule: {
2774
+ isExample: true,
2775
+ example: "$cascade",
2776
+ "@bit": { isExample: true }
2777
+ }
2778
+ },
2779
+ { "@absent": { isExample: true } },
2780
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2781
+ ],
2782
+ description: "Example marker / value on the match pair.",
2783
+ format: TagFormat.bitmarkText,
2784
+ nullable: true
2785
+ },
2441
2786
  {
2442
2787
  key: ConfigKey.tag_title,
2443
2788
  description: "Title of the match pair.",
@@ -2446,9 +2791,19 @@ var CARDSETS = {
2446
2791
  exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2447
2792
  },
2448
2793
  {
2794
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2795
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2796
+ // only fires when NO variant in the active card explicitly
2797
+ // carries `[@isCaseSensitive]`. When a value-side variant
2798
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2799
+ // synth is skipped here; the value-side explicit fire owns
2800
+ // the pair-level emission.
2449
2801
  key: ConfigKey.property_isCaseSensitive,
2450
2802
  exportJsonKey: [
2451
- { "@absent": { isCaseSensitive: "$ancestor" } },
2803
+ {
2804
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2805
+ rule: { isCaseSensitive: "$ancestor" }
2806
+ },
2452
2807
  { isCaseSensitive: "$" }
2453
2808
  ],
2454
2809
  description: "Property to indicate if the match is case sensitive.",
@@ -2477,10 +2832,85 @@ var CARDSETS = {
2477
2832
  {
2478
2833
  jsonKey: "values[]",
2479
2834
  exportJsonKey: { values: ["$"] },
2835
+ format: TextFormat2.plainText,
2480
2836
  tags: [
2481
2837
  {
2482
- key: ConfigKey.group_standardTags,
2483
- description: "Standard tags for the match pair."
2838
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2839
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2840
+ // for sideIdx > 0; the schema-side equivalent is to use a
2841
+ // group_standardTags variant that omits group_standardItemLead.
2842
+ key: ConfigKey.group_standardTagsNoItemLead,
2843
+ description: "Standard tags for the match pair (values side, no item/lead)."
2844
+ },
2845
+ {
2846
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2847
+ // `@example` from the bit header fills each pair's
2848
+ // `example` with the pair's first value (mirrors BPG's
2849
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2850
+ //
2851
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2852
+ // TAG 415 (keys side) which fires once per pair — keeping
2853
+ // it here would emit `example` twice per pair (one per
2854
+ // values-side parsed_variant) and merge them into a
2855
+ // multi-paragraph array.
2856
+ //
2857
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2858
+ // source `$` value (the current `++`-iteration's value
2859
+ // text). For single-value pairs this matches the old
2860
+ // `$parent.values[0]`; for multi-value pairs with
2861
+ // `[@example]` attached to a non-first value, it correctly
2862
+ // resolves to that value.
2863
+ //
2864
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2865
+ // routes pair-key `[@example:V]` (a per-pair cascade
2866
+ // source, bound via per-card CascadeCounters scope) and
2867
+ // bit-header `[@example:V]` (cardset cascade) into the
2868
+ // pair-level `example` via `$cascade`. The aggregator in
2869
+ // `populate_variants` fires this rule ONCE per pair (BPG
2870
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2871
+ // per-iter pollution.
2872
+ key: ConfigKey.property_example,
2873
+ exportJsonKey: [
2874
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2875
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2876
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2877
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2878
+ // scope opened by `build_card_item`.
2879
+ {
2880
+ predicates: ["@keyonly"],
2881
+ maxEmits: 1,
2882
+ rule: {
2883
+ isExample: true,
2884
+ example: "$parent",
2885
+ "@bit": { isExample: true }
2886
+ }
2887
+ },
2888
+ {
2889
+ predicates: ["@absent", { $cascade: "*" }],
2890
+ rule: {
2891
+ isExample: true,
2892
+ example: "$cascade",
2893
+ "@bit": { isExample: true }
2894
+ }
2895
+ },
2896
+ {
2897
+ "@absent": {
2898
+ isExample: true,
2899
+ example: "$parent",
2900
+ "@bit": { isExample: true }
2901
+ }
2902
+ },
2903
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2904
+ // form so per-`++`-iter values-side fires also first-win.
2905
+ {
2906
+ predicates: [],
2907
+ maxEmits: 1,
2908
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2909
+ }
2910
+ ],
2911
+ description: "Example marker / value on the match pair (values side).",
2912
+ format: TagFormat.bitmarkText,
2913
+ nullable: true
2484
2914
  },
2485
2915
  {
2486
2916
  key: ConfigKey.tag_title,
@@ -2490,9 +2920,13 @@ var CARDSETS = {
2490
2920
  exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2491
2921
  },
2492
2922
  {
2923
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2493
2924
  key: ConfigKey.property_isCaseSensitive,
2494
2925
  exportJsonKey: [
2495
- { "@absent": { isCaseSensitive: "$ancestor" } },
2926
+ {
2927
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2928
+ rule: { isCaseSensitive: "$ancestor" }
2929
+ },
2496
2930
  { isCaseSensitive: "$" }
2497
2931
  ],
2498
2932
  description: "Property to indicate if the match is case sensitive.",
@@ -2518,6 +2952,7 @@ var CARDSETS = {
2518
2952
  {
2519
2953
  jsonKey: "key",
2520
2954
  exportJsonKey: { key: "$" },
2955
+ format: TextFormat2.plainText,
2521
2956
  tags: [
2522
2957
  {
2523
2958
  key: ConfigKey.group_standardTags,
@@ -2547,10 +2982,12 @@ var CARDSETS = {
2547
2982
  {
2548
2983
  jsonKey: "values[]",
2549
2984
  exportJsonKey: { values: ["$"] },
2985
+ format: TextFormat2.plainText,
2550
2986
  tags: [
2551
2987
  {
2552
- key: ConfigKey.group_standardTags,
2553
- description: "Standard tags for the audio match pair."
2988
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2989
+ key: ConfigKey.group_standardTagsNoItemLead,
2990
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2554
2991
  },
2555
2992
  {
2556
2993
  key: ConfigKey.tag_title,
@@ -2583,6 +3020,7 @@ var CARDSETS = {
2583
3020
  {
2584
3021
  jsonKey: "key",
2585
3022
  exportJsonKey: { key: "$" },
3023
+ format: TextFormat2.plainText,
2586
3024
  tags: [
2587
3025
  {
2588
3026
  key: ConfigKey.group_standardTags,
@@ -2612,10 +3050,12 @@ var CARDSETS = {
2612
3050
  {
2613
3051
  jsonKey: "values[]",
2614
3052
  exportJsonKey: { values: ["$"] },
3053
+ format: TextFormat2.plainText,
2615
3054
  tags: [
2616
3055
  {
2617
- key: ConfigKey.group_standardTags,
2618
- description: "Standard tags for the image match pair."
3056
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
3057
+ key: ConfigKey.group_standardTagsNoItemLead,
3058
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2619
3059
  },
2620
3060
  {
2621
3061
  key: ConfigKey.tag_title,
@@ -2648,20 +3088,49 @@ var CARDSETS = {
2648
3088
  {
2649
3089
  jsonKey: "key",
2650
3090
  exportJsonKey: { key: "$" },
3091
+ format: TextFormat2.plainText,
2651
3092
  tags: [
2652
3093
  {
2653
3094
  key: ConfigKey.group_standardItemLeadInstructionHint,
2654
3095
  description: "Standard tags for lead, instruction, and hint."
2655
3096
  },
2656
3097
  {
3098
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
3099
+ // `[@example:V]` is consumed as the per-pair cascade source.
3100
+ // PLAN-077 R14 follow-up: investigation showed the fixture
3101
+ // reference is INCONSISTENT — bits without a cardset-level
3102
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
3103
+ // pair-level `example: V` at pair scope, but bits WITH a
3104
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
3105
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
3106
+ // example. Emitting `example: $` here satisfies bit 3 but
3107
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
3108
+ // 3. Conditional emission would require runtime gating on
3109
+ // the cardset cascade scope state. Left dropped pending a
3110
+ // dedicated plan — accepted divergence on bit 3's pair-
3111
+ // level emission (cells still get the per-cell `example`
3112
+ // correctly via PLAN-077 §4.2 + §4.3).
2657
3113
  key: ConfigKey.property_example,
2658
3114
  exportJsonKey: [
2659
- { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2660
- { "@absent": { isExample: true, "@bit": { isExample: true } } },
2661
- { isExample: true, example: "$", "@bit": { isExample: true } }
3115
+ {
3116
+ "@keyonly": {
3117
+ isExample: true,
3118
+ "@bit": { isExample: true },
3119
+ "@card": { isExample: true }
3120
+ }
3121
+ },
3122
+ {
3123
+ "@absent": {
3124
+ isExample: true,
3125
+ "@bit": { isExample: true },
3126
+ "@card": { isExample: true }
3127
+ }
3128
+ },
3129
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2662
3130
  ],
2663
3131
  description: "Example text for the match matrix.",
2664
- format: TagFormat.plainText
3132
+ format: TagFormat.plainText,
3133
+ nullable: true
2665
3134
  },
2666
3135
  {
2667
3136
  key: ConfigKey.tag_title,
@@ -2689,35 +3158,96 @@ var CARDSETS = {
2689
3158
  variants: [
2690
3159
  {
2691
3160
  jsonKey: "cells[{s}].values[]",
2692
- exportJsonKey: { cells: { $s: { values: ["$"] } } },
3161
+ exportJsonKey: [
3162
+ // PLAN-085 / R14: the positional-blank emission for an
3163
+ // effectively-absent cell side fires ONLY when the active
3164
+ // card has at least one `[#]` variant fire. This matches
3165
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
3166
+ // `[#]` exists anywhere in the title card, BPG returns
3167
+ // undefined and no `heading` is emitted. The phantom
3168
+ // `forValues:['']` placeholder is appropriate only when
3169
+ // the card is being treated as a title-row.
3170
+ {
3171
+ predicates: ["@absent", { "@variantHasTag": "#" }],
3172
+ rule: { "@bit": { heading: { forValues: [""] } } }
3173
+ },
3174
+ { cells: { $s: { values: ["$"] } } }
3175
+ ],
3176
+ format: TextFormat2.plainText,
2693
3177
  tags: [
2694
3178
  {
2695
3179
  key: ConfigKey.group_standardItemLeadInstructionHint,
2696
3180
  description: "Standard tags for lead, instruction, and hint."
2697
3181
  },
2698
3182
  {
3183
+ // PLAN-075: bare `$parent` reads the parent variant fire's
3184
+ // source `$` value (the current `++`-iteration's cell text).
3185
+ // Replaces `$parent.values[0]` which pinned to position 0
3186
+ // regardless of which `++`-value `[@example]` was on.
3187
+ //
3188
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
3189
+ // routes pair-key `[@example:V]` (a per-pair cascade
3190
+ // source, bound via per-card CascadeCounters scope) and
3191
+ // bit-header `[@example:V]` (cardset cascade) into the
3192
+ // per-cell `example` via `$cascade`. The aggregator in
3193
+ // `populate_variants` fires this rule ONCE per cell (BPG
3194
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
3195
+ // per-iter pollution.
3196
+ // @card scope-shift writes matrix-row container isExample.
2699
3197
  key: ConfigKey.property_example,
2700
3198
  exportJsonKey: [
2701
3199
  {
2702
3200
  "@keyonly": {
2703
- cells: { $s: { isExample: true, example: "$parent.values[0]" } },
2704
- "@bit": { isExample: true }
3201
+ cells: { $s: { isExample: true, example: "$parent" } },
3202
+ "@bit": { isExample: true },
3203
+ "@card": { isExample: true }
2705
3204
  }
2706
3205
  },
2707
3206
  {
2708
- "@absent": { cells: { $s: { isExample: true, example: "$parent.values[0]" } } }
3207
+ predicates: ["@absent", { $cascade: "*" }],
3208
+ rule: {
3209
+ cells: { $s: { isExample: true, example: "$cascade" } },
3210
+ "@bit": { isExample: true },
3211
+ "@card": { isExample: true }
3212
+ }
2709
3213
  },
2710
- { cells: { $s: { isExample: true, example: "$" } }, "@bit": { isExample: true } }
3214
+ {
3215
+ "@absent": {
3216
+ cells: { $s: { isExample: true, example: "$parent" } },
3217
+ "@bit": { isExample: true },
3218
+ "@card": { isExample: true }
3219
+ }
3220
+ },
3221
+ {
3222
+ cells: { $s: { isExample: true, example: "$" } },
3223
+ "@bit": { isExample: true },
3224
+ "@card": { isExample: true }
3225
+ }
2711
3226
  ],
2712
3227
  description: "Example text for the match matrix.",
2713
- format: TagFormat.plainText
3228
+ format: TagFormat.bitmarkText,
3229
+ nullable: true
2714
3230
  },
2715
3231
  {
3232
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3233
+ // empty-string positional blank into `forValues` so the
3234
+ // array stays aligned with the cell positions (matches BPG
3235
+ // `extractHeadingCard`'s "all value-side headings present"
3236
+ // shape). The `@keyonly` rule is required for two reasons:
3237
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3238
+ // in the unconditional rule has no source for bare); and
3239
+ // 2) the presence of any `@keyonly` rule disables the
3240
+ // optimised-mode `should_suppress_outcome` strip (see
3241
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3242
+ // survives the all-natural-defaults check.
2716
3243
  key: ConfigKey.tag_title,
2717
3244
  description: "Title of the match matrix.",
2718
3245
  format: TagFormat.plainText,
2719
3246
  jsonKey: "^heading.forValues",
2720
- exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
3247
+ exportJsonKey: [
3248
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3249
+ { "@bit": { heading: { forValues: ["$"] } } }
3250
+ ]
2721
3251
  },
2722
3252
  {
2723
3253
  key: ConfigKey.property_isCaseSensitive,
@@ -2766,6 +3296,35 @@ var CARDSETS = {
2766
3296
  {
2767
3297
  key: ConfigKey.group_standardTags,
2768
3298
  description: "Standard tags for the statement."
3299
+ },
3300
+ {
3301
+ // PLAN-072: cascade-fired `@example` from the bit header
3302
+ // fills each statement entry's `example` with the entry's
3303
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3304
+ // for the statements cardset. Per-array-item strip removes
3305
+ // `example: false` from incorrect entries so only correct
3306
+ // entries retain `example: true`.
3307
+ key: ConfigKey.property_example,
3308
+ exportJsonKey: [
3309
+ {
3310
+ "@keyonly": {
3311
+ isExample: true,
3312
+ example: true,
3313
+ "@bit": { isExample: true }
3314
+ }
3315
+ },
3316
+ {
3317
+ "@absent": {
3318
+ isExample: true,
3319
+ example: "$parent.isCorrect",
3320
+ "@bit": { isExample: true }
3321
+ }
3322
+ },
3323
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3324
+ ],
3325
+ description: "Example marker on the statement. Bare `[@example]` emits the boolean marker; valued `[@example:true|false]` emits the literal value. Cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3326
+ format: TagFormat.boolean,
3327
+ nullable: true
2769
3328
  }
2770
3329
  ],
2771
3330
  bodyAllowed: false
@@ -2794,6 +3353,71 @@ var CARDSETS = {
2794
3353
  {
2795
3354
  key: ConfigKey.group_standardTags,
2796
3355
  description: "Standard tags for the quiz."
3356
+ },
3357
+ {
3358
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3359
+ // quiz choices is delegated to group_trueFalse's
3360
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3361
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3362
+ // would over-fire on subsequent correct choices and on
3363
+ // incorrect ones — drop it.
3364
+ key: ConfigKey.property_example,
3365
+ exportJsonKey: [
3366
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3367
+ { isExample: true, "@bit": { isExample: true } }
3368
+ ],
3369
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3370
+ format: TagFormat.boolean,
3371
+ nullable: true
3372
+ }
3373
+ ],
3374
+ bodyAllowed: false
3375
+ }
3376
+ ]
3377
+ }
3378
+ ]
3379
+ },
3380
+ //
3381
+ // quiz (responses)
3382
+ //
3383
+ [CardSetConfigKey.quizResponses]: {
3384
+ jsonKey: "quizzes",
3385
+ exportJsonKey: { quizzes: "$" },
3386
+ sides: [
3387
+ {
3388
+ name: "responses",
3389
+ variants: [
3390
+ {
3391
+ jsonKey: null,
3392
+ tags: [
3393
+ {
3394
+ key: ConfigKey.group_trueFalseResponses,
3395
+ description: "Group for true/false questions."
3396
+ },
3397
+ {
3398
+ key: ConfigKey.group_standardTags,
3399
+ description: "Standard tags for the quiz."
3400
+ },
3401
+ {
3402
+ // PLAN-072: cascade-fired `@example` from the bit header
3403
+ // fills each response entry's `example` with the entry's
3404
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3405
+ // for the quiz-responses cardset (statements-style).
3406
+ key: ConfigKey.property_example,
3407
+ exportJsonKey: [
3408
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3409
+ {
3410
+ "@absent": {
3411
+ isExample: true,
3412
+ example: "$parent.isCorrect",
3413
+ "@bit": { isExample: true }
3414
+ }
3415
+ },
3416
+ { isExample: true, "@bit": { isExample: true } }
3417
+ ],
3418
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3419
+ format: TagFormat.boolean,
3420
+ nullable: true
2797
3421
  }
2798
3422
  ],
2799
3423
  bodyAllowed: false
@@ -2819,6 +3443,73 @@ var CARDSETS = {
2819
3443
  key: ConfigKey.group_standardItemLeadInstructionHint,
2820
3444
  description: "Standard tags for lead, instruction, and hint."
2821
3445
  },
3446
+ {
3447
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3448
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3449
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3450
+ // feedback-specific shape.
3451
+ //
3452
+ // Chain @example: `[+ X][@example]` writes
3453
+ // `isExample/example` onto the X choice only. BPG's
3454
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3455
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3456
+ // there is no bubble to bit-level or feedbacks-level
3457
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3458
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3459
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3460
+ // no feedback dispatch, so bit-header `[@example]` does
3461
+ // not flow into feedback choices.
3462
+ key: ConfigKey.tag_true,
3463
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3464
+ description: "Reason-required choice for feedback (`[+]`).",
3465
+ maxCount: Count.infinity,
3466
+ format: TagFormat.plainText,
3467
+ chain: [
3468
+ {
3469
+ key: ConfigKey.property_example,
3470
+ exportJsonKey: [
3471
+ { "@keyonly": { isExample: true, example: true } },
3472
+ { isExample: true, example: "$" }
3473
+ ],
3474
+ description: "Per-choice example marker for feedback `[+]`.",
3475
+ format: TagFormat.boolean,
3476
+ maxCount: 1,
3477
+ nullable: true
3478
+ },
3479
+ {
3480
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3481
+ // choice; same for `[?]` (hint), `[%]` (item),
3482
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3483
+ // Mirrors group_trueFalse's `+` chain shape.
3484
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3485
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3486
+ }
3487
+ ]
3488
+ },
3489
+ {
3490
+ key: ConfigKey.tag_false,
3491
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3492
+ description: "Reason-not-required choice for feedback (`[-]`).",
3493
+ maxCount: Count.infinity,
3494
+ format: TagFormat.plainText,
3495
+ chain: [
3496
+ {
3497
+ key: ConfigKey.property_example,
3498
+ exportJsonKey: [
3499
+ { "@keyonly": { isExample: true, example: true } },
3500
+ { isExample: true, example: "$" }
3501
+ ],
3502
+ description: "Per-choice example marker for feedback `[-]`.",
3503
+ format: TagFormat.boolean,
3504
+ maxCount: 1,
3505
+ nullable: true
3506
+ },
3507
+ {
3508
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3509
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3510
+ }
3511
+ ]
3512
+ },
2822
3513
  {
2823
3514
  key: ConfigKey.group_trueFalse,
2824
3515
  description: "Group for true/false feedback."
@@ -2839,31 +3530,71 @@ var CARDSETS = {
2839
3530
  name: "reason",
2840
3531
  variants: [
2841
3532
  {
3533
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3534
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3535
+ // under `reason.*`; each tag here spells out the full path.
3536
+ // Body is a plain string (BPG `parseFeedback` uses
3537
+ // `cardBodyStr ?? ''` for `reason.text`).
2842
3538
  jsonKey: "reason.text",
2843
3539
  exportJsonKey: { reason: { text: "$" } },
3540
+ format: TextFormat2.plainText,
2844
3541
  tags: [
2845
3542
  {
2846
- key: ConfigKey.group_standardItemLeadInstructionHint,
2847
- description: "Standard tags for lead, instruction, and hint."
3543
+ // Instruction lands under reason. Hint/item/lead/etc.
3544
+ // omitted: not exercised by any fixture and BPG's
3545
+ // `parseFeedback` spreads only the tags actually present
3546
+ // — adding the rest can be done lazily as fixtures
3547
+ // require them, with the same `{reason: {…}}` wrapper.
3548
+ key: ConfigKey.tag_instruction,
3549
+ exportJsonKey: { reason: { instruction: "$" } },
3550
+ description: "Instruction for the feedback reason.",
3551
+ format: TagFormat.bitmarkText
2848
3552
  },
2849
3553
  {
2850
3554
  key: ConfigKey.property_reasonableNumOfChars,
2851
3555
  exportJsonKey: [
2852
- { "@absent": { reasonableNumOfChars: "$ancestor" } },
2853
- { reasonableNumOfChars: "$" }
3556
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3557
+ { reason: { reasonableNumOfChars: "$" } }
2854
3558
  ],
2855
3559
  description: "Property for reasonable number of characters.",
2856
3560
  format: TagFormat.number
2857
3561
  },
2858
3562
  {
3563
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3564
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3565
+ // cascade. Local emission only.
3566
+ //
3567
+ // Both `reason.example` and `feedbacks[].example` are
3568
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3569
+ // here is `bitmarkText` so `$` coerces to a paragraph
3570
+ // array at both write sites.
2859
3571
  key: ConfigKey.property_example,
2860
3572
  exportJsonKey: [
2861
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2862
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2863
- { isExample: true, example: "$", "@bit": { isExample: true } }
3573
+ {
3574
+ // Bare `[@example]`: reason.example is TextAst BPG
3575
+ // coerces the boolean default to a paragraph
3576
+ // containing the string "true". feedbacks[].example
3577
+ // is not TextAst, stays a boolean. Two literal writes.
3578
+ "@keyonly": {
3579
+ reason: {
3580
+ isExample: true,
3581
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3582
+ },
3583
+ isExample: true,
3584
+ example: true,
3585
+ "@bit": { isExample: true }
3586
+ }
3587
+ },
3588
+ {
3589
+ reason: { isExample: true, example: "$" },
3590
+ isExample: true,
3591
+ example: "$",
3592
+ "@bit": { isExample: true }
3593
+ }
2864
3594
  ],
2865
- description: "Example text for the feedback.",
2866
- format: TagFormat.plainText
3595
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3596
+ format: TagFormat.bitmarkText,
3597
+ nullable: true
2867
3598
  },
2868
3599
  {
2869
3600
  key: ConfigKey.tag_title,
@@ -2927,6 +3658,39 @@ var CARDSETS = {
2927
3658
  {
2928
3659
  key: ConfigKey.group_standardTags,
2929
3660
  description: "Standard tags for the question."
3661
+ },
3662
+ {
3663
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3664
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3665
+ // does not propagate `example`/`isExample` onto question
3666
+ // entries. Only entry-local `[@example]` fires.
3667
+ //
3668
+ // BPG's `BaseBuilder.toExample` for interview questions
3669
+ // (Builder.ts:2575,2588) uses `data.__sampleSolutionAst`
3670
+ // as `defaultExample` — bare `[@example]` (which sets
3671
+ // `__isDefaultExample=true, example=undefined`) emits the
3672
+ // parsed sample-solution AST in the `example` slot. The
3673
+ // `@keyonly` rule mirrors that runtime behaviour via
3674
+ // `$parent.sampleSolution` — the chain-walker's post-walk
3675
+ // retry resolves this once the sibling `[@sampleSolution]`
3676
+ // chain has merged into the shared scope (interview's
3677
+ // `[@example]` typically precedes `[@sampleSolution]` in
3678
+ // source). Format coercion turns the resolved string
3679
+ // value into a PM tree.
3680
+ key: ConfigKey.property_example,
3681
+ exportJsonKey: [
3682
+ {
3683
+ "@keyonly": {
3684
+ isExample: true,
3685
+ example: "$parent.sampleSolution",
3686
+ "@bit": { isExample: true }
3687
+ }
3688
+ },
3689
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3690
+ ],
3691
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3692
+ format: TagFormat.bitmarkText,
3693
+ nullable: true
2930
3694
  }
2931
3695
  ]
2932
3696
  }
@@ -2953,14 +3717,16 @@ var CARDSETS = {
2953
3717
  description: "Standard tags for lead, instruction, and hint."
2954
3718
  },
2955
3719
  {
3720
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3721
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2956
3722
  key: ConfigKey.property_example,
2957
3723
  exportJsonKey: [
2958
3724
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2959
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2960
3725
  { isExample: true, example: "$", "@bit": { isExample: true } }
2961
3726
  ],
2962
- description: "Example text for the element.",
2963
- format: TagFormat.plainText
3727
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3728
+ format: TagFormat.plainText,
3729
+ nullable: true
2964
3730
  }
2965
3731
  ],
2966
3732
  repeatCount: Count.infinity
@@ -2972,9 +3738,57 @@ var CARDSETS = {
2972
3738
  //
2973
3739
  // table
2974
3740
  //
3741
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3742
+ // output (`{ columns?, data, columnWidths? }`):
3743
+ //
3744
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3745
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3746
+ // no qualifier, so they route to the `table-body` (default) section.
3747
+ //
3748
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3749
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3750
+ // the first header card's cells to `columns` and demote remaining
3751
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3752
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3753
+ // here (no per-cell metadata in basic shape) — declared on the cell
3754
+ // variant below.
3755
+ //
2975
3756
  [CardSetConfigKey.table]: {
2976
- jsonKey: "table.data",
2977
- exportJsonKey: { table: { data: "$" } },
3757
+ jsonKey: null,
3758
+ exportJsonKey: null,
3759
+ sections: {
3760
+ "table-header": {
3761
+ // First header card's row of cells → `columns`. Subsequent header
3762
+ // cards' rows → `data` (per-card append; `[$]` shape because
3763
+ // per-card emission fires once per card, multiple fires deep_merge-
3764
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3765
+ // both ROUTE cards to this section (when no explicit `==== table-
3766
+ // header ====` qualifier) and SELECT the rule body once routed.
3767
+ // First rule = first card with any `[#]` → `columns`; second rule
3768
+ // = subsequent cards in this section → `data` (per-row append).
3769
+ jsonKey: "table.columns",
3770
+ exportJsonKey: [
3771
+ {
3772
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3773
+ rule: { "@bit": { table: { columns: "$" } } }
3774
+ },
3775
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3776
+ { "@bit": { table: { data: ["$"] } } }
3777
+ ]
3778
+ },
3779
+ "table-body": {
3780
+ // Batched emission: `$` is the entire 2D array of body card rows.
3781
+ // This is the default section (no qualifier → cards land here).
3782
+ jsonKey: "table.data",
3783
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3784
+ isDefault: true
3785
+ },
3786
+ "table-footer": {
3787
+ // Batched emission: `$` is the entire 2D array of footer rows.
3788
+ jsonKey: "table.data",
3789
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3790
+ }
3791
+ },
2978
3792
  sides: [
2979
3793
  {
2980
3794
  name: "cell",
@@ -3015,6 +3829,8 @@ var CARDSETS = {
3015
3829
  },
3016
3830
  {
3017
3831
  key: ConfigKey.property_tableColWidth,
3832
+ jsonKey: "^table.columnWidths[{s}]",
3833
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
3018
3834
  description: "Width for the column.",
3019
3835
  format: TagFormat.number
3020
3836
  }
@@ -3033,7 +3849,22 @@ var CARDSETS = {
3033
3849
  sections: {
3034
3850
  "table-header": {
3035
3851
  jsonKey: "table.header.rows",
3036
- exportJsonKey: { table: { header: { rows: "$" } } },
3852
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3853
+ // Cards with no explicit `==== table-header ====` qualifier
3854
+ // route here if FIRST CARD contains any `[#]` tag in any
3855
+ // variant. Explicit-qualifier cards fall through to the
3856
+ // unconditional fallback rule. Both rules use the per-card
3857
+ // append shape `rows: ['$']` — the runtime triggers per-card
3858
+ // emission whenever the pattern carries predicates, and each
3859
+ // fire appends a single-element array (multi-fire deep-merge
3860
+ // concatenates).
3861
+ exportJsonKey: [
3862
+ {
3863
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3864
+ rule: { table: { header: { rows: ["$"] } } }
3865
+ },
3866
+ { table: { header: { rows: ["$"] } } }
3867
+ ],
3037
3868
  sideJsonKey: "cells[{s}]|set(title=true)",
3038
3869
  sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
3039
3870
  },
@@ -3066,13 +3897,13 @@ var CARDSETS = {
3066
3897
  },
3067
3898
  {
3068
3899
  key: ConfigKey.tag_title,
3069
- jsonKey: ".",
3070
- exportJsonKey: {
3071
- "@bit": {
3072
- table: { header: { rows: { cells: { cells: { $s: { content: "$" } } } } } }
3073
- }
3074
- },
3075
- description: "Title of the table cell."
3900
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3901
+ // `content` field. When the card is in the `table-header`
3902
+ // section, the section's sideExportJsonKey wraps each
3903
+ // cell with `title: true` automatically.
3904
+ jsonKey: "content",
3905
+ exportJsonKey: { content: "$" },
3906
+ description: "Title text of the table cell (header rows)."
3076
3907
  },
3077
3908
  {
3078
3909
  key: ConfigKey.property_tableCellType,
@@ -3104,6 +3935,9 @@ var CARDSETS = {
3104
3935
  },
3105
3936
  {
3106
3937
  key: ConfigKey.property_tableColWidth,
3938
+ // PLAN-080: write to the cell's `colwidth` field.
3939
+ jsonKey: "colwidth",
3940
+ exportJsonKey: { colwidth: "$" },
3107
3941
  description: "Width for the column.",
3108
3942
  format: TagFormat.number
3109
3943
  }
@@ -3141,7 +3975,8 @@ var CARDSETS = {
3141
3975
  {
3142
3976
  key: ConfigKey.resource_audio,
3143
3977
  description: "Audio resource for the pronunciation table.",
3144
- jsonKey: "audio"
3978
+ jsonKey: "audio",
3979
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
3145
3980
  }
3146
3981
  ],
3147
3982
  repeatCount: Count.infinity
@@ -3212,14 +4047,17 @@ var CARDSETS = {
3212
4047
  format: TagFormat.plainText
3213
4048
  },
3214
4049
  {
4050
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
4051
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
4052
+ // header cascade.
3215
4053
  key: ConfigKey.property_example,
3216
4054
  exportJsonKey: [
3217
4055
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3218
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
3219
4056
  { isExample: true, example: "$", "@bit": { isExample: true } }
3220
4057
  ],
3221
- description: "Example text for the bot action response.",
3222
- format: TagFormat.plainText
4058
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
4059
+ format: TagFormat.plainText,
4060
+ nullable: true
3223
4061
  }
3224
4062
  ]
3225
4063
  }
@@ -3285,6 +4123,38 @@ var CARDSETS = {
3285
4123
  ]
3286
4124
  },
3287
4125
  //
4126
+ // page-footer-sections — mirrors example-bit-list but emits cards under
4127
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
4128
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
4129
+ // routing as pure config so the Rust serializer can pick it up.
4130
+ //
4131
+ [CardSetConfigKey.pageFooterSections]: {
4132
+ jsonKey: "sections",
4133
+ exportJsonKey: { sections: "$" },
4134
+ sides: [
4135
+ {
4136
+ name: "item",
4137
+ variants: [
4138
+ {
4139
+ jsonKey: "body",
4140
+ exportJsonKey: { body: "$" },
4141
+ tags: [
4142
+ {
4143
+ key: ConfigKey.group_standardTags,
4144
+ description: "Standard tags for page-footer section bits."
4145
+ },
4146
+ {
4147
+ exportJsonKey: { title: "$" },
4148
+ key: ConfigKey.tag_title,
4149
+ description: "Title of the page-footer section."
4150
+ }
4151
+ ]
4152
+ }
4153
+ ]
4154
+ }
4155
+ ]
4156
+ },
4157
+ //
3288
4158
  // ingredients
3289
4159
  //
3290
4160
  [CardSetConfigKey.ingredients]: {
@@ -3615,7 +4485,11 @@ var GROUPS = {
3615
4485
  {
3616
4486
  key: ConfigKey.property_groupTag,
3617
4487
  jsonKey: "groupTag.name",
3618
- exportJsonKey: { groupTag: { name: "$" } },
4488
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4489
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4490
+ // same value fold into one entry; chained `@tag` children's
4491
+ // arrays merge as sets.
4492
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3619
4493
  description: "The group tag(s) for the bit",
3620
4494
  format: TagFormat.plainText,
3621
4495
  maxCount: Count.infinity,
@@ -3788,6 +4662,24 @@ var GROUPS = {
3788
4662
  }
3789
4663
  ]
3790
4664
  },
4665
+ [ConfigKey.group_standardInstructionHint]: {
4666
+ type: GroupConfigType.standard,
4667
+ description: "Standard group for instruction and hint tags only (no item/lead/pageNumber/marginNumber chain). Used by sides where item/lead are owned by another side (e.g. matchPairs value-side, which BPG strips item/lead from at sideIdx > 0).",
4668
+ tags: [
4669
+ {
4670
+ exportJsonKey: { instruction: "$" },
4671
+ key: ConfigKey.tag_instruction,
4672
+ name: "Instruction",
4673
+ description: "The instruction for the bit"
4674
+ },
4675
+ {
4676
+ exportJsonKey: { hint: "$" },
4677
+ key: ConfigKey.tag_hint,
4678
+ name: "Hint",
4679
+ description: "The hint for the bit"
4680
+ }
4681
+ ]
4682
+ },
3791
4683
  [ConfigKey.group_standardTags]: {
3792
4684
  type: GroupConfigType.standard,
3793
4685
  description: "Standard tags which apply to MOST (but not all) bits",
@@ -3801,17 +4693,45 @@ var GROUPS = {
3801
4693
  description: "Item, lead, page number, margin number, instruction and hint tags"
3802
4694
  },
3803
4695
  {
3804
- // Bit-level example tag: bare/synthesized records the marker only;
3805
- // valued [@example:text] emits `isExample: true` and `example` as
3806
- // PM-rendered rich text (bitmark+ format).
4696
+ // Bit-level example tag (default non-`isTopLevelExample` bits):
4697
+ // - bare `[@example]`: consume only (records cascade marker, emits
4698
+ // nothing at bit-level OLD parser scrubs bit-level isExample
4699
+ // for bits NOT in its `isTopLevelExample` allow-list).
4700
+ // - synthesized (descendant cascade): still emits `isExample: true`
4701
+ // at the descendant scope.
4702
+ // - valued `[@example:text]`: emit the example value only; bit-level
4703
+ // `isExample` is left to bubble from descendants.
4704
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4705
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4706
+ // matchMatrix, match, essay, etc.) override this entry with the
4707
+ // legacy emit-isExample shape.
3807
4708
  key: ConfigKey.property_example,
3808
- exportJsonKey: [
3809
- { "@keyonly": { isExample: true } },
3810
- { "@absent": { isExample: true } },
3811
- { isExample: true, example: "$" }
3812
- ],
4709
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3813
4710
  description: "The example(s) for the bit",
3814
- format: TagFormat.bitmarkText
4711
+ format: TagFormat.bitmarkText,
4712
+ nullable: true
4713
+ }
4714
+ ]
4715
+ },
4716
+ [ConfigKey.group_standardTagsNoItemLead]: {
4717
+ type: GroupConfigType.standard,
4718
+ description: "Standard tags mirror of group_standardTags but excluding the item/lead/pageNumber/marginNumber chain. Used by sides where item/lead are owned by another side (e.g. matchPairs value-side \u2014 BPG `parseMatchPairs` strips tags.item / tags.lead for sideIdx > 0).",
4719
+ tags: [
4720
+ {
4721
+ key: ConfigKey.group_standardAllBits,
4722
+ description: "All standard tags which apply to all bits"
4723
+ },
4724
+ {
4725
+ key: ConfigKey.group_standardInstructionHint,
4726
+ description: "Instruction and hint tags only (no item/lead chain)."
4727
+ },
4728
+ {
4729
+ // Mirror of group_standardTags property_example entry.
4730
+ key: ConfigKey.property_example,
4731
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4732
+ description: "The example(s) for the bit",
4733
+ format: TagFormat.bitmarkText,
4734
+ nullable: true
3815
4735
  }
3816
4736
  ]
3817
4737
  },
@@ -3876,8 +4796,8 @@ var GROUPS = {
3876
4796
  tags: [
3877
4797
  {
3878
4798
  key: ConfigKey.property_person,
3879
- jsonKey: "partner.name",
3880
- exportJsonKey: { partner: { name: "$" } },
4799
+ jsonKey: "person.name",
4800
+ exportJsonKey: { person: { name: "$" } },
3881
4801
  description: "A person",
3882
4802
  format: TagFormat.plainText,
3883
4803
  chain: [
@@ -3897,8 +4817,8 @@ var GROUPS = {
3897
4817
  {
3898
4818
  // Deprecated (parter renamed to person)
3899
4819
  key: ConfigKey.property_partner,
3900
- jsonKey: "partner.name",
3901
- exportJsonKey: { partner: { name: "$" } },
4820
+ jsonKey: "person.name",
4821
+ exportJsonKey: { person: { name: "$" } },
3902
4822
  description: "A partner",
3903
4823
  format: TagFormat.plainText,
3904
4824
  chain: [
@@ -3922,7 +4842,9 @@ var GROUPS = {
3922
4842
  description: "Gap chain",
3923
4843
  tags: [
3924
4844
  {
3925
- exportJsonKey: { solutions: [["$"]] },
4845
+ exportJsonKey: {
4846
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4847
+ },
3926
4848
  key: ConfigKey.tag_gap,
3927
4849
  description: "The value of a gap in the content",
3928
4850
  maxCount: Count.infinity,
@@ -3941,27 +4863,37 @@ var GROUPS = {
3941
4863
  description: "Item, lead, page number, margin number, instruction and hint tags"
3942
4864
  },
3943
4865
  {
4866
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4867
+ // `__solutionsAst[length - 1]` — the solution closest to the
4868
+ // `[@example]` tag at parse time. Our chain accumulator is built
4869
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4870
+ // the moment `[@example]` fires, `solutions` only contains the
4871
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4872
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4873
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4874
+ // solutions=["solution"], `[-1]`="solution".
3944
4875
  key: ConfigKey.property_example,
3945
4876
  jsonKey: "example",
3946
4877
  exportJsonKey: [
3947
4878
  {
3948
4879
  "@keyonly": {
3949
4880
  isExample: true,
3950
- example: "$parent.solutions[0]",
4881
+ example: "$parent.solutions[-1]",
3951
4882
  "@bit": { isExample: true }
3952
4883
  }
3953
4884
  },
3954
4885
  {
3955
4886
  "@absent": {
3956
4887
  isExample: true,
3957
- example: "$parent.solutions[0]",
4888
+ example: "$parent.solutions[-1]",
3958
4889
  "@bit": { isExample: true }
3959
4890
  }
3960
4891
  },
3961
4892
  { isExample: true, example: "$", "@bit": { isExample: true } }
3962
4893
  ],
3963
4894
  description: "An example for the gap",
3964
- format: TagFormat.bitmarkText
4895
+ format: TagFormat.bitmarkText,
4896
+ nullable: true
3965
4897
  },
3966
4898
  {
3967
4899
  key: ConfigKey.property_isCaseSensitive,
@@ -4010,6 +4942,99 @@ var GROUPS = {
4010
4942
  description: "Item, lead, page number, margin number, instruction and hint tags"
4011
4943
  },
4012
4944
  {
4945
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4946
+ // `@example` from the bit header marks EVERY choice with
4947
+ // `isExample: true` (matches BPG output), but only the
4948
+ // FIRST correct choice gets `example: true` (BPG
4949
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4950
+ //
4951
+ // Two `@absent` rules, picked first-match-wins:
4952
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4953
+ // `maxEmits:1` — fires once for the first correct.
4954
+ // 2. Plain `@absent` — fires on every remaining entry
4955
+ // (subsequent corrects and all incorrects), emitting
4956
+ // `isExample` only.
4957
+ // The `serialize_cards` cascade-counter scope holds the cap
4958
+ // across the whole cardset.
4959
+ // @card scope-shift writes choice-container isExample (e.g.
4960
+ // `quizzes[i].isExample`) — was previously provided by the
4961
+ // universal `bubble_is_example` post-walk, which was removed
4962
+ // in favour of explicit per-tag config. SYNTAX.md §8.
4963
+ key: ConfigKey.property_example,
4964
+ jsonKey: "example",
4965
+ exportJsonKey: [
4966
+ {
4967
+ "@keyonly": {
4968
+ isExample: true,
4969
+ example: "$parent.isCorrect",
4970
+ "@bit": { isExample: true },
4971
+ "@card": { isExample: true }
4972
+ }
4973
+ },
4974
+ {
4975
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4976
+ maxEmits: 1,
4977
+ rule: {
4978
+ isExample: true,
4979
+ example: true,
4980
+ "@bit": { isExample: true },
4981
+ "@card": { isExample: true }
4982
+ }
4983
+ },
4984
+ {
4985
+ "@absent": {
4986
+ isExample: true,
4987
+ "@bit": { isExample: true },
4988
+ "@card": { isExample: true }
4989
+ }
4990
+ },
4991
+ {
4992
+ isExample: true,
4993
+ example: "$",
4994
+ "@bit": { isExample: true },
4995
+ "@card": { isExample: true }
4996
+ }
4997
+ ],
4998
+ description: "An example for the true/false statement/question",
4999
+ format: TagFormat.boolean,
5000
+ nullable: true
5001
+ }
5002
+ ]
5003
+ },
5004
+ {
5005
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5006
+ key: ConfigKey.tag_false,
5007
+ description: "False value for a true/false statement/question",
5008
+ maxCount: Count.infinity,
5009
+ format: TagFormat.plainText,
5010
+ chain: [
5011
+ {
5012
+ key: ConfigKey.tag_true,
5013
+ jsonKey: "choices[]|set(isCorrect=true)",
5014
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
5015
+ description: "True values for a true/false statement/question",
5016
+ maxCount: Count.infinity,
5017
+ format: TagFormat.plainText
5018
+ },
5019
+ {
5020
+ key: ConfigKey.tag_false,
5021
+ jsonKey: "choices[]|set(isCorrect=false)",
5022
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5023
+ description: "False values for a true/false statement/question",
5024
+ maxCount: Count.infinity,
5025
+ format: TagFormat.plainText
5026
+ },
5027
+ {
5028
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5029
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5030
+ },
5031
+ {
5032
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
5033
+ // `@example` on a `-` (incorrect choice) emits
5034
+ // `isExample: true` only — no `example` (BPG fixtures show
5035
+ // `isExample` on every choice, but `example` only on the
5036
+ // first correct via firstCorrectOnly cascade).
5037
+ // @card scope-shift writes choice-container isExample.
4013
5038
  key: ConfigKey.property_example,
4014
5039
  jsonKey: "example",
4015
5040
  exportJsonKey: [
@@ -4017,25 +5042,99 @@ var GROUPS = {
4017
5042
  "@keyonly": {
4018
5043
  isExample: true,
4019
5044
  example: "$parent.isCorrect",
4020
- "@bit": { isExample: true }
5045
+ "@bit": { isExample: true },
5046
+ "@card": { isExample: true }
4021
5047
  }
4022
5048
  },
4023
5049
  {
4024
5050
  "@absent": {
5051
+ isExample: true,
5052
+ "@bit": { isExample: true },
5053
+ "@card": { isExample: true }
5054
+ }
5055
+ },
5056
+ {
5057
+ isExample: true,
5058
+ example: "$",
5059
+ "@bit": { isExample: true },
5060
+ "@card": { isExample: true }
5061
+ }
5062
+ ],
5063
+ description: "An example for the true/false statement/question (incorrect entry \u2014 isExample only on cascade)",
5064
+ format: TagFormat.boolean,
5065
+ nullable: true
5066
+ }
5067
+ ]
5068
+ }
5069
+ ]
5070
+ },
5071
+ [ConfigKey.group_trueFalseResponses]: {
5072
+ type: GroupConfigType.standard,
5073
+ description: "True/False Responses chain",
5074
+ tags: [
5075
+ {
5076
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
5077
+ key: ConfigKey.tag_true,
5078
+ description: "True value for a true/false statement/question",
5079
+ maxCount: Count.infinity,
5080
+ format: TagFormat.plainText,
5081
+ chain: [
5082
+ {
5083
+ key: ConfigKey.tag_true,
5084
+ jsonKey: "responses[]|set(isCorrect=true)",
5085
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
5086
+ description: "True values for a true/false statement/question",
5087
+ maxCount: Count.infinity,
5088
+ format: TagFormat.plainText
5089
+ },
5090
+ {
5091
+ key: ConfigKey.tag_false,
5092
+ jsonKey: "responses[]|set(isCorrect=false)",
5093
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
5094
+ description: "False values for a true/false statement/question",
5095
+ maxCount: Count.infinity,
5096
+ format: TagFormat.plainText
5097
+ },
5098
+ {
5099
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5100
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5101
+ },
5102
+ {
5103
+ // @card scope-shift writes response-container isExample.
5104
+ key: ConfigKey.property_example,
5105
+ jsonKey: "example",
5106
+ exportJsonKey: [
5107
+ {
5108
+ "@keyonly": {
4025
5109
  isExample: true,
4026
5110
  example: "$parent.isCorrect",
4027
- "@bit": { isExample: true }
5111
+ "@bit": { isExample: true },
5112
+ "@card": { isExample: true }
4028
5113
  }
4029
5114
  },
4030
- { isExample: true, example: "$", "@bit": { isExample: true } }
5115
+ {
5116
+ "@absent": {
5117
+ isExample: "$parent.isCorrect",
5118
+ example: "$parent.isCorrect",
5119
+ "@bit": { isExample: "$parent.isCorrect" },
5120
+ "@card": { isExample: "$parent.isCorrect" }
5121
+ }
5122
+ },
5123
+ {
5124
+ isExample: true,
5125
+ example: "$",
5126
+ "@bit": { isExample: true },
5127
+ "@card": { isExample: true }
5128
+ }
4031
5129
  ],
4032
5130
  description: "An example for the true/false statement/question",
4033
- format: TagFormat.bitmarkText
5131
+ format: TagFormat.boolean,
5132
+ nullable: true
4034
5133
  }
4035
5134
  ]
4036
5135
  },
4037
5136
  {
4038
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5137
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4039
5138
  key: ConfigKey.tag_false,
4040
5139
  description: "False value for a true/false statement/question",
4041
5140
  maxCount: Count.infinity,
@@ -4043,16 +5142,16 @@ var GROUPS = {
4043
5142
  chain: [
4044
5143
  {
4045
5144
  key: ConfigKey.tag_true,
4046
- jsonKey: "choices[]|set(isCorrect=true)",
4047
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
5145
+ jsonKey: "responses[]|set(isCorrect=true)",
5146
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4048
5147
  description: "True values for a true/false statement/question",
4049
5148
  maxCount: Count.infinity,
4050
5149
  format: TagFormat.plainText
4051
5150
  },
4052
5151
  {
4053
5152
  key: ConfigKey.tag_false,
4054
- jsonKey: "choices[]|set(isCorrect=false)",
4055
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5153
+ jsonKey: "responses[]|set(isCorrect=false)",
5154
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4056
5155
  description: "False values for a true/false statement/question",
4057
5156
  maxCount: Count.infinity,
4058
5157
  format: TagFormat.plainText
@@ -4062,6 +5161,7 @@ var GROUPS = {
4062
5161
  description: "Item, lead, page number, margin number, instruction and hint tags"
4063
5162
  },
4064
5163
  {
5164
+ // @card scope-shift writes response-container isExample.
4065
5165
  key: ConfigKey.property_example,
4066
5166
  jsonKey: "example",
4067
5167
  exportJsonKey: [
@@ -4069,20 +5169,333 @@ var GROUPS = {
4069
5169
  "@keyonly": {
4070
5170
  isExample: true,
4071
5171
  example: "$parent.isCorrect",
4072
- "@bit": { isExample: true }
5172
+ "@bit": { isExample: true },
5173
+ "@card": { isExample: true }
4073
5174
  }
4074
5175
  },
4075
5176
  {
4076
5177
  "@absent": {
4077
5178
  isExample: true,
4078
5179
  example: "$parent.isCorrect",
4079
- "@bit": { isExample: true }
5180
+ "@bit": { isExample: true },
5181
+ "@card": { isExample: true }
4080
5182
  }
4081
5183
  },
4082
- { isExample: true, example: "$", "@bit": { isExample: true } }
5184
+ {
5185
+ isExample: true,
5186
+ example: "$",
5187
+ "@bit": { isExample: true },
5188
+ "@card": { isExample: true }
5189
+ }
4083
5190
  ],
4084
5191
  description: "An example for the true/false statement/question",
4085
- format: TagFormat.bitmarkText
5192
+ format: TagFormat.boolean,
5193
+ nullable: true
5194
+ }
5195
+ ]
5196
+ }
5197
+ ]
5198
+ },
5199
+ [ConfigKey.group_trueFalseInlineSelect]: {
5200
+ type: GroupConfigType.standard,
5201
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
5202
+ tags: [
5203
+ {
5204
+ exportJsonKey: {
5205
+ "@body-inline": {
5206
+ type: "select",
5207
+ attrs: { options: [{ text: "$", isCorrect: true }] }
5208
+ }
5209
+ },
5210
+ key: ConfigKey.tag_true,
5211
+ description: "True value for an inline select body bit",
5212
+ maxCount: Count.infinity,
5213
+ format: TagFormat.plainText,
5214
+ chain: [
5215
+ {
5216
+ key: ConfigKey.tag_true,
5217
+ exportJsonKey: {},
5218
+ description: "True values for an inline select body bit",
5219
+ maxCount: Count.infinity,
5220
+ format: TagFormat.plainText
5221
+ },
5222
+ {
5223
+ key: ConfigKey.tag_false,
5224
+ exportJsonKey: {},
5225
+ description: "False values for an inline select body bit",
5226
+ maxCount: Count.infinity,
5227
+ format: TagFormat.plainText
5228
+ },
5229
+ {
5230
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5231
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5232
+ },
5233
+ {
5234
+ // PLAN-083: cascade-fired `@example` from bit header fires
5235
+ // `isExample:true, example:true` on the FIRST CORRECT option
5236
+ // only (BPG `fillBooleanExample(select.options,
5237
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
5238
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5239
+ // OR — any cascade ⇒ bit marked) and
5240
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5241
+ // on the select node).
5242
+ key: ConfigKey.property_example,
5243
+ jsonKey: "example",
5244
+ exportJsonKey: [
5245
+ {
5246
+ "@keyonly": {
5247
+ isExample: true,
5248
+ example: "$parent.isCorrect",
5249
+ "@bit": { isExample: true },
5250
+ "@body-inline": { attrs: { isExample: true } }
5251
+ }
5252
+ },
5253
+ {
5254
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5255
+ maxEmits: 1,
5256
+ rule: {
5257
+ isExample: true,
5258
+ example: true,
5259
+ "@bit": { isExample: true },
5260
+ "@body-inline": { attrs: { isExample: true } }
5261
+ }
5262
+ },
5263
+ {
5264
+ isExample: true,
5265
+ example: "$",
5266
+ "@bit": { isExample: true },
5267
+ "@body-inline": { attrs: { isExample: true } }
5268
+ }
5269
+ ],
5270
+ description: "An example for the inline select option (first correct only)",
5271
+ format: TagFormat.boolean,
5272
+ nullable: true
5273
+ }
5274
+ ]
5275
+ },
5276
+ {
5277
+ exportJsonKey: {
5278
+ "@body-inline": {
5279
+ type: "select",
5280
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5281
+ }
5282
+ },
5283
+ key: ConfigKey.tag_false,
5284
+ description: "False value for an inline select body bit",
5285
+ maxCount: Count.infinity,
5286
+ format: TagFormat.plainText,
5287
+ chain: [
5288
+ {
5289
+ key: ConfigKey.tag_true,
5290
+ exportJsonKey: {},
5291
+ description: "True values for an inline select body bit",
5292
+ maxCount: Count.infinity,
5293
+ format: TagFormat.plainText
5294
+ },
5295
+ {
5296
+ key: ConfigKey.tag_false,
5297
+ exportJsonKey: {},
5298
+ description: "False values for an inline select body bit",
5299
+ maxCount: Count.infinity,
5300
+ format: TagFormat.plainText
5301
+ },
5302
+ {
5303
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5304
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5305
+ },
5306
+ {
5307
+ // PLAN-083: identical to the `+` lead's chain @example —
5308
+ // cascade reaches the first correct option regardless of
5309
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5310
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5311
+ key: ConfigKey.property_example,
5312
+ jsonKey: "example",
5313
+ exportJsonKey: [
5314
+ {
5315
+ "@keyonly": {
5316
+ isExample: true,
5317
+ example: "$parent.isCorrect",
5318
+ "@bit": { isExample: true },
5319
+ "@body-inline": { attrs: { isExample: true } }
5320
+ }
5321
+ },
5322
+ {
5323
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5324
+ maxEmits: 1,
5325
+ rule: {
5326
+ isExample: true,
5327
+ example: true,
5328
+ "@bit": { isExample: true },
5329
+ "@body-inline": { attrs: { isExample: true } }
5330
+ }
5331
+ },
5332
+ {
5333
+ isExample: true,
5334
+ example: "$",
5335
+ "@bit": { isExample: true },
5336
+ "@body-inline": { attrs: { isExample: true } }
5337
+ }
5338
+ ],
5339
+ description: "An example for the inline select option (first correct only)",
5340
+ format: TagFormat.boolean,
5341
+ nullable: true
5342
+ }
5343
+ ]
5344
+ }
5345
+ ]
5346
+ },
5347
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5348
+ type: GroupConfigType.standard,
5349
+ description: 'True/False chain composed inline (highlight body-bit). +/- tags emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}` inline body nodes. Used by highlight-text bit.',
5350
+ tags: [
5351
+ {
5352
+ exportJsonKey: {
5353
+ "@body-inline": {
5354
+ type: "highlight",
5355
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5356
+ }
5357
+ },
5358
+ key: ConfigKey.tag_true,
5359
+ description: "Highlighted span in the body (correct)",
5360
+ maxCount: Count.infinity,
5361
+ format: TagFormat.plainText,
5362
+ chain: [
5363
+ {
5364
+ key: ConfigKey.tag_true,
5365
+ exportJsonKey: {},
5366
+ description: "Highlighted span values (correct)",
5367
+ maxCount: Count.infinity,
5368
+ format: TagFormat.plainText
5369
+ },
5370
+ {
5371
+ key: ConfigKey.tag_false,
5372
+ exportJsonKey: {},
5373
+ description: "Highlighted span values (incorrect)",
5374
+ maxCount: Count.infinity,
5375
+ format: TagFormat.plainText
5376
+ },
5377
+ {
5378
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5379
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5380
+ },
5381
+ {
5382
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5383
+ // `@example` from the bit header fires `example: true` +
5384
+ // `isExample: true` on the FIRST correct highlight only
5385
+ // (BPG `fillBooleanExample(highlight.texts,
5386
+ // firstCorrectOnly=true)`). Incorrect highlights get
5387
+ // nothing (unlike choices, the highlight-text fixture
5388
+ // shows no `isExample` on incorrect entries).
5389
+ key: ConfigKey.property_example,
5390
+ jsonKey: "example",
5391
+ exportJsonKey: [
5392
+ {
5393
+ "@keyonly": {
5394
+ isExample: true,
5395
+ example: "$parent.isCorrect",
5396
+ "@bit": { isExample: true },
5397
+ "@body-inline": { attrs: { isExample: true } }
5398
+ }
5399
+ },
5400
+ {
5401
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5402
+ maxEmits: 1,
5403
+ rule: {
5404
+ isExample: true,
5405
+ example: true,
5406
+ "@bit": { isExample: true },
5407
+ "@body-inline": { attrs: { isExample: true } }
5408
+ }
5409
+ },
5410
+ {
5411
+ isExample: true,
5412
+ example: "$",
5413
+ "@bit": { isExample: true },
5414
+ "@body-inline": { attrs: { isExample: true } }
5415
+ }
5416
+ ],
5417
+ description: "An example for the highlighted span (first correct only)",
5418
+ format: TagFormat.boolean,
5419
+ nullable: true
5420
+ }
5421
+ ]
5422
+ },
5423
+ {
5424
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5425
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5426
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5427
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5428
+ // through and nothing fires. The natural-default strip removes
5429
+ // `isCorrect: false` from the final texts entries (matches fixture).
5430
+ exportJsonKey: {
5431
+ "@body-inline": {
5432
+ type: "highlight",
5433
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5434
+ }
5435
+ },
5436
+ key: ConfigKey.tag_false,
5437
+ description: "Highlighted span in the body (incorrect)",
5438
+ maxCount: Count.infinity,
5439
+ format: TagFormat.plainText,
5440
+ chain: [
5441
+ {
5442
+ key: ConfigKey.tag_true,
5443
+ exportJsonKey: {},
5444
+ description: "Highlighted span values (correct)",
5445
+ maxCount: Count.infinity,
5446
+ format: TagFormat.plainText
5447
+ },
5448
+ {
5449
+ key: ConfigKey.tag_false,
5450
+ exportJsonKey: {},
5451
+ description: "Highlighted span values (incorrect)",
5452
+ maxCount: Count.infinity,
5453
+ format: TagFormat.plainText
5454
+ },
5455
+ {
5456
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5457
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5458
+ },
5459
+ {
5460
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5461
+ // `@example` on a `-` (incorrect highlight) emits nothing
5462
+ // (highlight-text fixture has no `isExample` on incorrect
5463
+ // entries; firstCorrectOnly cascade skips them entirely).
5464
+ // No `@absent` rule.
5465
+ // PLAN-083: identical to the `+` lead's chain @example —
5466
+ // cascade reaches the first correct option regardless of
5467
+ // which lead (+/-) opens the chain.
5468
+ key: ConfigKey.property_example,
5469
+ jsonKey: "example",
5470
+ exportJsonKey: [
5471
+ {
5472
+ "@keyonly": {
5473
+ isExample: true,
5474
+ example: "$parent.isCorrect",
5475
+ "@bit": { isExample: true },
5476
+ "@body-inline": { attrs: { isExample: true } }
5477
+ }
5478
+ },
5479
+ {
5480
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5481
+ maxEmits: 1,
5482
+ rule: {
5483
+ isExample: true,
5484
+ example: true,
5485
+ "@bit": { isExample: true },
5486
+ "@body-inline": { attrs: { isExample: true } }
5487
+ }
5488
+ },
5489
+ {
5490
+ isExample: true,
5491
+ example: "$",
5492
+ "@bit": { isExample: true },
5493
+ "@body-inline": { attrs: { isExample: true } }
5494
+ }
5495
+ ],
5496
+ description: "An example for the highlighted span (first correct only)",
5497
+ format: TagFormat.boolean,
5498
+ nullable: true
4086
5499
  }
4087
5500
  ]
4088
5501
  }
@@ -4095,7 +5508,11 @@ var GROUPS = {
4095
5508
  {
4096
5509
  key: ConfigKey.property_mark,
4097
5510
  jsonKey: "marks.mark",
4098
- exportJsonKey: { marks: { mark: ["$"] } },
5511
+ // `emphasis: 'underline'` is the per-entry default for mark
5512
+ // configs — mirrors the hardcoded default in BPG
5513
+ // `MarkConfigChainContentProcessor.ts:55-58`. Chained
5514
+ // `[@emphasis:…]` overrides via deep-merge into `marks[-1]`.
5515
+ exportJsonKey: { marks: [{ mark: "$", emphasis: "underline" }] },
4099
5516
  description: "The mark configuration",
4100
5517
  format: TagFormat.plainText,
4101
5518
  maxCount: Count.infinity,
@@ -4119,7 +5536,9 @@ var GROUPS = {
4119
5536
  description: "Mark chain",
4120
5537
  tags: [
4121
5538
  {
4122
- exportJsonKey: { solution: ["$"] },
5539
+ exportJsonKey: {
5540
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5541
+ },
4123
5542
  key: ConfigKey.tag_mark,
4124
5543
  description: "The marked content",
4125
5544
  maxCount: Count.infinity,
@@ -4128,19 +5547,30 @@ var GROUPS = {
4128
5547
  {
4129
5548
  key: ConfigKey.property_mark,
4130
5549
  jsonKey: "mark",
5550
+ exportJsonKey: { mark: "$" },
4131
5551
  description: "The mark configuration",
4132
5552
  format: TagFormat.plainText
4133
5553
  },
4134
5554
  {
4135
5555
  key: ConfigKey.property_example,
4136
5556
  jsonKey: "example",
4137
- exportJsonKey: [
4138
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
4139
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
4140
- { isExample: true, example: "$", "@bit": { isExample: true } }
4141
- ],
5557
+ // Per-mark `@example` is a boolean toggle. BPG's runtime
5558
+ // `ExampleTagContentProcessor.handleDefaultOnlyExample`
5559
+ // (lines 147-161) normalises ANY `[@example]` or
5560
+ // `[@example:V]` on a mark body-bit to
5561
+ // `__isDefaultExample=true, example=undefined`; the
5562
+ // downstream `toExample(_, _, defaultExample=true)` in
5563
+ // `BaseBuilder.ts:80-87` then emits `example: true`
5564
+ // regardless of source. Declare a single unconditional
5565
+ // boolean-shape rule so the exported schema matches that
5566
+ // runtime semantic instead of the misleading `example: $`
5567
+ // shape (which would only fire if `handleDefaultOnlyExample`
5568
+ // were removed). `@bit: {isExample: true}` cascades the
5569
+ // marker — but NOT the `example` value — to bit root.
5570
+ exportJsonKey: { isExample: true, example: true, "@bit": { isExample: true } },
4142
5571
  description: "An example for the marked content",
4143
- format: TagFormat.bitmarkText
5572
+ format: TagFormat.boolean,
5573
+ nullable: true
4144
5574
  }
4145
5575
  ]
4146
5576
  }
@@ -4569,6 +5999,42 @@ var GROUPS = {
4569
5999
  }
4570
6000
  ]
4571
6001
  },
6002
+ [ConfigKey.group_backgroundWallpaper]: {
6003
+ type: GroupConfigType.standard,
6004
+ description: "Background wallpaper tags",
6005
+ tags: [
6006
+ {
6007
+ key: ConfigKey.resource_backgroundWallpaper,
6008
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
6009
+ description: "Background wallpaper for the image, used to set a background for the image",
6010
+ chain: [
6011
+ {
6012
+ key: ConfigKey.group_resourceImageCommon,
6013
+ description: "Common resource image tags for images"
6014
+ }
6015
+ ]
6016
+ }
6017
+ ]
6018
+ },
6019
+ [ConfigKey.group_imageNoZoom]: {
6020
+ type: GroupConfigType.standard,
6021
+ description: "Image bit tags",
6022
+ tags: [
6023
+ {
6024
+ key: ConfigKey.group_backgroundWallpaper,
6025
+ description: "Background wallpaper tags for images, used to define background properties for images"
6026
+ },
6027
+ {
6028
+ key: ConfigKey.group_resourceBitTags,
6029
+ description: "Resource bit tags for images, used to define additional properties for images"
6030
+ },
6031
+ {
6032
+ key: ConfigKey.group_resourceImageNoZoom,
6033
+ description: "Resource image tags for images, used to attach images to the bit",
6034
+ minCount: 1
6035
+ }
6036
+ ]
6037
+ },
4572
6038
  //
4573
6039
  // Resource groups
4574
6040
  //
@@ -4688,6 +6154,58 @@ var GROUPS = {
4688
6154
  }
4689
6155
  ]
4690
6156
  },
6157
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
6158
+ type: GroupConfigType.standard,
6159
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
6160
+ tags: [
6161
+ {
6162
+ key: ConfigKey.group_resourceCommon,
6163
+ description: "Common resource properties"
6164
+ },
6165
+ {
6166
+ key: ConfigKey.property_src1x,
6167
+ description: "The source URL for the image at 1x resolution",
6168
+ format: TagFormat.plainText
6169
+ },
6170
+ {
6171
+ key: ConfigKey.property_src2x,
6172
+ description: "The source URL for the image at 2x resolution",
6173
+ format: TagFormat.plainText
6174
+ },
6175
+ {
6176
+ key: ConfigKey.property_src3x,
6177
+ description: "The source URL for the image at 3x resolution",
6178
+ format: TagFormat.plainText
6179
+ },
6180
+ {
6181
+ key: ConfigKey.property_src4x,
6182
+ description: "The source URL for the image at 4x resolution",
6183
+ format: TagFormat.plainText
6184
+ },
6185
+ {
6186
+ key: ConfigKey.property_width,
6187
+ description: "The width of the image",
6188
+ format: TagFormat.number
6189
+ },
6190
+ {
6191
+ key: ConfigKey.property_height,
6192
+ description: "The height of the image",
6193
+ format: TagFormat.number
6194
+ },
6195
+ {
6196
+ key: ConfigKey.property_alt,
6197
+ description: "The alternative text for the image",
6198
+ format: TagFormat.plainText
6199
+ },
6200
+ // @zoomDisabled with a true default
6201
+ {
6202
+ key: ConfigKey.property_zoomDisabled,
6203
+ description: "If true, zooming is disabled for the image (defaults to true)",
6204
+ format: TagFormat.boolean,
6205
+ defaultValue: "true"
6206
+ }
6207
+ ]
6208
+ },
4691
6209
  [ConfigKey.group_resourceAudioCommon]: {
4692
6210
  type: GroupConfigType.standard,
4693
6211
  description: "Common properties for audio resources",
@@ -4777,22 +6295,26 @@ var GROUPS = {
4777
6295
  {
4778
6296
  key: ConfigKey.property_src1x,
4779
6297
  description: "The source URL for the video at 1x resolution",
4780
- format: TagFormat.plainText
6298
+ format: TagFormat.plainText,
6299
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4781
6300
  },
4782
6301
  {
4783
6302
  key: ConfigKey.property_src2x,
4784
6303
  description: "The source URL for the video at 2x resolution",
4785
- format: TagFormat.plainText
6304
+ format: TagFormat.plainText,
6305
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4786
6306
  },
4787
6307
  {
4788
6308
  key: ConfigKey.property_src3x,
4789
6309
  description: "The source URL for the video at 3x resolution",
4790
- format: TagFormat.plainText
6310
+ format: TagFormat.plainText,
6311
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4791
6312
  },
4792
6313
  {
4793
6314
  key: ConfigKey.property_src4x,
4794
6315
  description: "The source URL for the video at 4x resolution",
4795
- format: TagFormat.plainText
6316
+ format: TagFormat.plainText,
6317
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4796
6318
  }
4797
6319
  ]
4798
6320
  },
@@ -4805,6 +6327,7 @@ var GROUPS = {
4805
6327
  tags: [
4806
6328
  {
4807
6329
  key: ConfigKey.resource_icon,
6330
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4808
6331
  description: "The icon resource",
4809
6332
  chain: [
4810
6333
  {
@@ -4832,12 +6355,30 @@ var GROUPS = {
4832
6355
  }
4833
6356
  ]
4834
6357
  },
6358
+ [ConfigKey.group_resourceImageNoZoom]: {
6359
+ type: GroupConfigType.resource,
6360
+ description: "Image resource where @zoomDisabled defaults to true",
6361
+ tags: [
6362
+ {
6363
+ key: ConfigKey.resource_image,
6364
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6365
+ description: "The image resource (no-zoom default)",
6366
+ chain: [
6367
+ {
6368
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6369
+ description: "Common image properties with @zoomDisabled defaulting to true"
6370
+ }
6371
+ ]
6372
+ }
6373
+ ]
6374
+ },
4835
6375
  [ConfigKey.group_resourceImagePortrait]: {
4836
6376
  type: GroupConfigType.resource,
4837
6377
  description: "Portrait image resource",
4838
6378
  tags: [
4839
6379
  {
4840
6380
  key: ConfigKey.resource_imagePortrait,
6381
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4841
6382
  description: "The portrait image resource",
4842
6383
  chain: [
4843
6384
  {
@@ -4854,6 +6395,7 @@ var GROUPS = {
4854
6395
  tags: [
4855
6396
  {
4856
6397
  key: ConfigKey.resource_imageLandscape,
6398
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4857
6399
  description: "The landscape image resource",
4858
6400
  chain: [
4859
6401
  {
@@ -4870,6 +6412,7 @@ var GROUPS = {
4870
6412
  tags: [
4871
6413
  {
4872
6414
  key: ConfigKey.resource_imageEmbed,
6415
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4873
6416
  description: "The embedded image resource",
4874
6417
  chain: [
4875
6418
  {
@@ -5043,6 +6586,7 @@ var GROUPS = {
5043
6586
  tags: [
5044
6587
  {
5045
6588
  key: ConfigKey.resource_article,
6589
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
5046
6590
  description: "The article resource",
5047
6591
  chain: [
5048
6592
  {
@@ -5059,6 +6603,7 @@ var GROUPS = {
5059
6603
  tags: [
5060
6604
  {
5061
6605
  key: ConfigKey.resource_articleEmbed,
6606
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
5062
6607
  description: "The embedded article resource",
5063
6608
  chain: [
5064
6609
  {
@@ -5075,6 +6620,7 @@ var GROUPS = {
5075
6620
  tags: [
5076
6621
  {
5077
6622
  key: ConfigKey.resource_articleLink,
6623
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
5078
6624
  description: "The link to the article resource",
5079
6625
  chain: [
5080
6626
  {
@@ -5345,7 +6891,12 @@ var ConfigHydrator = class {
5345
6891
  isDefault: v.isDefault,
5346
6892
  sideJsonKey: v.sideJsonKey,
5347
6893
  sideExportJsonKey: v.sideExportJsonKey,
5348
- hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey")
6894
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6895
+ // PLAN-085: cardinality fields pass through; defaults applied
6896
+ // downstream by the config consumer (Rust treats `0` / undefined
6897
+ // as unbounded).
6898
+ minCount: v.minCount,
6899
+ maxCount: v.maxCount
5349
6900
  };
5350
6901
  }
5351
6902
  return out;
@@ -5560,7 +7111,8 @@ var BITS = {
5560
7111
  { isExample: true, example: "$" }
5561
7112
  ],
5562
7113
  description: "The example text for the bit",
5563
- format: TagFormat.plainText
7114
+ format: TagFormat.plainText,
7115
+ nullable: true
5564
7116
  }
5565
7117
  ],
5566
7118
  rootExampleType: ExampleType.string
@@ -5899,7 +7451,8 @@ var BITS = {
5899
7451
  { isExample: true, example: "$" }
5900
7452
  ],
5901
7453
  description: "The example text for the bit",
5902
- format: TagFormat.plainText
7454
+ format: TagFormat.plainText,
7455
+ nullable: true
5903
7456
  }
5904
7457
  ],
5905
7458
  rootExampleType: ExampleType.string
@@ -6062,7 +7615,8 @@ var BITS = {
6062
7615
  { isExample: true, example: "$" }
6063
7616
  ],
6064
7617
  description: "The example text for the bit",
6065
- format: TagFormat.plainText
7618
+ format: TagFormat.plainText,
7619
+ nullable: true
6066
7620
  }
6067
7621
  ],
6068
7622
  rootExampleType: ExampleType.string
@@ -6213,7 +7767,8 @@ var BITS = {
6213
7767
  { isExample: true, example: "$" }
6214
7768
  ],
6215
7769
  description: "The example text for the bit",
6216
- format: TagFormat.plainText
7770
+ format: TagFormat.plainText,
7771
+ nullable: true
6217
7772
  }
6218
7773
  ],
6219
7774
  rootExampleType: ExampleType.string
@@ -6790,6 +8345,12 @@ var BITS = {
6790
8345
  description: "The progress of the chapter, used for tracking reading progress",
6791
8346
  format: TagFormat.boolean,
6792
8347
  defaultValue: "true"
8348
+ },
8349
+ {
8350
+ key: ConfigKey.property_isCollapsible,
8351
+ description: "If true, the chapter is collapsible",
8352
+ format: TagFormat.boolean,
8353
+ defaultValue: "false"
6793
8354
  }
6794
8355
  ]
6795
8356
  },
@@ -6808,8 +8369,31 @@ var BITS = {
6808
8369
  description: "Tags for gaps in cloze and multiple choice text bits"
6809
8370
  },
6810
8371
  {
6811
- key: ConfigKey.group_trueFalse,
8372
+ key: ConfigKey.group_trueFalseInlineSelect,
6812
8373
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8374
+ },
8375
+ {
8376
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8377
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8378
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8379
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8380
+ key: ConfigKey.property_isCaseSensitive,
8381
+ exportJsonKey: {},
8382
+ description: "If the cloze answers are case sensitive",
8383
+ format: TagFormat.boolean,
8384
+ defaultValue: "true"
8385
+ },
8386
+ {
8387
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8388
+ key: ConfigKey.property_example,
8389
+ exportJsonKey: [
8390
+ { "@keyonly": { isExample: true } },
8391
+ { "@absent": { isExample: true } },
8392
+ { isExample: true, example: "$" }
8393
+ ],
8394
+ description: "The example(s) for the bit",
8395
+ format: TagFormat.bitmarkText,
8396
+ nullable: true
6813
8397
  }
6814
8398
  ]
6815
8399
  },
@@ -6848,18 +8432,56 @@ var BITS = {
6848
8432
  {
6849
8433
  key: ConfigKey.group_gap,
6850
8434
  description: "Tags for gaps in cloze bits"
8435
+ },
8436
+ {
8437
+ // Override inherited @example: cloze is in OLD parser's
8438
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8439
+ // emits `isExample: true`.
8440
+ key: ConfigKey.property_example,
8441
+ exportJsonKey: [
8442
+ { "@keyonly": { isExample: true } },
8443
+ { "@absent": { isExample: true } },
8444
+ { isExample: true, example: "$" }
8445
+ ],
8446
+ description: "The example(s) for the bit",
8447
+ format: TagFormat.bitmarkText,
8448
+ nullable: true
6851
8449
  }
6852
8450
  ]
6853
8451
  },
6854
8452
  [BitType2.clozeInstructionGrouped]: {
6855
8453
  since: "1.3.0",
6856
8454
  baseBitType: BitType2.cloze,
6857
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8455
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8456
+ tags: [
8457
+ {
8458
+ key: ConfigKey.property_quizCountItems,
8459
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8460
+ format: TagFormat.boolean,
8461
+ defaultValue: "true"
8462
+ }
8463
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8464
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8465
+ ]
6858
8466
  },
6859
8467
  [BitType2.clozeSolutionGrouped]: {
6860
8468
  since: "1.3.0",
6861
8469
  baseBitType: BitType2.cloze,
6862
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8470
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8471
+ tags: [
8472
+ {
8473
+ key: ConfigKey.property_quizCountItems,
8474
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8475
+ format: TagFormat.boolean,
8476
+ defaultValue: "true"
8477
+ },
8478
+ {
8479
+ key: ConfigKey.property_quizStrikethroughSolutions,
8480
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8481
+ format: TagFormat.boolean,
8482
+ defaultValue: "true"
8483
+ }
8484
+ ]
6863
8485
  },
6864
8486
  [BitType2.clozeSeveral]: {
6865
8487
  since: "3.5.0",
@@ -6895,6 +8517,17 @@ var BITS = {
6895
8517
  {
6896
8518
  key: ConfigKey.group_quizCommon,
6897
8519
  description: "Common tags for quiz bits"
8520
+ },
8521
+ {
8522
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8523
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8524
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8525
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8526
+ key: ConfigKey.property_isCaseSensitive,
8527
+ exportJsonKey: {},
8528
+ description: "If the cloze answers are case sensitive",
8529
+ format: TagFormat.boolean,
8530
+ defaultValue: "true"
6898
8531
  }
6899
8532
  ],
6900
8533
  cardSet: CardSetConfigKey.clozeList
@@ -7061,6 +8694,7 @@ var BITS = {
7061
8694
  tags: [
7062
8695
  {
7063
8696
  key: ConfigKey.group_person,
8697
+ exportJsonKey: { partner: { name: "$" } },
7064
8698
  description: "Tags for the person in the conversation"
7065
8699
  }
7066
8700
  ]
@@ -7491,7 +9125,12 @@ var BITS = {
7491
9125
  maxCount: Count.infinity
7492
9126
  },
7493
9127
  {
7494
- // Override inherited @example from group_standardTags: string root example.
9128
+ // Override inherited @example from group_standardTags: emits both
9129
+ // `isExample` (bool) AND `example` (paragraph array). Declared as
9130
+ // bitmarkText because BPG `JsonGenerator.toExample` renders the
9131
+ // root-example value through the bit's textFormat (not this tag's
9132
+ // declared format) — `TagFormat.plainText` here would be ignored
9133
+ // at runtime and is misleading.
7495
9134
  key: ConfigKey.property_example,
7496
9135
  exportJsonKey: [
7497
9136
  { "@keyonly": { isExample: true, example: "true" } },
@@ -7499,7 +9138,8 @@ var BITS = {
7499
9138
  { isExample: true, example: "$" }
7500
9139
  ],
7501
9140
  description: "The example text for the essay bit",
7502
- format: TagFormat.plainText
9141
+ format: TagFormat.bitmarkText,
9142
+ nullable: true
7503
9143
  }
7504
9144
  ],
7505
9145
  rootExampleType: ExampleType.string
@@ -7533,7 +9173,8 @@ var BITS = {
7533
9173
  { isExample: true, example: "$" }
7534
9174
  ],
7535
9175
  description: "The example text for the example bit",
7536
- format: TagFormat.plainText
9176
+ format: TagFormat.plainText,
9177
+ nullable: true
7537
9178
  }
7538
9179
  ],
7539
9180
  rootExampleType: ExampleType.string
@@ -7619,7 +9260,19 @@ var BITS = {
7619
9260
  [BitType2.articleAi]: {
7620
9261
  since: "1.3.0",
7621
9262
  baseBitType: BitType2.article,
7622
- description: "Article AI bit, used to create AI-generated articles"
9263
+ description: "Article AI bit, used to create AI-generated articles",
9264
+ tags: [
9265
+ {
9266
+ // Shadows the inherited @aiGenerated from the standard group: this
9267
+ // bit type forces aiGenerated:true regardless of source value or
9268
+ // absence. Literal `true` in the export pattern ignores `$`.
9269
+ key: ConfigKey.property_aiGenerated,
9270
+ description: "AI-generated flag, forced true for article-ai bits",
9271
+ format: TagFormat.boolean,
9272
+ defaultValue: "true",
9273
+ exportJsonKey: { aiGenerated: true }
9274
+ }
9275
+ ]
7623
9276
  },
7624
9277
  [BitType2.articleAttachment]: {
7625
9278
  since: "1.3.0",
@@ -7944,7 +9597,19 @@ var BITS = {
7944
9597
  [BitType2.noteAi]: {
7945
9598
  since: "1.3.0",
7946
9599
  baseBitType: BitType2.note,
7947
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9600
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9601
+ tags: [
9602
+ {
9603
+ // Shadows the inherited @aiGenerated from the standard group: this
9604
+ // bit type forces aiGenerated:true regardless of source value or
9605
+ // absence. Literal `true` in the export pattern ignores `$`.
9606
+ key: ConfigKey.property_aiGenerated,
9607
+ description: "AI-generated flag, forced true for article-ai bits",
9608
+ format: TagFormat.boolean,
9609
+ defaultValue: "true",
9610
+ exportJsonKey: { aiGenerated: true }
9611
+ }
9612
+ ]
7948
9613
  },
7949
9614
  [BitType2.notebookArticle]: {
7950
9615
  since: "1.3.0",
@@ -8069,7 +9734,19 @@ var BITS = {
8069
9734
  [BitType2.summaryAi]: {
8070
9735
  since: "1.3.0",
8071
9736
  baseBitType: BitType2.summary,
8072
- description: "AI-generated summary bit"
9737
+ description: "AI-generated summary bit",
9738
+ tags: [
9739
+ {
9740
+ // Shadows the inherited @aiGenerated from the standard group: this
9741
+ // bit type forces aiGenerated:true regardless of source value or
9742
+ // absence. Literal `true` in the export pattern ignores `$`.
9743
+ key: ConfigKey.property_aiGenerated,
9744
+ description: "AI-generated flag, forced true for article-ai bits",
9745
+ format: TagFormat.boolean,
9746
+ defaultValue: "true",
9747
+ exportJsonKey: { aiGenerated: true }
9748
+ }
9749
+ ]
8073
9750
  },
8074
9751
  [BitType2.videoTranscript]: {
8075
9752
  since: "1.3.0",
@@ -8343,7 +10020,7 @@ var BITS = {
8343
10020
  description: "Product ID for the module product, used to link to a specific product",
8344
10021
  format: TagFormat.plainText,
8345
10022
  minCount: 1,
8346
- maxCount: Count.infinity
10023
+ maxCount: 1
8347
10024
  }
8348
10025
  ]
8349
10026
  },
@@ -8396,10 +10073,13 @@ var BITS = {
8396
10073
  description: "Resource bit tags for logo grave images, used to define additional properties"
8397
10074
  },
8398
10075
  {
8399
- // Image resource
8400
- key: ConfigKey.group_resourceImage,
10076
+ // Image resource — fully-expanded shape so each `&image` emission
10077
+ // produces a complete `{type, image: {src}}` object inside the
10078
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
10079
+ // `images[].image` via the `[]` last-element-merge semantic.
10080
+ key: ConfigKey.group_resourceImageNoZoom,
8401
10081
  description: "Resource image tags for logo grave images, used to attach images",
8402
- exportJsonKey: { images: "$" },
10082
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
8403
10083
  minCount: 1,
8404
10084
  maxCount: Count.infinity
8405
10085
  }
@@ -8689,6 +10369,10 @@ var BITS = {
8689
10369
  tags: [
8690
10370
  {
8691
10371
  key: ConfigKey.property_book,
10372
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10373
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10374
+ // inside the most-recently-appended entry as `reference: $`.
10375
+ exportJsonKey: { book: [{ book: "$" }] },
8692
10376
  description: "Book reference for the page, used to link to a specific book",
8693
10377
  maxCount: Count.infinity,
8694
10378
  chain: [
@@ -8756,7 +10440,9 @@ var BITS = {
8756
10440
  format: TagFormat.plainText
8757
10441
  }
8758
10442
  ],
8759
- cardSet: CardSetConfigKey.exampleBitList
10443
+ // page-footer emits cards under `sections` (vs `listItems` for the
10444
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10445
+ cardSet: CardSetConfigKey.pageFooterSections
8760
10446
  },
8761
10447
  [BitType2.legend]: {
8762
10448
  since: "3.12.0",
@@ -8798,13 +10484,52 @@ var BITS = {
8798
10484
  since: "1.34.0",
8799
10485
  baseBitType: BitType2._standard,
8800
10486
  description: "Definition list bit, used to create lists of definitions in articles or books",
10487
+ tags: [
10488
+ {
10489
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10490
+ key: ConfigKey.property_example,
10491
+ exportJsonKey: [
10492
+ { "@keyonly": { isExample: true } },
10493
+ { "@absent": { isExample: true } },
10494
+ { isExample: true, example: "$" }
10495
+ ],
10496
+ description: "The example(s) for the bit",
10497
+ format: TagFormat.bitmarkText,
10498
+ nullable: true
10499
+ }
10500
+ ],
8801
10501
  cardSet: CardSetConfigKey.definitionList
8802
10502
  },
10503
+ [BitType2.definitionListCollapsible]: {
10504
+ since: "5.25.0",
10505
+ baseBitType: BitType2.definitionList,
10506
+ description: "Collapsible definition list bit, used to create collapsible lists of definitions in articles or books"
10507
+ },
10508
+ [BitType2.standardDefinitionListNormative]: {
10509
+ since: "5.24.0",
10510
+ baseBitType: BitType2.definitionList,
10511
+ description: "Standard normative definition list bit, used to create normative standard definition lists in articles or books"
10512
+ },
10513
+ [BitType2.standardDefinitionListNonNormative]: {
10514
+ since: "5.24.0",
10515
+ baseBitType: BitType2.definitionList,
10516
+ description: "Standard non-normative definition list bit, used to create non-normative standard definition lists in articles or books"
10517
+ },
10518
+ [BitType2.smartStandardDefinitionListNormative]: {
10519
+ since: "5.24.0",
10520
+ baseBitType: BitType2.standardDefinitionListNormative,
10521
+ description: "Smart standard normative definition list bit, used to create normative smart standard definition lists in articles or books"
10522
+ },
10523
+ [BitType2.smartStandardDefinitionListNonNormative]: {
10524
+ since: "5.24.0",
10525
+ baseBitType: BitType2.standardDefinitionListNonNormative,
10526
+ description: "Smart standard non-normative definition list bit, used to create non-normative smart standard definition lists in articles or books"
10527
+ },
8803
10528
  [BitType2.metaSearchDefaultTerms]: {
8804
10529
  since: "3.12.0",
8805
10530
  baseBitType: BitType2._standard,
8806
10531
  description: "Meta search default terms bit, used to define default search terms in articles",
8807
- cardSet: CardSetConfigKey.definitionList
10532
+ cardSet: CardSetConfigKey.definitionListPlain
8808
10533
  },
8809
10534
  [BitType2.metaSearchDefaultTopics]: {
8810
10535
  since: "3.12.0",
@@ -8820,6 +10545,18 @@ var BITS = {
8820
10545
  {
8821
10546
  key: ConfigKey.group_quizCommon,
8822
10547
  description: "Common quiz tags for flashcards"
10548
+ },
10549
+ {
10550
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10551
+ key: ConfigKey.property_example,
10552
+ exportJsonKey: [
10553
+ { "@keyonly": { isExample: true } },
10554
+ { "@absent": { isExample: true } },
10555
+ { isExample: true, example: "$" }
10556
+ ],
10557
+ description: "The example(s) for the bit",
10558
+ format: TagFormat.bitmarkText,
10559
+ nullable: true
8823
10560
  }
8824
10561
  ],
8825
10562
  cardSet: CardSetConfigKey.flashcard
@@ -8827,12 +10564,20 @@ var BITS = {
8827
10564
  [BitType2.flashcard1]: {
8828
10565
  since: "1.3.0",
8829
10566
  baseBitType: BitType2.flashcard,
8830
- description: "Flashcard 1 bit"
10567
+ description: "Flashcard 1 bit",
10568
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10569
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10570
+ // bit type structurally allows only a single card.
10571
+ cardSet: CardSetConfigKey.flashcard1
8831
10572
  },
8832
10573
  [BitType2.qAndACard]: {
8833
10574
  since: "3.25.0",
8834
10575
  baseBitType: BitType2.flashcard1,
8835
- description: "Q&A card bit, used to create question and answer cards in articles or books"
10576
+ description: "Q&A card bit, used to create question and answer cards in articles or books",
10577
+ // PLAN-085: q-and-a-card permits multiple cards (some fixtures carry
10578
+ // two). Override the inherited `flashcard1` (max=1) cardset back to
10579
+ // the unbounded `flashcard` cardset.
10580
+ cardSet: CardSetConfigKey.flashcard
8836
10581
  },
8837
10582
  [BitType2.focusImage]: {
8838
10583
  since: "1.3.0",
@@ -8862,8 +10607,24 @@ var BITS = {
8862
10607
  description: "Common quiz tags for highlighted text"
8863
10608
  },
8864
10609
  {
8865
- key: ConfigKey.group_trueFalse,
8866
- description: "True/False quiz tags for highlighted text"
10610
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10611
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10612
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10613
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10614
+ key: ConfigKey.group_trueFalseInlineHighlight,
10615
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10616
+ },
10617
+ {
10618
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10619
+ key: ConfigKey.property_example,
10620
+ exportJsonKey: [
10621
+ { "@keyonly": { isExample: true } },
10622
+ { "@absent": { isExample: true } },
10623
+ { isExample: true, example: "$" }
10624
+ ],
10625
+ description: "The example(s) for the bit",
10626
+ format: TagFormat.bitmarkText,
10627
+ nullable: true
8867
10628
  }
8868
10629
  ]
8869
10630
  },
@@ -8873,15 +10634,8 @@ var BITS = {
8873
10634
  description: "Image bit, used to create images in articles or books",
8874
10635
  tags: [
8875
10636
  {
8876
- key: ConfigKey.resource_backgroundWallpaper,
8877
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8878
- description: "Background wallpaper for the image, used to set a background for the image",
8879
- chain: [
8880
- {
8881
- key: ConfigKey.group_resourceImageCommon,
8882
- description: "Common resource image tags for images"
8883
- }
8884
- ]
10637
+ key: ConfigKey.group_backgroundWallpaper,
10638
+ description: "Background wallpaper tags for images, used to define background properties for images"
8885
10639
  },
8886
10640
  {
8887
10641
  key: ConfigKey.group_resourceBitTags,
@@ -8984,8 +10738,15 @@ var BITS = {
8984
10738
  },
8985
10739
  [BitType2.imageSeparator]: {
8986
10740
  since: "1.4.15",
8987
- baseBitType: BitType2.image,
8988
- description: "Image separator bit, used to create separators in articles or books"
10741
+ baseBitType: BitType2._standard,
10742
+ description: "Image separator bit, used to create separators in articles or books",
10743
+ tags: [
10744
+ {
10745
+ key: ConfigKey.group_imageNoZoom,
10746
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10747
+ }
10748
+ ],
10749
+ resourceAttachmentAllowed: false
8989
10750
  },
8990
10751
  [BitType2.imageSeparatorAlt]: {
8991
10752
  since: "1.16.0",
@@ -9024,15 +10785,20 @@ var BITS = {
9024
10785
  },
9025
10786
  [BitType2.pageBanner]: {
9026
10787
  since: "1.4.3",
9027
- baseBitType: BitType2.image,
10788
+ baseBitType: BitType2._standard,
9028
10789
  description: "Page banner bit, used to create banners for pages in articles or books",
9029
10790
  tags: [
9030
10791
  {
9031
10792
  key: ConfigKey.property_slug,
9032
10793
  description: "Slug for the page banner, used to identify the banner in the system",
9033
10794
  format: TagFormat.plainText
10795
+ },
10796
+ {
10797
+ key: ConfigKey.group_imageNoZoom,
10798
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
9034
10799
  }
9035
- ]
10800
+ ],
10801
+ resourceAttachmentAllowed: false
9036
10802
  },
9037
10803
  [BitType2.pageHero]: {
9038
10804
  since: "1.11.0",
@@ -9047,7 +10813,7 @@ var BITS = {
9047
10813
  },
9048
10814
  [BitType2.tableImage]: {
9049
10815
  since: "1.5.15",
9050
- baseBitType: BitType2.table,
10816
+ baseBitType: BitType2.tableExtended,
9051
10817
  description: "Table image bit, used to create images in tables in articles or books",
9052
10818
  tags: [
9053
10819
  {
@@ -9056,15 +10822,8 @@ var BITS = {
9056
10822
  format: TagFormat.bitmarkText
9057
10823
  },
9058
10824
  {
9059
- key: ConfigKey.resource_backgroundWallpaper,
9060
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
9061
- description: "Background wallpaper for the image, used to set a background for the image",
9062
- chain: [
9063
- {
9064
- key: ConfigKey.group_resourceImageCommon,
9065
- description: "Common resource image tags for images"
9066
- }
9067
- ]
10825
+ key: ConfigKey.group_backgroundWallpaper,
10826
+ description: "Background wallpaper tags for images, used to define background properties for images"
9068
10827
  },
9069
10828
  {
9070
10829
  key: ConfigKey.group_resourceBitTags,
@@ -9207,10 +10966,10 @@ var BITS = {
9207
10966
  description: "Resource bit tags for logo grave images, used to define additional properties"
9208
10967
  },
9209
10968
  {
9210
- // Image resource
9211
- key: ConfigKey.group_resourceImage,
10969
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10970
+ key: ConfigKey.group_resourceImageNoZoom,
9212
10971
  description: "Resource image tags for logo grave images, used to attach images",
9213
- exportJsonKey: { logos: "$" },
10972
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
9214
10973
  minCount: 1,
9215
10974
  maxCount: Count.infinity
9216
10975
  }
@@ -9220,7 +10979,17 @@ var BITS = {
9220
10979
  [BitType2.prototypeImages]: {
9221
10980
  since: "1.6.1",
9222
10981
  baseBitType: BitType2.imagesLogoGrave,
9223
- description: "Prototype images bit, used to create prototype images in articles or books"
10982
+ description: "Prototype images bit, used to create prototype images in articles or books",
10983
+ tags: [
10984
+ {
10985
+ // Override inherited `logos` wrapper with `images` for this bit type.
10986
+ key: ConfigKey.group_resourceImageNoZoom,
10987
+ description: "Resource image tags for prototype images",
10988
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
10989
+ minCount: 1,
10990
+ maxCount: Count.infinity
10991
+ }
10992
+ ]
9224
10993
  },
9225
10994
  [BitType2.internalLink]: {
9226
10995
  since: "1.3.0",
@@ -9250,6 +11019,18 @@ var BITS = {
9250
11019
  exportJsonKey: {},
9251
11020
  description: "Reasonable number of characters for the interview, used to limit input size",
9252
11021
  format: TagFormat.number
11022
+ },
11023
+ {
11024
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11025
+ key: ConfigKey.property_example,
11026
+ exportJsonKey: [
11027
+ { "@keyonly": { isExample: true } },
11028
+ { "@absent": { isExample: true } },
11029
+ { isExample: true, example: "$" }
11030
+ ],
11031
+ description: "The example(s) for the bit",
11032
+ format: TagFormat.bitmarkText,
11033
+ nullable: true
9253
11034
  }
9254
11035
  ],
9255
11036
  cardSet: CardSetConfigKey.questions
@@ -9608,6 +11389,18 @@ var BITS = {
9608
11389
  {
9609
11390
  key: ConfigKey.group_mark,
9610
11391
  description: "Mark tags, used to define the content of marks"
11392
+ },
11393
+ {
11394
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11395
+ key: ConfigKey.property_example,
11396
+ exportJsonKey: [
11397
+ { "@keyonly": { isExample: true } },
11398
+ { "@absent": { isExample: true } },
11399
+ { isExample: true, example: "$" }
11400
+ ],
11401
+ description: "The example(s) for the bit",
11402
+ format: TagFormat.bitmarkText,
11403
+ nullable: true
9611
11404
  }
9612
11405
  ]
9613
11406
  },
@@ -9627,6 +11420,23 @@ var BITS = {
9627
11420
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9628
11421
  format: TagFormat.boolean,
9629
11422
  defaultValue: "true"
11423
+ },
11424
+ {
11425
+ // Override inherited @example: in `isTopLevelExample` allow-list
11426
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11427
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11428
+ // Bit-level fires emit only `isExample`; the @example value (when
11429
+ // provided) is recorded in the ancestor stack and consumed by
11430
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11431
+ key: ConfigKey.property_example,
11432
+ exportJsonKey: [
11433
+ { "@keyonly": { isExample: true } },
11434
+ { "@absent": { isExample: true } },
11435
+ { isExample: true }
11436
+ ],
11437
+ description: "The example(s) for the bit",
11438
+ format: TagFormat.bitmarkText,
11439
+ nullable: true
9630
11440
  }
9631
11441
  ],
9632
11442
  cardSet: CardSetConfigKey.matchPairs
@@ -9711,6 +11521,18 @@ var BITS = {
9711
11521
  {
9712
11522
  key: ConfigKey.group_trueFalse,
9713
11523
  description: "True/False tags for multiple choice 1 questions"
11524
+ },
11525
+ {
11526
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11527
+ key: ConfigKey.property_example,
11528
+ exportJsonKey: [
11529
+ { "@keyonly": { isExample: true } },
11530
+ { "@absent": { isExample: true } },
11531
+ { isExample: true, example: "$" }
11532
+ ],
11533
+ description: "The example(s) for the bit",
11534
+ format: TagFormat.bitmarkText,
11535
+ nullable: true
9714
11536
  }
9715
11537
  ]
9716
11538
  },
@@ -9732,8 +11554,20 @@ var BITS = {
9732
11554
  {
9733
11555
  key: ConfigKey.group_trueFalse,
9734
11556
  description: "True/False tags for multiple choice questions"
9735
- }
11557
+ },
9736
11558
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11559
+ {
11560
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11561
+ key: ConfigKey.property_example,
11562
+ exportJsonKey: [
11563
+ { "@keyonly": { isExample: true } },
11564
+ { "@absent": { isExample: true } },
11565
+ { isExample: true, example: "$" }
11566
+ ],
11567
+ description: "The example(s) for the bit",
11568
+ format: TagFormat.bitmarkText,
11569
+ nullable: true
11570
+ }
9737
11571
  ],
9738
11572
  cardSet: CardSetConfigKey.quiz
9739
11573
  },
@@ -9753,8 +11587,20 @@ var BITS = {
9753
11587
  description: "Common quiz tags for multiple choice text questions"
9754
11588
  },
9755
11589
  {
9756
- key: ConfigKey.group_trueFalse,
11590
+ key: ConfigKey.group_trueFalseInlineSelect,
9757
11591
  description: "True/False tags for multiple choice text questions"
11592
+ },
11593
+ {
11594
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11595
+ key: ConfigKey.property_example,
11596
+ exportJsonKey: [
11597
+ { "@keyonly": { isExample: true } },
11598
+ { "@absent": { isExample: true } },
11599
+ { isExample: true, example: "$" }
11600
+ ],
11601
+ description: "The example(s) for the bit",
11602
+ format: TagFormat.bitmarkText,
11603
+ nullable: true
9758
11604
  }
9759
11605
  ]
9760
11606
  },
@@ -9779,8 +11625,20 @@ var BITS = {
9779
11625
  description: "Common quiz tags for multiple response 1 questions"
9780
11626
  },
9781
11627
  {
9782
- key: ConfigKey.group_trueFalse,
11628
+ key: ConfigKey.group_trueFalseResponses,
9783
11629
  description: "True/False tags for multiple response 1 questions"
11630
+ },
11631
+ {
11632
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11633
+ key: ConfigKey.property_example,
11634
+ exportJsonKey: [
11635
+ { "@keyonly": { isExample: true } },
11636
+ { "@absent": { isExample: true } },
11637
+ { isExample: true, example: "$" }
11638
+ ],
11639
+ description: "The example(s) for the bit",
11640
+ format: TagFormat.bitmarkText,
11641
+ nullable: true
9784
11642
  }
9785
11643
  ]
9786
11644
  },
@@ -9800,12 +11658,24 @@ var BITS = {
9800
11658
  description: "Common quiz tags for multiple response questions"
9801
11659
  },
9802
11660
  {
9803
- key: ConfigKey.group_trueFalse,
11661
+ key: ConfigKey.group_trueFalseResponses,
9804
11662
  description: "True/False tags for multiple response questions"
9805
- }
11663
+ },
9806
11664
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11665
+ {
11666
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11667
+ key: ConfigKey.property_example,
11668
+ exportJsonKey: [
11669
+ { "@keyonly": { isExample: true } },
11670
+ { "@absent": { isExample: true } },
11671
+ { isExample: true, example: "$" }
11672
+ ],
11673
+ description: "The example(s) for the bit",
11674
+ format: TagFormat.bitmarkText,
11675
+ nullable: true
11676
+ }
9807
11677
  ],
9808
- cardSet: CardSetConfigKey.quiz
11678
+ cardSet: CardSetConfigKey.quizResponses
9809
11679
  },
9810
11680
  [BitType2.coachSelfReflectionMultipleResponse]: {
9811
11681
  since: "1.3.0",
@@ -10055,6 +11925,8 @@ var BITS = {
10055
11925
  },
10056
11926
  {
10057
11927
  key: ConfigKey.resource_platformIcon,
11928
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11929
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
10058
11930
  description: "The platform icon",
10059
11931
  chain: [
10060
11932
  {
@@ -10090,6 +11962,8 @@ var BITS = {
10090
11962
  tags: [
10091
11963
  {
10092
11964
  key: ConfigKey.resource_platformLogo,
11965
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11966
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
10093
11967
  description: "The platform logo",
10094
11968
  chain: [
10095
11969
  {
@@ -10301,7 +12175,8 @@ var BITS = {
10301
12175
  { isExample: true, example: "$" }
10302
12176
  ],
10303
12177
  description: "The example text for the review note bit",
10304
- format: TagFormat.plainText
12178
+ format: TagFormat.plainText,
12179
+ nullable: true
10305
12180
  }
10306
12181
  ],
10307
12182
  rootExampleType: ExampleType.string
@@ -10388,7 +12263,8 @@ var BITS = {
10388
12263
  { isExample: true, example: "$" }
10389
12264
  ],
10390
12265
  description: "The example flag for the bit (boolean)",
10391
- format: TagFormat.plainText
12266
+ format: TagFormat.boolean,
12267
+ nullable: true
10392
12268
  }
10393
12269
  ],
10394
12270
  cardSet: CardSetConfigKey.elements,
@@ -10662,15 +12538,8 @@ var BITS = {
10662
12538
  format: TagFormat.bitmarkText
10663
12539
  },
10664
12540
  {
10665
- key: ConfigKey.resource_backgroundWallpaper,
10666
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
10667
- description: "Background wallpaper for the image, used to set a background for the image",
10668
- chain: [
10669
- {
10670
- key: ConfigKey.group_resourceImageCommon,
10671
- description: "Common resource image tags for images"
10672
- }
10673
- ]
12541
+ key: ConfigKey.group_backgroundWallpaper,
12542
+ description: "Background wallpaper tags for images, used to define background properties for images"
10674
12543
  },
10675
12544
  {
10676
12545
  key: ConfigKey.group_resourceBitTags,
@@ -11047,16 +12916,20 @@ var BITS = {
11047
12916
  format: TagFormat.plainText
11048
12917
  },
11049
12918
  {
11050
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
12919
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12920
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12921
+ // not a `choices[]` array. Format is bitmarkText so the statement
12922
+ // body renders as a ProseMirror tree, matching reference fixtures.
12923
+ exportJsonKey: { statement: "$", isCorrect: true },
11051
12924
  key: ConfigKey.tag_true,
11052
12925
  description: "Tag for the true option in the true/false question",
11053
- format: TagFormat.plainText
12926
+ format: TagFormat.bitmarkText
11054
12927
  },
11055
12928
  {
11056
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
12929
+ exportJsonKey: { statement: "$", isCorrect: false },
11057
12930
  key: ConfigKey.tag_false,
11058
12931
  description: "Tag for the false option in the true/false question",
11059
- format: TagFormat.plainText
12932
+ format: TagFormat.bitmarkText
11060
12933
  },
11061
12934
  {
11062
12935
  // Override inherited @example from group_standardTags: boolean root
@@ -11067,14 +12940,24 @@ var BITS = {
11067
12940
  // sufficient for `[+...]` (correct) statements which dominate the
11068
12941
  // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
11069
12942
  // need either a runtime extension or an explicit `[@example:false]`.
12943
+ //
12944
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12945
+ // (a natural default that gets stripped in optimised mode, matching
12946
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12947
+ // STRING `"false"` which doesn't strip.
11070
12948
  key: ConfigKey.property_example,
11071
12949
  exportJsonKey: [
11072
12950
  { "@keyonly": { isExample: true, example: true } },
11073
12951
  { "@absent": { isExample: true, example: true } },
12952
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12953
+ // is the natural default for boolean, stripped in optimised mode.
12954
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12955
+ { "@value=false": { isExample: true } },
11074
12956
  { isExample: true, example: "$" }
11075
12957
  ],
11076
12958
  description: "The example flag for the bit (boolean)",
11077
- format: TagFormat.plainText
12959
+ format: TagFormat.boolean,
12960
+ nullable: true
11078
12961
  }
11079
12962
  ],
11080
12963
  rootExampleType: ExampleType.boolean
@@ -11098,6 +12981,18 @@ var BITS = {
11098
12981
  key: ConfigKey.property_labelFalse,
11099
12982
  description: "Label for the false option in the true/false question",
11100
12983
  format: TagFormat.plainText
12984
+ },
12985
+ {
12986
+ // Override inherited @example: in `isTopLevelExample` allow-list.
12987
+ key: ConfigKey.property_example,
12988
+ exportJsonKey: [
12989
+ { "@keyonly": { isExample: true } },
12990
+ { "@absent": { isExample: true } },
12991
+ { isExample: true, example: "$" }
12992
+ ],
12993
+ description: "The example(s) for the bit",
12994
+ format: TagFormat.bitmarkText,
12995
+ nullable: true
11101
12996
  }
11102
12997
  ],
11103
12998
  cardSet: CardSetConfigKey.statements
@@ -11287,6 +13182,11 @@ var BITS = {
11287
13182
  baseBitType: BitType2.vendorStripePricingTable,
11288
13183
  description: "Stripe pricing table bit with print-on-request customer/product IDs, used to embed Stripe pricing tables tied to a specific print-on-request customer and product",
11289
13184
  tags: [
13185
+ {
13186
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
13187
+ description: "Descriptive name for the Stripe pricing table",
13188
+ format: TagFormat.plainText
13189
+ },
11290
13190
  {
11291
13191
  key: ConfigKey.property_printOnRequestCustomerId,
11292
13192
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -11763,7 +13663,7 @@ var instance2 = new Config();
11763
13663
  // src/generated/package_info.ts
11764
13664
  var PACKAGE_INFO = {
11765
13665
  "name": "@gmb/bitmark-parser-generator",
11766
- "version": "5.23.0",
13666
+ "version": "5.25.0",
11767
13667
  "author": "Get More Brain Ltd",
11768
13668
  "license": "ISC",
11769
13669
  "description": "A bitmark parser and generator using Peggy.js"
@@ -12409,6 +14309,8 @@ var NodeType = {
12409
14309
  internalPrintPdf: "internalPrintPdf",
12410
14310
  internalPrintPdfValue: "internalPrintPdfValue",
12411
14311
  isCaseSensitive: "isCaseSensitive",
14312
+ isCollapsible: "isCollapsible",
14313
+ isCollapsibleValue: "isCollapsibleValue",
12412
14314
  isCommented: "isCommented",
12413
14315
  isCorrect: "isCorrect",
12414
14316
  isEditable: "isEditable",
@@ -12728,6 +14630,8 @@ var NodeType = {
12728
14630
  stillImageFilmLink: "stillImageFilmLink",
12729
14631
  stripePricingTableId: "stripePricingTableId",
12730
14632
  stripePricingTableIdValue: "stripePricingTableIdValue",
14633
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14634
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12731
14635
  stripePublishableKey: "stripePublishableKey",
12732
14636
  stripePublishableKeyValue: "stripePublishableKeyValue",
12733
14637
  subject: "subject",
@@ -26153,6 +28057,12 @@ var Builder = class extends BaseBuilder {
26153
28057
  data.stripePricingTableId,
26154
28058
  options
26155
28059
  ),
28060
+ stripePricingTableDescriptiveName: this.toAstProperty(
28061
+ bitType,
28062
+ ConfigKey.property_stripePricingTableDescriptiveName,
28063
+ data.stripePricingTableDescriptiveName,
28064
+ options
28065
+ ),
26156
28066
  stripePublishableKey: this.toAstProperty(
26157
28067
  bitType,
26158
28068
  ConfigKey.property_stripePublishableKey,
@@ -26639,6 +28549,12 @@ var Builder = class extends BaseBuilder {
26639
28549
  level: instance6.asNumber(data.level),
26640
28550
  toc: this.toAstProperty(bitType, ConfigKey.property_toc, data.toc, options),
26641
28551
  progress: this.toAstProperty(bitType, ConfigKey.property_progress, data.progress, options),
28552
+ isCollapsible: this.toAstProperty(
28553
+ bitType,
28554
+ ConfigKey.property_isCollapsible,
28555
+ data.isCollapsible,
28556
+ options
28557
+ ),
26642
28558
  anchor: data.anchor,
26643
28559
  reference: data.reference,
26644
28560
  referenceEnd: data.referenceEnd,
@@ -32185,6 +34101,7 @@ var JsonGenerator = class extends AstWalkerGenerator {
32185
34101
  if (instance2.isOfBitType(bitType, BitType2.chapter)) {
32186
34102
  if (bitJson.toc == null) bitJson.toc = true;
32187
34103
  if (bitJson.progress == null) bitJson.progress = true;
34104
+ if (bitJson.isCollapsible == null) bitJson.isCollapsible = false;
32188
34105
  if (bitJson.level == null) bitJson.level = 1;
32189
34106
  if (bitJson.body == null) bitJson.body = this.bodyDefault;
32190
34107
  }
@@ -34380,7 +36297,9 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34380
36297
  const cardSetType = bitConfig.cardSet?.configKey;
34381
36298
  switch (cardSetType) {
34382
36299
  case CardSetConfigKey.flashcard:
36300
+ case CardSetConfigKey.flashcard1:
34383
36301
  case CardSetConfigKey.definitionList:
36302
+ case CardSetConfigKey.definitionListPlain:
34384
36303
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
34385
36304
  break;
34386
36305
  case CardSetConfigKey.elements:
@@ -34393,6 +36312,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34393
36312
  result = parseFeedback(context, bitType, processedCardSet);
34394
36313
  break;
34395
36314
  case CardSetConfigKey.quiz:
36315
+ case CardSetConfigKey.quizResponses:
34396
36316
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
34397
36317
  break;
34398
36318
  case CardSetConfigKey.questions:
@@ -34419,6 +36339,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34419
36339
  break;
34420
36340
  case CardSetConfigKey.clozeList:
34421
36341
  case CardSetConfigKey.exampleBitList:
36342
+ case CardSetConfigKey.pageFooterSections:
34422
36343
  case CardSetConfigKey.bookReferenceList:
34423
36344
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
34424
36345
  break;
@@ -40445,7 +42366,11 @@ var InputFormat = {
40445
42366
  /**
40446
42367
  * Input is plain text
40447
42368
  */
40448
- plainText: "plainText"
42369
+ plainText: "plainText",
42370
+ /**
42371
+ * Input is HTML (used by convertHtmlTable)
42372
+ */
42373
+ html: "html"
40449
42374
  };
40450
42375
  var BitmarkParserGenerator = class {
40451
42376
  constructor() {
@@ -40980,6 +42905,87 @@ var BitmarkParserGenerator = class {
40980
42905
  const res = generator.generate(data);
40981
42906
  return res;
40982
42907
  }
42908
+ /**
42909
+ * Convert between HTML tables and bitmark tables.
42910
+ *
42911
+ * Direction is auto-detected (override with `options.inputFormat`):
42912
+ * - HTML input: every `<table>` is extracted and emitted as a `[.table-extended]` (or `[.table]`)
42913
+ * bit; output as bitmark (default), JSON, AST, or HTML (re-emit).
42914
+ * - bitmark / JSON / AST input: each `table` / `table-extended` bit is rendered as an HTML
42915
+ * `<table>` fragment.
42916
+ *
42917
+ * Cell content is reproduced verbatim except inline conversion to bitmark marks (bold, italic,
42918
+ * superscript, etc.), with lists and images mapped to their bitmark equivalents.
42919
+ *
42920
+ * @param input - HTML, bitmark, JSON or AST string; or a parsed JSON/AST value; or a file path.
42921
+ * @param options - the conversion options
42922
+ * @returns the converted output as a string (or object for JSON/AST), or void when writing a file
42923
+ * @throws Error if any error occurs
42924
+ */
42925
+ convertHtmlTable(input, options) {
42926
+ const opts = Object.assign({}, options);
42927
+ if (env.isBrowser && opts.outputFile) {
42928
+ throw new Error("Cannot write to file in browser environment");
42929
+ }
42930
+ let inStr = input;
42931
+ const inputIsString = typeof input === "string";
42932
+ if (env.isNode && inputIsString && fs.existsSync(inStr)) {
42933
+ inStr = fs.readFileSync(inStr, { encoding: "utf8" });
42934
+ }
42935
+ const isHtmlString = (s) => {
42936
+ const t = s.replace(/^\s+/, "");
42937
+ return t.startsWith("<") && /<table/i.test(t);
42938
+ };
42939
+ let htmlInput;
42940
+ if (opts.inputFormat === InputFormat.html) {
42941
+ htmlInput = true;
42942
+ } else if (opts.inputFormat != null) {
42943
+ htmlInput = false;
42944
+ } else {
42945
+ htmlInput = inputIsString && isHtmlString(inStr);
42946
+ }
42947
+ if (htmlInput) {
42948
+ const parser2 = new HtmlTableParser();
42949
+ const bits = parser2.parse(inStr, {
42950
+ tableFormat: opts.tableFormat ?? "table-extended",
42951
+ keepUnknownTags: opts.keepUnknownTags,
42952
+ onWarning: (m) => console.warn(m)
42953
+ });
42954
+ const outputFormat2 = opts.outputFormat ?? Output.bitmark;
42955
+ if (outputFormat2 === "html") {
42956
+ const json2 = this.convert(bits, { outputFormat: Output.json });
42957
+ return this.outputHtml(new HtmlTableGenerator().generate(json2), opts);
42958
+ }
42959
+ return this.convert(bits, {
42960
+ outputFormat: (0, import_superenum50.Enum)(Output).fromValue(outputFormat2),
42961
+ outputFile: opts.outputFile,
42962
+ fileOptions: opts.fileOptions,
42963
+ jsonOptions: opts.jsonOptions,
42964
+ bitmarkOptions: opts.bitmarkOptions
42965
+ });
42966
+ }
42967
+ const outputFormat = opts.outputFormat ?? "html";
42968
+ if (outputFormat !== "html") {
42969
+ throw new Error(
42970
+ "convertHtmlTable: non-HTML input can only be converted to HTML. Use convert() for bitmark/JSON/AST conversions."
42971
+ );
42972
+ }
42973
+ const json = this.convert(inputIsString ? inStr : input, { outputFormat: Output.json });
42974
+ return this.outputHtml(new HtmlTableGenerator().generate(json), opts);
42975
+ }
42976
+ /**
42977
+ * Write an HTML string to the configured output file, or return it.
42978
+ */
42979
+ outputHtml(html, opts) {
42980
+ if (opts.outputFile) {
42981
+ const output = opts.outputFile.toString();
42982
+ const flag = opts.fileOptions?.append ? "a" : "w";
42983
+ fs.ensureDirSync(path.dirname(output));
42984
+ fs.writeFileSync(output, html, { flag });
42985
+ return;
42986
+ }
42987
+ return html;
42988
+ }
40983
42989
  /**
40984
42990
  * Breakscape bitmark text.
40985
42991
  *
@@ -41258,6 +43264,728 @@ var BoundingBoxIndex = class _BoundingBoxIndex {
41258
43264
  }
41259
43265
  };
41260
43266
 
43267
+ // src/generator/html/HtmlTableGenerator.ts
43268
+ var MARK_TO_TAG = {
43269
+ bold: "b",
43270
+ italic: "i",
43271
+ superscript: "sup",
43272
+ subscript: "sub",
43273
+ strike: "s",
43274
+ del: "del",
43275
+ ins: "ins",
43276
+ underline: "u",
43277
+ highlight: "mark",
43278
+ code: "code"
43279
+ };
43280
+ var HtmlTableGenerator2 = class {
43281
+ constructor() {
43282
+ __publicField(this, "indentUnit", " ");
43283
+ }
43284
+ /**
43285
+ * Generate HTML table fragments from a string or JSON value.
43286
+ * @returns the concatenated `<table>` fragments, blank-line separated (empty string if none).
43287
+ */
43288
+ generate(input) {
43289
+ let data = input;
43290
+ if (typeof data === "string") {
43291
+ try {
43292
+ data = JSON.parse(data);
43293
+ } catch {
43294
+ return "";
43295
+ }
43296
+ }
43297
+ const bits = this.collectBits(data);
43298
+ const fragments = [];
43299
+ for (const bit of bits) {
43300
+ const html = this.renderTableBit(bit);
43301
+ if (html) fragments.push(html);
43302
+ }
43303
+ return fragments.join("\n\n");
43304
+ }
43305
+ // --- bit collection -------------------------------------------------------
43306
+ collectBits(data) {
43307
+ const out = [];
43308
+ const visit = (value) => {
43309
+ if (value == null || typeof value !== "object") return;
43310
+ if (Array.isArray(value)) {
43311
+ value.forEach(visit);
43312
+ return;
43313
+ }
43314
+ const obj = value;
43315
+ if ("bit" in obj && obj.bit && typeof obj.bit === "object") {
43316
+ out.push(obj.bit);
43317
+ } else if ("type" in obj) {
43318
+ out.push(obj);
43319
+ }
43320
+ };
43321
+ visit(data);
43322
+ return out;
43323
+ }
43324
+ // --- table rendering ------------------------------------------------------
43325
+ renderTableBit(bit) {
43326
+ const table = bit.table;
43327
+ if (!table) return "";
43328
+ if (bit.type !== "table" && bit.type !== "table-extended") return "";
43329
+ const extended = this.toExtended(table);
43330
+ const lines = [];
43331
+ lines.push("<table>");
43332
+ if (bit.caption != null) {
43333
+ const caption = this.cellContentToHtml(this.asTextAst(bit.caption));
43334
+ if (caption) lines.push(`${this.indentUnit}<caption>${caption}</caption>`);
43335
+ }
43336
+ this.renderSection(lines, "thead", extended.header, "header");
43337
+ this.renderSection(lines, "tbody", extended.body, "body");
43338
+ this.renderSection(lines, "tfoot", extended.footer, "footer");
43339
+ lines.push("</table>");
43340
+ return lines.join("\n");
43341
+ }
43342
+ toExtended(table) {
43343
+ const t = table;
43344
+ if (t.header || t.body || t.footer) return table;
43345
+ return convertBasicToExtendedTableFormat(table);
43346
+ }
43347
+ renderSection(lines, tag, section, key) {
43348
+ if (!section || !section.rows || section.rows.length === 0) return;
43349
+ lines.push(`${this.indentUnit}<${tag}>`);
43350
+ for (const row of section.rows) {
43351
+ lines.push(`${this.indentUnit.repeat(2)}<tr>`);
43352
+ for (const cell of row.cells ?? []) {
43353
+ lines.push(`${this.indentUnit.repeat(3)}${this.renderCell(cell, key)}`);
43354
+ }
43355
+ lines.push(`${this.indentUnit.repeat(2)}</tr>`);
43356
+ }
43357
+ lines.push(`${this.indentUnit}</${tag}>`);
43358
+ }
43359
+ renderCell(cell, _key) {
43360
+ const tag = cell.title === true ? "th" : "td";
43361
+ const attrs = [];
43362
+ if (cell.rowspan && cell.rowspan > 1) attrs.push(`rowspan="${cell.rowspan}"`);
43363
+ if (cell.colspan && cell.colspan > 1) attrs.push(`colspan="${cell.colspan}"`);
43364
+ if (cell.scope) attrs.push(`scope="${cell.scope}"`);
43365
+ if (cell.colwidth && cell.colwidth > 0) attrs.push(`width="${cell.colwidth}"`);
43366
+ const open = attrs.length > 0 ? `<${tag} ${attrs.join(" ")}>` : `<${tag}>`;
43367
+ return `${open}${this.cellContentToHtml(this.asTextAst(cell.content))}</${tag}>`;
43368
+ }
43369
+ // --- cell content (TextAst -> HTML) ---------------------------------------
43370
+ asTextAst(content) {
43371
+ if (content == null) return [];
43372
+ if (typeof content === "string") return [{ type: "text", text: content }];
43373
+ return content;
43374
+ }
43375
+ cellContentToHtml(blocks) {
43376
+ if (blocks.length === 0) return "";
43377
+ if (blocks.length === 1 && blocks[0].type === "paragraph") {
43378
+ return this.inlineToHtml(blocks[0].content ?? []);
43379
+ }
43380
+ return blocks.map((b) => this.blockToHtml(b)).join("");
43381
+ }
43382
+ blockToHtml(node) {
43383
+ switch (node.type) {
43384
+ case "paragraph":
43385
+ return `<p>${this.inlineToHtml(node.content ?? [])}</p>`;
43386
+ case "bulletList":
43387
+ case "noBulletList":
43388
+ case "orderedList":
43389
+ case "orderedListRoman":
43390
+ case "orderedListRomanLower":
43391
+ case "letteredList":
43392
+ case "letteredListLower":
43393
+ case "taskList":
43394
+ return this.listToHtml(node);
43395
+ case "image":
43396
+ case "imageInline":
43397
+ return this.imageToHtml(node);
43398
+ default:
43399
+ return this.inlineToHtml([node]);
43400
+ }
43401
+ }
43402
+ inlineToHtml(nodes) {
43403
+ return nodes.map((n) => this.inlineNodeToHtml(n)).join("");
43404
+ }
43405
+ inlineNodeToHtml(node) {
43406
+ if (node.type === "hardBreak") return "<br>";
43407
+ if (node.type === "image" || node.type === "imageInline") return this.imageToHtml(node);
43408
+ if (node.type === "text") {
43409
+ return this.wrapMarks(this.escapeText(node.text ?? ""), node.marks);
43410
+ }
43411
+ if (node.content) return this.inlineToHtml(node.content);
43412
+ return this.escapeText(node.text ?? "");
43413
+ }
43414
+ wrapMarks(text, marks) {
43415
+ if (!marks || marks.length === 0) return text;
43416
+ let open = "";
43417
+ let close = "";
43418
+ for (const mark of marks) {
43419
+ if (mark.type === "link") {
43420
+ const href = mark.attrs?.href ?? "";
43421
+ open += `<a href="${this.escapeAttr(href)}">`;
43422
+ close = `</a>${close}`;
43423
+ } else {
43424
+ const tag = MARK_TO_TAG[mark.type];
43425
+ if (tag) {
43426
+ open += `<${tag}>`;
43427
+ close = `</${tag}>${close}`;
43428
+ }
43429
+ }
43430
+ }
43431
+ return `${open}${text}${close}`;
43432
+ }
43433
+ // --- lists ----------------------------------------------------------------
43434
+ listToHtml(node) {
43435
+ const { tag, attrs } = this.listTag(node);
43436
+ const items = (node.content ?? []).map((item) => this.listItemToHtml(item, node.type === "taskList")).join("");
43437
+ return `<${tag}${attrs}>${items}</${tag}>`;
43438
+ }
43439
+ listTag(node) {
43440
+ const start = node.attrs?.start;
43441
+ const startAttr = typeof start === "number" && start !== 1 ? ` start="${start}"` : "";
43442
+ switch (node.type) {
43443
+ case "orderedList":
43444
+ return { tag: "ol", attrs: startAttr };
43445
+ case "orderedListRoman":
43446
+ return { tag: "ol", attrs: ` type="I"${startAttr}` };
43447
+ case "orderedListRomanLower":
43448
+ return { tag: "ol", attrs: ` type="i"${startAttr}` };
43449
+ case "letteredList":
43450
+ return { tag: "ol", attrs: ` type="A"${startAttr}` };
43451
+ case "letteredListLower":
43452
+ return { tag: "ol", attrs: ` type="a"${startAttr}` };
43453
+ case "noBulletList":
43454
+ return { tag: "ul", attrs: ' style="list-style:none"' };
43455
+ case "taskList":
43456
+ case "bulletList":
43457
+ default:
43458
+ return { tag: "ul", attrs: "" };
43459
+ }
43460
+ }
43461
+ listItemToHtml(item, task) {
43462
+ const inner = (item.content ?? []).map((b) => {
43463
+ if (b.type === "paragraph") return this.inlineToHtml(b.content ?? []);
43464
+ return this.blockToHtml(b);
43465
+ }).join("");
43466
+ if (task) {
43467
+ const checked = item.attrs?.checked ? " checked" : "";
43468
+ return `<li><input type="checkbox"${checked}>${inner}</li>`;
43469
+ }
43470
+ return `<li>${inner}</li>`;
43471
+ }
43472
+ // --- images ---------------------------------------------------------------
43473
+ imageToHtml(node) {
43474
+ const attrs = node.attrs ?? {};
43475
+ const parts = [];
43476
+ const src = attrs.src;
43477
+ parts.push(`src="${this.escapeAttr(typeof src === "string" ? src : "")}"`);
43478
+ if (typeof attrs.alt === "string") parts.push(`alt="${this.escapeAttr(attrs.alt)}"`);
43479
+ if (typeof attrs.title === "string") parts.push(`title="${this.escapeAttr(attrs.title)}"`);
43480
+ if (attrs.width != null) parts.push(`width="${attrs.width}"`);
43481
+ if (attrs.height != null) parts.push(`height="${attrs.height}"`);
43482
+ if (typeof attrs.class === "string" && attrs.class) {
43483
+ parts.push(`class="${this.escapeAttr(attrs.class)}"`);
43484
+ }
43485
+ return `<img ${parts.join(" ")}>`;
43486
+ }
43487
+ // --- escaping -------------------------------------------------------------
43488
+ escapeText(text) {
43489
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
43490
+ }
43491
+ escapeAttr(text) {
43492
+ return this.escapeText(text).replace(/"/g, "&quot;");
43493
+ }
43494
+ };
43495
+
43496
+ // src/parser/html/HtmlDom.ts
43497
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
43498
+ "area",
43499
+ "base",
43500
+ "br",
43501
+ "col",
43502
+ "embed",
43503
+ "hr",
43504
+ "img",
43505
+ "input",
43506
+ "link",
43507
+ "meta",
43508
+ "param",
43509
+ "source",
43510
+ "track",
43511
+ "wbr"
43512
+ ]);
43513
+ var IMPLIED_CLOSE = {
43514
+ li: /* @__PURE__ */ new Set(["li"]),
43515
+ td: /* @__PURE__ */ new Set(["td", "th"]),
43516
+ th: /* @__PURE__ */ new Set(["td", "th"]),
43517
+ tr: /* @__PURE__ */ new Set(["td", "th", "tr"]),
43518
+ thead: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43519
+ tbody: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43520
+ tfoot: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43521
+ p: /* @__PURE__ */ new Set(["p"]),
43522
+ option: /* @__PURE__ */ new Set(["option"])
43523
+ };
43524
+ var TAG_RE = /<(\/)?([a-zA-Z][a-zA-Z0-9-]*)((?:\s+(?:"[^"]*"|'[^']*'|[^<>"'])*)?)(\/)?>/g;
43525
+ var ATTR_RE = /([a-zA-Z_:][a-zA-Z0-9:._-]*)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s"'>]+))?/g;
43526
+ function parseAttrs(raw) {
43527
+ const attrs = {};
43528
+ if (!raw) return attrs;
43529
+ ATTR_RE.lastIndex = 0;
43530
+ let m;
43531
+ while ((m = ATTR_RE.exec(raw)) !== null) {
43532
+ const name = m[1].toLowerCase();
43533
+ let value = m[2] ?? "";
43534
+ if (value.length >= 2 && (value[0] === '"' || value[0] === "'")) {
43535
+ value = value.slice(1, -1);
43536
+ }
43537
+ attrs[name] = value;
43538
+ }
43539
+ return attrs;
43540
+ }
43541
+ function stripNonContent(html) {
43542
+ return html.replace(/<!--[\s\S]*?-->/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<!doctype[^>]*>/gi, "").replace(/<script\b[\s\S]*?<\/script>/gi, "").replace(/<style\b[\s\S]*?<\/style>/gi, "");
43543
+ }
43544
+ function tokenize(html) {
43545
+ const tokens = [];
43546
+ const clean = stripNonContent(html);
43547
+ TAG_RE.lastIndex = 0;
43548
+ let lastIndex = 0;
43549
+ let m;
43550
+ while ((m = TAG_RE.exec(clean)) !== null) {
43551
+ if (m.index > lastIndex) {
43552
+ tokens.push({ kind: "text", text: clean.slice(lastIndex, m.index) });
43553
+ }
43554
+ lastIndex = TAG_RE.lastIndex;
43555
+ const isClose = m[1] === "/";
43556
+ const name = m[2].toLowerCase();
43557
+ if (isClose) {
43558
+ tokens.push({ kind: "close", name });
43559
+ } else {
43560
+ tokens.push({
43561
+ kind: "open",
43562
+ name,
43563
+ attrs: parseAttrs(m[3]),
43564
+ selfClose: m[4] === "/"
43565
+ });
43566
+ }
43567
+ }
43568
+ if (lastIndex < clean.length) {
43569
+ tokens.push({ kind: "text", text: clean.slice(lastIndex) });
43570
+ }
43571
+ return tokens;
43572
+ }
43573
+ function parseHtml(html) {
43574
+ const root = { type: "element", name: "#root", attrs: {}, children: [] };
43575
+ const stack = [root];
43576
+ const top = () => stack[stack.length - 1];
43577
+ for (const token of tokenize(html)) {
43578
+ if (token.kind === "text") {
43579
+ if (token.text) top().children.push({ type: "text", value: token.text });
43580
+ continue;
43581
+ }
43582
+ if (token.kind === "open") {
43583
+ const name = token.name;
43584
+ const closes = IMPLIED_CLOSE[name];
43585
+ if (closes) {
43586
+ while (stack.length > 1 && closes.has(top().name)) stack.pop();
43587
+ }
43588
+ const el = {
43589
+ type: "element",
43590
+ name,
43591
+ attrs: token.attrs ?? {},
43592
+ children: []
43593
+ };
43594
+ top().children.push(el);
43595
+ if (!token.selfClose && !VOID_ELEMENTS.has(name)) {
43596
+ stack.push(el);
43597
+ }
43598
+ continue;
43599
+ }
43600
+ for (let i = stack.length - 1; i > 0; i--) {
43601
+ if (stack[i].name === token.name) {
43602
+ stack.length = i;
43603
+ break;
43604
+ }
43605
+ }
43606
+ }
43607
+ return root.children;
43608
+ }
43609
+ function childElements(el, names) {
43610
+ return el.children.filter((c) => c.type === "element" && names.has(c.name));
43611
+ }
43612
+
43613
+ // src/parser/html/HtmlTableParser.ts
43614
+ var MARK_TAGS = {
43615
+ b: "bold",
43616
+ strong: "bold",
43617
+ i: "italic",
43618
+ em: "italic",
43619
+ sup: "superscript",
43620
+ sub: "subscript",
43621
+ s: "strike",
43622
+ strike: "strike",
43623
+ del: "del",
43624
+ ins: "ins",
43625
+ u: "underline",
43626
+ mark: "highlight",
43627
+ code: "code"
43628
+ };
43629
+ var SECTION_TAGS = /* @__PURE__ */ new Set(["thead", "tbody", "tfoot"]);
43630
+ var ROW_TAGS = /* @__PURE__ */ new Set(["tr"]);
43631
+ var CELL_TAGS = /* @__PURE__ */ new Set(["td", "th"]);
43632
+ var LI_TAGS = /* @__PURE__ */ new Set(["li"]);
43633
+ var VALID_SCOPES = /* @__PURE__ */ new Set(["row", "col", "rowgroup", "colgroup"]);
43634
+ var NAMED_ENTITIES = {
43635
+ amp: "&",
43636
+ lt: "<",
43637
+ gt: ">",
43638
+ quot: '"',
43639
+ apos: "'",
43640
+ nbsp: "\xA0"
43641
+ };
43642
+ function decodeEntities(text) {
43643
+ return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (whole, body) => {
43644
+ if (body[0] === "#") {
43645
+ const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
43646
+ if (Number.isFinite(code) && code >= 0 && code <= 1114111) {
43647
+ try {
43648
+ return String.fromCodePoint(code);
43649
+ } catch {
43650
+ return whole;
43651
+ }
43652
+ }
43653
+ return whole;
43654
+ }
43655
+ return NAMED_ENTITIES[body] ?? whole;
43656
+ });
43657
+ }
43658
+ function intAttr(value) {
43659
+ if (value == null) return void 0;
43660
+ const n = Number.parseInt(value, 10);
43661
+ return Number.isInteger(n) ? n : void 0;
43662
+ }
43663
+ function isElement(node) {
43664
+ return node.type === "element";
43665
+ }
43666
+ var HtmlTableParser2 = class {
43667
+ constructor() {
43668
+ __publicField(this, "keepUnknownTags", false);
43669
+ }
43670
+ /**
43671
+ * Parse an HTML string and return one table bit per top-level `<table>`.
43672
+ */
43673
+ parse(html, options) {
43674
+ this.keepUnknownTags = options?.keepUnknownTags ?? false;
43675
+ const tableFormat = options?.tableFormat ?? "table-extended";
43676
+ const onWarning = options?.onWarning;
43677
+ const tables = this.collectTopLevelTables(parseHtml(html));
43678
+ const bits = [];
43679
+ for (const table of tables) {
43680
+ const bit = this.tableToBit(table, tableFormat, onWarning);
43681
+ if (bit) bits.push(bit);
43682
+ }
43683
+ return bits;
43684
+ }
43685
+ // --- table extraction -----------------------------------------------------
43686
+ /** Collect every `<table>` element, NOT descending into nested tables. */
43687
+ collectTopLevelTables(nodes) {
43688
+ const tables = [];
43689
+ const visit = (node) => {
43690
+ if (!isElement(node)) return;
43691
+ if (node.name === "table") {
43692
+ tables.push(node);
43693
+ return;
43694
+ }
43695
+ node.children.forEach(visit);
43696
+ };
43697
+ nodes.forEach(visit);
43698
+ return tables;
43699
+ }
43700
+ tableToBit(table, tableFormat, onWarning) {
43701
+ const header = [];
43702
+ const body = [];
43703
+ const footer = [];
43704
+ let caption;
43705
+ for (const child of table.children) {
43706
+ if (!isElement(child)) continue;
43707
+ if (child.name === "caption") {
43708
+ const capContent = this.inlineNodes(child.children, []);
43709
+ const trimmed = this.trimInline(capContent);
43710
+ if (trimmed.length > 0) caption = trimmed;
43711
+ } else if (SECTION_TAGS.has(child.name)) {
43712
+ const target = child.name === "thead" ? header : child.name === "tfoot" ? footer : body;
43713
+ for (const tr of childElements(child, ROW_TAGS)) {
43714
+ target.push(this.rowToCells(tr));
43715
+ }
43716
+ } else if (ROW_TAGS.has(child.name)) {
43717
+ body.push(this.rowToCells(child));
43718
+ }
43719
+ }
43720
+ if (header.length === 0 && body.length === 0 && footer.length === 0) {
43721
+ return void 0;
43722
+ }
43723
+ const tableExtended = {};
43724
+ if (header.length > 0) tableExtended.header = { rows: header };
43725
+ if (body.length > 0) tableExtended.body = { rows: body };
43726
+ if (footer.length > 0) tableExtended.footer = { rows: footer };
43727
+ let tableData = tableExtended;
43728
+ if (tableFormat === "table") {
43729
+ if (this.isLossyAsBasic(tableExtended)) {
43730
+ onWarning?.(
43731
+ "Converting an HTML table to [.table] loses rowspan/colspan/scope and multi-section structure; use --tableFormat table-extended to preserve them."
43732
+ );
43733
+ }
43734
+ tableData = convertExtendedToBasicTableFormat(tableExtended);
43735
+ }
43736
+ const bit = {
43737
+ type: tableFormat,
43738
+ format: "bitmark++",
43739
+ table: tableData
43740
+ };
43741
+ if (caption) bit.caption = caption;
43742
+ return bit;
43743
+ }
43744
+ rowToCells(tr) {
43745
+ const cells = [];
43746
+ for (const cell of childElements(tr, CELL_TAGS)) {
43747
+ cells.push(this.cellToJson(cell));
43748
+ }
43749
+ return { cells };
43750
+ }
43751
+ cellToJson(cell) {
43752
+ const json = {
43753
+ content: this.blockNodes(cell.children)
43754
+ };
43755
+ if (cell.name === "th") json.title = true;
43756
+ const rowspan = intAttr(cell.attrs.rowspan);
43757
+ if (rowspan != null && rowspan >= 2) json.rowspan = rowspan;
43758
+ const colspan = intAttr(cell.attrs.colspan);
43759
+ if (colspan != null && colspan >= 2) json.colspan = colspan;
43760
+ const scope = cell.attrs.scope?.toLowerCase();
43761
+ if (scope && VALID_SCOPES.has(scope)) {
43762
+ json.scope = scope;
43763
+ }
43764
+ const width = this.widthFromCell(cell);
43765
+ if (width != null) json.colwidth = width;
43766
+ return json;
43767
+ }
43768
+ widthFromCell(cell) {
43769
+ const attrWidth = intAttr(cell.attrs.width);
43770
+ if (attrWidth != null && attrWidth > 0) return attrWidth;
43771
+ const style = cell.attrs.style;
43772
+ if (style) {
43773
+ const m = style.match(/width\s*:\s*(\d+)\s*px/i);
43774
+ if (m) {
43775
+ const n = Number.parseInt(m[1], 10);
43776
+ if (Number.isInteger(n) && n > 0) return n;
43777
+ }
43778
+ }
43779
+ return void 0;
43780
+ }
43781
+ isLossyAsBasic(table) {
43782
+ if (table.footer) return true;
43783
+ if (table.header && table.header.rows.length > 1) return true;
43784
+ const sections = [table.header, table.body, table.footer];
43785
+ for (const section of sections) {
43786
+ if (!section) continue;
43787
+ for (const row of section.rows) {
43788
+ for (const cell of row.cells) {
43789
+ if (cell.rowspan || cell.colspan || cell.scope) return true;
43790
+ }
43791
+ }
43792
+ }
43793
+ return false;
43794
+ }
43795
+ // --- content conversion (HTML -> TextAst) ---------------------------------
43796
+ /**
43797
+ * Convert a cell/li's child nodes into block-level TextAst nodes (paragraphs, lists, images).
43798
+ */
43799
+ blockNodes(nodes) {
43800
+ const blocks = [];
43801
+ let inline = [];
43802
+ const flush = () => {
43803
+ const trimmed = this.trimInline(inline);
43804
+ inline = [];
43805
+ if (trimmed.length === 0) return;
43806
+ if (trimmed.length === 1 && trimmed[0].type === "imageInline") {
43807
+ blocks.push({ ...trimmed[0], type: "image" });
43808
+ } else {
43809
+ blocks.push({ type: "paragraph", content: trimmed, attrs: {} });
43810
+ }
43811
+ };
43812
+ for (const node of nodes) {
43813
+ if (!isElement(node)) {
43814
+ inline.push(...this.inlineText(node.value, []));
43815
+ continue;
43816
+ }
43817
+ const name = node.name;
43818
+ if (name === "p") {
43819
+ flush();
43820
+ const content = this.trimInline(this.inlineNodes(node.children, []));
43821
+ if (content.length > 0) {
43822
+ blocks.push({ type: "paragraph", content, attrs: {} });
43823
+ }
43824
+ } else if (this.isListTag(name)) {
43825
+ flush();
43826
+ const list = this.listNode(node);
43827
+ if (list) blocks.push(list);
43828
+ } else {
43829
+ inline.push(...this.inlineFromElement(node, []));
43830
+ }
43831
+ }
43832
+ flush();
43833
+ return blocks;
43834
+ }
43835
+ /** Convert child nodes to a flat list of inline TextNodes, applying the given marks. */
43836
+ inlineNodes(nodes, marks) {
43837
+ const out = [];
43838
+ for (const node of nodes) {
43839
+ if (!isElement(node)) {
43840
+ out.push(...this.inlineText(node.value, marks));
43841
+ } else {
43842
+ out.push(...this.inlineFromElement(node, marks));
43843
+ }
43844
+ }
43845
+ return out;
43846
+ }
43847
+ inlineFromElement(el, marks) {
43848
+ const name = el.name;
43849
+ if (name === "br") {
43850
+ return [{ type: "hardBreak" }];
43851
+ }
43852
+ if (name === "img") {
43853
+ return [this.imageNode(el, true)];
43854
+ }
43855
+ const markType = MARK_TAGS[name];
43856
+ if (markType) {
43857
+ return this.inlineNodes(el.children, [...marks, { type: markType }]);
43858
+ }
43859
+ if (name === "a") {
43860
+ const linkMark = {
43861
+ type: "link",
43862
+ attrs: {
43863
+ href: el.attrs.href ?? "",
43864
+ ...el.attrs.target ? { target: el.attrs.target } : {}
43865
+ }
43866
+ };
43867
+ return this.inlineNodes(el.children, [...marks, linkMark]);
43868
+ }
43869
+ if (this.keepUnknownTags) {
43870
+ const open = this.serializeOpenTag(el);
43871
+ const close = `</${name}>`;
43872
+ return [
43873
+ ...this.inlineText(open, marks),
43874
+ ...this.inlineNodes(el.children, marks),
43875
+ ...this.inlineText(close, marks)
43876
+ ];
43877
+ }
43878
+ return this.inlineNodes(el.children, marks);
43879
+ }
43880
+ inlineText(raw, marks) {
43881
+ const text = decodeEntities(raw).replace(/\s+/g, " ");
43882
+ if (text === "") return [];
43883
+ const node = { text, type: "text" };
43884
+ if (marks.length > 0) node.marks = marks.map((m) => ({ ...m }));
43885
+ return [node];
43886
+ }
43887
+ serializeOpenTag(el) {
43888
+ const attrs = Object.entries(el.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${v}"`).join("");
43889
+ return `<${el.name}${attrs}>`;
43890
+ }
43891
+ // --- lists ----------------------------------------------------------------
43892
+ isListTag(name) {
43893
+ return name === "ul" || name === "ol";
43894
+ }
43895
+ listNode(el) {
43896
+ const items = childElements(el, LI_TAGS);
43897
+ if (items.length === 0) return void 0;
43898
+ const isTask = el.name === "ul" && items.some((li) => {
43899
+ const firstEl = li.children.find(isElement);
43900
+ return firstEl?.name === "input" && firstEl.attrs.type?.toLowerCase() === "checkbox";
43901
+ });
43902
+ if (isTask) {
43903
+ return {
43904
+ type: "taskList",
43905
+ content: items.map((li) => this.taskItemNode(li))
43906
+ };
43907
+ }
43908
+ const type = this.listType(el);
43909
+ const node = {
43910
+ type,
43911
+ content: items.map((li) => this.listItemNode(li))
43912
+ };
43913
+ if (el.name === "ol") {
43914
+ const start = intAttr(el.attrs.start);
43915
+ if (start != null) {
43916
+ node.attrs = { start };
43917
+ }
43918
+ }
43919
+ return node;
43920
+ }
43921
+ listType(el) {
43922
+ if (el.name === "ol") {
43923
+ switch (el.attrs.type) {
43924
+ case "i":
43925
+ return "orderedListRomanLower";
43926
+ case "I":
43927
+ return "orderedListRoman";
43928
+ case "a":
43929
+ return "letteredListLower";
43930
+ case "A":
43931
+ return "letteredList";
43932
+ default:
43933
+ return "orderedList";
43934
+ }
43935
+ }
43936
+ const style = el.attrs.style ?? "";
43937
+ if (/list-style(-type)?\s*:\s*none/i.test(style)) return "noBulletList";
43938
+ return "bulletList";
43939
+ }
43940
+ listItemNode(li) {
43941
+ let content = this.blockNodes(li.children);
43942
+ if (content.length === 0) {
43943
+ content = [{ type: "paragraph", content: [], attrs: {} }];
43944
+ }
43945
+ return { type: "listItem", content };
43946
+ }
43947
+ taskItemNode(li) {
43948
+ const checkbox = li.children.find(
43949
+ (c) => isElement(c) && c.name === "input" && c.attrs.type?.toLowerCase() === "checkbox"
43950
+ );
43951
+ const checked = checkbox ? "checked" in checkbox.attrs : false;
43952
+ const rest = li.children.filter((c) => c !== checkbox);
43953
+ let content = this.blockNodes(rest);
43954
+ if (content.length === 0) {
43955
+ content = [{ type: "paragraph", content: [], attrs: {} }];
43956
+ }
43957
+ return { type: "taskItem", attrs: { checked }, content };
43958
+ }
43959
+ // --- images ---------------------------------------------------------------
43960
+ imageNode(el, inline) {
43961
+ const attrs = { src: el.attrs.src ?? "" };
43962
+ attrs.alt = el.attrs.alt ?? null;
43963
+ attrs.title = el.attrs.title ?? null;
43964
+ const width = intAttr(el.attrs.width);
43965
+ if (width != null) attrs.width = width;
43966
+ const height = intAttr(el.attrs.height);
43967
+ if (height != null) attrs.height = height;
43968
+ if (el.attrs.class) attrs.class = el.attrs.class;
43969
+ return { type: inline ? "imageInline" : "image", attrs };
43970
+ }
43971
+ // --- whitespace -----------------------------------------------------------
43972
+ /** Trim leading/trailing whitespace from inline content (edges only). */
43973
+ trimInline(nodes) {
43974
+ const out = nodes.slice();
43975
+ const isWs = (n) => n.type === "text" && /^\s*$/.test(n.text ?? "");
43976
+ while (out.length > 0 && isWs(out[0])) out.shift();
43977
+ if (out.length > 0 && out[0].type === "text") {
43978
+ out[0] = { ...out[0], text: (out[0].text ?? "").replace(/^\s+/, "") };
43979
+ }
43980
+ while (out.length > 0 && isWs(out[out.length - 1])) out.pop();
43981
+ if (out.length > 0 && out[out.length - 1].type === "text") {
43982
+ const last = out.length - 1;
43983
+ out[last] = { ...out[last], text: (out[last].text ?? "").replace(/\s+$/, "") };
43984
+ }
43985
+ return out.filter((n) => !(n.type === "text" && n.text === ""));
43986
+ }
43987
+ };
43988
+
41261
43989
  // src/index.ts
41262
43990
  init();
41263
43991
  // Annotate the CommonJS export names for ESM import in node:
@@ -41274,6 +44002,8 @@ init();
41274
44002
  BoundingBoxIndex,
41275
44003
  Builder,
41276
44004
  CardSetVersion,
44005
+ HtmlTableGenerator,
44006
+ HtmlTableParser,
41277
44007
  InfoFormat,
41278
44008
  InfoType,
41279
44009
  Input,