@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.
package/dist/index.cjs CHANGED
@@ -46,6 +46,8 @@ __export(index_exports, {
46
46
  Builder: () => Builder,
47
47
  CardSetVersion: () => CardSetVersion,
48
48
  FileWriter: () => FileWriter,
49
+ HtmlTableGenerator: () => HtmlTableGenerator,
50
+ HtmlTableParser: () => HtmlTableParser,
49
51
  InfoFormat: () => InfoFormat,
50
52
  InfoType: () => InfoType,
51
53
  Input: () => Input,
@@ -293,6 +295,7 @@ var BitType = {
293
295
  dangerAlt: "danger-alt",
294
296
  dangerCollapsible: "danger-collapsible",
295
297
  definitionList: "definition-list",
298
+ definitionListCollapsible: "definition-list-collapsible",
296
299
  definitionTerm: "definition-term",
297
300
  deleted: "deleted",
298
301
  details: "details",
@@ -573,6 +576,8 @@ var BitType = {
573
576
  smartStandardArticleNonNormativeCollapsible: "smart-standard-article-non-normative-collapsible",
574
577
  smartStandardArticleNormative: "smart-standard-article-normative",
575
578
  smartStandardArticleNormativeCollapsible: "smart-standard-article-normative-collapsible",
579
+ smartStandardDefinitionListNonNormative: "smart-standard-definition-list-non-normative",
580
+ smartStandardDefinitionListNormative: "smart-standard-definition-list-normative",
576
581
  smartStandardExampleNonNormative: "smart-standard-example-non-normative",
577
582
  smartStandardExampleNonNormativeCollapsible: "smart-standard-example-non-normative-collapsible",
578
583
  smartStandardExampleNormative: "smart-standard-example-normative",
@@ -623,6 +628,8 @@ var BitType = {
623
628
  smartStandardTableNormativeCollapsible: "smart-standard-table-normative-collapsible",
624
629
  standardArticleNonNormative: "standard-article-non-normative",
625
630
  standardArticleNormative: "standard-article-normative",
631
+ standardDefinitionListNonNormative: "standard-definition-list-non-normative",
632
+ standardDefinitionListNormative: "standard-definition-list-normative",
626
633
  standardExampleNonNormative: "standard-example-non-normative",
627
634
  standardExampleNormative: "standard-example-normative",
628
635
  standardImageFigureNonNormative: "standard-image-figure-non-normative",
@@ -792,10 +799,13 @@ var TextFormat = {
792
799
  var import_superenum7 = require("@ncoderz/superenum");
793
800
  var CardSetConfigKey = {
794
801
  flashcard: "flashcard",
802
+ flashcard1: "flashcard1",
795
803
  definitionList: "definitionList",
804
+ definitionListPlain: "definitionListPlain",
796
805
  elements: "elements",
797
806
  statements: "statements",
798
807
  quiz: "quiz",
808
+ quizResponses: "quizResponses",
799
809
  feedback: "feedback",
800
810
  questions: "questions",
801
811
  matchPairs: "matchPairs",
@@ -807,6 +817,7 @@ var CardSetConfigKey = {
807
817
  pronunciationTable: "pronunciationTable",
808
818
  botActionResponses: "botActionResponses",
809
819
  exampleBitList: "exampleBitList",
820
+ pageFooterSections: "pageFooterSections",
810
821
  clozeList: "clozeList",
811
822
  ingredients: "ingredients",
812
823
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -1223,12 +1234,17 @@ var groupKeys = {
1223
1234
  group_standardAllBits: "group_standardAllBits",
1224
1235
  group_standardItemLead: "group_standardItemLead",
1225
1236
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1237
+ group_standardInstructionHint: "group_standardInstructionHint",
1226
1238
  group_standardTags: "group_standardTags",
1239
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1227
1240
  group_imageSource: "group_imageSource",
1228
1241
  group_technicalTerm: "group_technicalTerm",
1229
1242
  group_person: "group_person",
1230
1243
  group_gap: "group_gap",
1231
1244
  group_trueFalse: "group_trueFalse",
1245
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1246
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1247
+ group_trueFalseResponses: "group_trueFalseResponses",
1232
1248
  group_markConfig: "group_markConfig",
1233
1249
  group_mark: "group_mark",
1234
1250
  group_bookCommon: "group_bookCommon",
@@ -1236,14 +1252,18 @@ var groupKeys = {
1236
1252
  group_advertisingCommon: "group_advertisingCommon",
1237
1253
  group_platformStylesCommon: "group_platformStylesCommon",
1238
1254
  group_quizCommon: "group_quizCommon",
1255
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1256
+ group_imageNoZoom: "group_imageNoZoom",
1239
1257
  group_resourceBitTags: "group_resourceBitTags",
1240
1258
  group_resourceCommon: "group_resourceCommon",
1241
1259
  group_resourceImageCommon: "group_resourceImageCommon",
1260
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1242
1261
  group_resourceAudioCommon: "group_resourceAudioCommon",
1243
1262
  group_resourceVideoCommon: "group_resourceVideoCommon",
1244
1263
  group_resourceIcon: "group_resourceIcon",
1245
1264
  // Alias for image
1246
1265
  group_resourceImage: "group_resourceImage",
1266
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1247
1267
  group_resourceImageEmbed: "group_resourceImageEmbed",
1248
1268
  group_resourceImageLink: "group_resourceImageLink",
1249
1269
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1395,6 +1415,7 @@ var propertyKeys = {
1395
1415
  property_enforceUpdateOverUserInput: "@enforceUpdateOverUserInput",
1396
1416
  property_extractorExtractionTimestamp: "@extractorExtractionTimestamp",
1397
1417
  property_isCaseSensitive: "@isCaseSensitive",
1418
+ property_isCollapsible: "@isCollapsible",
1398
1419
  property_isInfoOnly: "@isInfoOnly",
1399
1420
  property_isPublic: "@isPublic",
1400
1421
  property_isTemplate: "@isTemplate",
@@ -1520,6 +1541,7 @@ var propertyKeys = {
1520
1541
  property_src4x: "@src4x",
1521
1542
  property_srcAlt: "@srcAlt",
1522
1543
  property_stripePricingTableId: "@stripePricingTableId",
1544
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1523
1545
  property_stripePublishableKey: "@stripePublishableKey",
1524
1546
  property_subject: "@subject",
1525
1547
  property_subtype: "@subtype",
@@ -2271,7 +2293,7 @@ var CARDSETS = {
2271
2293
  variants: [
2272
2294
  {
2273
2295
  jsonKey: "question.text",
2274
- exportJsonKey: { question: { text: "$" } },
2296
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2275
2297
  tags: [
2276
2298
  {
2277
2299
  key: ConfigKey.group_standardTags,
@@ -2287,6 +2309,136 @@ var CARDSETS = {
2287
2309
  description: "Icon resource for the flashcard.",
2288
2310
  jsonKey: "question.icon|resource(type=image, key=image)",
2289
2311
  exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2312
+ },
2313
+ {
2314
+ key: ConfigKey.property_example,
2315
+ exportJsonKey: [
2316
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2317
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2318
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2319
+ ],
2320
+ description: "Example marker for the flashcard question.",
2321
+ format: TagFormat.bitmarkText,
2322
+ nullable: true
2323
+ }
2324
+ ]
2325
+ }
2326
+ ]
2327
+ },
2328
+ {
2329
+ name: "answer",
2330
+ variants: [
2331
+ {
2332
+ jsonKey: "answer.text",
2333
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2334
+ tags: [
2335
+ {
2336
+ key: ConfigKey.group_standardTags,
2337
+ description: "Standard tags for the flashcard."
2338
+ },
2339
+ {
2340
+ exportJsonKey: { title: "$" },
2341
+ key: ConfigKey.tag_title,
2342
+ description: "Title of the flashcard."
2343
+ },
2344
+ {
2345
+ key: ConfigKey.group_resourceIcon,
2346
+ description: "Icon resource for the flashcard.",
2347
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2348
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2349
+ },
2350
+ {
2351
+ key: ConfigKey.property_example,
2352
+ exportJsonKey: [
2353
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2354
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2355
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2356
+ ],
2357
+ description: "Example marker for the flashcard answer.",
2358
+ format: TagFormat.bitmarkText,
2359
+ nullable: true
2360
+ }
2361
+ ]
2362
+ },
2363
+ {
2364
+ jsonKey: "alternativeAnswers[].text",
2365
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2366
+ tags: [
2367
+ {
2368
+ key: ConfigKey.group_standardTags,
2369
+ description: "Standard tags for the flashcard."
2370
+ },
2371
+ {
2372
+ exportJsonKey: { title: "$" },
2373
+ key: ConfigKey.tag_title,
2374
+ description: "Title of the flashcard."
2375
+ },
2376
+ {
2377
+ key: ConfigKey.group_resourceIcon,
2378
+ description: "Icon resource for the flashcard.",
2379
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2380
+ exportJsonKey: {
2381
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2382
+ }
2383
+ }
2384
+ ],
2385
+ repeatCount: Count.infinity
2386
+ }
2387
+ ]
2388
+ }
2389
+ ]
2390
+ },
2391
+ //
2392
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2393
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2394
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2395
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2396
+ // cardset.
2397
+ //
2398
+ [CardSetConfigKey.flashcard1]: {
2399
+ jsonKey: "cards",
2400
+ exportJsonKey: { cards: "$" },
2401
+ sections: {
2402
+ default: {
2403
+ jsonKey: "cards",
2404
+ exportJsonKey: { cards: "$" },
2405
+ isDefault: true,
2406
+ maxCount: 1
2407
+ }
2408
+ },
2409
+ sides: [
2410
+ {
2411
+ name: "question",
2412
+ variants: [
2413
+ {
2414
+ jsonKey: "question.text",
2415
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2416
+ tags: [
2417
+ {
2418
+ key: ConfigKey.group_standardTags,
2419
+ description: "Standard tags for the flashcard."
2420
+ },
2421
+ {
2422
+ exportJsonKey: { title: "$" },
2423
+ key: ConfigKey.tag_title,
2424
+ description: "Title of the flashcard."
2425
+ },
2426
+ {
2427
+ key: ConfigKey.group_resourceIcon,
2428
+ description: "Icon resource for the flashcard.",
2429
+ jsonKey: "question.icon|resource(type=image, key=image)",
2430
+ exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2431
+ },
2432
+ {
2433
+ key: ConfigKey.property_example,
2434
+ exportJsonKey: [
2435
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2436
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2437
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2438
+ ],
2439
+ description: "Example marker for the flashcard question.",
2440
+ format: TagFormat.bitmarkText,
2441
+ nullable: true
2290
2442
  }
2291
2443
  ]
2292
2444
  }
@@ -2297,7 +2449,7 @@ var CARDSETS = {
2297
2449
  variants: [
2298
2450
  {
2299
2451
  jsonKey: "answer.text",
2300
- exportJsonKey: { answer: { text: "$" } },
2452
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2301
2453
  tags: [
2302
2454
  {
2303
2455
  key: ConfigKey.group_standardTags,
@@ -2313,6 +2465,17 @@ var CARDSETS = {
2313
2465
  description: "Icon resource for the flashcard.",
2314
2466
  jsonKey: "answer.icon|resource(type=image, key=image)",
2315
2467
  exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2468
+ },
2469
+ {
2470
+ key: ConfigKey.property_example,
2471
+ exportJsonKey: [
2472
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2473
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2474
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2475
+ ],
2476
+ description: "Example marker for the flashcard answer.",
2477
+ format: TagFormat.bitmarkText,
2478
+ nullable: true
2316
2479
  }
2317
2480
  ]
2318
2481
  },
@@ -2356,7 +2519,7 @@ var CARDSETS = {
2356
2519
  variants: [
2357
2520
  {
2358
2521
  jsonKey: "term.text",
2359
- exportJsonKey: { term: { text: "$" } },
2522
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2360
2523
  tags: [
2361
2524
  {
2362
2525
  key: ConfigKey.group_standardTags,
@@ -2375,6 +2538,17 @@ var CARDSETS = {
2375
2538
  format: TagFormat.plainText,
2376
2539
  jsonKey: "term.icon|resource(type=image, key=image)",
2377
2540
  exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2541
+ },
2542
+ {
2543
+ key: ConfigKey.property_example,
2544
+ exportJsonKey: [
2545
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2546
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2547
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2548
+ ],
2549
+ description: "Example marker for the definition term.",
2550
+ format: TagFormat.bitmarkText,
2551
+ nullable: true
2378
2552
  }
2379
2553
  ]
2380
2554
  }
@@ -2385,7 +2559,7 @@ var CARDSETS = {
2385
2559
  variants: [
2386
2560
  {
2387
2561
  jsonKey: "definition.text",
2388
- exportJsonKey: { definition: { text: "$" } },
2562
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2389
2563
  tags: [
2390
2564
  {
2391
2565
  key: ConfigKey.group_standardTags,
@@ -2403,6 +2577,17 @@ var CARDSETS = {
2403
2577
  description: "Icon resource for the definition.",
2404
2578
  jsonKey: "definition.icon|resource(type=image, key=image)",
2405
2579
  exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2580
+ },
2581
+ {
2582
+ key: ConfigKey.property_example,
2583
+ exportJsonKey: [
2584
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2585
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2586
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2587
+ ],
2588
+ description: "Example marker for the definition.",
2589
+ format: TagFormat.bitmarkText,
2590
+ nullable: true
2406
2591
  }
2407
2592
  ]
2408
2593
  },
@@ -2435,6 +2620,126 @@ var CARDSETS = {
2435
2620
  ]
2436
2621
  },
2437
2622
  //
2623
+ // definition-list-plain
2624
+ //
2625
+ // Same shape as `definitionList`, but every variant body is rendered as
2626
+ // plain text (string), not bitmark+ AST. Used by
2627
+ // `meta-search-default-terms` / `meta-search-default-topics`, which need
2628
+ // term/definition text emitted verbatim instead of as a ProseMirror tree.
2629
+ //
2630
+ [CardSetConfigKey.definitionListPlain]: {
2631
+ jsonKey: "definitions",
2632
+ exportJsonKey: { definitions: "$" },
2633
+ sides: [
2634
+ {
2635
+ name: "term",
2636
+ variants: [
2637
+ {
2638
+ jsonKey: "term.text",
2639
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2640
+ format: TextFormat.plainText,
2641
+ tags: [
2642
+ {
2643
+ key: ConfigKey.group_standardTags,
2644
+ description: "Standard tags for the definition."
2645
+ },
2646
+ {
2647
+ key: ConfigKey.tag_title,
2648
+ description: "Title of the definition.",
2649
+ format: TagFormat.plainText,
2650
+ jsonKey: "^heading.forKeys",
2651
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2652
+ },
2653
+ {
2654
+ key: ConfigKey.group_resourceIcon,
2655
+ description: "Icon resource for the definition.",
2656
+ format: TagFormat.plainText,
2657
+ jsonKey: "term.icon|resource(type=image, key=image)",
2658
+ exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2659
+ },
2660
+ {
2661
+ key: ConfigKey.property_example,
2662
+ exportJsonKey: [
2663
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2664
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2665
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2666
+ ],
2667
+ description: "Example marker for the definition term.",
2668
+ format: TagFormat.bitmarkText,
2669
+ nullable: true
2670
+ }
2671
+ ]
2672
+ }
2673
+ ]
2674
+ },
2675
+ {
2676
+ name: "definition",
2677
+ variants: [
2678
+ {
2679
+ jsonKey: "definition.text",
2680
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2681
+ format: TextFormat.plainText,
2682
+ tags: [
2683
+ {
2684
+ key: ConfigKey.group_standardTags,
2685
+ description: "Standard tags for the definition."
2686
+ },
2687
+ {
2688
+ key: ConfigKey.tag_title,
2689
+ description: "Title of the definition.",
2690
+ format: TagFormat.plainText,
2691
+ jsonKey: "^heading.forValues",
2692
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2693
+ },
2694
+ {
2695
+ key: ConfigKey.group_resourceIcon,
2696
+ description: "Icon resource for the definition.",
2697
+ jsonKey: "definition.icon|resource(type=image, key=image)",
2698
+ exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2699
+ },
2700
+ {
2701
+ key: ConfigKey.property_example,
2702
+ exportJsonKey: [
2703
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2704
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2705
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2706
+ ],
2707
+ description: "Example marker for the definition.",
2708
+ format: TagFormat.bitmarkText,
2709
+ nullable: true
2710
+ }
2711
+ ]
2712
+ },
2713
+ {
2714
+ jsonKey: "alternativeDefinitions[].text",
2715
+ exportJsonKey: { alternativeDefinitions: [{ text: "$" }] },
2716
+ format: TextFormat.plainText,
2717
+ tags: [
2718
+ {
2719
+ key: ConfigKey.group_standardTags,
2720
+ description: "Standard tags for the definition."
2721
+ },
2722
+ {
2723
+ exportJsonKey: { title: "$" },
2724
+ key: ConfigKey.tag_title,
2725
+ description: "Title of the definition."
2726
+ },
2727
+ {
2728
+ key: ConfigKey.group_resourceIcon,
2729
+ description: "Icon resource for the definition.",
2730
+ jsonKey: "alternativeDefinitions[].icon|resource(type=image, key=image)",
2731
+ exportJsonKey: {
2732
+ alternativeDefinitions: [{ icon: { type: "image", image: { src: "$" } } }]
2733
+ }
2734
+ }
2735
+ ],
2736
+ repeatCount: Count.infinity
2737
+ }
2738
+ ]
2739
+ }
2740
+ ]
2741
+ },
2742
+ //
2438
2743
  // match-pairs
2439
2744
  //
2440
2745
  [CardSetConfigKey.matchPairs]: {
@@ -2447,11 +2752,51 @@ var CARDSETS = {
2447
2752
  {
2448
2753
  jsonKey: "key",
2449
2754
  exportJsonKey: { key: "$" },
2755
+ format: TextFormat.plainText,
2450
2756
  tags: [
2451
2757
  {
2452
2758
  key: ConfigKey.group_standardTags,
2453
2759
  description: "Standard tags for the match pair."
2454
2760
  },
2761
+ {
2762
+ // Pair-level example: emit `isExample` alongside `example`
2763
+ // (matches OLD parser's per-pair allow-list behaviour).
2764
+ //
2765
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2766
+ // in the cardset) fills each pair's `example` with `V`
2767
+ // once per pair from the keys-side cascade fire (single
2768
+ // parsed_variant per pair — no per-value duplication).
2769
+ //
2770
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2771
+ // at the pair-object level. BPG `parseMatchPairs`
2772
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2773
+ // per PAIR (the `exampleCard` accumulator), placed at the
2774
+ // pair object. Unlike matchMatrix (per-cell example),
2775
+ // matchPairs has no nested per-cell layer, so the pair-key
2776
+ // `[@example:V]` must double-emit as the visible field.
2777
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2778
+ // expects `example: "packen"` at pair-object level from
2779
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2780
+ // draft applied to both keys-side tags; the matchPairs
2781
+ // half was reverted after fixture parity testing.
2782
+ key: ConfigKey.property_example,
2783
+ exportJsonKey: [
2784
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2785
+ {
2786
+ predicates: ["@absent", { $cascade: "*" }],
2787
+ rule: {
2788
+ isExample: true,
2789
+ example: "$cascade",
2790
+ "@bit": { isExample: true }
2791
+ }
2792
+ },
2793
+ { "@absent": { isExample: true } },
2794
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2795
+ ],
2796
+ description: "Example marker / value on the match pair.",
2797
+ format: TagFormat.bitmarkText,
2798
+ nullable: true
2799
+ },
2455
2800
  {
2456
2801
  key: ConfigKey.tag_title,
2457
2802
  description: "Title of the match pair.",
@@ -2460,9 +2805,19 @@ var CARDSETS = {
2460
2805
  exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2461
2806
  },
2462
2807
  {
2808
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2809
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2810
+ // only fires when NO variant in the active card explicitly
2811
+ // carries `[@isCaseSensitive]`. When a value-side variant
2812
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2813
+ // synth is skipped here; the value-side explicit fire owns
2814
+ // the pair-level emission.
2463
2815
  key: ConfigKey.property_isCaseSensitive,
2464
2816
  exportJsonKey: [
2465
- { "@absent": { isCaseSensitive: "$ancestor" } },
2817
+ {
2818
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2819
+ rule: { isCaseSensitive: "$ancestor" }
2820
+ },
2466
2821
  { isCaseSensitive: "$" }
2467
2822
  ],
2468
2823
  description: "Property to indicate if the match is case sensitive.",
@@ -2491,10 +2846,85 @@ var CARDSETS = {
2491
2846
  {
2492
2847
  jsonKey: "values[]",
2493
2848
  exportJsonKey: { values: ["$"] },
2849
+ format: TextFormat.plainText,
2494
2850
  tags: [
2495
2851
  {
2496
- key: ConfigKey.group_standardTags,
2497
- description: "Standard tags for the match pair."
2852
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2853
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2854
+ // for sideIdx > 0; the schema-side equivalent is to use a
2855
+ // group_standardTags variant that omits group_standardItemLead.
2856
+ key: ConfigKey.group_standardTagsNoItemLead,
2857
+ description: "Standard tags for the match pair (values side, no item/lead)."
2858
+ },
2859
+ {
2860
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2861
+ // `@example` from the bit header fills each pair's
2862
+ // `example` with the pair's first value (mirrors BPG's
2863
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2864
+ //
2865
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2866
+ // TAG 415 (keys side) which fires once per pair — keeping
2867
+ // it here would emit `example` twice per pair (one per
2868
+ // values-side parsed_variant) and merge them into a
2869
+ // multi-paragraph array.
2870
+ //
2871
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2872
+ // source `$` value (the current `++`-iteration's value
2873
+ // text). For single-value pairs this matches the old
2874
+ // `$parent.values[0]`; for multi-value pairs with
2875
+ // `[@example]` attached to a non-first value, it correctly
2876
+ // resolves to that value.
2877
+ //
2878
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2879
+ // routes pair-key `[@example:V]` (a per-pair cascade
2880
+ // source, bound via per-card CascadeCounters scope) and
2881
+ // bit-header `[@example:V]` (cardset cascade) into the
2882
+ // pair-level `example` via `$cascade`. The aggregator in
2883
+ // `populate_variants` fires this rule ONCE per pair (BPG
2884
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2885
+ // per-iter pollution.
2886
+ key: ConfigKey.property_example,
2887
+ exportJsonKey: [
2888
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2889
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2890
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2891
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2892
+ // scope opened by `build_card_item`.
2893
+ {
2894
+ predicates: ["@keyonly"],
2895
+ maxEmits: 1,
2896
+ rule: {
2897
+ isExample: true,
2898
+ example: "$parent",
2899
+ "@bit": { isExample: true }
2900
+ }
2901
+ },
2902
+ {
2903
+ predicates: ["@absent", { $cascade: "*" }],
2904
+ rule: {
2905
+ isExample: true,
2906
+ example: "$cascade",
2907
+ "@bit": { isExample: true }
2908
+ }
2909
+ },
2910
+ {
2911
+ "@absent": {
2912
+ isExample: true,
2913
+ example: "$parent",
2914
+ "@bit": { isExample: true }
2915
+ }
2916
+ },
2917
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2918
+ // form so per-`++`-iter values-side fires also first-win.
2919
+ {
2920
+ predicates: [],
2921
+ maxEmits: 1,
2922
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2923
+ }
2924
+ ],
2925
+ description: "Example marker / value on the match pair (values side).",
2926
+ format: TagFormat.bitmarkText,
2927
+ nullable: true
2498
2928
  },
2499
2929
  {
2500
2930
  key: ConfigKey.tag_title,
@@ -2504,9 +2934,13 @@ var CARDSETS = {
2504
2934
  exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2505
2935
  },
2506
2936
  {
2937
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2507
2938
  key: ConfigKey.property_isCaseSensitive,
2508
2939
  exportJsonKey: [
2509
- { "@absent": { isCaseSensitive: "$ancestor" } },
2940
+ {
2941
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2942
+ rule: { isCaseSensitive: "$ancestor" }
2943
+ },
2510
2944
  { isCaseSensitive: "$" }
2511
2945
  ],
2512
2946
  description: "Property to indicate if the match is case sensitive.",
@@ -2532,6 +2966,7 @@ var CARDSETS = {
2532
2966
  {
2533
2967
  jsonKey: "key",
2534
2968
  exportJsonKey: { key: "$" },
2969
+ format: TextFormat.plainText,
2535
2970
  tags: [
2536
2971
  {
2537
2972
  key: ConfigKey.group_standardTags,
@@ -2561,10 +2996,12 @@ var CARDSETS = {
2561
2996
  {
2562
2997
  jsonKey: "values[]",
2563
2998
  exportJsonKey: { values: ["$"] },
2999
+ format: TextFormat.plainText,
2564
3000
  tags: [
2565
3001
  {
2566
- key: ConfigKey.group_standardTags,
2567
- description: "Standard tags for the audio match pair."
3002
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
3003
+ key: ConfigKey.group_standardTagsNoItemLead,
3004
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2568
3005
  },
2569
3006
  {
2570
3007
  key: ConfigKey.tag_title,
@@ -2597,6 +3034,7 @@ var CARDSETS = {
2597
3034
  {
2598
3035
  jsonKey: "key",
2599
3036
  exportJsonKey: { key: "$" },
3037
+ format: TextFormat.plainText,
2600
3038
  tags: [
2601
3039
  {
2602
3040
  key: ConfigKey.group_standardTags,
@@ -2626,10 +3064,12 @@ var CARDSETS = {
2626
3064
  {
2627
3065
  jsonKey: "values[]",
2628
3066
  exportJsonKey: { values: ["$"] },
3067
+ format: TextFormat.plainText,
2629
3068
  tags: [
2630
3069
  {
2631
- key: ConfigKey.group_standardTags,
2632
- description: "Standard tags for the image match pair."
3070
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
3071
+ key: ConfigKey.group_standardTagsNoItemLead,
3072
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2633
3073
  },
2634
3074
  {
2635
3075
  key: ConfigKey.tag_title,
@@ -2662,20 +3102,49 @@ var CARDSETS = {
2662
3102
  {
2663
3103
  jsonKey: "key",
2664
3104
  exportJsonKey: { key: "$" },
3105
+ format: TextFormat.plainText,
2665
3106
  tags: [
2666
3107
  {
2667
3108
  key: ConfigKey.group_standardItemLeadInstructionHint,
2668
3109
  description: "Standard tags for lead, instruction, and hint."
2669
3110
  },
2670
3111
  {
3112
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
3113
+ // `[@example:V]` is consumed as the per-pair cascade source.
3114
+ // PLAN-077 R14 follow-up: investigation showed the fixture
3115
+ // reference is INCONSISTENT — bits without a cardset-level
3116
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
3117
+ // pair-level `example: V` at pair scope, but bits WITH a
3118
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
3119
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
3120
+ // example. Emitting `example: $` here satisfies bit 3 but
3121
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
3122
+ // 3. Conditional emission would require runtime gating on
3123
+ // the cardset cascade scope state. Left dropped pending a
3124
+ // dedicated plan — accepted divergence on bit 3's pair-
3125
+ // level emission (cells still get the per-cell `example`
3126
+ // correctly via PLAN-077 §4.2 + §4.3).
2671
3127
  key: ConfigKey.property_example,
2672
3128
  exportJsonKey: [
2673
- { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2674
- { "@absent": { isExample: true, "@bit": { isExample: true } } },
2675
- { isExample: true, example: "$", "@bit": { isExample: true } }
3129
+ {
3130
+ "@keyonly": {
3131
+ isExample: true,
3132
+ "@bit": { isExample: true },
3133
+ "@card": { isExample: true }
3134
+ }
3135
+ },
3136
+ {
3137
+ "@absent": {
3138
+ isExample: true,
3139
+ "@bit": { isExample: true },
3140
+ "@card": { isExample: true }
3141
+ }
3142
+ },
3143
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2676
3144
  ],
2677
3145
  description: "Example text for the match matrix.",
2678
- format: TagFormat.plainText
3146
+ format: TagFormat.plainText,
3147
+ nullable: true
2679
3148
  },
2680
3149
  {
2681
3150
  key: ConfigKey.tag_title,
@@ -2703,35 +3172,96 @@ var CARDSETS = {
2703
3172
  variants: [
2704
3173
  {
2705
3174
  jsonKey: "cells[{s}].values[]",
2706
- exportJsonKey: { cells: { $s: { values: ["$"] } } },
3175
+ exportJsonKey: [
3176
+ // PLAN-085 / R14: the positional-blank emission for an
3177
+ // effectively-absent cell side fires ONLY when the active
3178
+ // card has at least one `[#]` variant fire. This matches
3179
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
3180
+ // `[#]` exists anywhere in the title card, BPG returns
3181
+ // undefined and no `heading` is emitted. The phantom
3182
+ // `forValues:['']` placeholder is appropriate only when
3183
+ // the card is being treated as a title-row.
3184
+ {
3185
+ predicates: ["@absent", { "@variantHasTag": "#" }],
3186
+ rule: { "@bit": { heading: { forValues: [""] } } }
3187
+ },
3188
+ { cells: { $s: { values: ["$"] } } }
3189
+ ],
3190
+ format: TextFormat.plainText,
2707
3191
  tags: [
2708
3192
  {
2709
3193
  key: ConfigKey.group_standardItemLeadInstructionHint,
2710
3194
  description: "Standard tags for lead, instruction, and hint."
2711
3195
  },
2712
3196
  {
3197
+ // PLAN-075: bare `$parent` reads the parent variant fire's
3198
+ // source `$` value (the current `++`-iteration's cell text).
3199
+ // Replaces `$parent.values[0]` which pinned to position 0
3200
+ // regardless of which `++`-value `[@example]` was on.
3201
+ //
3202
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
3203
+ // routes pair-key `[@example:V]` (a per-pair cascade
3204
+ // source, bound via per-card CascadeCounters scope) and
3205
+ // bit-header `[@example:V]` (cardset cascade) into the
3206
+ // per-cell `example` via `$cascade`. The aggregator in
3207
+ // `populate_variants` fires this rule ONCE per cell (BPG
3208
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
3209
+ // per-iter pollution.
3210
+ // @card scope-shift writes matrix-row container isExample.
2713
3211
  key: ConfigKey.property_example,
2714
3212
  exportJsonKey: [
2715
3213
  {
2716
3214
  "@keyonly": {
2717
- cells: { $s: { isExample: true, example: "$parent.values[0]" } },
2718
- "@bit": { isExample: true }
3215
+ cells: { $s: { isExample: true, example: "$parent" } },
3216
+ "@bit": { isExample: true },
3217
+ "@card": { isExample: true }
2719
3218
  }
2720
3219
  },
2721
3220
  {
2722
- "@absent": { cells: { $s: { isExample: true, example: "$parent.values[0]" } } }
3221
+ predicates: ["@absent", { $cascade: "*" }],
3222
+ rule: {
3223
+ cells: { $s: { isExample: true, example: "$cascade" } },
3224
+ "@bit": { isExample: true },
3225
+ "@card": { isExample: true }
3226
+ }
2723
3227
  },
2724
- { cells: { $s: { isExample: true, example: "$" } }, "@bit": { isExample: true } }
3228
+ {
3229
+ "@absent": {
3230
+ cells: { $s: { isExample: true, example: "$parent" } },
3231
+ "@bit": { isExample: true },
3232
+ "@card": { isExample: true }
3233
+ }
3234
+ },
3235
+ {
3236
+ cells: { $s: { isExample: true, example: "$" } },
3237
+ "@bit": { isExample: true },
3238
+ "@card": { isExample: true }
3239
+ }
2725
3240
  ],
2726
3241
  description: "Example text for the match matrix.",
2727
- format: TagFormat.plainText
3242
+ format: TagFormat.bitmarkText,
3243
+ nullable: true
2728
3244
  },
2729
3245
  {
3246
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3247
+ // empty-string positional blank into `forValues` so the
3248
+ // array stays aligned with the cell positions (matches BPG
3249
+ // `extractHeadingCard`'s "all value-side headings present"
3250
+ // shape). The `@keyonly` rule is required for two reasons:
3251
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3252
+ // in the unconditional rule has no source for bare); and
3253
+ // 2) the presence of any `@keyonly` rule disables the
3254
+ // optimised-mode `should_suppress_outcome` strip (see
3255
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3256
+ // survives the all-natural-defaults check.
2730
3257
  key: ConfigKey.tag_title,
2731
3258
  description: "Title of the match matrix.",
2732
3259
  format: TagFormat.plainText,
2733
3260
  jsonKey: "^heading.forValues",
2734
- exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
3261
+ exportJsonKey: [
3262
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3263
+ { "@bit": { heading: { forValues: ["$"] } } }
3264
+ ]
2735
3265
  },
2736
3266
  {
2737
3267
  key: ConfigKey.property_isCaseSensitive,
@@ -2780,6 +3310,35 @@ var CARDSETS = {
2780
3310
  {
2781
3311
  key: ConfigKey.group_standardTags,
2782
3312
  description: "Standard tags for the statement."
3313
+ },
3314
+ {
3315
+ // PLAN-072: cascade-fired `@example` from the bit header
3316
+ // fills each statement entry's `example` with the entry's
3317
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3318
+ // for the statements cardset. Per-array-item strip removes
3319
+ // `example: false` from incorrect entries so only correct
3320
+ // entries retain `example: true`.
3321
+ key: ConfigKey.property_example,
3322
+ exportJsonKey: [
3323
+ {
3324
+ "@keyonly": {
3325
+ isExample: true,
3326
+ example: true,
3327
+ "@bit": { isExample: true }
3328
+ }
3329
+ },
3330
+ {
3331
+ "@absent": {
3332
+ isExample: true,
3333
+ example: "$parent.isCorrect",
3334
+ "@bit": { isExample: true }
3335
+ }
3336
+ },
3337
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3338
+ ],
3339
+ 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`.",
3340
+ format: TagFormat.boolean,
3341
+ nullable: true
2783
3342
  }
2784
3343
  ],
2785
3344
  bodyAllowed: false
@@ -2808,6 +3367,71 @@ var CARDSETS = {
2808
3367
  {
2809
3368
  key: ConfigKey.group_standardTags,
2810
3369
  description: "Standard tags for the quiz."
3370
+ },
3371
+ {
3372
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3373
+ // quiz choices is delegated to group_trueFalse's
3374
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3375
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3376
+ // would over-fire on subsequent correct choices and on
3377
+ // incorrect ones — drop it.
3378
+ key: ConfigKey.property_example,
3379
+ exportJsonKey: [
3380
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3381
+ { isExample: true, "@bit": { isExample: true } }
3382
+ ],
3383
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3384
+ format: TagFormat.boolean,
3385
+ nullable: true
3386
+ }
3387
+ ],
3388
+ bodyAllowed: false
3389
+ }
3390
+ ]
3391
+ }
3392
+ ]
3393
+ },
3394
+ //
3395
+ // quiz (responses)
3396
+ //
3397
+ [CardSetConfigKey.quizResponses]: {
3398
+ jsonKey: "quizzes",
3399
+ exportJsonKey: { quizzes: "$" },
3400
+ sides: [
3401
+ {
3402
+ name: "responses",
3403
+ variants: [
3404
+ {
3405
+ jsonKey: null,
3406
+ tags: [
3407
+ {
3408
+ key: ConfigKey.group_trueFalseResponses,
3409
+ description: "Group for true/false questions."
3410
+ },
3411
+ {
3412
+ key: ConfigKey.group_standardTags,
3413
+ description: "Standard tags for the quiz."
3414
+ },
3415
+ {
3416
+ // PLAN-072: cascade-fired `@example` from the bit header
3417
+ // fills each response entry's `example` with the entry's
3418
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3419
+ // for the quiz-responses cardset (statements-style).
3420
+ key: ConfigKey.property_example,
3421
+ exportJsonKey: [
3422
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3423
+ {
3424
+ "@absent": {
3425
+ isExample: true,
3426
+ example: "$parent.isCorrect",
3427
+ "@bit": { isExample: true }
3428
+ }
3429
+ },
3430
+ { isExample: true, "@bit": { isExample: true } }
3431
+ ],
3432
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3433
+ format: TagFormat.boolean,
3434
+ nullable: true
2811
3435
  }
2812
3436
  ],
2813
3437
  bodyAllowed: false
@@ -2833,6 +3457,73 @@ var CARDSETS = {
2833
3457
  key: ConfigKey.group_standardItemLeadInstructionHint,
2834
3458
  description: "Standard tags for lead, instruction, and hint."
2835
3459
  },
3460
+ {
3461
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3462
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3463
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3464
+ // feedback-specific shape.
3465
+ //
3466
+ // Chain @example: `[+ X][@example]` writes
3467
+ // `isExample/example` onto the X choice only. BPG's
3468
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3469
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3470
+ // there is no bubble to bit-level or feedbacks-level
3471
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3472
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3473
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3474
+ // no feedback dispatch, so bit-header `[@example]` does
3475
+ // not flow into feedback choices.
3476
+ key: ConfigKey.tag_true,
3477
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3478
+ description: "Reason-required choice for feedback (`[+]`).",
3479
+ maxCount: Count.infinity,
3480
+ format: TagFormat.plainText,
3481
+ chain: [
3482
+ {
3483
+ key: ConfigKey.property_example,
3484
+ exportJsonKey: [
3485
+ { "@keyonly": { isExample: true, example: true } },
3486
+ { isExample: true, example: "$" }
3487
+ ],
3488
+ description: "Per-choice example marker for feedback `[+]`.",
3489
+ format: TagFormat.boolean,
3490
+ maxCount: 1,
3491
+ nullable: true
3492
+ },
3493
+ {
3494
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3495
+ // choice; same for `[?]` (hint), `[%]` (item),
3496
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3497
+ // Mirrors group_trueFalse's `+` chain shape.
3498
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3499
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3500
+ }
3501
+ ]
3502
+ },
3503
+ {
3504
+ key: ConfigKey.tag_false,
3505
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3506
+ description: "Reason-not-required choice for feedback (`[-]`).",
3507
+ maxCount: Count.infinity,
3508
+ format: TagFormat.plainText,
3509
+ chain: [
3510
+ {
3511
+ key: ConfigKey.property_example,
3512
+ exportJsonKey: [
3513
+ { "@keyonly": { isExample: true, example: true } },
3514
+ { isExample: true, example: "$" }
3515
+ ],
3516
+ description: "Per-choice example marker for feedback `[-]`.",
3517
+ format: TagFormat.boolean,
3518
+ maxCount: 1,
3519
+ nullable: true
3520
+ },
3521
+ {
3522
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3523
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3524
+ }
3525
+ ]
3526
+ },
2836
3527
  {
2837
3528
  key: ConfigKey.group_trueFalse,
2838
3529
  description: "Group for true/false feedback."
@@ -2853,31 +3544,71 @@ var CARDSETS = {
2853
3544
  name: "reason",
2854
3545
  variants: [
2855
3546
  {
3547
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3548
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3549
+ // under `reason.*`; each tag here spells out the full path.
3550
+ // Body is a plain string (BPG `parseFeedback` uses
3551
+ // `cardBodyStr ?? ''` for `reason.text`).
2856
3552
  jsonKey: "reason.text",
2857
3553
  exportJsonKey: { reason: { text: "$" } },
3554
+ format: TextFormat.plainText,
2858
3555
  tags: [
2859
3556
  {
2860
- key: ConfigKey.group_standardItemLeadInstructionHint,
2861
- description: "Standard tags for lead, instruction, and hint."
3557
+ // Instruction lands under reason. Hint/item/lead/etc.
3558
+ // omitted: not exercised by any fixture and BPG's
3559
+ // `parseFeedback` spreads only the tags actually present
3560
+ // — adding the rest can be done lazily as fixtures
3561
+ // require them, with the same `{reason: {…}}` wrapper.
3562
+ key: ConfigKey.tag_instruction,
3563
+ exportJsonKey: { reason: { instruction: "$" } },
3564
+ description: "Instruction for the feedback reason.",
3565
+ format: TagFormat.bitmarkText
2862
3566
  },
2863
3567
  {
2864
3568
  key: ConfigKey.property_reasonableNumOfChars,
2865
3569
  exportJsonKey: [
2866
- { "@absent": { reasonableNumOfChars: "$ancestor" } },
2867
- { reasonableNumOfChars: "$" }
3570
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3571
+ { reason: { reasonableNumOfChars: "$" } }
2868
3572
  ],
2869
3573
  description: "Property for reasonable number of characters.",
2870
3574
  format: TagFormat.number
2871
3575
  },
2872
3576
  {
3577
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3578
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3579
+ // cascade. Local emission only.
3580
+ //
3581
+ // Both `reason.example` and `feedbacks[].example` are
3582
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3583
+ // here is `bitmarkText` so `$` coerces to a paragraph
3584
+ // array at both write sites.
2873
3585
  key: ConfigKey.property_example,
2874
3586
  exportJsonKey: [
2875
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2876
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2877
- { isExample: true, example: "$", "@bit": { isExample: true } }
3587
+ {
3588
+ // Bare `[@example]`: reason.example is TextAst BPG
3589
+ // coerces the boolean default to a paragraph
3590
+ // containing the string "true". feedbacks[].example
3591
+ // is not TextAst, stays a boolean. Two literal writes.
3592
+ "@keyonly": {
3593
+ reason: {
3594
+ isExample: true,
3595
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3596
+ },
3597
+ isExample: true,
3598
+ example: true,
3599
+ "@bit": { isExample: true }
3600
+ }
3601
+ },
3602
+ {
3603
+ reason: { isExample: true, example: "$" },
3604
+ isExample: true,
3605
+ example: "$",
3606
+ "@bit": { isExample: true }
3607
+ }
2878
3608
  ],
2879
- description: "Example text for the feedback.",
2880
- format: TagFormat.plainText
3609
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3610
+ format: TagFormat.bitmarkText,
3611
+ nullable: true
2881
3612
  },
2882
3613
  {
2883
3614
  key: ConfigKey.tag_title,
@@ -2941,6 +3672,39 @@ var CARDSETS = {
2941
3672
  {
2942
3673
  key: ConfigKey.group_standardTags,
2943
3674
  description: "Standard tags for the question."
3675
+ },
3676
+ {
3677
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3678
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3679
+ // does not propagate `example`/`isExample` onto question
3680
+ // entries. Only entry-local `[@example]` fires.
3681
+ //
3682
+ // BPG's `BaseBuilder.toExample` for interview questions
3683
+ // (Builder.ts:2575,2588) uses `data.__sampleSolutionAst`
3684
+ // as `defaultExample` — bare `[@example]` (which sets
3685
+ // `__isDefaultExample=true, example=undefined`) emits the
3686
+ // parsed sample-solution AST in the `example` slot. The
3687
+ // `@keyonly` rule mirrors that runtime behaviour via
3688
+ // `$parent.sampleSolution` — the chain-walker's post-walk
3689
+ // retry resolves this once the sibling `[@sampleSolution]`
3690
+ // chain has merged into the shared scope (interview's
3691
+ // `[@example]` typically precedes `[@sampleSolution]` in
3692
+ // source). Format coercion turns the resolved string
3693
+ // value into a PM tree.
3694
+ key: ConfigKey.property_example,
3695
+ exportJsonKey: [
3696
+ {
3697
+ "@keyonly": {
3698
+ isExample: true,
3699
+ example: "$parent.sampleSolution",
3700
+ "@bit": { isExample: true }
3701
+ }
3702
+ },
3703
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3704
+ ],
3705
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3706
+ format: TagFormat.bitmarkText,
3707
+ nullable: true
2944
3708
  }
2945
3709
  ]
2946
3710
  }
@@ -2967,14 +3731,16 @@ var CARDSETS = {
2967
3731
  description: "Standard tags for lead, instruction, and hint."
2968
3732
  },
2969
3733
  {
3734
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3735
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2970
3736
  key: ConfigKey.property_example,
2971
3737
  exportJsonKey: [
2972
3738
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2973
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2974
3739
  { isExample: true, example: "$", "@bit": { isExample: true } }
2975
3740
  ],
2976
- description: "Example text for the element.",
2977
- format: TagFormat.plainText
3741
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3742
+ format: TagFormat.plainText,
3743
+ nullable: true
2978
3744
  }
2979
3745
  ],
2980
3746
  repeatCount: Count.infinity
@@ -2986,9 +3752,57 @@ var CARDSETS = {
2986
3752
  //
2987
3753
  // table
2988
3754
  //
3755
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3756
+ // output (`{ columns?, data, columnWidths? }`):
3757
+ //
3758
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3759
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3760
+ // no qualifier, so they route to the `table-body` (default) section.
3761
+ //
3762
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3763
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3764
+ // the first header card's cells to `columns` and demote remaining
3765
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3766
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3767
+ // here (no per-cell metadata in basic shape) — declared on the cell
3768
+ // variant below.
3769
+ //
2989
3770
  [CardSetConfigKey.table]: {
2990
- jsonKey: "table.data",
2991
- exportJsonKey: { table: { data: "$" } },
3771
+ jsonKey: null,
3772
+ exportJsonKey: null,
3773
+ sections: {
3774
+ "table-header": {
3775
+ // First header card's row of cells → `columns`. Subsequent header
3776
+ // cards' rows → `data` (per-card append; `[$]` shape because
3777
+ // per-card emission fires once per card, multiple fires deep_merge-
3778
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3779
+ // both ROUTE cards to this section (when no explicit `==== table-
3780
+ // header ====` qualifier) and SELECT the rule body once routed.
3781
+ // First rule = first card with any `[#]` → `columns`; second rule
3782
+ // = subsequent cards in this section → `data` (per-row append).
3783
+ jsonKey: "table.columns",
3784
+ exportJsonKey: [
3785
+ {
3786
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3787
+ rule: { "@bit": { table: { columns: "$" } } }
3788
+ },
3789
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3790
+ { "@bit": { table: { data: ["$"] } } }
3791
+ ]
3792
+ },
3793
+ "table-body": {
3794
+ // Batched emission: `$` is the entire 2D array of body card rows.
3795
+ // This is the default section (no qualifier → cards land here).
3796
+ jsonKey: "table.data",
3797
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3798
+ isDefault: true
3799
+ },
3800
+ "table-footer": {
3801
+ // Batched emission: `$` is the entire 2D array of footer rows.
3802
+ jsonKey: "table.data",
3803
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3804
+ }
3805
+ },
2992
3806
  sides: [
2993
3807
  {
2994
3808
  name: "cell",
@@ -3029,6 +3843,8 @@ var CARDSETS = {
3029
3843
  },
3030
3844
  {
3031
3845
  key: ConfigKey.property_tableColWidth,
3846
+ jsonKey: "^table.columnWidths[{s}]",
3847
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
3032
3848
  description: "Width for the column.",
3033
3849
  format: TagFormat.number
3034
3850
  }
@@ -3047,7 +3863,22 @@ var CARDSETS = {
3047
3863
  sections: {
3048
3864
  "table-header": {
3049
3865
  jsonKey: "table.header.rows",
3050
- exportJsonKey: { table: { header: { rows: "$" } } },
3866
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3867
+ // Cards with no explicit `==== table-header ====` qualifier
3868
+ // route here if FIRST CARD contains any `[#]` tag in any
3869
+ // variant. Explicit-qualifier cards fall through to the
3870
+ // unconditional fallback rule. Both rules use the per-card
3871
+ // append shape `rows: ['$']` — the runtime triggers per-card
3872
+ // emission whenever the pattern carries predicates, and each
3873
+ // fire appends a single-element array (multi-fire deep-merge
3874
+ // concatenates).
3875
+ exportJsonKey: [
3876
+ {
3877
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3878
+ rule: { table: { header: { rows: ["$"] } } }
3879
+ },
3880
+ { table: { header: { rows: ["$"] } } }
3881
+ ],
3051
3882
  sideJsonKey: "cells[{s}]|set(title=true)",
3052
3883
  sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
3053
3884
  },
@@ -3080,13 +3911,13 @@ var CARDSETS = {
3080
3911
  },
3081
3912
  {
3082
3913
  key: ConfigKey.tag_title,
3083
- jsonKey: ".",
3084
- exportJsonKey: {
3085
- "@bit": {
3086
- table: { header: { rows: { cells: { cells: { $s: { content: "$" } } } } } }
3087
- }
3088
- },
3089
- description: "Title of the table cell."
3914
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3915
+ // `content` field. When the card is in the `table-header`
3916
+ // section, the section's sideExportJsonKey wraps each
3917
+ // cell with `title: true` automatically.
3918
+ jsonKey: "content",
3919
+ exportJsonKey: { content: "$" },
3920
+ description: "Title text of the table cell (header rows)."
3090
3921
  },
3091
3922
  {
3092
3923
  key: ConfigKey.property_tableCellType,
@@ -3118,6 +3949,9 @@ var CARDSETS = {
3118
3949
  },
3119
3950
  {
3120
3951
  key: ConfigKey.property_tableColWidth,
3952
+ // PLAN-080: write to the cell's `colwidth` field.
3953
+ jsonKey: "colwidth",
3954
+ exportJsonKey: { colwidth: "$" },
3121
3955
  description: "Width for the column.",
3122
3956
  format: TagFormat.number
3123
3957
  }
@@ -3155,7 +3989,8 @@ var CARDSETS = {
3155
3989
  {
3156
3990
  key: ConfigKey.resource_audio,
3157
3991
  description: "Audio resource for the pronunciation table.",
3158
- jsonKey: "audio"
3992
+ jsonKey: "audio",
3993
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
3159
3994
  }
3160
3995
  ],
3161
3996
  repeatCount: Count.infinity
@@ -3226,14 +4061,17 @@ var CARDSETS = {
3226
4061
  format: TagFormat.plainText
3227
4062
  },
3228
4063
  {
4064
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
4065
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
4066
+ // header cascade.
3229
4067
  key: ConfigKey.property_example,
3230
4068
  exportJsonKey: [
3231
4069
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3232
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
3233
4070
  { isExample: true, example: "$", "@bit": { isExample: true } }
3234
4071
  ],
3235
- description: "Example text for the bot action response.",
3236
- format: TagFormat.plainText
4072
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
4073
+ format: TagFormat.plainText,
4074
+ nullable: true
3237
4075
  }
3238
4076
  ]
3239
4077
  }
@@ -3299,6 +4137,38 @@ var CARDSETS = {
3299
4137
  ]
3300
4138
  },
3301
4139
  //
4140
+ // page-footer-sections — mirrors example-bit-list but emits cards under
4141
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
4142
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
4143
+ // routing as pure config so the Rust serializer can pick it up.
4144
+ //
4145
+ [CardSetConfigKey.pageFooterSections]: {
4146
+ jsonKey: "sections",
4147
+ exportJsonKey: { sections: "$" },
4148
+ sides: [
4149
+ {
4150
+ name: "item",
4151
+ variants: [
4152
+ {
4153
+ jsonKey: "body",
4154
+ exportJsonKey: { body: "$" },
4155
+ tags: [
4156
+ {
4157
+ key: ConfigKey.group_standardTags,
4158
+ description: "Standard tags for page-footer section bits."
4159
+ },
4160
+ {
4161
+ exportJsonKey: { title: "$" },
4162
+ key: ConfigKey.tag_title,
4163
+ description: "Title of the page-footer section."
4164
+ }
4165
+ ]
4166
+ }
4167
+ ]
4168
+ }
4169
+ ]
4170
+ },
4171
+ //
3302
4172
  // ingredients
3303
4173
  //
3304
4174
  [CardSetConfigKey.ingredients]: {
@@ -3629,7 +4499,11 @@ var GROUPS = {
3629
4499
  {
3630
4500
  key: ConfigKey.property_groupTag,
3631
4501
  jsonKey: "groupTag.name",
3632
- exportJsonKey: { groupTag: { name: "$" } },
4502
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4503
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4504
+ // same value fold into one entry; chained `@tag` children's
4505
+ // arrays merge as sets.
4506
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3633
4507
  description: "The group tag(s) for the bit",
3634
4508
  format: TagFormat.plainText,
3635
4509
  maxCount: Count.infinity,
@@ -3802,6 +4676,24 @@ var GROUPS = {
3802
4676
  }
3803
4677
  ]
3804
4678
  },
4679
+ [ConfigKey.group_standardInstructionHint]: {
4680
+ type: GroupConfigType.standard,
4681
+ 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).",
4682
+ tags: [
4683
+ {
4684
+ exportJsonKey: { instruction: "$" },
4685
+ key: ConfigKey.tag_instruction,
4686
+ name: "Instruction",
4687
+ description: "The instruction for the bit"
4688
+ },
4689
+ {
4690
+ exportJsonKey: { hint: "$" },
4691
+ key: ConfigKey.tag_hint,
4692
+ name: "Hint",
4693
+ description: "The hint for the bit"
4694
+ }
4695
+ ]
4696
+ },
3805
4697
  [ConfigKey.group_standardTags]: {
3806
4698
  type: GroupConfigType.standard,
3807
4699
  description: "Standard tags which apply to MOST (but not all) bits",
@@ -3815,17 +4707,45 @@ var GROUPS = {
3815
4707
  description: "Item, lead, page number, margin number, instruction and hint tags"
3816
4708
  },
3817
4709
  {
3818
- // Bit-level example tag: bare/synthesized records the marker only;
3819
- // valued [@example:text] emits `isExample: true` and `example` as
3820
- // PM-rendered rich text (bitmark+ format).
4710
+ // Bit-level example tag (default non-`isTopLevelExample` bits):
4711
+ // - bare `[@example]`: consume only (records cascade marker, emits
4712
+ // nothing at bit-level OLD parser scrubs bit-level isExample
4713
+ // for bits NOT in its `isTopLevelExample` allow-list).
4714
+ // - synthesized (descendant cascade): still emits `isExample: true`
4715
+ // at the descendant scope.
4716
+ // - valued `[@example:text]`: emit the example value only; bit-level
4717
+ // `isExample` is left to bubble from descendants.
4718
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4719
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4720
+ // matchMatrix, match, essay, etc.) override this entry with the
4721
+ // legacy emit-isExample shape.
3821
4722
  key: ConfigKey.property_example,
3822
- exportJsonKey: [
3823
- { "@keyonly": { isExample: true } },
3824
- { "@absent": { isExample: true } },
3825
- { isExample: true, example: "$" }
3826
- ],
4723
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3827
4724
  description: "The example(s) for the bit",
3828
- format: TagFormat.bitmarkText
4725
+ format: TagFormat.bitmarkText,
4726
+ nullable: true
4727
+ }
4728
+ ]
4729
+ },
4730
+ [ConfigKey.group_standardTagsNoItemLead]: {
4731
+ type: GroupConfigType.standard,
4732
+ 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).",
4733
+ tags: [
4734
+ {
4735
+ key: ConfigKey.group_standardAllBits,
4736
+ description: "All standard tags which apply to all bits"
4737
+ },
4738
+ {
4739
+ key: ConfigKey.group_standardInstructionHint,
4740
+ description: "Instruction and hint tags only (no item/lead chain)."
4741
+ },
4742
+ {
4743
+ // Mirror of group_standardTags property_example entry.
4744
+ key: ConfigKey.property_example,
4745
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4746
+ description: "The example(s) for the bit",
4747
+ format: TagFormat.bitmarkText,
4748
+ nullable: true
3829
4749
  }
3830
4750
  ]
3831
4751
  },
@@ -3890,8 +4810,8 @@ var GROUPS = {
3890
4810
  tags: [
3891
4811
  {
3892
4812
  key: ConfigKey.property_person,
3893
- jsonKey: "partner.name",
3894
- exportJsonKey: { partner: { name: "$" } },
4813
+ jsonKey: "person.name",
4814
+ exportJsonKey: { person: { name: "$" } },
3895
4815
  description: "A person",
3896
4816
  format: TagFormat.plainText,
3897
4817
  chain: [
@@ -3911,8 +4831,8 @@ var GROUPS = {
3911
4831
  {
3912
4832
  // Deprecated (parter renamed to person)
3913
4833
  key: ConfigKey.property_partner,
3914
- jsonKey: "partner.name",
3915
- exportJsonKey: { partner: { name: "$" } },
4834
+ jsonKey: "person.name",
4835
+ exportJsonKey: { person: { name: "$" } },
3916
4836
  description: "A partner",
3917
4837
  format: TagFormat.plainText,
3918
4838
  chain: [
@@ -3936,7 +4856,9 @@ var GROUPS = {
3936
4856
  description: "Gap chain",
3937
4857
  tags: [
3938
4858
  {
3939
- exportJsonKey: { solutions: [["$"]] },
4859
+ exportJsonKey: {
4860
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4861
+ },
3940
4862
  key: ConfigKey.tag_gap,
3941
4863
  description: "The value of a gap in the content",
3942
4864
  maxCount: Count.infinity,
@@ -3955,27 +4877,37 @@ var GROUPS = {
3955
4877
  description: "Item, lead, page number, margin number, instruction and hint tags"
3956
4878
  },
3957
4879
  {
4880
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4881
+ // `__solutionsAst[length - 1]` — the solution closest to the
4882
+ // `[@example]` tag at parse time. Our chain accumulator is built
4883
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4884
+ // the moment `[@example]` fires, `solutions` only contains the
4885
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4886
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4887
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4888
+ // solutions=["solution"], `[-1]`="solution".
3958
4889
  key: ConfigKey.property_example,
3959
4890
  jsonKey: "example",
3960
4891
  exportJsonKey: [
3961
4892
  {
3962
4893
  "@keyonly": {
3963
4894
  isExample: true,
3964
- example: "$parent.solutions[0]",
4895
+ example: "$parent.solutions[-1]",
3965
4896
  "@bit": { isExample: true }
3966
4897
  }
3967
4898
  },
3968
4899
  {
3969
4900
  "@absent": {
3970
4901
  isExample: true,
3971
- example: "$parent.solutions[0]",
4902
+ example: "$parent.solutions[-1]",
3972
4903
  "@bit": { isExample: true }
3973
4904
  }
3974
4905
  },
3975
4906
  { isExample: true, example: "$", "@bit": { isExample: true } }
3976
4907
  ],
3977
4908
  description: "An example for the gap",
3978
- format: TagFormat.bitmarkText
4909
+ format: TagFormat.bitmarkText,
4910
+ nullable: true
3979
4911
  },
3980
4912
  {
3981
4913
  key: ConfigKey.property_isCaseSensitive,
@@ -4024,6 +4956,99 @@ var GROUPS = {
4024
4956
  description: "Item, lead, page number, margin number, instruction and hint tags"
4025
4957
  },
4026
4958
  {
4959
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4960
+ // `@example` from the bit header marks EVERY choice with
4961
+ // `isExample: true` (matches BPG output), but only the
4962
+ // FIRST correct choice gets `example: true` (BPG
4963
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4964
+ //
4965
+ // Two `@absent` rules, picked first-match-wins:
4966
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4967
+ // `maxEmits:1` — fires once for the first correct.
4968
+ // 2. Plain `@absent` — fires on every remaining entry
4969
+ // (subsequent corrects and all incorrects), emitting
4970
+ // `isExample` only.
4971
+ // The `serialize_cards` cascade-counter scope holds the cap
4972
+ // across the whole cardset.
4973
+ // @card scope-shift writes choice-container isExample (e.g.
4974
+ // `quizzes[i].isExample`) — was previously provided by the
4975
+ // universal `bubble_is_example` post-walk, which was removed
4976
+ // in favour of explicit per-tag config. SYNTAX.md §8.
4977
+ key: ConfigKey.property_example,
4978
+ jsonKey: "example",
4979
+ exportJsonKey: [
4980
+ {
4981
+ "@keyonly": {
4982
+ isExample: true,
4983
+ example: "$parent.isCorrect",
4984
+ "@bit": { isExample: true },
4985
+ "@card": { isExample: true }
4986
+ }
4987
+ },
4988
+ {
4989
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4990
+ maxEmits: 1,
4991
+ rule: {
4992
+ isExample: true,
4993
+ example: true,
4994
+ "@bit": { isExample: true },
4995
+ "@card": { isExample: true }
4996
+ }
4997
+ },
4998
+ {
4999
+ "@absent": {
5000
+ isExample: true,
5001
+ "@bit": { isExample: true },
5002
+ "@card": { isExample: true }
5003
+ }
5004
+ },
5005
+ {
5006
+ isExample: true,
5007
+ example: "$",
5008
+ "@bit": { isExample: true },
5009
+ "@card": { isExample: true }
5010
+ }
5011
+ ],
5012
+ description: "An example for the true/false statement/question",
5013
+ format: TagFormat.boolean,
5014
+ nullable: true
5015
+ }
5016
+ ]
5017
+ },
5018
+ {
5019
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5020
+ key: ConfigKey.tag_false,
5021
+ description: "False value for a true/false statement/question",
5022
+ maxCount: Count.infinity,
5023
+ format: TagFormat.plainText,
5024
+ chain: [
5025
+ {
5026
+ key: ConfigKey.tag_true,
5027
+ jsonKey: "choices[]|set(isCorrect=true)",
5028
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
5029
+ description: "True values for a true/false statement/question",
5030
+ maxCount: Count.infinity,
5031
+ format: TagFormat.plainText
5032
+ },
5033
+ {
5034
+ key: ConfigKey.tag_false,
5035
+ jsonKey: "choices[]|set(isCorrect=false)",
5036
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5037
+ description: "False values for a true/false statement/question",
5038
+ maxCount: Count.infinity,
5039
+ format: TagFormat.plainText
5040
+ },
5041
+ {
5042
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5043
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5044
+ },
5045
+ {
5046
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
5047
+ // `@example` on a `-` (incorrect choice) emits
5048
+ // `isExample: true` only — no `example` (BPG fixtures show
5049
+ // `isExample` on every choice, but `example` only on the
5050
+ // first correct via firstCorrectOnly cascade).
5051
+ // @card scope-shift writes choice-container isExample.
4027
5052
  key: ConfigKey.property_example,
4028
5053
  jsonKey: "example",
4029
5054
  exportJsonKey: [
@@ -4031,25 +5056,99 @@ var GROUPS = {
4031
5056
  "@keyonly": {
4032
5057
  isExample: true,
4033
5058
  example: "$parent.isCorrect",
4034
- "@bit": { isExample: true }
5059
+ "@bit": { isExample: true },
5060
+ "@card": { isExample: true }
4035
5061
  }
4036
5062
  },
4037
5063
  {
4038
5064
  "@absent": {
5065
+ isExample: true,
5066
+ "@bit": { isExample: true },
5067
+ "@card": { isExample: true }
5068
+ }
5069
+ },
5070
+ {
5071
+ isExample: true,
5072
+ example: "$",
5073
+ "@bit": { isExample: true },
5074
+ "@card": { isExample: true }
5075
+ }
5076
+ ],
5077
+ description: "An example for the true/false statement/question (incorrect entry \u2014 isExample only on cascade)",
5078
+ format: TagFormat.boolean,
5079
+ nullable: true
5080
+ }
5081
+ ]
5082
+ }
5083
+ ]
5084
+ },
5085
+ [ConfigKey.group_trueFalseResponses]: {
5086
+ type: GroupConfigType.standard,
5087
+ description: "True/False Responses chain",
5088
+ tags: [
5089
+ {
5090
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
5091
+ key: ConfigKey.tag_true,
5092
+ description: "True value for a true/false statement/question",
5093
+ maxCount: Count.infinity,
5094
+ format: TagFormat.plainText,
5095
+ chain: [
5096
+ {
5097
+ key: ConfigKey.tag_true,
5098
+ jsonKey: "responses[]|set(isCorrect=true)",
5099
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
5100
+ description: "True values for a true/false statement/question",
5101
+ maxCount: Count.infinity,
5102
+ format: TagFormat.plainText
5103
+ },
5104
+ {
5105
+ key: ConfigKey.tag_false,
5106
+ jsonKey: "responses[]|set(isCorrect=false)",
5107
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
5108
+ description: "False values for a true/false statement/question",
5109
+ maxCount: Count.infinity,
5110
+ format: TagFormat.plainText
5111
+ },
5112
+ {
5113
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5114
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5115
+ },
5116
+ {
5117
+ // @card scope-shift writes response-container isExample.
5118
+ key: ConfigKey.property_example,
5119
+ jsonKey: "example",
5120
+ exportJsonKey: [
5121
+ {
5122
+ "@keyonly": {
4039
5123
  isExample: true,
4040
5124
  example: "$parent.isCorrect",
4041
- "@bit": { isExample: true }
5125
+ "@bit": { isExample: true },
5126
+ "@card": { isExample: true }
4042
5127
  }
4043
5128
  },
4044
- { isExample: true, example: "$", "@bit": { isExample: true } }
5129
+ {
5130
+ "@absent": {
5131
+ isExample: "$parent.isCorrect",
5132
+ example: "$parent.isCorrect",
5133
+ "@bit": { isExample: "$parent.isCorrect" },
5134
+ "@card": { isExample: "$parent.isCorrect" }
5135
+ }
5136
+ },
5137
+ {
5138
+ isExample: true,
5139
+ example: "$",
5140
+ "@bit": { isExample: true },
5141
+ "@card": { isExample: true }
5142
+ }
4045
5143
  ],
4046
5144
  description: "An example for the true/false statement/question",
4047
- format: TagFormat.bitmarkText
5145
+ format: TagFormat.boolean,
5146
+ nullable: true
4048
5147
  }
4049
5148
  ]
4050
5149
  },
4051
5150
  {
4052
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5151
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4053
5152
  key: ConfigKey.tag_false,
4054
5153
  description: "False value for a true/false statement/question",
4055
5154
  maxCount: Count.infinity,
@@ -4057,16 +5156,16 @@ var GROUPS = {
4057
5156
  chain: [
4058
5157
  {
4059
5158
  key: ConfigKey.tag_true,
4060
- jsonKey: "choices[]|set(isCorrect=true)",
4061
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
5159
+ jsonKey: "responses[]|set(isCorrect=true)",
5160
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4062
5161
  description: "True values for a true/false statement/question",
4063
5162
  maxCount: Count.infinity,
4064
5163
  format: TagFormat.plainText
4065
5164
  },
4066
5165
  {
4067
5166
  key: ConfigKey.tag_false,
4068
- jsonKey: "choices[]|set(isCorrect=false)",
4069
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5167
+ jsonKey: "responses[]|set(isCorrect=false)",
5168
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4070
5169
  description: "False values for a true/false statement/question",
4071
5170
  maxCount: Count.infinity,
4072
5171
  format: TagFormat.plainText
@@ -4076,6 +5175,7 @@ var GROUPS = {
4076
5175
  description: "Item, lead, page number, margin number, instruction and hint tags"
4077
5176
  },
4078
5177
  {
5178
+ // @card scope-shift writes response-container isExample.
4079
5179
  key: ConfigKey.property_example,
4080
5180
  jsonKey: "example",
4081
5181
  exportJsonKey: [
@@ -4083,20 +5183,333 @@ var GROUPS = {
4083
5183
  "@keyonly": {
4084
5184
  isExample: true,
4085
5185
  example: "$parent.isCorrect",
4086
- "@bit": { isExample: true }
5186
+ "@bit": { isExample: true },
5187
+ "@card": { isExample: true }
4087
5188
  }
4088
5189
  },
4089
5190
  {
4090
5191
  "@absent": {
4091
5192
  isExample: true,
4092
5193
  example: "$parent.isCorrect",
4093
- "@bit": { isExample: true }
5194
+ "@bit": { isExample: true },
5195
+ "@card": { isExample: true }
4094
5196
  }
4095
5197
  },
4096
- { isExample: true, example: "$", "@bit": { isExample: true } }
5198
+ {
5199
+ isExample: true,
5200
+ example: "$",
5201
+ "@bit": { isExample: true },
5202
+ "@card": { isExample: true }
5203
+ }
4097
5204
  ],
4098
5205
  description: "An example for the true/false statement/question",
4099
- format: TagFormat.bitmarkText
5206
+ format: TagFormat.boolean,
5207
+ nullable: true
5208
+ }
5209
+ ]
5210
+ }
5211
+ ]
5212
+ },
5213
+ [ConfigKey.group_trueFalseInlineSelect]: {
5214
+ type: GroupConfigType.standard,
5215
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
5216
+ tags: [
5217
+ {
5218
+ exportJsonKey: {
5219
+ "@body-inline": {
5220
+ type: "select",
5221
+ attrs: { options: [{ text: "$", isCorrect: true }] }
5222
+ }
5223
+ },
5224
+ key: ConfigKey.tag_true,
5225
+ description: "True value for an inline select body bit",
5226
+ maxCount: Count.infinity,
5227
+ format: TagFormat.plainText,
5228
+ chain: [
5229
+ {
5230
+ key: ConfigKey.tag_true,
5231
+ exportJsonKey: {},
5232
+ description: "True values for an inline select body bit",
5233
+ maxCount: Count.infinity,
5234
+ format: TagFormat.plainText
5235
+ },
5236
+ {
5237
+ key: ConfigKey.tag_false,
5238
+ exportJsonKey: {},
5239
+ description: "False values for an inline select body bit",
5240
+ maxCount: Count.infinity,
5241
+ format: TagFormat.plainText
5242
+ },
5243
+ {
5244
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5245
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5246
+ },
5247
+ {
5248
+ // PLAN-083: cascade-fired `@example` from bit header fires
5249
+ // `isExample:true, example:true` on the FIRST CORRECT option
5250
+ // only (BPG `fillBooleanExample(select.options,
5251
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
5252
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5253
+ // OR — any cascade ⇒ bit marked) and
5254
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5255
+ // on the select node).
5256
+ key: ConfigKey.property_example,
5257
+ jsonKey: "example",
5258
+ exportJsonKey: [
5259
+ {
5260
+ "@keyonly": {
5261
+ isExample: true,
5262
+ example: "$parent.isCorrect",
5263
+ "@bit": { isExample: true },
5264
+ "@body-inline": { attrs: { isExample: true } }
5265
+ }
5266
+ },
5267
+ {
5268
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5269
+ maxEmits: 1,
5270
+ rule: {
5271
+ isExample: true,
5272
+ example: true,
5273
+ "@bit": { isExample: true },
5274
+ "@body-inline": { attrs: { isExample: true } }
5275
+ }
5276
+ },
5277
+ {
5278
+ isExample: true,
5279
+ example: "$",
5280
+ "@bit": { isExample: true },
5281
+ "@body-inline": { attrs: { isExample: true } }
5282
+ }
5283
+ ],
5284
+ description: "An example for the inline select option (first correct only)",
5285
+ format: TagFormat.boolean,
5286
+ nullable: true
5287
+ }
5288
+ ]
5289
+ },
5290
+ {
5291
+ exportJsonKey: {
5292
+ "@body-inline": {
5293
+ type: "select",
5294
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5295
+ }
5296
+ },
5297
+ key: ConfigKey.tag_false,
5298
+ description: "False value for an inline select body bit",
5299
+ maxCount: Count.infinity,
5300
+ format: TagFormat.plainText,
5301
+ chain: [
5302
+ {
5303
+ key: ConfigKey.tag_true,
5304
+ exportJsonKey: {},
5305
+ description: "True values for an inline select body bit",
5306
+ maxCount: Count.infinity,
5307
+ format: TagFormat.plainText
5308
+ },
5309
+ {
5310
+ key: ConfigKey.tag_false,
5311
+ exportJsonKey: {},
5312
+ description: "False values for an inline select body bit",
5313
+ maxCount: Count.infinity,
5314
+ format: TagFormat.plainText
5315
+ },
5316
+ {
5317
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5318
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5319
+ },
5320
+ {
5321
+ // PLAN-083: identical to the `+` lead's chain @example —
5322
+ // cascade reaches the first correct option regardless of
5323
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5324
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5325
+ key: ConfigKey.property_example,
5326
+ jsonKey: "example",
5327
+ exportJsonKey: [
5328
+ {
5329
+ "@keyonly": {
5330
+ isExample: true,
5331
+ example: "$parent.isCorrect",
5332
+ "@bit": { isExample: true },
5333
+ "@body-inline": { attrs: { isExample: true } }
5334
+ }
5335
+ },
5336
+ {
5337
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5338
+ maxEmits: 1,
5339
+ rule: {
5340
+ isExample: true,
5341
+ example: true,
5342
+ "@bit": { isExample: true },
5343
+ "@body-inline": { attrs: { isExample: true } }
5344
+ }
5345
+ },
5346
+ {
5347
+ isExample: true,
5348
+ example: "$",
5349
+ "@bit": { isExample: true },
5350
+ "@body-inline": { attrs: { isExample: true } }
5351
+ }
5352
+ ],
5353
+ description: "An example for the inline select option (first correct only)",
5354
+ format: TagFormat.boolean,
5355
+ nullable: true
5356
+ }
5357
+ ]
5358
+ }
5359
+ ]
5360
+ },
5361
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5362
+ type: GroupConfigType.standard,
5363
+ 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.',
5364
+ tags: [
5365
+ {
5366
+ exportJsonKey: {
5367
+ "@body-inline": {
5368
+ type: "highlight",
5369
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5370
+ }
5371
+ },
5372
+ key: ConfigKey.tag_true,
5373
+ description: "Highlighted span in the body (correct)",
5374
+ maxCount: Count.infinity,
5375
+ format: TagFormat.plainText,
5376
+ chain: [
5377
+ {
5378
+ key: ConfigKey.tag_true,
5379
+ exportJsonKey: {},
5380
+ description: "Highlighted span values (correct)",
5381
+ maxCount: Count.infinity,
5382
+ format: TagFormat.plainText
5383
+ },
5384
+ {
5385
+ key: ConfigKey.tag_false,
5386
+ exportJsonKey: {},
5387
+ description: "Highlighted span values (incorrect)",
5388
+ maxCount: Count.infinity,
5389
+ format: TagFormat.plainText
5390
+ },
5391
+ {
5392
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5393
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5394
+ },
5395
+ {
5396
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5397
+ // `@example` from the bit header fires `example: true` +
5398
+ // `isExample: true` on the FIRST correct highlight only
5399
+ // (BPG `fillBooleanExample(highlight.texts,
5400
+ // firstCorrectOnly=true)`). Incorrect highlights get
5401
+ // nothing (unlike choices, the highlight-text fixture
5402
+ // shows no `isExample` on incorrect entries).
5403
+ key: ConfigKey.property_example,
5404
+ jsonKey: "example",
5405
+ exportJsonKey: [
5406
+ {
5407
+ "@keyonly": {
5408
+ isExample: true,
5409
+ example: "$parent.isCorrect",
5410
+ "@bit": { isExample: true },
5411
+ "@body-inline": { attrs: { isExample: true } }
5412
+ }
5413
+ },
5414
+ {
5415
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5416
+ maxEmits: 1,
5417
+ rule: {
5418
+ isExample: true,
5419
+ example: true,
5420
+ "@bit": { isExample: true },
5421
+ "@body-inline": { attrs: { isExample: true } }
5422
+ }
5423
+ },
5424
+ {
5425
+ isExample: true,
5426
+ example: "$",
5427
+ "@bit": { isExample: true },
5428
+ "@body-inline": { attrs: { isExample: true } }
5429
+ }
5430
+ ],
5431
+ description: "An example for the highlighted span (first correct only)",
5432
+ format: TagFormat.boolean,
5433
+ nullable: true
5434
+ }
5435
+ ]
5436
+ },
5437
+ {
5438
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5439
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5440
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5441
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5442
+ // through and nothing fires. The natural-default strip removes
5443
+ // `isCorrect: false` from the final texts entries (matches fixture).
5444
+ exportJsonKey: {
5445
+ "@body-inline": {
5446
+ type: "highlight",
5447
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5448
+ }
5449
+ },
5450
+ key: ConfigKey.tag_false,
5451
+ description: "Highlighted span in the body (incorrect)",
5452
+ maxCount: Count.infinity,
5453
+ format: TagFormat.plainText,
5454
+ chain: [
5455
+ {
5456
+ key: ConfigKey.tag_true,
5457
+ exportJsonKey: {},
5458
+ description: "Highlighted span values (correct)",
5459
+ maxCount: Count.infinity,
5460
+ format: TagFormat.plainText
5461
+ },
5462
+ {
5463
+ key: ConfigKey.tag_false,
5464
+ exportJsonKey: {},
5465
+ description: "Highlighted span values (incorrect)",
5466
+ maxCount: Count.infinity,
5467
+ format: TagFormat.plainText
5468
+ },
5469
+ {
5470
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5471
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5472
+ },
5473
+ {
5474
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5475
+ // `@example` on a `-` (incorrect highlight) emits nothing
5476
+ // (highlight-text fixture has no `isExample` on incorrect
5477
+ // entries; firstCorrectOnly cascade skips them entirely).
5478
+ // No `@absent` rule.
5479
+ // PLAN-083: identical to the `+` lead's chain @example —
5480
+ // cascade reaches the first correct option regardless of
5481
+ // which lead (+/-) opens the chain.
5482
+ key: ConfigKey.property_example,
5483
+ jsonKey: "example",
5484
+ exportJsonKey: [
5485
+ {
5486
+ "@keyonly": {
5487
+ isExample: true,
5488
+ example: "$parent.isCorrect",
5489
+ "@bit": { isExample: true },
5490
+ "@body-inline": { attrs: { isExample: true } }
5491
+ }
5492
+ },
5493
+ {
5494
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5495
+ maxEmits: 1,
5496
+ rule: {
5497
+ isExample: true,
5498
+ example: true,
5499
+ "@bit": { isExample: true },
5500
+ "@body-inline": { attrs: { isExample: true } }
5501
+ }
5502
+ },
5503
+ {
5504
+ isExample: true,
5505
+ example: "$",
5506
+ "@bit": { isExample: true },
5507
+ "@body-inline": { attrs: { isExample: true } }
5508
+ }
5509
+ ],
5510
+ description: "An example for the highlighted span (first correct only)",
5511
+ format: TagFormat.boolean,
5512
+ nullable: true
4100
5513
  }
4101
5514
  ]
4102
5515
  }
@@ -4109,7 +5522,11 @@ var GROUPS = {
4109
5522
  {
4110
5523
  key: ConfigKey.property_mark,
4111
5524
  jsonKey: "marks.mark",
4112
- exportJsonKey: { marks: { mark: ["$"] } },
5525
+ // `emphasis: 'underline'` is the per-entry default for mark
5526
+ // configs — mirrors the hardcoded default in BPG
5527
+ // `MarkConfigChainContentProcessor.ts:55-58`. Chained
5528
+ // `[@emphasis:…]` overrides via deep-merge into `marks[-1]`.
5529
+ exportJsonKey: { marks: [{ mark: "$", emphasis: "underline" }] },
4113
5530
  description: "The mark configuration",
4114
5531
  format: TagFormat.plainText,
4115
5532
  maxCount: Count.infinity,
@@ -4133,7 +5550,9 @@ var GROUPS = {
4133
5550
  description: "Mark chain",
4134
5551
  tags: [
4135
5552
  {
4136
- exportJsonKey: { solution: ["$"] },
5553
+ exportJsonKey: {
5554
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5555
+ },
4137
5556
  key: ConfigKey.tag_mark,
4138
5557
  description: "The marked content",
4139
5558
  maxCount: Count.infinity,
@@ -4142,19 +5561,30 @@ var GROUPS = {
4142
5561
  {
4143
5562
  key: ConfigKey.property_mark,
4144
5563
  jsonKey: "mark",
5564
+ exportJsonKey: { mark: "$" },
4145
5565
  description: "The mark configuration",
4146
5566
  format: TagFormat.plainText
4147
5567
  },
4148
5568
  {
4149
5569
  key: ConfigKey.property_example,
4150
5570
  jsonKey: "example",
4151
- exportJsonKey: [
4152
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
4153
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
4154
- { isExample: true, example: "$", "@bit": { isExample: true } }
4155
- ],
5571
+ // Per-mark `@example` is a boolean toggle. BPG's runtime
5572
+ // `ExampleTagContentProcessor.handleDefaultOnlyExample`
5573
+ // (lines 147-161) normalises ANY `[@example]` or
5574
+ // `[@example:V]` on a mark body-bit to
5575
+ // `__isDefaultExample=true, example=undefined`; the
5576
+ // downstream `toExample(_, _, defaultExample=true)` in
5577
+ // `BaseBuilder.ts:80-87` then emits `example: true`
5578
+ // regardless of source. Declare a single unconditional
5579
+ // boolean-shape rule so the exported schema matches that
5580
+ // runtime semantic instead of the misleading `example: $`
5581
+ // shape (which would only fire if `handleDefaultOnlyExample`
5582
+ // were removed). `@bit: {isExample: true}` cascades the
5583
+ // marker — but NOT the `example` value — to bit root.
5584
+ exportJsonKey: { isExample: true, example: true, "@bit": { isExample: true } },
4156
5585
  description: "An example for the marked content",
4157
- format: TagFormat.bitmarkText
5586
+ format: TagFormat.boolean,
5587
+ nullable: true
4158
5588
  }
4159
5589
  ]
4160
5590
  }
@@ -4583,6 +6013,42 @@ var GROUPS = {
4583
6013
  }
4584
6014
  ]
4585
6015
  },
6016
+ [ConfigKey.group_backgroundWallpaper]: {
6017
+ type: GroupConfigType.standard,
6018
+ description: "Background wallpaper tags",
6019
+ tags: [
6020
+ {
6021
+ key: ConfigKey.resource_backgroundWallpaper,
6022
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
6023
+ description: "Background wallpaper for the image, used to set a background for the image",
6024
+ chain: [
6025
+ {
6026
+ key: ConfigKey.group_resourceImageCommon,
6027
+ description: "Common resource image tags for images"
6028
+ }
6029
+ ]
6030
+ }
6031
+ ]
6032
+ },
6033
+ [ConfigKey.group_imageNoZoom]: {
6034
+ type: GroupConfigType.standard,
6035
+ description: "Image bit tags",
6036
+ tags: [
6037
+ {
6038
+ key: ConfigKey.group_backgroundWallpaper,
6039
+ description: "Background wallpaper tags for images, used to define background properties for images"
6040
+ },
6041
+ {
6042
+ key: ConfigKey.group_resourceBitTags,
6043
+ description: "Resource bit tags for images, used to define additional properties for images"
6044
+ },
6045
+ {
6046
+ key: ConfigKey.group_resourceImageNoZoom,
6047
+ description: "Resource image tags for images, used to attach images to the bit",
6048
+ minCount: 1
6049
+ }
6050
+ ]
6051
+ },
4586
6052
  //
4587
6053
  // Resource groups
4588
6054
  //
@@ -4702,6 +6168,58 @@ var GROUPS = {
4702
6168
  }
4703
6169
  ]
4704
6170
  },
6171
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
6172
+ type: GroupConfigType.standard,
6173
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
6174
+ tags: [
6175
+ {
6176
+ key: ConfigKey.group_resourceCommon,
6177
+ description: "Common resource properties"
6178
+ },
6179
+ {
6180
+ key: ConfigKey.property_src1x,
6181
+ description: "The source URL for the image at 1x resolution",
6182
+ format: TagFormat.plainText
6183
+ },
6184
+ {
6185
+ key: ConfigKey.property_src2x,
6186
+ description: "The source URL for the image at 2x resolution",
6187
+ format: TagFormat.plainText
6188
+ },
6189
+ {
6190
+ key: ConfigKey.property_src3x,
6191
+ description: "The source URL for the image at 3x resolution",
6192
+ format: TagFormat.plainText
6193
+ },
6194
+ {
6195
+ key: ConfigKey.property_src4x,
6196
+ description: "The source URL for the image at 4x resolution",
6197
+ format: TagFormat.plainText
6198
+ },
6199
+ {
6200
+ key: ConfigKey.property_width,
6201
+ description: "The width of the image",
6202
+ format: TagFormat.number
6203
+ },
6204
+ {
6205
+ key: ConfigKey.property_height,
6206
+ description: "The height of the image",
6207
+ format: TagFormat.number
6208
+ },
6209
+ {
6210
+ key: ConfigKey.property_alt,
6211
+ description: "The alternative text for the image",
6212
+ format: TagFormat.plainText
6213
+ },
6214
+ // @zoomDisabled with a true default
6215
+ {
6216
+ key: ConfigKey.property_zoomDisabled,
6217
+ description: "If true, zooming is disabled for the image (defaults to true)",
6218
+ format: TagFormat.boolean,
6219
+ defaultValue: "true"
6220
+ }
6221
+ ]
6222
+ },
4705
6223
  [ConfigKey.group_resourceAudioCommon]: {
4706
6224
  type: GroupConfigType.standard,
4707
6225
  description: "Common properties for audio resources",
@@ -4791,22 +6309,26 @@ var GROUPS = {
4791
6309
  {
4792
6310
  key: ConfigKey.property_src1x,
4793
6311
  description: "The source URL for the video at 1x resolution",
4794
- format: TagFormat.plainText
6312
+ format: TagFormat.plainText,
6313
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4795
6314
  },
4796
6315
  {
4797
6316
  key: ConfigKey.property_src2x,
4798
6317
  description: "The source URL for the video at 2x resolution",
4799
- format: TagFormat.plainText
6318
+ format: TagFormat.plainText,
6319
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4800
6320
  },
4801
6321
  {
4802
6322
  key: ConfigKey.property_src3x,
4803
6323
  description: "The source URL for the video at 3x resolution",
4804
- format: TagFormat.plainText
6324
+ format: TagFormat.plainText,
6325
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4805
6326
  },
4806
6327
  {
4807
6328
  key: ConfigKey.property_src4x,
4808
6329
  description: "The source URL for the video at 4x resolution",
4809
- format: TagFormat.plainText
6330
+ format: TagFormat.plainText,
6331
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4810
6332
  }
4811
6333
  ]
4812
6334
  },
@@ -4819,6 +6341,7 @@ var GROUPS = {
4819
6341
  tags: [
4820
6342
  {
4821
6343
  key: ConfigKey.resource_icon,
6344
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4822
6345
  description: "The icon resource",
4823
6346
  chain: [
4824
6347
  {
@@ -4846,12 +6369,30 @@ var GROUPS = {
4846
6369
  }
4847
6370
  ]
4848
6371
  },
6372
+ [ConfigKey.group_resourceImageNoZoom]: {
6373
+ type: GroupConfigType.resource,
6374
+ description: "Image resource where @zoomDisabled defaults to true",
6375
+ tags: [
6376
+ {
6377
+ key: ConfigKey.resource_image,
6378
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6379
+ description: "The image resource (no-zoom default)",
6380
+ chain: [
6381
+ {
6382
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6383
+ description: "Common image properties with @zoomDisabled defaulting to true"
6384
+ }
6385
+ ]
6386
+ }
6387
+ ]
6388
+ },
4849
6389
  [ConfigKey.group_resourceImagePortrait]: {
4850
6390
  type: GroupConfigType.resource,
4851
6391
  description: "Portrait image resource",
4852
6392
  tags: [
4853
6393
  {
4854
6394
  key: ConfigKey.resource_imagePortrait,
6395
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4855
6396
  description: "The portrait image resource",
4856
6397
  chain: [
4857
6398
  {
@@ -4868,6 +6409,7 @@ var GROUPS = {
4868
6409
  tags: [
4869
6410
  {
4870
6411
  key: ConfigKey.resource_imageLandscape,
6412
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4871
6413
  description: "The landscape image resource",
4872
6414
  chain: [
4873
6415
  {
@@ -4884,6 +6426,7 @@ var GROUPS = {
4884
6426
  tags: [
4885
6427
  {
4886
6428
  key: ConfigKey.resource_imageEmbed,
6429
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4887
6430
  description: "The embedded image resource",
4888
6431
  chain: [
4889
6432
  {
@@ -5057,6 +6600,7 @@ var GROUPS = {
5057
6600
  tags: [
5058
6601
  {
5059
6602
  key: ConfigKey.resource_article,
6603
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
5060
6604
  description: "The article resource",
5061
6605
  chain: [
5062
6606
  {
@@ -5073,6 +6617,7 @@ var GROUPS = {
5073
6617
  tags: [
5074
6618
  {
5075
6619
  key: ConfigKey.resource_articleEmbed,
6620
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
5076
6621
  description: "The embedded article resource",
5077
6622
  chain: [
5078
6623
  {
@@ -5089,6 +6634,7 @@ var GROUPS = {
5089
6634
  tags: [
5090
6635
  {
5091
6636
  key: ConfigKey.resource_articleLink,
6637
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
5092
6638
  description: "The link to the article resource",
5093
6639
  chain: [
5094
6640
  {
@@ -5359,7 +6905,12 @@ var ConfigHydrator = class {
5359
6905
  isDefault: v.isDefault,
5360
6906
  sideJsonKey: v.sideJsonKey,
5361
6907
  sideExportJsonKey: v.sideExportJsonKey,
5362
- hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey")
6908
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6909
+ // PLAN-085: cardinality fields pass through; defaults applied
6910
+ // downstream by the config consumer (Rust treats `0` / undefined
6911
+ // as unbounded).
6912
+ minCount: v.minCount,
6913
+ maxCount: v.maxCount
5363
6914
  };
5364
6915
  }
5365
6916
  return out;
@@ -5574,7 +7125,8 @@ var BITS = {
5574
7125
  { isExample: true, example: "$" }
5575
7126
  ],
5576
7127
  description: "The example text for the bit",
5577
- format: TagFormat.plainText
7128
+ format: TagFormat.plainText,
7129
+ nullable: true
5578
7130
  }
5579
7131
  ],
5580
7132
  rootExampleType: ExampleType.string
@@ -5913,7 +7465,8 @@ var BITS = {
5913
7465
  { isExample: true, example: "$" }
5914
7466
  ],
5915
7467
  description: "The example text for the bit",
5916
- format: TagFormat.plainText
7468
+ format: TagFormat.plainText,
7469
+ nullable: true
5917
7470
  }
5918
7471
  ],
5919
7472
  rootExampleType: ExampleType.string
@@ -6076,7 +7629,8 @@ var BITS = {
6076
7629
  { isExample: true, example: "$" }
6077
7630
  ],
6078
7631
  description: "The example text for the bit",
6079
- format: TagFormat.plainText
7632
+ format: TagFormat.plainText,
7633
+ nullable: true
6080
7634
  }
6081
7635
  ],
6082
7636
  rootExampleType: ExampleType.string
@@ -6227,7 +7781,8 @@ var BITS = {
6227
7781
  { isExample: true, example: "$" }
6228
7782
  ],
6229
7783
  description: "The example text for the bit",
6230
- format: TagFormat.plainText
7784
+ format: TagFormat.plainText,
7785
+ nullable: true
6231
7786
  }
6232
7787
  ],
6233
7788
  rootExampleType: ExampleType.string
@@ -6804,6 +8359,12 @@ var BITS = {
6804
8359
  description: "The progress of the chapter, used for tracking reading progress",
6805
8360
  format: TagFormat.boolean,
6806
8361
  defaultValue: "true"
8362
+ },
8363
+ {
8364
+ key: ConfigKey.property_isCollapsible,
8365
+ description: "If true, the chapter is collapsible",
8366
+ format: TagFormat.boolean,
8367
+ defaultValue: "false"
6807
8368
  }
6808
8369
  ]
6809
8370
  },
@@ -6822,8 +8383,31 @@ var BITS = {
6822
8383
  description: "Tags for gaps in cloze and multiple choice text bits"
6823
8384
  },
6824
8385
  {
6825
- key: ConfigKey.group_trueFalse,
8386
+ key: ConfigKey.group_trueFalseInlineSelect,
6826
8387
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8388
+ },
8389
+ {
8390
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8391
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8392
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8393
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8394
+ key: ConfigKey.property_isCaseSensitive,
8395
+ exportJsonKey: {},
8396
+ description: "If the cloze answers are case sensitive",
8397
+ format: TagFormat.boolean,
8398
+ defaultValue: "true"
8399
+ },
8400
+ {
8401
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8402
+ key: ConfigKey.property_example,
8403
+ exportJsonKey: [
8404
+ { "@keyonly": { isExample: true } },
8405
+ { "@absent": { isExample: true } },
8406
+ { isExample: true, example: "$" }
8407
+ ],
8408
+ description: "The example(s) for the bit",
8409
+ format: TagFormat.bitmarkText,
8410
+ nullable: true
6827
8411
  }
6828
8412
  ]
6829
8413
  },
@@ -6862,18 +8446,56 @@ var BITS = {
6862
8446
  {
6863
8447
  key: ConfigKey.group_gap,
6864
8448
  description: "Tags for gaps in cloze bits"
8449
+ },
8450
+ {
8451
+ // Override inherited @example: cloze is in OLD parser's
8452
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8453
+ // emits `isExample: true`.
8454
+ key: ConfigKey.property_example,
8455
+ exportJsonKey: [
8456
+ { "@keyonly": { isExample: true } },
8457
+ { "@absent": { isExample: true } },
8458
+ { isExample: true, example: "$" }
8459
+ ],
8460
+ description: "The example(s) for the bit",
8461
+ format: TagFormat.bitmarkText,
8462
+ nullable: true
6865
8463
  }
6866
8464
  ]
6867
8465
  },
6868
8466
  [BitType.clozeInstructionGrouped]: {
6869
8467
  since: "1.3.0",
6870
8468
  baseBitType: BitType.cloze,
6871
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8469
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8470
+ tags: [
8471
+ {
8472
+ key: ConfigKey.property_quizCountItems,
8473
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8474
+ format: TagFormat.boolean,
8475
+ defaultValue: "true"
8476
+ }
8477
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8478
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8479
+ ]
6872
8480
  },
6873
8481
  [BitType.clozeSolutionGrouped]: {
6874
8482
  since: "1.3.0",
6875
8483
  baseBitType: BitType.cloze,
6876
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8484
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8485
+ tags: [
8486
+ {
8487
+ key: ConfigKey.property_quizCountItems,
8488
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8489
+ format: TagFormat.boolean,
8490
+ defaultValue: "true"
8491
+ },
8492
+ {
8493
+ key: ConfigKey.property_quizStrikethroughSolutions,
8494
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8495
+ format: TagFormat.boolean,
8496
+ defaultValue: "true"
8497
+ }
8498
+ ]
6877
8499
  },
6878
8500
  [BitType.clozeSeveral]: {
6879
8501
  since: "3.5.0",
@@ -6909,6 +8531,17 @@ var BITS = {
6909
8531
  {
6910
8532
  key: ConfigKey.group_quizCommon,
6911
8533
  description: "Common tags for quiz bits"
8534
+ },
8535
+ {
8536
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8537
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8538
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8539
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8540
+ key: ConfigKey.property_isCaseSensitive,
8541
+ exportJsonKey: {},
8542
+ description: "If the cloze answers are case sensitive",
8543
+ format: TagFormat.boolean,
8544
+ defaultValue: "true"
6912
8545
  }
6913
8546
  ],
6914
8547
  cardSet: CardSetConfigKey.clozeList
@@ -7075,6 +8708,7 @@ var BITS = {
7075
8708
  tags: [
7076
8709
  {
7077
8710
  key: ConfigKey.group_person,
8711
+ exportJsonKey: { partner: { name: "$" } },
7078
8712
  description: "Tags for the person in the conversation"
7079
8713
  }
7080
8714
  ]
@@ -7505,7 +9139,12 @@ var BITS = {
7505
9139
  maxCount: Count.infinity
7506
9140
  },
7507
9141
  {
7508
- // Override inherited @example from group_standardTags: string root example.
9142
+ // Override inherited @example from group_standardTags: emits both
9143
+ // `isExample` (bool) AND `example` (paragraph array). Declared as
9144
+ // bitmarkText because BPG `JsonGenerator.toExample` renders the
9145
+ // root-example value through the bit's textFormat (not this tag's
9146
+ // declared format) — `TagFormat.plainText` here would be ignored
9147
+ // at runtime and is misleading.
7509
9148
  key: ConfigKey.property_example,
7510
9149
  exportJsonKey: [
7511
9150
  { "@keyonly": { isExample: true, example: "true" } },
@@ -7513,7 +9152,8 @@ var BITS = {
7513
9152
  { isExample: true, example: "$" }
7514
9153
  ],
7515
9154
  description: "The example text for the essay bit",
7516
- format: TagFormat.plainText
9155
+ format: TagFormat.bitmarkText,
9156
+ nullable: true
7517
9157
  }
7518
9158
  ],
7519
9159
  rootExampleType: ExampleType.string
@@ -7547,7 +9187,8 @@ var BITS = {
7547
9187
  { isExample: true, example: "$" }
7548
9188
  ],
7549
9189
  description: "The example text for the example bit",
7550
- format: TagFormat.plainText
9190
+ format: TagFormat.plainText,
9191
+ nullable: true
7551
9192
  }
7552
9193
  ],
7553
9194
  rootExampleType: ExampleType.string
@@ -7633,7 +9274,19 @@ var BITS = {
7633
9274
  [BitType.articleAi]: {
7634
9275
  since: "1.3.0",
7635
9276
  baseBitType: BitType.article,
7636
- description: "Article AI bit, used to create AI-generated articles"
9277
+ description: "Article AI bit, used to create AI-generated articles",
9278
+ tags: [
9279
+ {
9280
+ // Shadows the inherited @aiGenerated from the standard group: this
9281
+ // bit type forces aiGenerated:true regardless of source value or
9282
+ // absence. Literal `true` in the export pattern ignores `$`.
9283
+ key: ConfigKey.property_aiGenerated,
9284
+ description: "AI-generated flag, forced true for article-ai bits",
9285
+ format: TagFormat.boolean,
9286
+ defaultValue: "true",
9287
+ exportJsonKey: { aiGenerated: true }
9288
+ }
9289
+ ]
7637
9290
  },
7638
9291
  [BitType.articleAttachment]: {
7639
9292
  since: "1.3.0",
@@ -7958,7 +9611,19 @@ var BITS = {
7958
9611
  [BitType.noteAi]: {
7959
9612
  since: "1.3.0",
7960
9613
  baseBitType: BitType.note,
7961
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9614
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9615
+ tags: [
9616
+ {
9617
+ // Shadows the inherited @aiGenerated from the standard group: this
9618
+ // bit type forces aiGenerated:true regardless of source value or
9619
+ // absence. Literal `true` in the export pattern ignores `$`.
9620
+ key: ConfigKey.property_aiGenerated,
9621
+ description: "AI-generated flag, forced true for article-ai bits",
9622
+ format: TagFormat.boolean,
9623
+ defaultValue: "true",
9624
+ exportJsonKey: { aiGenerated: true }
9625
+ }
9626
+ ]
7962
9627
  },
7963
9628
  [BitType.notebookArticle]: {
7964
9629
  since: "1.3.0",
@@ -8083,7 +9748,19 @@ var BITS = {
8083
9748
  [BitType.summaryAi]: {
8084
9749
  since: "1.3.0",
8085
9750
  baseBitType: BitType.summary,
8086
- description: "AI-generated summary bit"
9751
+ description: "AI-generated summary bit",
9752
+ tags: [
9753
+ {
9754
+ // Shadows the inherited @aiGenerated from the standard group: this
9755
+ // bit type forces aiGenerated:true regardless of source value or
9756
+ // absence. Literal `true` in the export pattern ignores `$`.
9757
+ key: ConfigKey.property_aiGenerated,
9758
+ description: "AI-generated flag, forced true for article-ai bits",
9759
+ format: TagFormat.boolean,
9760
+ defaultValue: "true",
9761
+ exportJsonKey: { aiGenerated: true }
9762
+ }
9763
+ ]
8087
9764
  },
8088
9765
  [BitType.videoTranscript]: {
8089
9766
  since: "1.3.0",
@@ -8357,7 +10034,7 @@ var BITS = {
8357
10034
  description: "Product ID for the module product, used to link to a specific product",
8358
10035
  format: TagFormat.plainText,
8359
10036
  minCount: 1,
8360
- maxCount: Count.infinity
10037
+ maxCount: 1
8361
10038
  }
8362
10039
  ]
8363
10040
  },
@@ -8410,10 +10087,13 @@ var BITS = {
8410
10087
  description: "Resource bit tags for logo grave images, used to define additional properties"
8411
10088
  },
8412
10089
  {
8413
- // Image resource
8414
- key: ConfigKey.group_resourceImage,
10090
+ // Image resource — fully-expanded shape so each `&image` emission
10091
+ // produces a complete `{type, image: {src}}` object inside the
10092
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
10093
+ // `images[].image` via the `[]` last-element-merge semantic.
10094
+ key: ConfigKey.group_resourceImageNoZoom,
8415
10095
  description: "Resource image tags for logo grave images, used to attach images",
8416
- exportJsonKey: { images: "$" },
10096
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
8417
10097
  minCount: 1,
8418
10098
  maxCount: Count.infinity
8419
10099
  }
@@ -8703,6 +10383,10 @@ var BITS = {
8703
10383
  tags: [
8704
10384
  {
8705
10385
  key: ConfigKey.property_book,
10386
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10387
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10388
+ // inside the most-recently-appended entry as `reference: $`.
10389
+ exportJsonKey: { book: [{ book: "$" }] },
8706
10390
  description: "Book reference for the page, used to link to a specific book",
8707
10391
  maxCount: Count.infinity,
8708
10392
  chain: [
@@ -8770,7 +10454,9 @@ var BITS = {
8770
10454
  format: TagFormat.plainText
8771
10455
  }
8772
10456
  ],
8773
- cardSet: CardSetConfigKey.exampleBitList
10457
+ // page-footer emits cards under `sections` (vs `listItems` for the
10458
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10459
+ cardSet: CardSetConfigKey.pageFooterSections
8774
10460
  },
8775
10461
  [BitType.legend]: {
8776
10462
  since: "3.12.0",
@@ -8812,13 +10498,52 @@ var BITS = {
8812
10498
  since: "1.34.0",
8813
10499
  baseBitType: BitType._standard,
8814
10500
  description: "Definition list bit, used to create lists of definitions in articles or books",
10501
+ tags: [
10502
+ {
10503
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10504
+ key: ConfigKey.property_example,
10505
+ exportJsonKey: [
10506
+ { "@keyonly": { isExample: true } },
10507
+ { "@absent": { isExample: true } },
10508
+ { isExample: true, example: "$" }
10509
+ ],
10510
+ description: "The example(s) for the bit",
10511
+ format: TagFormat.bitmarkText,
10512
+ nullable: true
10513
+ }
10514
+ ],
8815
10515
  cardSet: CardSetConfigKey.definitionList
8816
10516
  },
10517
+ [BitType.definitionListCollapsible]: {
10518
+ since: "5.25.0",
10519
+ baseBitType: BitType.definitionList,
10520
+ description: "Collapsible definition list bit, used to create collapsible lists of definitions in articles or books"
10521
+ },
10522
+ [BitType.standardDefinitionListNormative]: {
10523
+ since: "5.24.0",
10524
+ baseBitType: BitType.definitionList,
10525
+ description: "Standard normative definition list bit, used to create normative standard definition lists in articles or books"
10526
+ },
10527
+ [BitType.standardDefinitionListNonNormative]: {
10528
+ since: "5.24.0",
10529
+ baseBitType: BitType.definitionList,
10530
+ description: "Standard non-normative definition list bit, used to create non-normative standard definition lists in articles or books"
10531
+ },
10532
+ [BitType.smartStandardDefinitionListNormative]: {
10533
+ since: "5.24.0",
10534
+ baseBitType: BitType.standardDefinitionListNormative,
10535
+ description: "Smart standard normative definition list bit, used to create normative smart standard definition lists in articles or books"
10536
+ },
10537
+ [BitType.smartStandardDefinitionListNonNormative]: {
10538
+ since: "5.24.0",
10539
+ baseBitType: BitType.standardDefinitionListNonNormative,
10540
+ description: "Smart standard non-normative definition list bit, used to create non-normative smart standard definition lists in articles or books"
10541
+ },
8817
10542
  [BitType.metaSearchDefaultTerms]: {
8818
10543
  since: "3.12.0",
8819
10544
  baseBitType: BitType._standard,
8820
10545
  description: "Meta search default terms bit, used to define default search terms in articles",
8821
- cardSet: CardSetConfigKey.definitionList
10546
+ cardSet: CardSetConfigKey.definitionListPlain
8822
10547
  },
8823
10548
  [BitType.metaSearchDefaultTopics]: {
8824
10549
  since: "3.12.0",
@@ -8834,6 +10559,18 @@ var BITS = {
8834
10559
  {
8835
10560
  key: ConfigKey.group_quizCommon,
8836
10561
  description: "Common quiz tags for flashcards"
10562
+ },
10563
+ {
10564
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10565
+ key: ConfigKey.property_example,
10566
+ exportJsonKey: [
10567
+ { "@keyonly": { isExample: true } },
10568
+ { "@absent": { isExample: true } },
10569
+ { isExample: true, example: "$" }
10570
+ ],
10571
+ description: "The example(s) for the bit",
10572
+ format: TagFormat.bitmarkText,
10573
+ nullable: true
8837
10574
  }
8838
10575
  ],
8839
10576
  cardSet: CardSetConfigKey.flashcard
@@ -8841,12 +10578,20 @@ var BITS = {
8841
10578
  [BitType.flashcard1]: {
8842
10579
  since: "1.3.0",
8843
10580
  baseBitType: BitType.flashcard,
8844
- description: "Flashcard 1 bit"
10581
+ description: "Flashcard 1 bit",
10582
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10583
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10584
+ // bit type structurally allows only a single card.
10585
+ cardSet: CardSetConfigKey.flashcard1
8845
10586
  },
8846
10587
  [BitType.qAndACard]: {
8847
10588
  since: "3.25.0",
8848
10589
  baseBitType: BitType.flashcard1,
8849
- description: "Q&A card bit, used to create question and answer cards in articles or books"
10590
+ description: "Q&A card bit, used to create question and answer cards in articles or books",
10591
+ // PLAN-085: q-and-a-card permits multiple cards (some fixtures carry
10592
+ // two). Override the inherited `flashcard1` (max=1) cardset back to
10593
+ // the unbounded `flashcard` cardset.
10594
+ cardSet: CardSetConfigKey.flashcard
8850
10595
  },
8851
10596
  [BitType.focusImage]: {
8852
10597
  since: "1.3.0",
@@ -8876,8 +10621,24 @@ var BITS = {
8876
10621
  description: "Common quiz tags for highlighted text"
8877
10622
  },
8878
10623
  {
8879
- key: ConfigKey.group_trueFalse,
8880
- description: "True/False quiz tags for highlighted text"
10624
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10625
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10626
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10627
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10628
+ key: ConfigKey.group_trueFalseInlineHighlight,
10629
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10630
+ },
10631
+ {
10632
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10633
+ key: ConfigKey.property_example,
10634
+ exportJsonKey: [
10635
+ { "@keyonly": { isExample: true } },
10636
+ { "@absent": { isExample: true } },
10637
+ { isExample: true, example: "$" }
10638
+ ],
10639
+ description: "The example(s) for the bit",
10640
+ format: TagFormat.bitmarkText,
10641
+ nullable: true
8881
10642
  }
8882
10643
  ]
8883
10644
  },
@@ -8887,15 +10648,8 @@ var BITS = {
8887
10648
  description: "Image bit, used to create images in articles or books",
8888
10649
  tags: [
8889
10650
  {
8890
- key: ConfigKey.resource_backgroundWallpaper,
8891
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8892
- description: "Background wallpaper for the image, used to set a background for the image",
8893
- chain: [
8894
- {
8895
- key: ConfigKey.group_resourceImageCommon,
8896
- description: "Common resource image tags for images"
8897
- }
8898
- ]
10651
+ key: ConfigKey.group_backgroundWallpaper,
10652
+ description: "Background wallpaper tags for images, used to define background properties for images"
8899
10653
  },
8900
10654
  {
8901
10655
  key: ConfigKey.group_resourceBitTags,
@@ -8998,8 +10752,15 @@ var BITS = {
8998
10752
  },
8999
10753
  [BitType.imageSeparator]: {
9000
10754
  since: "1.4.15",
9001
- baseBitType: BitType.image,
9002
- description: "Image separator bit, used to create separators in articles or books"
10755
+ baseBitType: BitType._standard,
10756
+ description: "Image separator bit, used to create separators in articles or books",
10757
+ tags: [
10758
+ {
10759
+ key: ConfigKey.group_imageNoZoom,
10760
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10761
+ }
10762
+ ],
10763
+ resourceAttachmentAllowed: false
9003
10764
  },
9004
10765
  [BitType.imageSeparatorAlt]: {
9005
10766
  since: "1.16.0",
@@ -9038,15 +10799,20 @@ var BITS = {
9038
10799
  },
9039
10800
  [BitType.pageBanner]: {
9040
10801
  since: "1.4.3",
9041
- baseBitType: BitType.image,
10802
+ baseBitType: BitType._standard,
9042
10803
  description: "Page banner bit, used to create banners for pages in articles or books",
9043
10804
  tags: [
9044
10805
  {
9045
10806
  key: ConfigKey.property_slug,
9046
10807
  description: "Slug for the page banner, used to identify the banner in the system",
9047
10808
  format: TagFormat.plainText
10809
+ },
10810
+ {
10811
+ key: ConfigKey.group_imageNoZoom,
10812
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
9048
10813
  }
9049
- ]
10814
+ ],
10815
+ resourceAttachmentAllowed: false
9050
10816
  },
9051
10817
  [BitType.pageHero]: {
9052
10818
  since: "1.11.0",
@@ -9061,7 +10827,7 @@ var BITS = {
9061
10827
  },
9062
10828
  [BitType.tableImage]: {
9063
10829
  since: "1.5.15",
9064
- baseBitType: BitType.table,
10830
+ baseBitType: BitType.tableExtended,
9065
10831
  description: "Table image bit, used to create images in tables in articles or books",
9066
10832
  tags: [
9067
10833
  {
@@ -9070,15 +10836,8 @@ var BITS = {
9070
10836
  format: TagFormat.bitmarkText
9071
10837
  },
9072
10838
  {
9073
- key: ConfigKey.resource_backgroundWallpaper,
9074
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
9075
- description: "Background wallpaper for the image, used to set a background for the image",
9076
- chain: [
9077
- {
9078
- key: ConfigKey.group_resourceImageCommon,
9079
- description: "Common resource image tags for images"
9080
- }
9081
- ]
10839
+ key: ConfigKey.group_backgroundWallpaper,
10840
+ description: "Background wallpaper tags for images, used to define background properties for images"
9082
10841
  },
9083
10842
  {
9084
10843
  key: ConfigKey.group_resourceBitTags,
@@ -9221,10 +10980,10 @@ var BITS = {
9221
10980
  description: "Resource bit tags for logo grave images, used to define additional properties"
9222
10981
  },
9223
10982
  {
9224
- // Image resource
9225
- key: ConfigKey.group_resourceImage,
10983
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10984
+ key: ConfigKey.group_resourceImageNoZoom,
9226
10985
  description: "Resource image tags for logo grave images, used to attach images",
9227
- exportJsonKey: { logos: "$" },
10986
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
9228
10987
  minCount: 1,
9229
10988
  maxCount: Count.infinity
9230
10989
  }
@@ -9234,7 +10993,17 @@ var BITS = {
9234
10993
  [BitType.prototypeImages]: {
9235
10994
  since: "1.6.1",
9236
10995
  baseBitType: BitType.imagesLogoGrave,
9237
- description: "Prototype images bit, used to create prototype images in articles or books"
10996
+ description: "Prototype images bit, used to create prototype images in articles or books",
10997
+ tags: [
10998
+ {
10999
+ // Override inherited `logos` wrapper with `images` for this bit type.
11000
+ key: ConfigKey.group_resourceImageNoZoom,
11001
+ description: "Resource image tags for prototype images",
11002
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
11003
+ minCount: 1,
11004
+ maxCount: Count.infinity
11005
+ }
11006
+ ]
9238
11007
  },
9239
11008
  [BitType.internalLink]: {
9240
11009
  since: "1.3.0",
@@ -9264,6 +11033,18 @@ var BITS = {
9264
11033
  exportJsonKey: {},
9265
11034
  description: "Reasonable number of characters for the interview, used to limit input size",
9266
11035
  format: TagFormat.number
11036
+ },
11037
+ {
11038
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11039
+ key: ConfigKey.property_example,
11040
+ exportJsonKey: [
11041
+ { "@keyonly": { isExample: true } },
11042
+ { "@absent": { isExample: true } },
11043
+ { isExample: true, example: "$" }
11044
+ ],
11045
+ description: "The example(s) for the bit",
11046
+ format: TagFormat.bitmarkText,
11047
+ nullable: true
9267
11048
  }
9268
11049
  ],
9269
11050
  cardSet: CardSetConfigKey.questions
@@ -9622,6 +11403,18 @@ var BITS = {
9622
11403
  {
9623
11404
  key: ConfigKey.group_mark,
9624
11405
  description: "Mark tags, used to define the content of marks"
11406
+ },
11407
+ {
11408
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11409
+ key: ConfigKey.property_example,
11410
+ exportJsonKey: [
11411
+ { "@keyonly": { isExample: true } },
11412
+ { "@absent": { isExample: true } },
11413
+ { isExample: true, example: "$" }
11414
+ ],
11415
+ description: "The example(s) for the bit",
11416
+ format: TagFormat.bitmarkText,
11417
+ nullable: true
9625
11418
  }
9626
11419
  ]
9627
11420
  },
@@ -9641,6 +11434,23 @@ var BITS = {
9641
11434
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9642
11435
  format: TagFormat.boolean,
9643
11436
  defaultValue: "true"
11437
+ },
11438
+ {
11439
+ // Override inherited @example: in `isTopLevelExample` allow-list
11440
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11441
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11442
+ // Bit-level fires emit only `isExample`; the @example value (when
11443
+ // provided) is recorded in the ancestor stack and consumed by
11444
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11445
+ key: ConfigKey.property_example,
11446
+ exportJsonKey: [
11447
+ { "@keyonly": { isExample: true } },
11448
+ { "@absent": { isExample: true } },
11449
+ { isExample: true }
11450
+ ],
11451
+ description: "The example(s) for the bit",
11452
+ format: TagFormat.bitmarkText,
11453
+ nullable: true
9644
11454
  }
9645
11455
  ],
9646
11456
  cardSet: CardSetConfigKey.matchPairs
@@ -9725,6 +11535,18 @@ var BITS = {
9725
11535
  {
9726
11536
  key: ConfigKey.group_trueFalse,
9727
11537
  description: "True/False tags for multiple choice 1 questions"
11538
+ },
11539
+ {
11540
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11541
+ key: ConfigKey.property_example,
11542
+ exportJsonKey: [
11543
+ { "@keyonly": { isExample: true } },
11544
+ { "@absent": { isExample: true } },
11545
+ { isExample: true, example: "$" }
11546
+ ],
11547
+ description: "The example(s) for the bit",
11548
+ format: TagFormat.bitmarkText,
11549
+ nullable: true
9728
11550
  }
9729
11551
  ]
9730
11552
  },
@@ -9746,8 +11568,20 @@ var BITS = {
9746
11568
  {
9747
11569
  key: ConfigKey.group_trueFalse,
9748
11570
  description: "True/False tags for multiple choice questions"
9749
- }
11571
+ },
9750
11572
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11573
+ {
11574
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11575
+ key: ConfigKey.property_example,
11576
+ exportJsonKey: [
11577
+ { "@keyonly": { isExample: true } },
11578
+ { "@absent": { isExample: true } },
11579
+ { isExample: true, example: "$" }
11580
+ ],
11581
+ description: "The example(s) for the bit",
11582
+ format: TagFormat.bitmarkText,
11583
+ nullable: true
11584
+ }
9751
11585
  ],
9752
11586
  cardSet: CardSetConfigKey.quiz
9753
11587
  },
@@ -9767,8 +11601,20 @@ var BITS = {
9767
11601
  description: "Common quiz tags for multiple choice text questions"
9768
11602
  },
9769
11603
  {
9770
- key: ConfigKey.group_trueFalse,
11604
+ key: ConfigKey.group_trueFalseInlineSelect,
9771
11605
  description: "True/False tags for multiple choice text questions"
11606
+ },
11607
+ {
11608
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11609
+ key: ConfigKey.property_example,
11610
+ exportJsonKey: [
11611
+ { "@keyonly": { isExample: true } },
11612
+ { "@absent": { isExample: true } },
11613
+ { isExample: true, example: "$" }
11614
+ ],
11615
+ description: "The example(s) for the bit",
11616
+ format: TagFormat.bitmarkText,
11617
+ nullable: true
9772
11618
  }
9773
11619
  ]
9774
11620
  },
@@ -9793,8 +11639,20 @@ var BITS = {
9793
11639
  description: "Common quiz tags for multiple response 1 questions"
9794
11640
  },
9795
11641
  {
9796
- key: ConfigKey.group_trueFalse,
11642
+ key: ConfigKey.group_trueFalseResponses,
9797
11643
  description: "True/False tags for multiple response 1 questions"
11644
+ },
11645
+ {
11646
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11647
+ key: ConfigKey.property_example,
11648
+ exportJsonKey: [
11649
+ { "@keyonly": { isExample: true } },
11650
+ { "@absent": { isExample: true } },
11651
+ { isExample: true, example: "$" }
11652
+ ],
11653
+ description: "The example(s) for the bit",
11654
+ format: TagFormat.bitmarkText,
11655
+ nullable: true
9798
11656
  }
9799
11657
  ]
9800
11658
  },
@@ -9814,12 +11672,24 @@ var BITS = {
9814
11672
  description: "Common quiz tags for multiple response questions"
9815
11673
  },
9816
11674
  {
9817
- key: ConfigKey.group_trueFalse,
11675
+ key: ConfigKey.group_trueFalseResponses,
9818
11676
  description: "True/False tags for multiple response questions"
9819
- }
11677
+ },
9820
11678
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11679
+ {
11680
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11681
+ key: ConfigKey.property_example,
11682
+ exportJsonKey: [
11683
+ { "@keyonly": { isExample: true } },
11684
+ { "@absent": { isExample: true } },
11685
+ { isExample: true, example: "$" }
11686
+ ],
11687
+ description: "The example(s) for the bit",
11688
+ format: TagFormat.bitmarkText,
11689
+ nullable: true
11690
+ }
9821
11691
  ],
9822
- cardSet: CardSetConfigKey.quiz
11692
+ cardSet: CardSetConfigKey.quizResponses
9823
11693
  },
9824
11694
  [BitType.coachSelfReflectionMultipleResponse]: {
9825
11695
  since: "1.3.0",
@@ -10069,6 +11939,8 @@ var BITS = {
10069
11939
  },
10070
11940
  {
10071
11941
  key: ConfigKey.resource_platformIcon,
11942
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11943
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
10072
11944
  description: "The platform icon",
10073
11945
  chain: [
10074
11946
  {
@@ -10104,6 +11976,8 @@ var BITS = {
10104
11976
  tags: [
10105
11977
  {
10106
11978
  key: ConfigKey.resource_platformLogo,
11979
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11980
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
10107
11981
  description: "The platform logo",
10108
11982
  chain: [
10109
11983
  {
@@ -10315,7 +12189,8 @@ var BITS = {
10315
12189
  { isExample: true, example: "$" }
10316
12190
  ],
10317
12191
  description: "The example text for the review note bit",
10318
- format: TagFormat.plainText
12192
+ format: TagFormat.plainText,
12193
+ nullable: true
10319
12194
  }
10320
12195
  ],
10321
12196
  rootExampleType: ExampleType.string
@@ -10402,7 +12277,8 @@ var BITS = {
10402
12277
  { isExample: true, example: "$" }
10403
12278
  ],
10404
12279
  description: "The example flag for the bit (boolean)",
10405
- format: TagFormat.plainText
12280
+ format: TagFormat.boolean,
12281
+ nullable: true
10406
12282
  }
10407
12283
  ],
10408
12284
  cardSet: CardSetConfigKey.elements,
@@ -10676,15 +12552,8 @@ var BITS = {
10676
12552
  format: TagFormat.bitmarkText
10677
12553
  },
10678
12554
  {
10679
- key: ConfigKey.resource_backgroundWallpaper,
10680
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
10681
- description: "Background wallpaper for the image, used to set a background for the image",
10682
- chain: [
10683
- {
10684
- key: ConfigKey.group_resourceImageCommon,
10685
- description: "Common resource image tags for images"
10686
- }
10687
- ]
12555
+ key: ConfigKey.group_backgroundWallpaper,
12556
+ description: "Background wallpaper tags for images, used to define background properties for images"
10688
12557
  },
10689
12558
  {
10690
12559
  key: ConfigKey.group_resourceBitTags,
@@ -11061,16 +12930,20 @@ var BITS = {
11061
12930
  format: TagFormat.plainText
11062
12931
  },
11063
12932
  {
11064
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
12933
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12934
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12935
+ // not a `choices[]` array. Format is bitmarkText so the statement
12936
+ // body renders as a ProseMirror tree, matching reference fixtures.
12937
+ exportJsonKey: { statement: "$", isCorrect: true },
11065
12938
  key: ConfigKey.tag_true,
11066
12939
  description: "Tag for the true option in the true/false question",
11067
- format: TagFormat.plainText
12940
+ format: TagFormat.bitmarkText
11068
12941
  },
11069
12942
  {
11070
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
12943
+ exportJsonKey: { statement: "$", isCorrect: false },
11071
12944
  key: ConfigKey.tag_false,
11072
12945
  description: "Tag for the false option in the true/false question",
11073
- format: TagFormat.plainText
12946
+ format: TagFormat.bitmarkText
11074
12947
  },
11075
12948
  {
11076
12949
  // Override inherited @example from group_standardTags: boolean root
@@ -11081,14 +12954,24 @@ var BITS = {
11081
12954
  // sufficient for `[+...]` (correct) statements which dominate the
11082
12955
  // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
11083
12956
  // need either a runtime extension or an explicit `[@example:false]`.
12957
+ //
12958
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12959
+ // (a natural default that gets stripped in optimised mode, matching
12960
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12961
+ // STRING `"false"` which doesn't strip.
11084
12962
  key: ConfigKey.property_example,
11085
12963
  exportJsonKey: [
11086
12964
  { "@keyonly": { isExample: true, example: true } },
11087
12965
  { "@absent": { isExample: true, example: true } },
12966
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12967
+ // is the natural default for boolean, stripped in optimised mode.
12968
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12969
+ { "@value=false": { isExample: true } },
11088
12970
  { isExample: true, example: "$" }
11089
12971
  ],
11090
12972
  description: "The example flag for the bit (boolean)",
11091
- format: TagFormat.plainText
12973
+ format: TagFormat.boolean,
12974
+ nullable: true
11092
12975
  }
11093
12976
  ],
11094
12977
  rootExampleType: ExampleType.boolean
@@ -11112,6 +12995,18 @@ var BITS = {
11112
12995
  key: ConfigKey.property_labelFalse,
11113
12996
  description: "Label for the false option in the true/false question",
11114
12997
  format: TagFormat.plainText
12998
+ },
12999
+ {
13000
+ // Override inherited @example: in `isTopLevelExample` allow-list.
13001
+ key: ConfigKey.property_example,
13002
+ exportJsonKey: [
13003
+ { "@keyonly": { isExample: true } },
13004
+ { "@absent": { isExample: true } },
13005
+ { isExample: true, example: "$" }
13006
+ ],
13007
+ description: "The example(s) for the bit",
13008
+ format: TagFormat.bitmarkText,
13009
+ nullable: true
11115
13010
  }
11116
13011
  ],
11117
13012
  cardSet: CardSetConfigKey.statements
@@ -11301,6 +13196,11 @@ var BITS = {
11301
13196
  baseBitType: BitType.vendorStripePricingTable,
11302
13197
  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",
11303
13198
  tags: [
13199
+ {
13200
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
13201
+ description: "Descriptive name for the Stripe pricing table",
13202
+ format: TagFormat.plainText
13203
+ },
11304
13204
  {
11305
13205
  key: ConfigKey.property_printOnRequestCustomerId,
11306
13206
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -11777,7 +13677,7 @@ var instance2 = new Config();
11777
13677
  // src/generated/package_info.ts
11778
13678
  var PACKAGE_INFO = {
11779
13679
  "name": "@gmb/bitmark-parser-generator",
11780
- "version": "5.23.0",
13680
+ "version": "5.25.0",
11781
13681
  "author": "Get More Brain Ltd",
11782
13682
  "license": "ISC",
11783
13683
  "description": "A bitmark parser and generator using Peggy.js"
@@ -12423,6 +14323,8 @@ var NodeType = {
12423
14323
  internalPrintPdf: "internalPrintPdf",
12424
14324
  internalPrintPdfValue: "internalPrintPdfValue",
12425
14325
  isCaseSensitive: "isCaseSensitive",
14326
+ isCollapsible: "isCollapsible",
14327
+ isCollapsibleValue: "isCollapsibleValue",
12426
14328
  isCommented: "isCommented",
12427
14329
  isCorrect: "isCorrect",
12428
14330
  isEditable: "isEditable",
@@ -12742,6 +14644,8 @@ var NodeType = {
12742
14644
  stillImageFilmLink: "stillImageFilmLink",
12743
14645
  stripePricingTableId: "stripePricingTableId",
12744
14646
  stripePricingTableIdValue: "stripePricingTableIdValue",
14647
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14648
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12745
14649
  stripePublishableKey: "stripePublishableKey",
12746
14650
  stripePublishableKeyValue: "stripePublishableKeyValue",
12747
14651
  subject: "subject",
@@ -26167,6 +28071,12 @@ var Builder = class extends BaseBuilder {
26167
28071
  data.stripePricingTableId,
26168
28072
  options
26169
28073
  ),
28074
+ stripePricingTableDescriptiveName: this.toAstProperty(
28075
+ bitType,
28076
+ ConfigKey.property_stripePricingTableDescriptiveName,
28077
+ data.stripePricingTableDescriptiveName,
28078
+ options
28079
+ ),
26170
28080
  stripePublishableKey: this.toAstProperty(
26171
28081
  bitType,
26172
28082
  ConfigKey.property_stripePublishableKey,
@@ -26653,6 +28563,12 @@ var Builder = class extends BaseBuilder {
26653
28563
  level: instance6.asNumber(data.level),
26654
28564
  toc: this.toAstProperty(bitType, ConfigKey.property_toc, data.toc, options),
26655
28565
  progress: this.toAstProperty(bitType, ConfigKey.property_progress, data.progress, options),
28566
+ isCollapsible: this.toAstProperty(
28567
+ bitType,
28568
+ ConfigKey.property_isCollapsible,
28569
+ data.isCollapsible,
28570
+ options
28571
+ ),
26656
28572
  anchor: data.anchor,
26657
28573
  reference: data.reference,
26658
28574
  referenceEnd: data.referenceEnd,
@@ -32199,6 +34115,7 @@ var JsonGenerator = class extends AstWalkerGenerator {
32199
34115
  if (instance2.isOfBitType(bitType, BitType.chapter)) {
32200
34116
  if (bitJson.toc == null) bitJson.toc = true;
32201
34117
  if (bitJson.progress == null) bitJson.progress = true;
34118
+ if (bitJson.isCollapsible == null) bitJson.isCollapsible = false;
32202
34119
  if (bitJson.level == null) bitJson.level = 1;
32203
34120
  if (bitJson.body == null) bitJson.body = this.bodyDefault;
32204
34121
  }
@@ -34412,7 +36329,9 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34412
36329
  const cardSetType = bitConfig.cardSet?.configKey;
34413
36330
  switch (cardSetType) {
34414
36331
  case CardSetConfigKey.flashcard:
36332
+ case CardSetConfigKey.flashcard1:
34415
36333
  case CardSetConfigKey.definitionList:
36334
+ case CardSetConfigKey.definitionListPlain:
34416
36335
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
34417
36336
  break;
34418
36337
  case CardSetConfigKey.elements:
@@ -34425,6 +36344,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34425
36344
  result = parseFeedback(context, bitType, processedCardSet);
34426
36345
  break;
34427
36346
  case CardSetConfigKey.quiz:
36347
+ case CardSetConfigKey.quizResponses:
34428
36348
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
34429
36349
  break;
34430
36350
  case CardSetConfigKey.questions:
@@ -34451,6 +36371,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34451
36371
  break;
34452
36372
  case CardSetConfigKey.clozeList:
34453
36373
  case CardSetConfigKey.exampleBitList:
36374
+ case CardSetConfigKey.pageFooterSections:
34454
36375
  case CardSetConfigKey.bookReferenceList:
34455
36376
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
34456
36377
  break;
@@ -40747,6 +42668,235 @@ var BitmarkFileGenerator = class {
40747
42668
  }
40748
42669
  };
40749
42670
 
42671
+ // src/generator/html/HtmlTableGenerator.ts
42672
+ var MARK_TO_TAG = {
42673
+ bold: "b",
42674
+ italic: "i",
42675
+ superscript: "sup",
42676
+ subscript: "sub",
42677
+ strike: "s",
42678
+ del: "del",
42679
+ ins: "ins",
42680
+ underline: "u",
42681
+ highlight: "mark",
42682
+ code: "code"
42683
+ };
42684
+ var HtmlTableGenerator = class {
42685
+ constructor() {
42686
+ __publicField(this, "indentUnit", " ");
42687
+ }
42688
+ /**
42689
+ * Generate HTML table fragments from a string or JSON value.
42690
+ * @returns the concatenated `<table>` fragments, blank-line separated (empty string if none).
42691
+ */
42692
+ generate(input) {
42693
+ let data = input;
42694
+ if (typeof data === "string") {
42695
+ try {
42696
+ data = JSON.parse(data);
42697
+ } catch {
42698
+ return "";
42699
+ }
42700
+ }
42701
+ const bits = this.collectBits(data);
42702
+ const fragments = [];
42703
+ for (const bit of bits) {
42704
+ const html = this.renderTableBit(bit);
42705
+ if (html) fragments.push(html);
42706
+ }
42707
+ return fragments.join("\n\n");
42708
+ }
42709
+ // --- bit collection -------------------------------------------------------
42710
+ collectBits(data) {
42711
+ const out = [];
42712
+ const visit = (value) => {
42713
+ if (value == null || typeof value !== "object") return;
42714
+ if (Array.isArray(value)) {
42715
+ value.forEach(visit);
42716
+ return;
42717
+ }
42718
+ const obj = value;
42719
+ if ("bit" in obj && obj.bit && typeof obj.bit === "object") {
42720
+ out.push(obj.bit);
42721
+ } else if ("type" in obj) {
42722
+ out.push(obj);
42723
+ }
42724
+ };
42725
+ visit(data);
42726
+ return out;
42727
+ }
42728
+ // --- table rendering ------------------------------------------------------
42729
+ renderTableBit(bit) {
42730
+ const table = bit.table;
42731
+ if (!table) return "";
42732
+ if (bit.type !== "table" && bit.type !== "table-extended") return "";
42733
+ const extended = this.toExtended(table);
42734
+ const lines = [];
42735
+ lines.push("<table>");
42736
+ if (bit.caption != null) {
42737
+ const caption = this.cellContentToHtml(this.asTextAst(bit.caption));
42738
+ if (caption) lines.push(`${this.indentUnit}<caption>${caption}</caption>`);
42739
+ }
42740
+ this.renderSection(lines, "thead", extended.header, "header");
42741
+ this.renderSection(lines, "tbody", extended.body, "body");
42742
+ this.renderSection(lines, "tfoot", extended.footer, "footer");
42743
+ lines.push("</table>");
42744
+ return lines.join("\n");
42745
+ }
42746
+ toExtended(table) {
42747
+ const t = table;
42748
+ if (t.header || t.body || t.footer) return table;
42749
+ return convertBasicToExtendedTableFormat(table);
42750
+ }
42751
+ renderSection(lines, tag, section, key) {
42752
+ if (!section || !section.rows || section.rows.length === 0) return;
42753
+ lines.push(`${this.indentUnit}<${tag}>`);
42754
+ for (const row of section.rows) {
42755
+ lines.push(`${this.indentUnit.repeat(2)}<tr>`);
42756
+ for (const cell of row.cells ?? []) {
42757
+ lines.push(`${this.indentUnit.repeat(3)}${this.renderCell(cell, key)}`);
42758
+ }
42759
+ lines.push(`${this.indentUnit.repeat(2)}</tr>`);
42760
+ }
42761
+ lines.push(`${this.indentUnit}</${tag}>`);
42762
+ }
42763
+ renderCell(cell, _key) {
42764
+ const tag = cell.title === true ? "th" : "td";
42765
+ const attrs = [];
42766
+ if (cell.rowspan && cell.rowspan > 1) attrs.push(`rowspan="${cell.rowspan}"`);
42767
+ if (cell.colspan && cell.colspan > 1) attrs.push(`colspan="${cell.colspan}"`);
42768
+ if (cell.scope) attrs.push(`scope="${cell.scope}"`);
42769
+ if (cell.colwidth && cell.colwidth > 0) attrs.push(`width="${cell.colwidth}"`);
42770
+ const open = attrs.length > 0 ? `<${tag} ${attrs.join(" ")}>` : `<${tag}>`;
42771
+ return `${open}${this.cellContentToHtml(this.asTextAst(cell.content))}</${tag}>`;
42772
+ }
42773
+ // --- cell content (TextAst -> HTML) ---------------------------------------
42774
+ asTextAst(content) {
42775
+ if (content == null) return [];
42776
+ if (typeof content === "string") return [{ type: "text", text: content }];
42777
+ return content;
42778
+ }
42779
+ cellContentToHtml(blocks) {
42780
+ if (blocks.length === 0) return "";
42781
+ if (blocks.length === 1 && blocks[0].type === "paragraph") {
42782
+ return this.inlineToHtml(blocks[0].content ?? []);
42783
+ }
42784
+ return blocks.map((b) => this.blockToHtml(b)).join("");
42785
+ }
42786
+ blockToHtml(node) {
42787
+ switch (node.type) {
42788
+ case "paragraph":
42789
+ return `<p>${this.inlineToHtml(node.content ?? [])}</p>`;
42790
+ case "bulletList":
42791
+ case "noBulletList":
42792
+ case "orderedList":
42793
+ case "orderedListRoman":
42794
+ case "orderedListRomanLower":
42795
+ case "letteredList":
42796
+ case "letteredListLower":
42797
+ case "taskList":
42798
+ return this.listToHtml(node);
42799
+ case "image":
42800
+ case "imageInline":
42801
+ return this.imageToHtml(node);
42802
+ default:
42803
+ return this.inlineToHtml([node]);
42804
+ }
42805
+ }
42806
+ inlineToHtml(nodes) {
42807
+ return nodes.map((n) => this.inlineNodeToHtml(n)).join("");
42808
+ }
42809
+ inlineNodeToHtml(node) {
42810
+ if (node.type === "hardBreak") return "<br>";
42811
+ if (node.type === "image" || node.type === "imageInline") return this.imageToHtml(node);
42812
+ if (node.type === "text") {
42813
+ return this.wrapMarks(this.escapeText(node.text ?? ""), node.marks);
42814
+ }
42815
+ if (node.content) return this.inlineToHtml(node.content);
42816
+ return this.escapeText(node.text ?? "");
42817
+ }
42818
+ wrapMarks(text, marks) {
42819
+ if (!marks || marks.length === 0) return text;
42820
+ let open = "";
42821
+ let close = "";
42822
+ for (const mark of marks) {
42823
+ if (mark.type === "link") {
42824
+ const href = mark.attrs?.href ?? "";
42825
+ open += `<a href="${this.escapeAttr(href)}">`;
42826
+ close = `</a>${close}`;
42827
+ } else {
42828
+ const tag = MARK_TO_TAG[mark.type];
42829
+ if (tag) {
42830
+ open += `<${tag}>`;
42831
+ close = `</${tag}>${close}`;
42832
+ }
42833
+ }
42834
+ }
42835
+ return `${open}${text}${close}`;
42836
+ }
42837
+ // --- lists ----------------------------------------------------------------
42838
+ listToHtml(node) {
42839
+ const { tag, attrs } = this.listTag(node);
42840
+ const items = (node.content ?? []).map((item) => this.listItemToHtml(item, node.type === "taskList")).join("");
42841
+ return `<${tag}${attrs}>${items}</${tag}>`;
42842
+ }
42843
+ listTag(node) {
42844
+ const start = node.attrs?.start;
42845
+ const startAttr = typeof start === "number" && start !== 1 ? ` start="${start}"` : "";
42846
+ switch (node.type) {
42847
+ case "orderedList":
42848
+ return { tag: "ol", attrs: startAttr };
42849
+ case "orderedListRoman":
42850
+ return { tag: "ol", attrs: ` type="I"${startAttr}` };
42851
+ case "orderedListRomanLower":
42852
+ return { tag: "ol", attrs: ` type="i"${startAttr}` };
42853
+ case "letteredList":
42854
+ return { tag: "ol", attrs: ` type="A"${startAttr}` };
42855
+ case "letteredListLower":
42856
+ return { tag: "ol", attrs: ` type="a"${startAttr}` };
42857
+ case "noBulletList":
42858
+ return { tag: "ul", attrs: ' style="list-style:none"' };
42859
+ case "taskList":
42860
+ case "bulletList":
42861
+ default:
42862
+ return { tag: "ul", attrs: "" };
42863
+ }
42864
+ }
42865
+ listItemToHtml(item, task) {
42866
+ const inner = (item.content ?? []).map((b) => {
42867
+ if (b.type === "paragraph") return this.inlineToHtml(b.content ?? []);
42868
+ return this.blockToHtml(b);
42869
+ }).join("");
42870
+ if (task) {
42871
+ const checked = item.attrs?.checked ? " checked" : "";
42872
+ return `<li><input type="checkbox"${checked}>${inner}</li>`;
42873
+ }
42874
+ return `<li>${inner}</li>`;
42875
+ }
42876
+ // --- images ---------------------------------------------------------------
42877
+ imageToHtml(node) {
42878
+ const attrs = node.attrs ?? {};
42879
+ const parts = [];
42880
+ const src = attrs.src;
42881
+ parts.push(`src="${this.escapeAttr(typeof src === "string" ? src : "")}"`);
42882
+ if (typeof attrs.alt === "string") parts.push(`alt="${this.escapeAttr(attrs.alt)}"`);
42883
+ if (typeof attrs.title === "string") parts.push(`title="${this.escapeAttr(attrs.title)}"`);
42884
+ if (attrs.width != null) parts.push(`width="${attrs.width}"`);
42885
+ if (attrs.height != null) parts.push(`height="${attrs.height}"`);
42886
+ if (typeof attrs.class === "string" && attrs.class) {
42887
+ parts.push(`class="${this.escapeAttr(attrs.class)}"`);
42888
+ }
42889
+ return `<img ${parts.join(" ")}>`;
42890
+ }
42891
+ // --- escaping -------------------------------------------------------------
42892
+ escapeText(text) {
42893
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
42894
+ }
42895
+ escapeAttr(text) {
42896
+ return this.escapeText(text).replace(/"/g, "&quot;");
42897
+ }
42898
+ };
42899
+
40750
42900
  // src/generator/json/JsonFileGenerator.ts
40751
42901
  var import_node_fs4 = require("fs");
40752
42902
  var JsonFileGenerator = class {
@@ -41245,6 +43395,13 @@ var ConfigBuilder = class {
41245
43395
  const cardSetConfig = CARDSETS[cardSetKey];
41246
43396
  if (!cardSetConfig) return void 0;
41247
43397
  const normalizedKey = normalizeCardKey(cardSetKey);
43398
+ const variantTagEntriesTypeOrder = [
43399
+ BitTagConfigKeyType.group,
43400
+ BitTagConfigKeyType.tag,
43401
+ BitTagConfigKeyType.property,
43402
+ BitTagConfigKeyType.resource,
43403
+ BitTagConfigKeyType.unknown
43404
+ ];
41248
43405
  const sides = cardSetConfig.sides.map((side, sideIdx) => {
41249
43406
  const sidePath = `cardSets.${normalizedKey} / sides.${side.name ?? `[${sideIdx}]`}`;
41250
43407
  const variants = side.variants.map((variant, variantIdx) => {
@@ -41255,7 +43412,7 @@ var ConfigBuilder = class {
41255
43412
  const tagB = b[1];
41256
43413
  const typeA = typeFromConfigKey(tagA.key);
41257
43414
  const typeB = typeFromConfigKey(tagB.key);
41258
- const typeOrder = tagEntriesTypeOrder.indexOf(typeA) - tagEntriesTypeOrder.indexOf(typeB);
43415
+ const typeOrder = variantTagEntriesTypeOrder.indexOf(typeA) - variantTagEntriesTypeOrder.indexOf(typeB);
41259
43416
  return typeOrder;
41260
43417
  });
41261
43418
  for (const [, variantTag] of variantTagEntries) {
@@ -41295,6 +43452,10 @@ var ConfigBuilder = class {
41295
43452
  name: sectionName,
41296
43453
  ...section.isDefault ? { isDefault: true } : {},
41297
43454
  ...cardJsonKeyField(section, "jsonKey", "exportJsonKey", sectionPath),
43455
+ // PLAN-085: per-section cardinality. Emit only when non-default
43456
+ // (treat `0` / undefined as "unbounded — omit").
43457
+ ...section.minCount != null && section.minCount !== 0 ? { min: section.minCount } : {},
43458
+ ...section.maxCount != null && section.maxCount !== 0 ? { max: section.maxCount } : {},
41298
43459
  sides: cardSides
41299
43460
  };
41300
43461
  });
@@ -41364,10 +43525,20 @@ var ConfigBuilder = class {
41364
43525
  return typeOrder;
41365
43526
  });
41366
43527
  const isInheritedFrom = bitGroupConfigKeys.has(b.bitType);
43528
+ const indexByKey = /* @__PURE__ */ new Map();
43529
+ const pushOrReplace = (entry) => {
43530
+ const existing = indexByKey.get(entry.key);
43531
+ if (existing !== void 0) {
43532
+ tags2[existing] = entry;
43533
+ } else {
43534
+ indexByKey.set(entry.key, tags2.length);
43535
+ tags2.push(entry);
43536
+ }
43537
+ };
41367
43538
  if (isInheritedFrom) {
41368
43539
  const resolvedGroups = resolveGroupReferences(`group-${b.bitType}`);
41369
43540
  for (const ref of resolvedGroups) {
41370
- tags2.push({
43541
+ pushOrReplace({
41371
43542
  type: "group",
41372
43543
  key: ref.key,
41373
43544
  ...ref.hasExportJsonKey ? { jsonKey: ref.exportJsonKey } : {},
@@ -41380,7 +43551,7 @@ var ConfigBuilder = class {
41380
43551
  if (b.baseBitType) {
41381
43552
  const resolvedGroups = resolveGroupReferences(`group-${b.baseBitType}`);
41382
43553
  for (const ref of resolvedGroups) {
41383
- tags2.push({
43554
+ pushOrReplace({
41384
43555
  type: "group",
41385
43556
  key: ref.key,
41386
43557
  ...ref.hasExportJsonKey ? { jsonKey: ref.exportJsonKey } : {},
@@ -41391,7 +43562,9 @@ var ConfigBuilder = class {
41391
43562
  }
41392
43563
  }
41393
43564
  for (const [_tagKey, tag] of tagEntries) {
41394
- tags2.push(...processTagEntries(tag, []));
43565
+ for (const entry of processTagEntries(tag, [])) {
43566
+ pushOrReplace(entry);
43567
+ }
41395
43568
  }
41396
43569
  }
41397
43570
  let cardRef;
@@ -41700,6 +43873,499 @@ var ConfigBuilder = class {
41700
43873
  }
41701
43874
  };
41702
43875
 
43876
+ // src/parser/html/HtmlDom.ts
43877
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
43878
+ "area",
43879
+ "base",
43880
+ "br",
43881
+ "col",
43882
+ "embed",
43883
+ "hr",
43884
+ "img",
43885
+ "input",
43886
+ "link",
43887
+ "meta",
43888
+ "param",
43889
+ "source",
43890
+ "track",
43891
+ "wbr"
43892
+ ]);
43893
+ var IMPLIED_CLOSE = {
43894
+ li: /* @__PURE__ */ new Set(["li"]),
43895
+ td: /* @__PURE__ */ new Set(["td", "th"]),
43896
+ th: /* @__PURE__ */ new Set(["td", "th"]),
43897
+ tr: /* @__PURE__ */ new Set(["td", "th", "tr"]),
43898
+ thead: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43899
+ tbody: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43900
+ tfoot: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43901
+ p: /* @__PURE__ */ new Set(["p"]),
43902
+ option: /* @__PURE__ */ new Set(["option"])
43903
+ };
43904
+ var TAG_RE = /<(\/)?([a-zA-Z][a-zA-Z0-9-]*)((?:\s+(?:"[^"]*"|'[^']*'|[^<>"'])*)?)(\/)?>/g;
43905
+ var ATTR_RE = /([a-zA-Z_:][a-zA-Z0-9:._-]*)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s"'>]+))?/g;
43906
+ function parseAttrs(raw) {
43907
+ const attrs = {};
43908
+ if (!raw) return attrs;
43909
+ ATTR_RE.lastIndex = 0;
43910
+ let m;
43911
+ while ((m = ATTR_RE.exec(raw)) !== null) {
43912
+ const name = m[1].toLowerCase();
43913
+ let value = m[2] ?? "";
43914
+ if (value.length >= 2 && (value[0] === '"' || value[0] === "'")) {
43915
+ value = value.slice(1, -1);
43916
+ }
43917
+ attrs[name] = value;
43918
+ }
43919
+ return attrs;
43920
+ }
43921
+ function stripNonContent(html) {
43922
+ 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, "");
43923
+ }
43924
+ function tokenize(html) {
43925
+ const tokens = [];
43926
+ const clean = stripNonContent(html);
43927
+ TAG_RE.lastIndex = 0;
43928
+ let lastIndex = 0;
43929
+ let m;
43930
+ while ((m = TAG_RE.exec(clean)) !== null) {
43931
+ if (m.index > lastIndex) {
43932
+ tokens.push({ kind: "text", text: clean.slice(lastIndex, m.index) });
43933
+ }
43934
+ lastIndex = TAG_RE.lastIndex;
43935
+ const isClose = m[1] === "/";
43936
+ const name = m[2].toLowerCase();
43937
+ if (isClose) {
43938
+ tokens.push({ kind: "close", name });
43939
+ } else {
43940
+ tokens.push({
43941
+ kind: "open",
43942
+ name,
43943
+ attrs: parseAttrs(m[3]),
43944
+ selfClose: m[4] === "/"
43945
+ });
43946
+ }
43947
+ }
43948
+ if (lastIndex < clean.length) {
43949
+ tokens.push({ kind: "text", text: clean.slice(lastIndex) });
43950
+ }
43951
+ return tokens;
43952
+ }
43953
+ function parseHtml(html) {
43954
+ const root = { type: "element", name: "#root", attrs: {}, children: [] };
43955
+ const stack = [root];
43956
+ const top = () => stack[stack.length - 1];
43957
+ for (const token of tokenize(html)) {
43958
+ if (token.kind === "text") {
43959
+ if (token.text) top().children.push({ type: "text", value: token.text });
43960
+ continue;
43961
+ }
43962
+ if (token.kind === "open") {
43963
+ const name = token.name;
43964
+ const closes = IMPLIED_CLOSE[name];
43965
+ if (closes) {
43966
+ while (stack.length > 1 && closes.has(top().name)) stack.pop();
43967
+ }
43968
+ const el = {
43969
+ type: "element",
43970
+ name,
43971
+ attrs: token.attrs ?? {},
43972
+ children: []
43973
+ };
43974
+ top().children.push(el);
43975
+ if (!token.selfClose && !VOID_ELEMENTS.has(name)) {
43976
+ stack.push(el);
43977
+ }
43978
+ continue;
43979
+ }
43980
+ for (let i = stack.length - 1; i > 0; i--) {
43981
+ if (stack[i].name === token.name) {
43982
+ stack.length = i;
43983
+ break;
43984
+ }
43985
+ }
43986
+ }
43987
+ return root.children;
43988
+ }
43989
+ function childElements(el, names) {
43990
+ return el.children.filter((c) => c.type === "element" && names.has(c.name));
43991
+ }
43992
+
43993
+ // src/parser/html/HtmlTableParser.ts
43994
+ var MARK_TAGS = {
43995
+ b: "bold",
43996
+ strong: "bold",
43997
+ i: "italic",
43998
+ em: "italic",
43999
+ sup: "superscript",
44000
+ sub: "subscript",
44001
+ s: "strike",
44002
+ strike: "strike",
44003
+ del: "del",
44004
+ ins: "ins",
44005
+ u: "underline",
44006
+ mark: "highlight",
44007
+ code: "code"
44008
+ };
44009
+ var SECTION_TAGS = /* @__PURE__ */ new Set(["thead", "tbody", "tfoot"]);
44010
+ var ROW_TAGS = /* @__PURE__ */ new Set(["tr"]);
44011
+ var CELL_TAGS = /* @__PURE__ */ new Set(["td", "th"]);
44012
+ var LI_TAGS = /* @__PURE__ */ new Set(["li"]);
44013
+ var VALID_SCOPES = /* @__PURE__ */ new Set(["row", "col", "rowgroup", "colgroup"]);
44014
+ var NAMED_ENTITIES = {
44015
+ amp: "&",
44016
+ lt: "<",
44017
+ gt: ">",
44018
+ quot: '"',
44019
+ apos: "'",
44020
+ nbsp: "\xA0"
44021
+ };
44022
+ function decodeEntities(text) {
44023
+ return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (whole, body) => {
44024
+ if (body[0] === "#") {
44025
+ const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
44026
+ if (Number.isFinite(code) && code >= 0 && code <= 1114111) {
44027
+ try {
44028
+ return String.fromCodePoint(code);
44029
+ } catch {
44030
+ return whole;
44031
+ }
44032
+ }
44033
+ return whole;
44034
+ }
44035
+ return NAMED_ENTITIES[body] ?? whole;
44036
+ });
44037
+ }
44038
+ function intAttr(value) {
44039
+ if (value == null) return void 0;
44040
+ const n = Number.parseInt(value, 10);
44041
+ return Number.isInteger(n) ? n : void 0;
44042
+ }
44043
+ function isElement(node) {
44044
+ return node.type === "element";
44045
+ }
44046
+ var HtmlTableParser = class {
44047
+ constructor() {
44048
+ __publicField(this, "keepUnknownTags", false);
44049
+ }
44050
+ /**
44051
+ * Parse an HTML string and return one table bit per top-level `<table>`.
44052
+ */
44053
+ parse(html, options) {
44054
+ this.keepUnknownTags = options?.keepUnknownTags ?? false;
44055
+ const tableFormat = options?.tableFormat ?? "table-extended";
44056
+ const onWarning = options?.onWarning;
44057
+ const tables = this.collectTopLevelTables(parseHtml(html));
44058
+ const bits = [];
44059
+ for (const table of tables) {
44060
+ const bit = this.tableToBit(table, tableFormat, onWarning);
44061
+ if (bit) bits.push(bit);
44062
+ }
44063
+ return bits;
44064
+ }
44065
+ // --- table extraction -----------------------------------------------------
44066
+ /** Collect every `<table>` element, NOT descending into nested tables. */
44067
+ collectTopLevelTables(nodes) {
44068
+ const tables = [];
44069
+ const visit = (node) => {
44070
+ if (!isElement(node)) return;
44071
+ if (node.name === "table") {
44072
+ tables.push(node);
44073
+ return;
44074
+ }
44075
+ node.children.forEach(visit);
44076
+ };
44077
+ nodes.forEach(visit);
44078
+ return tables;
44079
+ }
44080
+ tableToBit(table, tableFormat, onWarning) {
44081
+ const header = [];
44082
+ const body = [];
44083
+ const footer = [];
44084
+ let caption;
44085
+ for (const child of table.children) {
44086
+ if (!isElement(child)) continue;
44087
+ if (child.name === "caption") {
44088
+ const capContent = this.inlineNodes(child.children, []);
44089
+ const trimmed = this.trimInline(capContent);
44090
+ if (trimmed.length > 0) caption = trimmed;
44091
+ } else if (SECTION_TAGS.has(child.name)) {
44092
+ const target = child.name === "thead" ? header : child.name === "tfoot" ? footer : body;
44093
+ for (const tr of childElements(child, ROW_TAGS)) {
44094
+ target.push(this.rowToCells(tr));
44095
+ }
44096
+ } else if (ROW_TAGS.has(child.name)) {
44097
+ body.push(this.rowToCells(child));
44098
+ }
44099
+ }
44100
+ if (header.length === 0 && body.length === 0 && footer.length === 0) {
44101
+ return void 0;
44102
+ }
44103
+ const tableExtended = {};
44104
+ if (header.length > 0) tableExtended.header = { rows: header };
44105
+ if (body.length > 0) tableExtended.body = { rows: body };
44106
+ if (footer.length > 0) tableExtended.footer = { rows: footer };
44107
+ let tableData = tableExtended;
44108
+ if (tableFormat === "table") {
44109
+ if (this.isLossyAsBasic(tableExtended)) {
44110
+ onWarning?.(
44111
+ "Converting an HTML table to [.table] loses rowspan/colspan/scope and multi-section structure; use --tableFormat table-extended to preserve them."
44112
+ );
44113
+ }
44114
+ tableData = convertExtendedToBasicTableFormat(tableExtended);
44115
+ }
44116
+ const bit = {
44117
+ type: tableFormat,
44118
+ format: "bitmark++",
44119
+ table: tableData
44120
+ };
44121
+ if (caption) bit.caption = caption;
44122
+ return bit;
44123
+ }
44124
+ rowToCells(tr) {
44125
+ const cells = [];
44126
+ for (const cell of childElements(tr, CELL_TAGS)) {
44127
+ cells.push(this.cellToJson(cell));
44128
+ }
44129
+ return { cells };
44130
+ }
44131
+ cellToJson(cell) {
44132
+ const json = {
44133
+ content: this.blockNodes(cell.children)
44134
+ };
44135
+ if (cell.name === "th") json.title = true;
44136
+ const rowspan = intAttr(cell.attrs.rowspan);
44137
+ if (rowspan != null && rowspan >= 2) json.rowspan = rowspan;
44138
+ const colspan = intAttr(cell.attrs.colspan);
44139
+ if (colspan != null && colspan >= 2) json.colspan = colspan;
44140
+ const scope = cell.attrs.scope?.toLowerCase();
44141
+ if (scope && VALID_SCOPES.has(scope)) {
44142
+ json.scope = scope;
44143
+ }
44144
+ const width = this.widthFromCell(cell);
44145
+ if (width != null) json.colwidth = width;
44146
+ return json;
44147
+ }
44148
+ widthFromCell(cell) {
44149
+ const attrWidth = intAttr(cell.attrs.width);
44150
+ if (attrWidth != null && attrWidth > 0) return attrWidth;
44151
+ const style = cell.attrs.style;
44152
+ if (style) {
44153
+ const m = style.match(/width\s*:\s*(\d+)\s*px/i);
44154
+ if (m) {
44155
+ const n = Number.parseInt(m[1], 10);
44156
+ if (Number.isInteger(n) && n > 0) return n;
44157
+ }
44158
+ }
44159
+ return void 0;
44160
+ }
44161
+ isLossyAsBasic(table) {
44162
+ if (table.footer) return true;
44163
+ if (table.header && table.header.rows.length > 1) return true;
44164
+ const sections = [table.header, table.body, table.footer];
44165
+ for (const section of sections) {
44166
+ if (!section) continue;
44167
+ for (const row of section.rows) {
44168
+ for (const cell of row.cells) {
44169
+ if (cell.rowspan || cell.colspan || cell.scope) return true;
44170
+ }
44171
+ }
44172
+ }
44173
+ return false;
44174
+ }
44175
+ // --- content conversion (HTML -> TextAst) ---------------------------------
44176
+ /**
44177
+ * Convert a cell/li's child nodes into block-level TextAst nodes (paragraphs, lists, images).
44178
+ */
44179
+ blockNodes(nodes) {
44180
+ const blocks = [];
44181
+ let inline = [];
44182
+ const flush = () => {
44183
+ const trimmed = this.trimInline(inline);
44184
+ inline = [];
44185
+ if (trimmed.length === 0) return;
44186
+ if (trimmed.length === 1 && trimmed[0].type === "imageInline") {
44187
+ blocks.push({ ...trimmed[0], type: "image" });
44188
+ } else {
44189
+ blocks.push({ type: "paragraph", content: trimmed, attrs: {} });
44190
+ }
44191
+ };
44192
+ for (const node of nodes) {
44193
+ if (!isElement(node)) {
44194
+ inline.push(...this.inlineText(node.value, []));
44195
+ continue;
44196
+ }
44197
+ const name = node.name;
44198
+ if (name === "p") {
44199
+ flush();
44200
+ const content = this.trimInline(this.inlineNodes(node.children, []));
44201
+ if (content.length > 0) {
44202
+ blocks.push({ type: "paragraph", content, attrs: {} });
44203
+ }
44204
+ } else if (this.isListTag(name)) {
44205
+ flush();
44206
+ const list = this.listNode(node);
44207
+ if (list) blocks.push(list);
44208
+ } else {
44209
+ inline.push(...this.inlineFromElement(node, []));
44210
+ }
44211
+ }
44212
+ flush();
44213
+ return blocks;
44214
+ }
44215
+ /** Convert child nodes to a flat list of inline TextNodes, applying the given marks. */
44216
+ inlineNodes(nodes, marks) {
44217
+ const out = [];
44218
+ for (const node of nodes) {
44219
+ if (!isElement(node)) {
44220
+ out.push(...this.inlineText(node.value, marks));
44221
+ } else {
44222
+ out.push(...this.inlineFromElement(node, marks));
44223
+ }
44224
+ }
44225
+ return out;
44226
+ }
44227
+ inlineFromElement(el, marks) {
44228
+ const name = el.name;
44229
+ if (name === "br") {
44230
+ return [{ type: "hardBreak" }];
44231
+ }
44232
+ if (name === "img") {
44233
+ return [this.imageNode(el, true)];
44234
+ }
44235
+ const markType = MARK_TAGS[name];
44236
+ if (markType) {
44237
+ return this.inlineNodes(el.children, [...marks, { type: markType }]);
44238
+ }
44239
+ if (name === "a") {
44240
+ const linkMark = {
44241
+ type: "link",
44242
+ attrs: {
44243
+ href: el.attrs.href ?? "",
44244
+ ...el.attrs.target ? { target: el.attrs.target } : {}
44245
+ }
44246
+ };
44247
+ return this.inlineNodes(el.children, [...marks, linkMark]);
44248
+ }
44249
+ if (this.keepUnknownTags) {
44250
+ const open = this.serializeOpenTag(el);
44251
+ const close = `</${name}>`;
44252
+ return [
44253
+ ...this.inlineText(open, marks),
44254
+ ...this.inlineNodes(el.children, marks),
44255
+ ...this.inlineText(close, marks)
44256
+ ];
44257
+ }
44258
+ return this.inlineNodes(el.children, marks);
44259
+ }
44260
+ inlineText(raw, marks) {
44261
+ const text = decodeEntities(raw).replace(/\s+/g, " ");
44262
+ if (text === "") return [];
44263
+ const node = { text, type: "text" };
44264
+ if (marks.length > 0) node.marks = marks.map((m) => ({ ...m }));
44265
+ return [node];
44266
+ }
44267
+ serializeOpenTag(el) {
44268
+ const attrs = Object.entries(el.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${v}"`).join("");
44269
+ return `<${el.name}${attrs}>`;
44270
+ }
44271
+ // --- lists ----------------------------------------------------------------
44272
+ isListTag(name) {
44273
+ return name === "ul" || name === "ol";
44274
+ }
44275
+ listNode(el) {
44276
+ const items = childElements(el, LI_TAGS);
44277
+ if (items.length === 0) return void 0;
44278
+ const isTask = el.name === "ul" && items.some((li) => {
44279
+ const firstEl = li.children.find(isElement);
44280
+ return firstEl?.name === "input" && firstEl.attrs.type?.toLowerCase() === "checkbox";
44281
+ });
44282
+ if (isTask) {
44283
+ return {
44284
+ type: "taskList",
44285
+ content: items.map((li) => this.taskItemNode(li))
44286
+ };
44287
+ }
44288
+ const type = this.listType(el);
44289
+ const node = {
44290
+ type,
44291
+ content: items.map((li) => this.listItemNode(li))
44292
+ };
44293
+ if (el.name === "ol") {
44294
+ const start = intAttr(el.attrs.start);
44295
+ if (start != null) {
44296
+ node.attrs = { start };
44297
+ }
44298
+ }
44299
+ return node;
44300
+ }
44301
+ listType(el) {
44302
+ if (el.name === "ol") {
44303
+ switch (el.attrs.type) {
44304
+ case "i":
44305
+ return "orderedListRomanLower";
44306
+ case "I":
44307
+ return "orderedListRoman";
44308
+ case "a":
44309
+ return "letteredListLower";
44310
+ case "A":
44311
+ return "letteredList";
44312
+ default:
44313
+ return "orderedList";
44314
+ }
44315
+ }
44316
+ const style = el.attrs.style ?? "";
44317
+ if (/list-style(-type)?\s*:\s*none/i.test(style)) return "noBulletList";
44318
+ return "bulletList";
44319
+ }
44320
+ listItemNode(li) {
44321
+ let content = this.blockNodes(li.children);
44322
+ if (content.length === 0) {
44323
+ content = [{ type: "paragraph", content: [], attrs: {} }];
44324
+ }
44325
+ return { type: "listItem", content };
44326
+ }
44327
+ taskItemNode(li) {
44328
+ const checkbox = li.children.find(
44329
+ (c) => isElement(c) && c.name === "input" && c.attrs.type?.toLowerCase() === "checkbox"
44330
+ );
44331
+ const checked = checkbox ? "checked" in checkbox.attrs : false;
44332
+ const rest = li.children.filter((c) => c !== checkbox);
44333
+ let content = this.blockNodes(rest);
44334
+ if (content.length === 0) {
44335
+ content = [{ type: "paragraph", content: [], attrs: {} }];
44336
+ }
44337
+ return { type: "taskItem", attrs: { checked }, content };
44338
+ }
44339
+ // --- images ---------------------------------------------------------------
44340
+ imageNode(el, inline) {
44341
+ const attrs = { src: el.attrs.src ?? "" };
44342
+ attrs.alt = el.attrs.alt ?? null;
44343
+ attrs.title = el.attrs.title ?? null;
44344
+ const width = intAttr(el.attrs.width);
44345
+ if (width != null) attrs.width = width;
44346
+ const height = intAttr(el.attrs.height);
44347
+ if (height != null) attrs.height = height;
44348
+ if (el.attrs.class) attrs.class = el.attrs.class;
44349
+ return { type: inline ? "imageInline" : "image", attrs };
44350
+ }
44351
+ // --- whitespace -----------------------------------------------------------
44352
+ /** Trim leading/trailing whitespace from inline content (edges only). */
44353
+ trimInline(nodes) {
44354
+ const out = nodes.slice();
44355
+ const isWs = (n) => n.type === "text" && /^\s*$/.test(n.text ?? "");
44356
+ while (out.length > 0 && isWs(out[0])) out.shift();
44357
+ if (out.length > 0 && out[0].type === "text") {
44358
+ out[0] = { ...out[0], text: (out[0].text ?? "").replace(/^\s+/, "") };
44359
+ }
44360
+ while (out.length > 0 && isWs(out[out.length - 1])) out.pop();
44361
+ if (out.length > 0 && out[out.length - 1].type === "text") {
44362
+ const last = out.length - 1;
44363
+ out[last] = { ...out[last], text: (out[last].text ?? "").replace(/\s+$/, "") };
44364
+ }
44365
+ return out.filter((n) => !(n.type === "text" && n.text === ""));
44366
+ }
44367
+ };
44368
+
41703
44369
  // src/BitmarkParserGenerator.ts
41704
44370
  var Input = {
41705
44371
  /**
@@ -41741,7 +44407,11 @@ var InputFormat = {
41741
44407
  /**
41742
44408
  * Input is plain text
41743
44409
  */
41744
- plainText: "plainText"
44410
+ plainText: "plainText",
44411
+ /**
44412
+ * Input is HTML (used by convertHtmlTable)
44413
+ */
44414
+ html: "html"
41745
44415
  };
41746
44416
  var BitmarkParserGenerator = class {
41747
44417
  constructor() {
@@ -42276,6 +44946,87 @@ var BitmarkParserGenerator = class {
42276
44946
  const res = generator.generate(data);
42277
44947
  return res;
42278
44948
  }
44949
+ /**
44950
+ * Convert between HTML tables and bitmark tables.
44951
+ *
44952
+ * Direction is auto-detected (override with `options.inputFormat`):
44953
+ * - HTML input: every `<table>` is extracted and emitted as a `[.table-extended]` (or `[.table]`)
44954
+ * bit; output as bitmark (default), JSON, AST, or HTML (re-emit).
44955
+ * - bitmark / JSON / AST input: each `table` / `table-extended` bit is rendered as an HTML
44956
+ * `<table>` fragment.
44957
+ *
44958
+ * Cell content is reproduced verbatim except inline conversion to bitmark marks (bold, italic,
44959
+ * superscript, etc.), with lists and images mapped to their bitmark equivalents.
44960
+ *
44961
+ * @param input - HTML, bitmark, JSON or AST string; or a parsed JSON/AST value; or a file path.
44962
+ * @param options - the conversion options
44963
+ * @returns the converted output as a string (or object for JSON/AST), or void when writing a file
44964
+ * @throws Error if any error occurs
44965
+ */
44966
+ convertHtmlTable(input, options) {
44967
+ const opts = Object.assign({}, options);
44968
+ if (env.isBrowser && opts.outputFile) {
44969
+ throw new Error("Cannot write to file in browser environment");
44970
+ }
44971
+ let inStr = input;
44972
+ const inputIsString = typeof input === "string";
44973
+ if (env.isNode && inputIsString && import_fs_extra4.default.existsSync(inStr)) {
44974
+ inStr = import_fs_extra4.default.readFileSync(inStr, { encoding: "utf8" });
44975
+ }
44976
+ const isHtmlString = (s) => {
44977
+ const t = s.replace(/^\s+/, "");
44978
+ return t.startsWith("<") && /<table/i.test(t);
44979
+ };
44980
+ let htmlInput;
44981
+ if (opts.inputFormat === InputFormat.html) {
44982
+ htmlInput = true;
44983
+ } else if (opts.inputFormat != null) {
44984
+ htmlInput = false;
44985
+ } else {
44986
+ htmlInput = inputIsString && isHtmlString(inStr);
44987
+ }
44988
+ if (htmlInput) {
44989
+ const parser2 = new HtmlTableParser();
44990
+ const bits = parser2.parse(inStr, {
44991
+ tableFormat: opts.tableFormat ?? "table-extended",
44992
+ keepUnknownTags: opts.keepUnknownTags,
44993
+ onWarning: (m) => console.warn(m)
44994
+ });
44995
+ const outputFormat2 = opts.outputFormat ?? Output.bitmark;
44996
+ if (outputFormat2 === "html") {
44997
+ const json2 = this.convert(bits, { outputFormat: Output.json });
44998
+ return this.outputHtml(new HtmlTableGenerator().generate(json2), opts);
44999
+ }
45000
+ return this.convert(bits, {
45001
+ outputFormat: (0, import_superenum51.Enum)(Output).fromValue(outputFormat2),
45002
+ outputFile: opts.outputFile,
45003
+ fileOptions: opts.fileOptions,
45004
+ jsonOptions: opts.jsonOptions,
45005
+ bitmarkOptions: opts.bitmarkOptions
45006
+ });
45007
+ }
45008
+ const outputFormat = opts.outputFormat ?? "html";
45009
+ if (outputFormat !== "html") {
45010
+ throw new Error(
45011
+ "convertHtmlTable: non-HTML input can only be converted to HTML. Use convert() for bitmark/JSON/AST conversions."
45012
+ );
45013
+ }
45014
+ const json = this.convert(inputIsString ? inStr : input, { outputFormat: Output.json });
45015
+ return this.outputHtml(new HtmlTableGenerator().generate(json), opts);
45016
+ }
45017
+ /**
45018
+ * Write an HTML string to the configured output file, or return it.
45019
+ */
45020
+ outputHtml(html, opts) {
45021
+ if (opts.outputFile) {
45022
+ const output = opts.outputFile.toString();
45023
+ const flag = opts.fileOptions?.append ? "a" : "w";
45024
+ import_fs_extra4.default.ensureDirSync(import_node_path4.default.dirname(output));
45025
+ import_fs_extra4.default.writeFileSync(output, html, { flag });
45026
+ return;
45027
+ }
45028
+ return html;
45029
+ }
42279
45030
  /**
42280
45031
  * Breakscape bitmark text.
42281
45032
  *
@@ -42572,6 +45323,8 @@ init();
42572
45323
  Builder,
42573
45324
  CardSetVersion,
42574
45325
  FileWriter,
45326
+ HtmlTableGenerator,
45327
+ HtmlTableParser,
42575
45328
  InfoFormat,
42576
45329
  InfoType,
42577
45330
  Input,