@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.
@@ -230,6 +230,7 @@ var BitType2 = {
230
230
  dangerAlt: "danger-alt",
231
231
  dangerCollapsible: "danger-collapsible",
232
232
  definitionList: "definition-list",
233
+ definitionListCollapsible: "definition-list-collapsible",
233
234
  definitionTerm: "definition-term",
234
235
  deleted: "deleted",
235
236
  details: "details",
@@ -510,6 +511,8 @@ var BitType2 = {
510
511
  smartStandardArticleNonNormativeCollapsible: "smart-standard-article-non-normative-collapsible",
511
512
  smartStandardArticleNormative: "smart-standard-article-normative",
512
513
  smartStandardArticleNormativeCollapsible: "smart-standard-article-normative-collapsible",
514
+ smartStandardDefinitionListNonNormative: "smart-standard-definition-list-non-normative",
515
+ smartStandardDefinitionListNormative: "smart-standard-definition-list-normative",
513
516
  smartStandardExampleNonNormative: "smart-standard-example-non-normative",
514
517
  smartStandardExampleNonNormativeCollapsible: "smart-standard-example-non-normative-collapsible",
515
518
  smartStandardExampleNormative: "smart-standard-example-normative",
@@ -560,6 +563,8 @@ var BitType2 = {
560
563
  smartStandardTableNormativeCollapsible: "smart-standard-table-normative-collapsible",
561
564
  standardArticleNonNormative: "standard-article-non-normative",
562
565
  standardArticleNormative: "standard-article-normative",
566
+ standardDefinitionListNonNormative: "standard-definition-list-non-normative",
567
+ standardDefinitionListNormative: "standard-definition-list-normative",
563
568
  standardExampleNonNormative: "standard-example-non-normative",
564
569
  standardExampleNormative: "standard-example-normative",
565
570
  standardImageFigureNonNormative: "standard-image-figure-non-normative",
@@ -729,10 +734,13 @@ var TextFormat2 = {
729
734
  import "@ncoderz/superenum";
730
735
  var CardSetConfigKey = {
731
736
  flashcard: "flashcard",
737
+ flashcard1: "flashcard1",
732
738
  definitionList: "definitionList",
739
+ definitionListPlain: "definitionListPlain",
733
740
  elements: "elements",
734
741
  statements: "statements",
735
742
  quiz: "quiz",
743
+ quizResponses: "quizResponses",
736
744
  feedback: "feedback",
737
745
  questions: "questions",
738
746
  matchPairs: "matchPairs",
@@ -744,6 +752,7 @@ var CardSetConfigKey = {
744
752
  pronunciationTable: "pronunciationTable",
745
753
  botActionResponses: "botActionResponses",
746
754
  exampleBitList: "exampleBitList",
755
+ pageFooterSections: "pageFooterSections",
747
756
  clozeList: "clozeList",
748
757
  ingredients: "ingredients",
749
758
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -1160,12 +1169,17 @@ var groupKeys = {
1160
1169
  group_standardAllBits: "group_standardAllBits",
1161
1170
  group_standardItemLead: "group_standardItemLead",
1162
1171
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1172
+ group_standardInstructionHint: "group_standardInstructionHint",
1163
1173
  group_standardTags: "group_standardTags",
1174
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1164
1175
  group_imageSource: "group_imageSource",
1165
1176
  group_technicalTerm: "group_technicalTerm",
1166
1177
  group_person: "group_person",
1167
1178
  group_gap: "group_gap",
1168
1179
  group_trueFalse: "group_trueFalse",
1180
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1181
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1182
+ group_trueFalseResponses: "group_trueFalseResponses",
1169
1183
  group_markConfig: "group_markConfig",
1170
1184
  group_mark: "group_mark",
1171
1185
  group_bookCommon: "group_bookCommon",
@@ -1173,14 +1187,18 @@ var groupKeys = {
1173
1187
  group_advertisingCommon: "group_advertisingCommon",
1174
1188
  group_platformStylesCommon: "group_platformStylesCommon",
1175
1189
  group_quizCommon: "group_quizCommon",
1190
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1191
+ group_imageNoZoom: "group_imageNoZoom",
1176
1192
  group_resourceBitTags: "group_resourceBitTags",
1177
1193
  group_resourceCommon: "group_resourceCommon",
1178
1194
  group_resourceImageCommon: "group_resourceImageCommon",
1195
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1179
1196
  group_resourceAudioCommon: "group_resourceAudioCommon",
1180
1197
  group_resourceVideoCommon: "group_resourceVideoCommon",
1181
1198
  group_resourceIcon: "group_resourceIcon",
1182
1199
  // Alias for image
1183
1200
  group_resourceImage: "group_resourceImage",
1201
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1184
1202
  group_resourceImageEmbed: "group_resourceImageEmbed",
1185
1203
  group_resourceImageLink: "group_resourceImageLink",
1186
1204
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1332,6 +1350,7 @@ var propertyKeys = {
1332
1350
  property_enforceUpdateOverUserInput: "@enforceUpdateOverUserInput",
1333
1351
  property_extractorExtractionTimestamp: "@extractorExtractionTimestamp",
1334
1352
  property_isCaseSensitive: "@isCaseSensitive",
1353
+ property_isCollapsible: "@isCollapsible",
1335
1354
  property_isInfoOnly: "@isInfoOnly",
1336
1355
  property_isPublic: "@isPublic",
1337
1356
  property_isTemplate: "@isTemplate",
@@ -1457,6 +1476,7 @@ var propertyKeys = {
1457
1476
  property_src4x: "@src4x",
1458
1477
  property_srcAlt: "@srcAlt",
1459
1478
  property_stripePricingTableId: "@stripePricingTableId",
1479
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1460
1480
  property_stripePublishableKey: "@stripePublishableKey",
1461
1481
  property_subject: "@subject",
1462
1482
  property_subtype: "@subtype",
@@ -2208,7 +2228,7 @@ var CARDSETS = {
2208
2228
  variants: [
2209
2229
  {
2210
2230
  jsonKey: "question.text",
2211
- exportJsonKey: { question: { text: "$" } },
2231
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2212
2232
  tags: [
2213
2233
  {
2214
2234
  key: ConfigKey.group_standardTags,
@@ -2224,6 +2244,136 @@ var CARDSETS = {
2224
2244
  description: "Icon resource for the flashcard.",
2225
2245
  jsonKey: "question.icon|resource(type=image, key=image)",
2226
2246
  exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2247
+ },
2248
+ {
2249
+ key: ConfigKey.property_example,
2250
+ exportJsonKey: [
2251
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2252
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2253
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2254
+ ],
2255
+ description: "Example marker for the flashcard question.",
2256
+ format: TagFormat.bitmarkText,
2257
+ nullable: true
2258
+ }
2259
+ ]
2260
+ }
2261
+ ]
2262
+ },
2263
+ {
2264
+ name: "answer",
2265
+ variants: [
2266
+ {
2267
+ jsonKey: "answer.text",
2268
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2269
+ tags: [
2270
+ {
2271
+ key: ConfigKey.group_standardTags,
2272
+ description: "Standard tags for the flashcard."
2273
+ },
2274
+ {
2275
+ exportJsonKey: { title: "$" },
2276
+ key: ConfigKey.tag_title,
2277
+ description: "Title of the flashcard."
2278
+ },
2279
+ {
2280
+ key: ConfigKey.group_resourceIcon,
2281
+ description: "Icon resource for the flashcard.",
2282
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2283
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2284
+ },
2285
+ {
2286
+ key: ConfigKey.property_example,
2287
+ exportJsonKey: [
2288
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2289
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2290
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2291
+ ],
2292
+ description: "Example marker for the flashcard answer.",
2293
+ format: TagFormat.bitmarkText,
2294
+ nullable: true
2295
+ }
2296
+ ]
2297
+ },
2298
+ {
2299
+ jsonKey: "alternativeAnswers[].text",
2300
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2301
+ tags: [
2302
+ {
2303
+ key: ConfigKey.group_standardTags,
2304
+ description: "Standard tags for the flashcard."
2305
+ },
2306
+ {
2307
+ exportJsonKey: { title: "$" },
2308
+ key: ConfigKey.tag_title,
2309
+ description: "Title of the flashcard."
2310
+ },
2311
+ {
2312
+ key: ConfigKey.group_resourceIcon,
2313
+ description: "Icon resource for the flashcard.",
2314
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2315
+ exportJsonKey: {
2316
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2317
+ }
2318
+ }
2319
+ ],
2320
+ repeatCount: Count.infinity
2321
+ }
2322
+ ]
2323
+ }
2324
+ ]
2325
+ },
2326
+ //
2327
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2328
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2329
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2330
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2331
+ // cardset.
2332
+ //
2333
+ [CardSetConfigKey.flashcard1]: {
2334
+ jsonKey: "cards",
2335
+ exportJsonKey: { cards: "$" },
2336
+ sections: {
2337
+ default: {
2338
+ jsonKey: "cards",
2339
+ exportJsonKey: { cards: "$" },
2340
+ isDefault: true,
2341
+ maxCount: 1
2342
+ }
2343
+ },
2344
+ sides: [
2345
+ {
2346
+ name: "question",
2347
+ variants: [
2348
+ {
2349
+ jsonKey: "question.text",
2350
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2351
+ tags: [
2352
+ {
2353
+ key: ConfigKey.group_standardTags,
2354
+ description: "Standard tags for the flashcard."
2355
+ },
2356
+ {
2357
+ exportJsonKey: { title: "$" },
2358
+ key: ConfigKey.tag_title,
2359
+ description: "Title of the flashcard."
2360
+ },
2361
+ {
2362
+ key: ConfigKey.group_resourceIcon,
2363
+ description: "Icon resource for the flashcard.",
2364
+ jsonKey: "question.icon|resource(type=image, key=image)",
2365
+ exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2366
+ },
2367
+ {
2368
+ key: ConfigKey.property_example,
2369
+ exportJsonKey: [
2370
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2371
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2372
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2373
+ ],
2374
+ description: "Example marker for the flashcard question.",
2375
+ format: TagFormat.bitmarkText,
2376
+ nullable: true
2227
2377
  }
2228
2378
  ]
2229
2379
  }
@@ -2234,7 +2384,7 @@ var CARDSETS = {
2234
2384
  variants: [
2235
2385
  {
2236
2386
  jsonKey: "answer.text",
2237
- exportJsonKey: { answer: { text: "$" } },
2387
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2238
2388
  tags: [
2239
2389
  {
2240
2390
  key: ConfigKey.group_standardTags,
@@ -2250,6 +2400,17 @@ var CARDSETS = {
2250
2400
  description: "Icon resource for the flashcard.",
2251
2401
  jsonKey: "answer.icon|resource(type=image, key=image)",
2252
2402
  exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2403
+ },
2404
+ {
2405
+ key: ConfigKey.property_example,
2406
+ exportJsonKey: [
2407
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2408
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2409
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2410
+ ],
2411
+ description: "Example marker for the flashcard answer.",
2412
+ format: TagFormat.bitmarkText,
2413
+ nullable: true
2253
2414
  }
2254
2415
  ]
2255
2416
  },
@@ -2293,7 +2454,7 @@ var CARDSETS = {
2293
2454
  variants: [
2294
2455
  {
2295
2456
  jsonKey: "term.text",
2296
- exportJsonKey: { term: { text: "$" } },
2457
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2297
2458
  tags: [
2298
2459
  {
2299
2460
  key: ConfigKey.group_standardTags,
@@ -2312,6 +2473,17 @@ var CARDSETS = {
2312
2473
  format: TagFormat.plainText,
2313
2474
  jsonKey: "term.icon|resource(type=image, key=image)",
2314
2475
  exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2476
+ },
2477
+ {
2478
+ key: ConfigKey.property_example,
2479
+ exportJsonKey: [
2480
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2481
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2482
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2483
+ ],
2484
+ description: "Example marker for the definition term.",
2485
+ format: TagFormat.bitmarkText,
2486
+ nullable: true
2315
2487
  }
2316
2488
  ]
2317
2489
  }
@@ -2322,7 +2494,7 @@ var CARDSETS = {
2322
2494
  variants: [
2323
2495
  {
2324
2496
  jsonKey: "definition.text",
2325
- exportJsonKey: { definition: { text: "$" } },
2497
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2326
2498
  tags: [
2327
2499
  {
2328
2500
  key: ConfigKey.group_standardTags,
@@ -2340,6 +2512,17 @@ var CARDSETS = {
2340
2512
  description: "Icon resource for the definition.",
2341
2513
  jsonKey: "definition.icon|resource(type=image, key=image)",
2342
2514
  exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2515
+ },
2516
+ {
2517
+ key: ConfigKey.property_example,
2518
+ exportJsonKey: [
2519
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2520
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2521
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2522
+ ],
2523
+ description: "Example marker for the definition.",
2524
+ format: TagFormat.bitmarkText,
2525
+ nullable: true
2343
2526
  }
2344
2527
  ]
2345
2528
  },
@@ -2372,6 +2555,126 @@ var CARDSETS = {
2372
2555
  ]
2373
2556
  },
2374
2557
  //
2558
+ // definition-list-plain
2559
+ //
2560
+ // Same shape as `definitionList`, but every variant body is rendered as
2561
+ // plain text (string), not bitmark+ AST. Used by
2562
+ // `meta-search-default-terms` / `meta-search-default-topics`, which need
2563
+ // term/definition text emitted verbatim instead of as a ProseMirror tree.
2564
+ //
2565
+ [CardSetConfigKey.definitionListPlain]: {
2566
+ jsonKey: "definitions",
2567
+ exportJsonKey: { definitions: "$" },
2568
+ sides: [
2569
+ {
2570
+ name: "term",
2571
+ variants: [
2572
+ {
2573
+ jsonKey: "term.text",
2574
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2575
+ format: TextFormat2.plainText,
2576
+ tags: [
2577
+ {
2578
+ key: ConfigKey.group_standardTags,
2579
+ description: "Standard tags for the definition."
2580
+ },
2581
+ {
2582
+ key: ConfigKey.tag_title,
2583
+ description: "Title of the definition.",
2584
+ format: TagFormat.plainText,
2585
+ jsonKey: "^heading.forKeys",
2586
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2587
+ },
2588
+ {
2589
+ key: ConfigKey.group_resourceIcon,
2590
+ description: "Icon resource for the definition.",
2591
+ format: TagFormat.plainText,
2592
+ jsonKey: "term.icon|resource(type=image, key=image)",
2593
+ exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2594
+ },
2595
+ {
2596
+ key: ConfigKey.property_example,
2597
+ exportJsonKey: [
2598
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2599
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2600
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2601
+ ],
2602
+ description: "Example marker for the definition term.",
2603
+ format: TagFormat.bitmarkText,
2604
+ nullable: true
2605
+ }
2606
+ ]
2607
+ }
2608
+ ]
2609
+ },
2610
+ {
2611
+ name: "definition",
2612
+ variants: [
2613
+ {
2614
+ jsonKey: "definition.text",
2615
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2616
+ format: TextFormat2.plainText,
2617
+ tags: [
2618
+ {
2619
+ key: ConfigKey.group_standardTags,
2620
+ description: "Standard tags for the definition."
2621
+ },
2622
+ {
2623
+ key: ConfigKey.tag_title,
2624
+ description: "Title of the definition.",
2625
+ format: TagFormat.plainText,
2626
+ jsonKey: "^heading.forValues",
2627
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2628
+ },
2629
+ {
2630
+ key: ConfigKey.group_resourceIcon,
2631
+ description: "Icon resource for the definition.",
2632
+ jsonKey: "definition.icon|resource(type=image, key=image)",
2633
+ exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2634
+ },
2635
+ {
2636
+ key: ConfigKey.property_example,
2637
+ exportJsonKey: [
2638
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2639
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2640
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2641
+ ],
2642
+ description: "Example marker for the definition.",
2643
+ format: TagFormat.bitmarkText,
2644
+ nullable: true
2645
+ }
2646
+ ]
2647
+ },
2648
+ {
2649
+ jsonKey: "alternativeDefinitions[].text",
2650
+ exportJsonKey: { alternativeDefinitions: [{ text: "$" }] },
2651
+ format: TextFormat2.plainText,
2652
+ tags: [
2653
+ {
2654
+ key: ConfigKey.group_standardTags,
2655
+ description: "Standard tags for the definition."
2656
+ },
2657
+ {
2658
+ exportJsonKey: { title: "$" },
2659
+ key: ConfigKey.tag_title,
2660
+ description: "Title of the definition."
2661
+ },
2662
+ {
2663
+ key: ConfigKey.group_resourceIcon,
2664
+ description: "Icon resource for the definition.",
2665
+ jsonKey: "alternativeDefinitions[].icon|resource(type=image, key=image)",
2666
+ exportJsonKey: {
2667
+ alternativeDefinitions: [{ icon: { type: "image", image: { src: "$" } } }]
2668
+ }
2669
+ }
2670
+ ],
2671
+ repeatCount: Count.infinity
2672
+ }
2673
+ ]
2674
+ }
2675
+ ]
2676
+ },
2677
+ //
2375
2678
  // match-pairs
2376
2679
  //
2377
2680
  [CardSetConfigKey.matchPairs]: {
@@ -2384,11 +2687,51 @@ var CARDSETS = {
2384
2687
  {
2385
2688
  jsonKey: "key",
2386
2689
  exportJsonKey: { key: "$" },
2690
+ format: TextFormat2.plainText,
2387
2691
  tags: [
2388
2692
  {
2389
2693
  key: ConfigKey.group_standardTags,
2390
2694
  description: "Standard tags for the match pair."
2391
2695
  },
2696
+ {
2697
+ // Pair-level example: emit `isExample` alongside `example`
2698
+ // (matches OLD parser's per-pair allow-list behaviour).
2699
+ //
2700
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2701
+ // in the cardset) fills each pair's `example` with `V`
2702
+ // once per pair from the keys-side cascade fire (single
2703
+ // parsed_variant per pair — no per-value duplication).
2704
+ //
2705
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2706
+ // at the pair-object level. BPG `parseMatchPairs`
2707
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2708
+ // per PAIR (the `exampleCard` accumulator), placed at the
2709
+ // pair object. Unlike matchMatrix (per-cell example),
2710
+ // matchPairs has no nested per-cell layer, so the pair-key
2711
+ // `[@example:V]` must double-emit as the visible field.
2712
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2713
+ // expects `example: "packen"` at pair-object level from
2714
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2715
+ // draft applied to both keys-side tags; the matchPairs
2716
+ // half was reverted after fixture parity testing.
2717
+ key: ConfigKey.property_example,
2718
+ exportJsonKey: [
2719
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2720
+ {
2721
+ predicates: ["@absent", { $cascade: "*" }],
2722
+ rule: {
2723
+ isExample: true,
2724
+ example: "$cascade",
2725
+ "@bit": { isExample: true }
2726
+ }
2727
+ },
2728
+ { "@absent": { isExample: true } },
2729
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2730
+ ],
2731
+ description: "Example marker / value on the match pair.",
2732
+ format: TagFormat.bitmarkText,
2733
+ nullable: true
2734
+ },
2392
2735
  {
2393
2736
  key: ConfigKey.tag_title,
2394
2737
  description: "Title of the match pair.",
@@ -2397,9 +2740,19 @@ var CARDSETS = {
2397
2740
  exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2398
2741
  },
2399
2742
  {
2743
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2744
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2745
+ // only fires when NO variant in the active card explicitly
2746
+ // carries `[@isCaseSensitive]`. When a value-side variant
2747
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2748
+ // synth is skipped here; the value-side explicit fire owns
2749
+ // the pair-level emission.
2400
2750
  key: ConfigKey.property_isCaseSensitive,
2401
2751
  exportJsonKey: [
2402
- { "@absent": { isCaseSensitive: "$ancestor" } },
2752
+ {
2753
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2754
+ rule: { isCaseSensitive: "$ancestor" }
2755
+ },
2403
2756
  { isCaseSensitive: "$" }
2404
2757
  ],
2405
2758
  description: "Property to indicate if the match is case sensitive.",
@@ -2428,10 +2781,85 @@ var CARDSETS = {
2428
2781
  {
2429
2782
  jsonKey: "values[]",
2430
2783
  exportJsonKey: { values: ["$"] },
2784
+ format: TextFormat2.plainText,
2431
2785
  tags: [
2432
2786
  {
2433
- key: ConfigKey.group_standardTags,
2434
- description: "Standard tags for the match pair."
2787
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2788
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2789
+ // for sideIdx > 0; the schema-side equivalent is to use a
2790
+ // group_standardTags variant that omits group_standardItemLead.
2791
+ key: ConfigKey.group_standardTagsNoItemLead,
2792
+ description: "Standard tags for the match pair (values side, no item/lead)."
2793
+ },
2794
+ {
2795
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2796
+ // `@example` from the bit header fills each pair's
2797
+ // `example` with the pair's first value (mirrors BPG's
2798
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2799
+ //
2800
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2801
+ // TAG 415 (keys side) which fires once per pair — keeping
2802
+ // it here would emit `example` twice per pair (one per
2803
+ // values-side parsed_variant) and merge them into a
2804
+ // multi-paragraph array.
2805
+ //
2806
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2807
+ // source `$` value (the current `++`-iteration's value
2808
+ // text). For single-value pairs this matches the old
2809
+ // `$parent.values[0]`; for multi-value pairs with
2810
+ // `[@example]` attached to a non-first value, it correctly
2811
+ // resolves to that value.
2812
+ //
2813
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2814
+ // routes pair-key `[@example:V]` (a per-pair cascade
2815
+ // source, bound via per-card CascadeCounters scope) and
2816
+ // bit-header `[@example:V]` (cardset cascade) into the
2817
+ // pair-level `example` via `$cascade`. The aggregator in
2818
+ // `populate_variants` fires this rule ONCE per pair (BPG
2819
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2820
+ // per-iter pollution.
2821
+ key: ConfigKey.property_example,
2822
+ exportJsonKey: [
2823
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2824
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2825
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2826
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2827
+ // scope opened by `build_card_item`.
2828
+ {
2829
+ predicates: ["@keyonly"],
2830
+ maxEmits: 1,
2831
+ rule: {
2832
+ isExample: true,
2833
+ example: "$parent",
2834
+ "@bit": { isExample: true }
2835
+ }
2836
+ },
2837
+ {
2838
+ predicates: ["@absent", { $cascade: "*" }],
2839
+ rule: {
2840
+ isExample: true,
2841
+ example: "$cascade",
2842
+ "@bit": { isExample: true }
2843
+ }
2844
+ },
2845
+ {
2846
+ "@absent": {
2847
+ isExample: true,
2848
+ example: "$parent",
2849
+ "@bit": { isExample: true }
2850
+ }
2851
+ },
2852
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2853
+ // form so per-`++`-iter values-side fires also first-win.
2854
+ {
2855
+ predicates: [],
2856
+ maxEmits: 1,
2857
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2858
+ }
2859
+ ],
2860
+ description: "Example marker / value on the match pair (values side).",
2861
+ format: TagFormat.bitmarkText,
2862
+ nullable: true
2435
2863
  },
2436
2864
  {
2437
2865
  key: ConfigKey.tag_title,
@@ -2441,9 +2869,13 @@ var CARDSETS = {
2441
2869
  exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2442
2870
  },
2443
2871
  {
2872
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2444
2873
  key: ConfigKey.property_isCaseSensitive,
2445
2874
  exportJsonKey: [
2446
- { "@absent": { isCaseSensitive: "$ancestor" } },
2875
+ {
2876
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2877
+ rule: { isCaseSensitive: "$ancestor" }
2878
+ },
2447
2879
  { isCaseSensitive: "$" }
2448
2880
  ],
2449
2881
  description: "Property to indicate if the match is case sensitive.",
@@ -2469,6 +2901,7 @@ var CARDSETS = {
2469
2901
  {
2470
2902
  jsonKey: "key",
2471
2903
  exportJsonKey: { key: "$" },
2904
+ format: TextFormat2.plainText,
2472
2905
  tags: [
2473
2906
  {
2474
2907
  key: ConfigKey.group_standardTags,
@@ -2498,10 +2931,12 @@ var CARDSETS = {
2498
2931
  {
2499
2932
  jsonKey: "values[]",
2500
2933
  exportJsonKey: { values: ["$"] },
2934
+ format: TextFormat2.plainText,
2501
2935
  tags: [
2502
2936
  {
2503
- key: ConfigKey.group_standardTags,
2504
- description: "Standard tags for the audio match pair."
2937
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2938
+ key: ConfigKey.group_standardTagsNoItemLead,
2939
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2505
2940
  },
2506
2941
  {
2507
2942
  key: ConfigKey.tag_title,
@@ -2534,6 +2969,7 @@ var CARDSETS = {
2534
2969
  {
2535
2970
  jsonKey: "key",
2536
2971
  exportJsonKey: { key: "$" },
2972
+ format: TextFormat2.plainText,
2537
2973
  tags: [
2538
2974
  {
2539
2975
  key: ConfigKey.group_standardTags,
@@ -2563,10 +2999,12 @@ var CARDSETS = {
2563
2999
  {
2564
3000
  jsonKey: "values[]",
2565
3001
  exportJsonKey: { values: ["$"] },
3002
+ format: TextFormat2.plainText,
2566
3003
  tags: [
2567
3004
  {
2568
- key: ConfigKey.group_standardTags,
2569
- description: "Standard tags for the image match pair."
3005
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
3006
+ key: ConfigKey.group_standardTagsNoItemLead,
3007
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2570
3008
  },
2571
3009
  {
2572
3010
  key: ConfigKey.tag_title,
@@ -2599,20 +3037,49 @@ var CARDSETS = {
2599
3037
  {
2600
3038
  jsonKey: "key",
2601
3039
  exportJsonKey: { key: "$" },
3040
+ format: TextFormat2.plainText,
2602
3041
  tags: [
2603
3042
  {
2604
3043
  key: ConfigKey.group_standardItemLeadInstructionHint,
2605
3044
  description: "Standard tags for lead, instruction, and hint."
2606
3045
  },
2607
3046
  {
3047
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
3048
+ // `[@example:V]` is consumed as the per-pair cascade source.
3049
+ // PLAN-077 R14 follow-up: investigation showed the fixture
3050
+ // reference is INCONSISTENT — bits without a cardset-level
3051
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
3052
+ // pair-level `example: V` at pair scope, but bits WITH a
3053
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
3054
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
3055
+ // example. Emitting `example: $` here satisfies bit 3 but
3056
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
3057
+ // 3. Conditional emission would require runtime gating on
3058
+ // the cardset cascade scope state. Left dropped pending a
3059
+ // dedicated plan — accepted divergence on bit 3's pair-
3060
+ // level emission (cells still get the per-cell `example`
3061
+ // correctly via PLAN-077 §4.2 + §4.3).
2608
3062
  key: ConfigKey.property_example,
2609
3063
  exportJsonKey: [
2610
- { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2611
- { "@absent": { isExample: true, "@bit": { isExample: true } } },
2612
- { isExample: true, example: "$", "@bit": { isExample: true } }
3064
+ {
3065
+ "@keyonly": {
3066
+ isExample: true,
3067
+ "@bit": { isExample: true },
3068
+ "@card": { isExample: true }
3069
+ }
3070
+ },
3071
+ {
3072
+ "@absent": {
3073
+ isExample: true,
3074
+ "@bit": { isExample: true },
3075
+ "@card": { isExample: true }
3076
+ }
3077
+ },
3078
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2613
3079
  ],
2614
3080
  description: "Example text for the match matrix.",
2615
- format: TagFormat.plainText
3081
+ format: TagFormat.plainText,
3082
+ nullable: true
2616
3083
  },
2617
3084
  {
2618
3085
  key: ConfigKey.tag_title,
@@ -2640,35 +3107,96 @@ var CARDSETS = {
2640
3107
  variants: [
2641
3108
  {
2642
3109
  jsonKey: "cells[{s}].values[]",
2643
- exportJsonKey: { cells: { $s: { values: ["$"] } } },
3110
+ exportJsonKey: [
3111
+ // PLAN-085 / R14: the positional-blank emission for an
3112
+ // effectively-absent cell side fires ONLY when the active
3113
+ // card has at least one `[#]` variant fire. This matches
3114
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
3115
+ // `[#]` exists anywhere in the title card, BPG returns
3116
+ // undefined and no `heading` is emitted. The phantom
3117
+ // `forValues:['']` placeholder is appropriate only when
3118
+ // the card is being treated as a title-row.
3119
+ {
3120
+ predicates: ["@absent", { "@variantHasTag": "#" }],
3121
+ rule: { "@bit": { heading: { forValues: [""] } } }
3122
+ },
3123
+ { cells: { $s: { values: ["$"] } } }
3124
+ ],
3125
+ format: TextFormat2.plainText,
2644
3126
  tags: [
2645
3127
  {
2646
3128
  key: ConfigKey.group_standardItemLeadInstructionHint,
2647
3129
  description: "Standard tags for lead, instruction, and hint."
2648
3130
  },
2649
3131
  {
3132
+ // PLAN-075: bare `$parent` reads the parent variant fire's
3133
+ // source `$` value (the current `++`-iteration's cell text).
3134
+ // Replaces `$parent.values[0]` which pinned to position 0
3135
+ // regardless of which `++`-value `[@example]` was on.
3136
+ //
3137
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
3138
+ // routes pair-key `[@example:V]` (a per-pair cascade
3139
+ // source, bound via per-card CascadeCounters scope) and
3140
+ // bit-header `[@example:V]` (cardset cascade) into the
3141
+ // per-cell `example` via `$cascade`. The aggregator in
3142
+ // `populate_variants` fires this rule ONCE per cell (BPG
3143
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
3144
+ // per-iter pollution.
3145
+ // @card scope-shift writes matrix-row container isExample.
2650
3146
  key: ConfigKey.property_example,
2651
3147
  exportJsonKey: [
2652
3148
  {
2653
3149
  "@keyonly": {
2654
- cells: { $s: { isExample: true, example: "$parent.values[0]" } },
2655
- "@bit": { isExample: true }
3150
+ cells: { $s: { isExample: true, example: "$parent" } },
3151
+ "@bit": { isExample: true },
3152
+ "@card": { isExample: true }
2656
3153
  }
2657
3154
  },
2658
3155
  {
2659
- "@absent": { cells: { $s: { isExample: true, example: "$parent.values[0]" } } }
3156
+ predicates: ["@absent", { $cascade: "*" }],
3157
+ rule: {
3158
+ cells: { $s: { isExample: true, example: "$cascade" } },
3159
+ "@bit": { isExample: true },
3160
+ "@card": { isExample: true }
3161
+ }
2660
3162
  },
2661
- { cells: { $s: { isExample: true, example: "$" } }, "@bit": { isExample: true } }
3163
+ {
3164
+ "@absent": {
3165
+ cells: { $s: { isExample: true, example: "$parent" } },
3166
+ "@bit": { isExample: true },
3167
+ "@card": { isExample: true }
3168
+ }
3169
+ },
3170
+ {
3171
+ cells: { $s: { isExample: true, example: "$" } },
3172
+ "@bit": { isExample: true },
3173
+ "@card": { isExample: true }
3174
+ }
2662
3175
  ],
2663
3176
  description: "Example text for the match matrix.",
2664
- format: TagFormat.plainText
3177
+ format: TagFormat.bitmarkText,
3178
+ nullable: true
2665
3179
  },
2666
3180
  {
3181
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3182
+ // empty-string positional blank into `forValues` so the
3183
+ // array stays aligned with the cell positions (matches BPG
3184
+ // `extractHeadingCard`'s "all value-side headings present"
3185
+ // shape). The `@keyonly` rule is required for two reasons:
3186
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3187
+ // in the unconditional rule has no source for bare); and
3188
+ // 2) the presence of any `@keyonly` rule disables the
3189
+ // optimised-mode `should_suppress_outcome` strip (see
3190
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3191
+ // survives the all-natural-defaults check.
2667
3192
  key: ConfigKey.tag_title,
2668
3193
  description: "Title of the match matrix.",
2669
3194
  format: TagFormat.plainText,
2670
3195
  jsonKey: "^heading.forValues",
2671
- exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
3196
+ exportJsonKey: [
3197
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3198
+ { "@bit": { heading: { forValues: ["$"] } } }
3199
+ ]
2672
3200
  },
2673
3201
  {
2674
3202
  key: ConfigKey.property_isCaseSensitive,
@@ -2717,6 +3245,35 @@ var CARDSETS = {
2717
3245
  {
2718
3246
  key: ConfigKey.group_standardTags,
2719
3247
  description: "Standard tags for the statement."
3248
+ },
3249
+ {
3250
+ // PLAN-072: cascade-fired `@example` from the bit header
3251
+ // fills each statement entry's `example` with the entry's
3252
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3253
+ // for the statements cardset. Per-array-item strip removes
3254
+ // `example: false` from incorrect entries so only correct
3255
+ // entries retain `example: true`.
3256
+ key: ConfigKey.property_example,
3257
+ exportJsonKey: [
3258
+ {
3259
+ "@keyonly": {
3260
+ isExample: true,
3261
+ example: true,
3262
+ "@bit": { isExample: true }
3263
+ }
3264
+ },
3265
+ {
3266
+ "@absent": {
3267
+ isExample: true,
3268
+ example: "$parent.isCorrect",
3269
+ "@bit": { isExample: true }
3270
+ }
3271
+ },
3272
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3273
+ ],
3274
+ 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`.",
3275
+ format: TagFormat.boolean,
3276
+ nullable: true
2720
3277
  }
2721
3278
  ],
2722
3279
  bodyAllowed: false
@@ -2745,6 +3302,71 @@ var CARDSETS = {
2745
3302
  {
2746
3303
  key: ConfigKey.group_standardTags,
2747
3304
  description: "Standard tags for the quiz."
3305
+ },
3306
+ {
3307
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3308
+ // quiz choices is delegated to group_trueFalse's
3309
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3310
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3311
+ // would over-fire on subsequent correct choices and on
3312
+ // incorrect ones — drop it.
3313
+ key: ConfigKey.property_example,
3314
+ exportJsonKey: [
3315
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3316
+ { isExample: true, "@bit": { isExample: true } }
3317
+ ],
3318
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3319
+ format: TagFormat.boolean,
3320
+ nullable: true
3321
+ }
3322
+ ],
3323
+ bodyAllowed: false
3324
+ }
3325
+ ]
3326
+ }
3327
+ ]
3328
+ },
3329
+ //
3330
+ // quiz (responses)
3331
+ //
3332
+ [CardSetConfigKey.quizResponses]: {
3333
+ jsonKey: "quizzes",
3334
+ exportJsonKey: { quizzes: "$" },
3335
+ sides: [
3336
+ {
3337
+ name: "responses",
3338
+ variants: [
3339
+ {
3340
+ jsonKey: null,
3341
+ tags: [
3342
+ {
3343
+ key: ConfigKey.group_trueFalseResponses,
3344
+ description: "Group for true/false questions."
3345
+ },
3346
+ {
3347
+ key: ConfigKey.group_standardTags,
3348
+ description: "Standard tags for the quiz."
3349
+ },
3350
+ {
3351
+ // PLAN-072: cascade-fired `@example` from the bit header
3352
+ // fills each response entry's `example` with the entry's
3353
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3354
+ // for the quiz-responses cardset (statements-style).
3355
+ key: ConfigKey.property_example,
3356
+ exportJsonKey: [
3357
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3358
+ {
3359
+ "@absent": {
3360
+ isExample: true,
3361
+ example: "$parent.isCorrect",
3362
+ "@bit": { isExample: true }
3363
+ }
3364
+ },
3365
+ { isExample: true, "@bit": { isExample: true } }
3366
+ ],
3367
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3368
+ format: TagFormat.boolean,
3369
+ nullable: true
2748
3370
  }
2749
3371
  ],
2750
3372
  bodyAllowed: false
@@ -2770,6 +3392,73 @@ var CARDSETS = {
2770
3392
  key: ConfigKey.group_standardItemLeadInstructionHint,
2771
3393
  description: "Standard tags for lead, instruction, and hint."
2772
3394
  },
3395
+ {
3396
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3397
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3398
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3399
+ // feedback-specific shape.
3400
+ //
3401
+ // Chain @example: `[+ X][@example]` writes
3402
+ // `isExample/example` onto the X choice only. BPG's
3403
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3404
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3405
+ // there is no bubble to bit-level or feedbacks-level
3406
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3407
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3408
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3409
+ // no feedback dispatch, so bit-header `[@example]` does
3410
+ // not flow into feedback choices.
3411
+ key: ConfigKey.tag_true,
3412
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3413
+ description: "Reason-required choice for feedback (`[+]`).",
3414
+ maxCount: Count.infinity,
3415
+ format: TagFormat.plainText,
3416
+ chain: [
3417
+ {
3418
+ key: ConfigKey.property_example,
3419
+ exportJsonKey: [
3420
+ { "@keyonly": { isExample: true, example: true } },
3421
+ { isExample: true, example: "$" }
3422
+ ],
3423
+ description: "Per-choice example marker for feedback `[+]`.",
3424
+ format: TagFormat.boolean,
3425
+ maxCount: 1,
3426
+ nullable: true
3427
+ },
3428
+ {
3429
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3430
+ // choice; same for `[?]` (hint), `[%]` (item),
3431
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3432
+ // Mirrors group_trueFalse's `+` chain shape.
3433
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3434
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3435
+ }
3436
+ ]
3437
+ },
3438
+ {
3439
+ key: ConfigKey.tag_false,
3440
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3441
+ description: "Reason-not-required choice for feedback (`[-]`).",
3442
+ maxCount: Count.infinity,
3443
+ format: TagFormat.plainText,
3444
+ chain: [
3445
+ {
3446
+ key: ConfigKey.property_example,
3447
+ exportJsonKey: [
3448
+ { "@keyonly": { isExample: true, example: true } },
3449
+ { isExample: true, example: "$" }
3450
+ ],
3451
+ description: "Per-choice example marker for feedback `[-]`.",
3452
+ format: TagFormat.boolean,
3453
+ maxCount: 1,
3454
+ nullable: true
3455
+ },
3456
+ {
3457
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3458
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3459
+ }
3460
+ ]
3461
+ },
2773
3462
  {
2774
3463
  key: ConfigKey.group_trueFalse,
2775
3464
  description: "Group for true/false feedback."
@@ -2790,31 +3479,71 @@ var CARDSETS = {
2790
3479
  name: "reason",
2791
3480
  variants: [
2792
3481
  {
3482
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3483
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3484
+ // under `reason.*`; each tag here spells out the full path.
3485
+ // Body is a plain string (BPG `parseFeedback` uses
3486
+ // `cardBodyStr ?? ''` for `reason.text`).
2793
3487
  jsonKey: "reason.text",
2794
3488
  exportJsonKey: { reason: { text: "$" } },
3489
+ format: TextFormat2.plainText,
2795
3490
  tags: [
2796
3491
  {
2797
- key: ConfigKey.group_standardItemLeadInstructionHint,
2798
- description: "Standard tags for lead, instruction, and hint."
3492
+ // Instruction lands under reason. Hint/item/lead/etc.
3493
+ // omitted: not exercised by any fixture and BPG's
3494
+ // `parseFeedback` spreads only the tags actually present
3495
+ // — adding the rest can be done lazily as fixtures
3496
+ // require them, with the same `{reason: {…}}` wrapper.
3497
+ key: ConfigKey.tag_instruction,
3498
+ exportJsonKey: { reason: { instruction: "$" } },
3499
+ description: "Instruction for the feedback reason.",
3500
+ format: TagFormat.bitmarkText
2799
3501
  },
2800
3502
  {
2801
3503
  key: ConfigKey.property_reasonableNumOfChars,
2802
3504
  exportJsonKey: [
2803
- { "@absent": { reasonableNumOfChars: "$ancestor" } },
2804
- { reasonableNumOfChars: "$" }
3505
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3506
+ { reason: { reasonableNumOfChars: "$" } }
2805
3507
  ],
2806
3508
  description: "Property for reasonable number of characters.",
2807
3509
  format: TagFormat.number
2808
3510
  },
2809
3511
  {
3512
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3513
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3514
+ // cascade. Local emission only.
3515
+ //
3516
+ // Both `reason.example` and `feedbacks[].example` are
3517
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3518
+ // here is `bitmarkText` so `$` coerces to a paragraph
3519
+ // array at both write sites.
2810
3520
  key: ConfigKey.property_example,
2811
3521
  exportJsonKey: [
2812
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2813
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2814
- { isExample: true, example: "$", "@bit": { isExample: true } }
3522
+ {
3523
+ // Bare `[@example]`: reason.example is TextAst BPG
3524
+ // coerces the boolean default to a paragraph
3525
+ // containing the string "true". feedbacks[].example
3526
+ // is not TextAst, stays a boolean. Two literal writes.
3527
+ "@keyonly": {
3528
+ reason: {
3529
+ isExample: true,
3530
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3531
+ },
3532
+ isExample: true,
3533
+ example: true,
3534
+ "@bit": { isExample: true }
3535
+ }
3536
+ },
3537
+ {
3538
+ reason: { isExample: true, example: "$" },
3539
+ isExample: true,
3540
+ example: "$",
3541
+ "@bit": { isExample: true }
3542
+ }
2815
3543
  ],
2816
- description: "Example text for the feedback.",
2817
- format: TagFormat.plainText
3544
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3545
+ format: TagFormat.bitmarkText,
3546
+ nullable: true
2818
3547
  },
2819
3548
  {
2820
3549
  key: ConfigKey.tag_title,
@@ -2878,6 +3607,39 @@ var CARDSETS = {
2878
3607
  {
2879
3608
  key: ConfigKey.group_standardTags,
2880
3609
  description: "Standard tags for the question."
3610
+ },
3611
+ {
3612
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3613
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3614
+ // does not propagate `example`/`isExample` onto question
3615
+ // entries. Only entry-local `[@example]` fires.
3616
+ //
3617
+ // BPG's `BaseBuilder.toExample` for interview questions
3618
+ // (Builder.ts:2575,2588) uses `data.__sampleSolutionAst`
3619
+ // as `defaultExample` — bare `[@example]` (which sets
3620
+ // `__isDefaultExample=true, example=undefined`) emits the
3621
+ // parsed sample-solution AST in the `example` slot. The
3622
+ // `@keyonly` rule mirrors that runtime behaviour via
3623
+ // `$parent.sampleSolution` — the chain-walker's post-walk
3624
+ // retry resolves this once the sibling `[@sampleSolution]`
3625
+ // chain has merged into the shared scope (interview's
3626
+ // `[@example]` typically precedes `[@sampleSolution]` in
3627
+ // source). Format coercion turns the resolved string
3628
+ // value into a PM tree.
3629
+ key: ConfigKey.property_example,
3630
+ exportJsonKey: [
3631
+ {
3632
+ "@keyonly": {
3633
+ isExample: true,
3634
+ example: "$parent.sampleSolution",
3635
+ "@bit": { isExample: true }
3636
+ }
3637
+ },
3638
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3639
+ ],
3640
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3641
+ format: TagFormat.bitmarkText,
3642
+ nullable: true
2881
3643
  }
2882
3644
  ]
2883
3645
  }
@@ -2904,14 +3666,16 @@ var CARDSETS = {
2904
3666
  description: "Standard tags for lead, instruction, and hint."
2905
3667
  },
2906
3668
  {
3669
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3670
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2907
3671
  key: ConfigKey.property_example,
2908
3672
  exportJsonKey: [
2909
3673
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2910
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2911
3674
  { isExample: true, example: "$", "@bit": { isExample: true } }
2912
3675
  ],
2913
- description: "Example text for the element.",
2914
- format: TagFormat.plainText
3676
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3677
+ format: TagFormat.plainText,
3678
+ nullable: true
2915
3679
  }
2916
3680
  ],
2917
3681
  repeatCount: Count.infinity
@@ -2923,9 +3687,57 @@ var CARDSETS = {
2923
3687
  //
2924
3688
  // table
2925
3689
  //
3690
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3691
+ // output (`{ columns?, data, columnWidths? }`):
3692
+ //
3693
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3694
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3695
+ // no qualifier, so they route to the `table-body` (default) section.
3696
+ //
3697
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3698
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3699
+ // the first header card's cells to `columns` and demote remaining
3700
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3701
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3702
+ // here (no per-cell metadata in basic shape) — declared on the cell
3703
+ // variant below.
3704
+ //
2926
3705
  [CardSetConfigKey.table]: {
2927
- jsonKey: "table.data",
2928
- exportJsonKey: { table: { data: "$" } },
3706
+ jsonKey: null,
3707
+ exportJsonKey: null,
3708
+ sections: {
3709
+ "table-header": {
3710
+ // First header card's row of cells → `columns`. Subsequent header
3711
+ // cards' rows → `data` (per-card append; `[$]` shape because
3712
+ // per-card emission fires once per card, multiple fires deep_merge-
3713
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3714
+ // both ROUTE cards to this section (when no explicit `==== table-
3715
+ // header ====` qualifier) and SELECT the rule body once routed.
3716
+ // First rule = first card with any `[#]` → `columns`; second rule
3717
+ // = subsequent cards in this section → `data` (per-row append).
3718
+ jsonKey: "table.columns",
3719
+ exportJsonKey: [
3720
+ {
3721
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3722
+ rule: { "@bit": { table: { columns: "$" } } }
3723
+ },
3724
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3725
+ { "@bit": { table: { data: ["$"] } } }
3726
+ ]
3727
+ },
3728
+ "table-body": {
3729
+ // Batched emission: `$` is the entire 2D array of body card rows.
3730
+ // This is the default section (no qualifier → cards land here).
3731
+ jsonKey: "table.data",
3732
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3733
+ isDefault: true
3734
+ },
3735
+ "table-footer": {
3736
+ // Batched emission: `$` is the entire 2D array of footer rows.
3737
+ jsonKey: "table.data",
3738
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3739
+ }
3740
+ },
2929
3741
  sides: [
2930
3742
  {
2931
3743
  name: "cell",
@@ -2966,6 +3778,8 @@ var CARDSETS = {
2966
3778
  },
2967
3779
  {
2968
3780
  key: ConfigKey.property_tableColWidth,
3781
+ jsonKey: "^table.columnWidths[{s}]",
3782
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
2969
3783
  description: "Width for the column.",
2970
3784
  format: TagFormat.number
2971
3785
  }
@@ -2984,7 +3798,22 @@ var CARDSETS = {
2984
3798
  sections: {
2985
3799
  "table-header": {
2986
3800
  jsonKey: "table.header.rows",
2987
- exportJsonKey: { table: { header: { rows: "$" } } },
3801
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3802
+ // Cards with no explicit `==== table-header ====` qualifier
3803
+ // route here if FIRST CARD contains any `[#]` tag in any
3804
+ // variant. Explicit-qualifier cards fall through to the
3805
+ // unconditional fallback rule. Both rules use the per-card
3806
+ // append shape `rows: ['$']` — the runtime triggers per-card
3807
+ // emission whenever the pattern carries predicates, and each
3808
+ // fire appends a single-element array (multi-fire deep-merge
3809
+ // concatenates).
3810
+ exportJsonKey: [
3811
+ {
3812
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3813
+ rule: { table: { header: { rows: ["$"] } } }
3814
+ },
3815
+ { table: { header: { rows: ["$"] } } }
3816
+ ],
2988
3817
  sideJsonKey: "cells[{s}]|set(title=true)",
2989
3818
  sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
2990
3819
  },
@@ -3017,13 +3846,13 @@ var CARDSETS = {
3017
3846
  },
3018
3847
  {
3019
3848
  key: ConfigKey.tag_title,
3020
- jsonKey: ".",
3021
- exportJsonKey: {
3022
- "@bit": {
3023
- table: { header: { rows: { cells: { cells: { $s: { content: "$" } } } } } }
3024
- }
3025
- },
3026
- description: "Title of the table cell."
3849
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3850
+ // `content` field. When the card is in the `table-header`
3851
+ // section, the section's sideExportJsonKey wraps each
3852
+ // cell with `title: true` automatically.
3853
+ jsonKey: "content",
3854
+ exportJsonKey: { content: "$" },
3855
+ description: "Title text of the table cell (header rows)."
3027
3856
  },
3028
3857
  {
3029
3858
  key: ConfigKey.property_tableCellType,
@@ -3055,6 +3884,9 @@ var CARDSETS = {
3055
3884
  },
3056
3885
  {
3057
3886
  key: ConfigKey.property_tableColWidth,
3887
+ // PLAN-080: write to the cell's `colwidth` field.
3888
+ jsonKey: "colwidth",
3889
+ exportJsonKey: { colwidth: "$" },
3058
3890
  description: "Width for the column.",
3059
3891
  format: TagFormat.number
3060
3892
  }
@@ -3092,7 +3924,8 @@ var CARDSETS = {
3092
3924
  {
3093
3925
  key: ConfigKey.resource_audio,
3094
3926
  description: "Audio resource for the pronunciation table.",
3095
- jsonKey: "audio"
3927
+ jsonKey: "audio",
3928
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
3096
3929
  }
3097
3930
  ],
3098
3931
  repeatCount: Count.infinity
@@ -3163,14 +3996,17 @@ var CARDSETS = {
3163
3996
  format: TagFormat.plainText
3164
3997
  },
3165
3998
  {
3999
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
4000
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
4001
+ // header cascade.
3166
4002
  key: ConfigKey.property_example,
3167
4003
  exportJsonKey: [
3168
4004
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3169
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
3170
4005
  { isExample: true, example: "$", "@bit": { isExample: true } }
3171
4006
  ],
3172
- description: "Example text for the bot action response.",
3173
- format: TagFormat.plainText
4007
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
4008
+ format: TagFormat.plainText,
4009
+ nullable: true
3174
4010
  }
3175
4011
  ]
3176
4012
  }
@@ -3236,6 +4072,38 @@ var CARDSETS = {
3236
4072
  ]
3237
4073
  },
3238
4074
  //
4075
+ // page-footer-sections — mirrors example-bit-list but emits cards under
4076
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
4077
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
4078
+ // routing as pure config so the Rust serializer can pick it up.
4079
+ //
4080
+ [CardSetConfigKey.pageFooterSections]: {
4081
+ jsonKey: "sections",
4082
+ exportJsonKey: { sections: "$" },
4083
+ sides: [
4084
+ {
4085
+ name: "item",
4086
+ variants: [
4087
+ {
4088
+ jsonKey: "body",
4089
+ exportJsonKey: { body: "$" },
4090
+ tags: [
4091
+ {
4092
+ key: ConfigKey.group_standardTags,
4093
+ description: "Standard tags for page-footer section bits."
4094
+ },
4095
+ {
4096
+ exportJsonKey: { title: "$" },
4097
+ key: ConfigKey.tag_title,
4098
+ description: "Title of the page-footer section."
4099
+ }
4100
+ ]
4101
+ }
4102
+ ]
4103
+ }
4104
+ ]
4105
+ },
4106
+ //
3239
4107
  // ingredients
3240
4108
  //
3241
4109
  [CardSetConfigKey.ingredients]: {
@@ -3566,7 +4434,11 @@ var GROUPS = {
3566
4434
  {
3567
4435
  key: ConfigKey.property_groupTag,
3568
4436
  jsonKey: "groupTag.name",
3569
- exportJsonKey: { groupTag: { name: "$" } },
4437
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4438
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4439
+ // same value fold into one entry; chained `@tag` children's
4440
+ // arrays merge as sets.
4441
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3570
4442
  description: "The group tag(s) for the bit",
3571
4443
  format: TagFormat.plainText,
3572
4444
  maxCount: Count.infinity,
@@ -3739,6 +4611,24 @@ var GROUPS = {
3739
4611
  }
3740
4612
  ]
3741
4613
  },
4614
+ [ConfigKey.group_standardInstructionHint]: {
4615
+ type: GroupConfigType.standard,
4616
+ 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).",
4617
+ tags: [
4618
+ {
4619
+ exportJsonKey: { instruction: "$" },
4620
+ key: ConfigKey.tag_instruction,
4621
+ name: "Instruction",
4622
+ description: "The instruction for the bit"
4623
+ },
4624
+ {
4625
+ exportJsonKey: { hint: "$" },
4626
+ key: ConfigKey.tag_hint,
4627
+ name: "Hint",
4628
+ description: "The hint for the bit"
4629
+ }
4630
+ ]
4631
+ },
3742
4632
  [ConfigKey.group_standardTags]: {
3743
4633
  type: GroupConfigType.standard,
3744
4634
  description: "Standard tags which apply to MOST (but not all) bits",
@@ -3752,17 +4642,45 @@ var GROUPS = {
3752
4642
  description: "Item, lead, page number, margin number, instruction and hint tags"
3753
4643
  },
3754
4644
  {
3755
- // Bit-level example tag: bare/synthesized records the marker only;
3756
- // valued [@example:text] emits `isExample: true` and `example` as
3757
- // PM-rendered rich text (bitmark+ format).
4645
+ // Bit-level example tag (default non-`isTopLevelExample` bits):
4646
+ // - bare `[@example]`: consume only (records cascade marker, emits
4647
+ // nothing at bit-level OLD parser scrubs bit-level isExample
4648
+ // for bits NOT in its `isTopLevelExample` allow-list).
4649
+ // - synthesized (descendant cascade): still emits `isExample: true`
4650
+ // at the descendant scope.
4651
+ // - valued `[@example:text]`: emit the example value only; bit-level
4652
+ // `isExample` is left to bubble from descendants.
4653
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4654
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4655
+ // matchMatrix, match, essay, etc.) override this entry with the
4656
+ // legacy emit-isExample shape.
3758
4657
  key: ConfigKey.property_example,
3759
- exportJsonKey: [
3760
- { "@keyonly": { isExample: true } },
3761
- { "@absent": { isExample: true } },
3762
- { isExample: true, example: "$" }
3763
- ],
4658
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3764
4659
  description: "The example(s) for the bit",
3765
- format: TagFormat.bitmarkText
4660
+ format: TagFormat.bitmarkText,
4661
+ nullable: true
4662
+ }
4663
+ ]
4664
+ },
4665
+ [ConfigKey.group_standardTagsNoItemLead]: {
4666
+ type: GroupConfigType.standard,
4667
+ 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).",
4668
+ tags: [
4669
+ {
4670
+ key: ConfigKey.group_standardAllBits,
4671
+ description: "All standard tags which apply to all bits"
4672
+ },
4673
+ {
4674
+ key: ConfigKey.group_standardInstructionHint,
4675
+ description: "Instruction and hint tags only (no item/lead chain)."
4676
+ },
4677
+ {
4678
+ // Mirror of group_standardTags property_example entry.
4679
+ key: ConfigKey.property_example,
4680
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4681
+ description: "The example(s) for the bit",
4682
+ format: TagFormat.bitmarkText,
4683
+ nullable: true
3766
4684
  }
3767
4685
  ]
3768
4686
  },
@@ -3827,8 +4745,8 @@ var GROUPS = {
3827
4745
  tags: [
3828
4746
  {
3829
4747
  key: ConfigKey.property_person,
3830
- jsonKey: "partner.name",
3831
- exportJsonKey: { partner: { name: "$" } },
4748
+ jsonKey: "person.name",
4749
+ exportJsonKey: { person: { name: "$" } },
3832
4750
  description: "A person",
3833
4751
  format: TagFormat.plainText,
3834
4752
  chain: [
@@ -3848,8 +4766,8 @@ var GROUPS = {
3848
4766
  {
3849
4767
  // Deprecated (parter renamed to person)
3850
4768
  key: ConfigKey.property_partner,
3851
- jsonKey: "partner.name",
3852
- exportJsonKey: { partner: { name: "$" } },
4769
+ jsonKey: "person.name",
4770
+ exportJsonKey: { person: { name: "$" } },
3853
4771
  description: "A partner",
3854
4772
  format: TagFormat.plainText,
3855
4773
  chain: [
@@ -3873,7 +4791,9 @@ var GROUPS = {
3873
4791
  description: "Gap chain",
3874
4792
  tags: [
3875
4793
  {
3876
- exportJsonKey: { solutions: [["$"]] },
4794
+ exportJsonKey: {
4795
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4796
+ },
3877
4797
  key: ConfigKey.tag_gap,
3878
4798
  description: "The value of a gap in the content",
3879
4799
  maxCount: Count.infinity,
@@ -3892,27 +4812,37 @@ var GROUPS = {
3892
4812
  description: "Item, lead, page number, margin number, instruction and hint tags"
3893
4813
  },
3894
4814
  {
4815
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4816
+ // `__solutionsAst[length - 1]` — the solution closest to the
4817
+ // `[@example]` tag at parse time. Our chain accumulator is built
4818
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4819
+ // the moment `[@example]` fires, `solutions` only contains the
4820
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4821
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4822
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4823
+ // solutions=["solution"], `[-1]`="solution".
3895
4824
  key: ConfigKey.property_example,
3896
4825
  jsonKey: "example",
3897
4826
  exportJsonKey: [
3898
4827
  {
3899
4828
  "@keyonly": {
3900
4829
  isExample: true,
3901
- example: "$parent.solutions[0]",
4830
+ example: "$parent.solutions[-1]",
3902
4831
  "@bit": { isExample: true }
3903
4832
  }
3904
4833
  },
3905
4834
  {
3906
4835
  "@absent": {
3907
4836
  isExample: true,
3908
- example: "$parent.solutions[0]",
4837
+ example: "$parent.solutions[-1]",
3909
4838
  "@bit": { isExample: true }
3910
4839
  }
3911
4840
  },
3912
4841
  { isExample: true, example: "$", "@bit": { isExample: true } }
3913
4842
  ],
3914
4843
  description: "An example for the gap",
3915
- format: TagFormat.bitmarkText
4844
+ format: TagFormat.bitmarkText,
4845
+ nullable: true
3916
4846
  },
3917
4847
  {
3918
4848
  key: ConfigKey.property_isCaseSensitive,
@@ -3961,6 +4891,99 @@ var GROUPS = {
3961
4891
  description: "Item, lead, page number, margin number, instruction and hint tags"
3962
4892
  },
3963
4893
  {
4894
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4895
+ // `@example` from the bit header marks EVERY choice with
4896
+ // `isExample: true` (matches BPG output), but only the
4897
+ // FIRST correct choice gets `example: true` (BPG
4898
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4899
+ //
4900
+ // Two `@absent` rules, picked first-match-wins:
4901
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4902
+ // `maxEmits:1` — fires once for the first correct.
4903
+ // 2. Plain `@absent` — fires on every remaining entry
4904
+ // (subsequent corrects and all incorrects), emitting
4905
+ // `isExample` only.
4906
+ // The `serialize_cards` cascade-counter scope holds the cap
4907
+ // across the whole cardset.
4908
+ // @card scope-shift writes choice-container isExample (e.g.
4909
+ // `quizzes[i].isExample`) — was previously provided by the
4910
+ // universal `bubble_is_example` post-walk, which was removed
4911
+ // in favour of explicit per-tag config. SYNTAX.md §8.
4912
+ key: ConfigKey.property_example,
4913
+ jsonKey: "example",
4914
+ exportJsonKey: [
4915
+ {
4916
+ "@keyonly": {
4917
+ isExample: true,
4918
+ example: "$parent.isCorrect",
4919
+ "@bit": { isExample: true },
4920
+ "@card": { isExample: true }
4921
+ }
4922
+ },
4923
+ {
4924
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4925
+ maxEmits: 1,
4926
+ rule: {
4927
+ isExample: true,
4928
+ example: true,
4929
+ "@bit": { isExample: true },
4930
+ "@card": { isExample: true }
4931
+ }
4932
+ },
4933
+ {
4934
+ "@absent": {
4935
+ isExample: true,
4936
+ "@bit": { isExample: true },
4937
+ "@card": { isExample: true }
4938
+ }
4939
+ },
4940
+ {
4941
+ isExample: true,
4942
+ example: "$",
4943
+ "@bit": { isExample: true },
4944
+ "@card": { isExample: true }
4945
+ }
4946
+ ],
4947
+ description: "An example for the true/false statement/question",
4948
+ format: TagFormat.boolean,
4949
+ nullable: true
4950
+ }
4951
+ ]
4952
+ },
4953
+ {
4954
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
4955
+ key: ConfigKey.tag_false,
4956
+ description: "False value for a true/false statement/question",
4957
+ maxCount: Count.infinity,
4958
+ format: TagFormat.plainText,
4959
+ chain: [
4960
+ {
4961
+ key: ConfigKey.tag_true,
4962
+ jsonKey: "choices[]|set(isCorrect=true)",
4963
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
4964
+ description: "True values for a true/false statement/question",
4965
+ maxCount: Count.infinity,
4966
+ format: TagFormat.plainText
4967
+ },
4968
+ {
4969
+ key: ConfigKey.tag_false,
4970
+ jsonKey: "choices[]|set(isCorrect=false)",
4971
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
4972
+ description: "False values for a true/false statement/question",
4973
+ maxCount: Count.infinity,
4974
+ format: TagFormat.plainText
4975
+ },
4976
+ {
4977
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4978
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4979
+ },
4980
+ {
4981
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4982
+ // `@example` on a `-` (incorrect choice) emits
4983
+ // `isExample: true` only — no `example` (BPG fixtures show
4984
+ // `isExample` on every choice, but `example` only on the
4985
+ // first correct via firstCorrectOnly cascade).
4986
+ // @card scope-shift writes choice-container isExample.
3964
4987
  key: ConfigKey.property_example,
3965
4988
  jsonKey: "example",
3966
4989
  exportJsonKey: [
@@ -3968,25 +4991,99 @@ var GROUPS = {
3968
4991
  "@keyonly": {
3969
4992
  isExample: true,
3970
4993
  example: "$parent.isCorrect",
3971
- "@bit": { isExample: true }
4994
+ "@bit": { isExample: true },
4995
+ "@card": { isExample: true }
3972
4996
  }
3973
4997
  },
3974
4998
  {
3975
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 (incorrect entry \u2014 isExample only on cascade)",
5013
+ format: TagFormat.boolean,
5014
+ nullable: true
5015
+ }
5016
+ ]
5017
+ }
5018
+ ]
5019
+ },
5020
+ [ConfigKey.group_trueFalseResponses]: {
5021
+ type: GroupConfigType.standard,
5022
+ description: "True/False Responses chain",
5023
+ tags: [
5024
+ {
5025
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
5026
+ key: ConfigKey.tag_true,
5027
+ description: "True value for a true/false statement/question",
5028
+ maxCount: Count.infinity,
5029
+ format: TagFormat.plainText,
5030
+ chain: [
5031
+ {
5032
+ key: ConfigKey.tag_true,
5033
+ jsonKey: "responses[]|set(isCorrect=true)",
5034
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
5035
+ description: "True values for a true/false statement/question",
5036
+ maxCount: Count.infinity,
5037
+ format: TagFormat.plainText
5038
+ },
5039
+ {
5040
+ key: ConfigKey.tag_false,
5041
+ jsonKey: "responses[]|set(isCorrect=false)",
5042
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
5043
+ description: "False values for a true/false statement/question",
5044
+ maxCount: Count.infinity,
5045
+ format: TagFormat.plainText
5046
+ },
5047
+ {
5048
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5049
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5050
+ },
5051
+ {
5052
+ // @card scope-shift writes response-container isExample.
5053
+ key: ConfigKey.property_example,
5054
+ jsonKey: "example",
5055
+ exportJsonKey: [
5056
+ {
5057
+ "@keyonly": {
3976
5058
  isExample: true,
3977
5059
  example: "$parent.isCorrect",
3978
- "@bit": { isExample: true }
5060
+ "@bit": { isExample: true },
5061
+ "@card": { isExample: true }
3979
5062
  }
3980
5063
  },
3981
- { isExample: true, example: "$", "@bit": { isExample: true } }
5064
+ {
5065
+ "@absent": {
5066
+ isExample: "$parent.isCorrect",
5067
+ example: "$parent.isCorrect",
5068
+ "@bit": { isExample: "$parent.isCorrect" },
5069
+ "@card": { isExample: "$parent.isCorrect" }
5070
+ }
5071
+ },
5072
+ {
5073
+ isExample: true,
5074
+ example: "$",
5075
+ "@bit": { isExample: true },
5076
+ "@card": { isExample: true }
5077
+ }
3982
5078
  ],
3983
5079
  description: "An example for the true/false statement/question",
3984
- format: TagFormat.bitmarkText
5080
+ format: TagFormat.boolean,
5081
+ nullable: true
3985
5082
  }
3986
5083
  ]
3987
5084
  },
3988
5085
  {
3989
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5086
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
3990
5087
  key: ConfigKey.tag_false,
3991
5088
  description: "False value for a true/false statement/question",
3992
5089
  maxCount: Count.infinity,
@@ -3994,16 +5091,16 @@ var GROUPS = {
3994
5091
  chain: [
3995
5092
  {
3996
5093
  key: ConfigKey.tag_true,
3997
- jsonKey: "choices[]|set(isCorrect=true)",
3998
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
5094
+ jsonKey: "responses[]|set(isCorrect=true)",
5095
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
3999
5096
  description: "True values for a true/false statement/question",
4000
5097
  maxCount: Count.infinity,
4001
5098
  format: TagFormat.plainText
4002
5099
  },
4003
5100
  {
4004
5101
  key: ConfigKey.tag_false,
4005
- jsonKey: "choices[]|set(isCorrect=false)",
4006
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5102
+ jsonKey: "responses[]|set(isCorrect=false)",
5103
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4007
5104
  description: "False values for a true/false statement/question",
4008
5105
  maxCount: Count.infinity,
4009
5106
  format: TagFormat.plainText
@@ -4013,6 +5110,7 @@ var GROUPS = {
4013
5110
  description: "Item, lead, page number, margin number, instruction and hint tags"
4014
5111
  },
4015
5112
  {
5113
+ // @card scope-shift writes response-container isExample.
4016
5114
  key: ConfigKey.property_example,
4017
5115
  jsonKey: "example",
4018
5116
  exportJsonKey: [
@@ -4020,20 +5118,333 @@ var GROUPS = {
4020
5118
  "@keyonly": {
4021
5119
  isExample: true,
4022
5120
  example: "$parent.isCorrect",
4023
- "@bit": { isExample: true }
5121
+ "@bit": { isExample: true },
5122
+ "@card": { isExample: true }
4024
5123
  }
4025
5124
  },
4026
5125
  {
4027
5126
  "@absent": {
4028
5127
  isExample: true,
4029
5128
  example: "$parent.isCorrect",
4030
- "@bit": { isExample: true }
5129
+ "@bit": { isExample: true },
5130
+ "@card": { isExample: true }
4031
5131
  }
4032
5132
  },
4033
- { isExample: true, example: "$", "@bit": { isExample: true } }
5133
+ {
5134
+ isExample: true,
5135
+ example: "$",
5136
+ "@bit": { isExample: true },
5137
+ "@card": { isExample: true }
5138
+ }
4034
5139
  ],
4035
5140
  description: "An example for the true/false statement/question",
4036
- format: TagFormat.bitmarkText
5141
+ format: TagFormat.boolean,
5142
+ nullable: true
5143
+ }
5144
+ ]
5145
+ }
5146
+ ]
5147
+ },
5148
+ [ConfigKey.group_trueFalseInlineSelect]: {
5149
+ type: GroupConfigType.standard,
5150
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
5151
+ tags: [
5152
+ {
5153
+ exportJsonKey: {
5154
+ "@body-inline": {
5155
+ type: "select",
5156
+ attrs: { options: [{ text: "$", isCorrect: true }] }
5157
+ }
5158
+ },
5159
+ key: ConfigKey.tag_true,
5160
+ description: "True value for an inline select body bit",
5161
+ maxCount: Count.infinity,
5162
+ format: TagFormat.plainText,
5163
+ chain: [
5164
+ {
5165
+ key: ConfigKey.tag_true,
5166
+ exportJsonKey: {},
5167
+ description: "True values for an inline select body bit",
5168
+ maxCount: Count.infinity,
5169
+ format: TagFormat.plainText
5170
+ },
5171
+ {
5172
+ key: ConfigKey.tag_false,
5173
+ exportJsonKey: {},
5174
+ description: "False values for an inline select body bit",
5175
+ maxCount: Count.infinity,
5176
+ format: TagFormat.plainText
5177
+ },
5178
+ {
5179
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5180
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5181
+ },
5182
+ {
5183
+ // PLAN-083: cascade-fired `@example` from bit header fires
5184
+ // `isExample:true, example:true` on the FIRST CORRECT option
5185
+ // only (BPG `fillBooleanExample(select.options,
5186
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
5187
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5188
+ // OR — any cascade ⇒ bit marked) and
5189
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5190
+ // on the select node).
5191
+ key: ConfigKey.property_example,
5192
+ jsonKey: "example",
5193
+ exportJsonKey: [
5194
+ {
5195
+ "@keyonly": {
5196
+ isExample: true,
5197
+ example: "$parent.isCorrect",
5198
+ "@bit": { isExample: true },
5199
+ "@body-inline": { attrs: { isExample: true } }
5200
+ }
5201
+ },
5202
+ {
5203
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5204
+ maxEmits: 1,
5205
+ rule: {
5206
+ isExample: true,
5207
+ example: true,
5208
+ "@bit": { isExample: true },
5209
+ "@body-inline": { attrs: { isExample: true } }
5210
+ }
5211
+ },
5212
+ {
5213
+ isExample: true,
5214
+ example: "$",
5215
+ "@bit": { isExample: true },
5216
+ "@body-inline": { attrs: { isExample: true } }
5217
+ }
5218
+ ],
5219
+ description: "An example for the inline select option (first correct only)",
5220
+ format: TagFormat.boolean,
5221
+ nullable: true
5222
+ }
5223
+ ]
5224
+ },
5225
+ {
5226
+ exportJsonKey: {
5227
+ "@body-inline": {
5228
+ type: "select",
5229
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5230
+ }
5231
+ },
5232
+ key: ConfigKey.tag_false,
5233
+ description: "False value for an inline select body bit",
5234
+ maxCount: Count.infinity,
5235
+ format: TagFormat.plainText,
5236
+ chain: [
5237
+ {
5238
+ key: ConfigKey.tag_true,
5239
+ exportJsonKey: {},
5240
+ description: "True values for an inline select body bit",
5241
+ maxCount: Count.infinity,
5242
+ format: TagFormat.plainText
5243
+ },
5244
+ {
5245
+ key: ConfigKey.tag_false,
5246
+ exportJsonKey: {},
5247
+ description: "False values for an inline select body bit",
5248
+ maxCount: Count.infinity,
5249
+ format: TagFormat.plainText
5250
+ },
5251
+ {
5252
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5253
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5254
+ },
5255
+ {
5256
+ // PLAN-083: identical to the `+` lead's chain @example —
5257
+ // cascade reaches the first correct option regardless of
5258
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5259
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5260
+ key: ConfigKey.property_example,
5261
+ jsonKey: "example",
5262
+ exportJsonKey: [
5263
+ {
5264
+ "@keyonly": {
5265
+ isExample: true,
5266
+ example: "$parent.isCorrect",
5267
+ "@bit": { isExample: true },
5268
+ "@body-inline": { attrs: { isExample: true } }
5269
+ }
5270
+ },
5271
+ {
5272
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5273
+ maxEmits: 1,
5274
+ rule: {
5275
+ isExample: true,
5276
+ example: true,
5277
+ "@bit": { isExample: true },
5278
+ "@body-inline": { attrs: { isExample: true } }
5279
+ }
5280
+ },
5281
+ {
5282
+ isExample: true,
5283
+ example: "$",
5284
+ "@bit": { isExample: true },
5285
+ "@body-inline": { attrs: { isExample: true } }
5286
+ }
5287
+ ],
5288
+ description: "An example for the inline select option (first correct only)",
5289
+ format: TagFormat.boolean,
5290
+ nullable: true
5291
+ }
5292
+ ]
5293
+ }
5294
+ ]
5295
+ },
5296
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5297
+ type: GroupConfigType.standard,
5298
+ 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.',
5299
+ tags: [
5300
+ {
5301
+ exportJsonKey: {
5302
+ "@body-inline": {
5303
+ type: "highlight",
5304
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5305
+ }
5306
+ },
5307
+ key: ConfigKey.tag_true,
5308
+ description: "Highlighted span in the body (correct)",
5309
+ maxCount: Count.infinity,
5310
+ format: TagFormat.plainText,
5311
+ chain: [
5312
+ {
5313
+ key: ConfigKey.tag_true,
5314
+ exportJsonKey: {},
5315
+ description: "Highlighted span values (correct)",
5316
+ maxCount: Count.infinity,
5317
+ format: TagFormat.plainText
5318
+ },
5319
+ {
5320
+ key: ConfigKey.tag_false,
5321
+ exportJsonKey: {},
5322
+ description: "Highlighted span values (incorrect)",
5323
+ maxCount: Count.infinity,
5324
+ format: TagFormat.plainText
5325
+ },
5326
+ {
5327
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5328
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5329
+ },
5330
+ {
5331
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5332
+ // `@example` from the bit header fires `example: true` +
5333
+ // `isExample: true` on the FIRST correct highlight only
5334
+ // (BPG `fillBooleanExample(highlight.texts,
5335
+ // firstCorrectOnly=true)`). Incorrect highlights get
5336
+ // nothing (unlike choices, the highlight-text fixture
5337
+ // shows no `isExample` on incorrect entries).
5338
+ key: ConfigKey.property_example,
5339
+ jsonKey: "example",
5340
+ exportJsonKey: [
5341
+ {
5342
+ "@keyonly": {
5343
+ isExample: true,
5344
+ example: "$parent.isCorrect",
5345
+ "@bit": { isExample: true },
5346
+ "@body-inline": { attrs: { isExample: true } }
5347
+ }
5348
+ },
5349
+ {
5350
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5351
+ maxEmits: 1,
5352
+ rule: {
5353
+ isExample: true,
5354
+ example: true,
5355
+ "@bit": { isExample: true },
5356
+ "@body-inline": { attrs: { isExample: true } }
5357
+ }
5358
+ },
5359
+ {
5360
+ isExample: true,
5361
+ example: "$",
5362
+ "@bit": { isExample: true },
5363
+ "@body-inline": { attrs: { isExample: true } }
5364
+ }
5365
+ ],
5366
+ description: "An example for the highlighted span (first correct only)",
5367
+ format: TagFormat.boolean,
5368
+ nullable: true
5369
+ }
5370
+ ]
5371
+ },
5372
+ {
5373
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5374
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5375
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5376
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5377
+ // through and nothing fires. The natural-default strip removes
5378
+ // `isCorrect: false` from the final texts entries (matches fixture).
5379
+ exportJsonKey: {
5380
+ "@body-inline": {
5381
+ type: "highlight",
5382
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5383
+ }
5384
+ },
5385
+ key: ConfigKey.tag_false,
5386
+ description: "Highlighted span in the body (incorrect)",
5387
+ maxCount: Count.infinity,
5388
+ format: TagFormat.plainText,
5389
+ chain: [
5390
+ {
5391
+ key: ConfigKey.tag_true,
5392
+ exportJsonKey: {},
5393
+ description: "Highlighted span values (correct)",
5394
+ maxCount: Count.infinity,
5395
+ format: TagFormat.plainText
5396
+ },
5397
+ {
5398
+ key: ConfigKey.tag_false,
5399
+ exportJsonKey: {},
5400
+ description: "Highlighted span values (incorrect)",
5401
+ maxCount: Count.infinity,
5402
+ format: TagFormat.plainText
5403
+ },
5404
+ {
5405
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5406
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5407
+ },
5408
+ {
5409
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5410
+ // `@example` on a `-` (incorrect highlight) emits nothing
5411
+ // (highlight-text fixture has no `isExample` on incorrect
5412
+ // entries; firstCorrectOnly cascade skips them entirely).
5413
+ // No `@absent` rule.
5414
+ // PLAN-083: identical to the `+` lead's chain @example —
5415
+ // cascade reaches the first correct option regardless of
5416
+ // which lead (+/-) opens the chain.
5417
+ key: ConfigKey.property_example,
5418
+ jsonKey: "example",
5419
+ exportJsonKey: [
5420
+ {
5421
+ "@keyonly": {
5422
+ isExample: true,
5423
+ example: "$parent.isCorrect",
5424
+ "@bit": { isExample: true },
5425
+ "@body-inline": { attrs: { isExample: true } }
5426
+ }
5427
+ },
5428
+ {
5429
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5430
+ maxEmits: 1,
5431
+ rule: {
5432
+ isExample: true,
5433
+ example: true,
5434
+ "@bit": { isExample: true },
5435
+ "@body-inline": { attrs: { isExample: true } }
5436
+ }
5437
+ },
5438
+ {
5439
+ isExample: true,
5440
+ example: "$",
5441
+ "@bit": { isExample: true },
5442
+ "@body-inline": { attrs: { isExample: true } }
5443
+ }
5444
+ ],
5445
+ description: "An example for the highlighted span (first correct only)",
5446
+ format: TagFormat.boolean,
5447
+ nullable: true
4037
5448
  }
4038
5449
  ]
4039
5450
  }
@@ -4046,7 +5457,11 @@ var GROUPS = {
4046
5457
  {
4047
5458
  key: ConfigKey.property_mark,
4048
5459
  jsonKey: "marks.mark",
4049
- exportJsonKey: { marks: { mark: ["$"] } },
5460
+ // `emphasis: 'underline'` is the per-entry default for mark
5461
+ // configs — mirrors the hardcoded default in BPG
5462
+ // `MarkConfigChainContentProcessor.ts:55-58`. Chained
5463
+ // `[@emphasis:…]` overrides via deep-merge into `marks[-1]`.
5464
+ exportJsonKey: { marks: [{ mark: "$", emphasis: "underline" }] },
4050
5465
  description: "The mark configuration",
4051
5466
  format: TagFormat.plainText,
4052
5467
  maxCount: Count.infinity,
@@ -4070,7 +5485,9 @@ var GROUPS = {
4070
5485
  description: "Mark chain",
4071
5486
  tags: [
4072
5487
  {
4073
- exportJsonKey: { solution: ["$"] },
5488
+ exportJsonKey: {
5489
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5490
+ },
4074
5491
  key: ConfigKey.tag_mark,
4075
5492
  description: "The marked content",
4076
5493
  maxCount: Count.infinity,
@@ -4079,19 +5496,30 @@ var GROUPS = {
4079
5496
  {
4080
5497
  key: ConfigKey.property_mark,
4081
5498
  jsonKey: "mark",
5499
+ exportJsonKey: { mark: "$" },
4082
5500
  description: "The mark configuration",
4083
5501
  format: TagFormat.plainText
4084
5502
  },
4085
5503
  {
4086
5504
  key: ConfigKey.property_example,
4087
5505
  jsonKey: "example",
4088
- exportJsonKey: [
4089
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
4090
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
4091
- { isExample: true, example: "$", "@bit": { isExample: true } }
4092
- ],
5506
+ // Per-mark `@example` is a boolean toggle. BPG's runtime
5507
+ // `ExampleTagContentProcessor.handleDefaultOnlyExample`
5508
+ // (lines 147-161) normalises ANY `[@example]` or
5509
+ // `[@example:V]` on a mark body-bit to
5510
+ // `__isDefaultExample=true, example=undefined`; the
5511
+ // downstream `toExample(_, _, defaultExample=true)` in
5512
+ // `BaseBuilder.ts:80-87` then emits `example: true`
5513
+ // regardless of source. Declare a single unconditional
5514
+ // boolean-shape rule so the exported schema matches that
5515
+ // runtime semantic instead of the misleading `example: $`
5516
+ // shape (which would only fire if `handleDefaultOnlyExample`
5517
+ // were removed). `@bit: {isExample: true}` cascades the
5518
+ // marker — but NOT the `example` value — to bit root.
5519
+ exportJsonKey: { isExample: true, example: true, "@bit": { isExample: true } },
4093
5520
  description: "An example for the marked content",
4094
- format: TagFormat.bitmarkText
5521
+ format: TagFormat.boolean,
5522
+ nullable: true
4095
5523
  }
4096
5524
  ]
4097
5525
  }
@@ -4520,6 +5948,42 @@ var GROUPS = {
4520
5948
  }
4521
5949
  ]
4522
5950
  },
5951
+ [ConfigKey.group_backgroundWallpaper]: {
5952
+ type: GroupConfigType.standard,
5953
+ description: "Background wallpaper tags",
5954
+ tags: [
5955
+ {
5956
+ key: ConfigKey.resource_backgroundWallpaper,
5957
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
5958
+ description: "Background wallpaper for the image, used to set a background for the image",
5959
+ chain: [
5960
+ {
5961
+ key: ConfigKey.group_resourceImageCommon,
5962
+ description: "Common resource image tags for images"
5963
+ }
5964
+ ]
5965
+ }
5966
+ ]
5967
+ },
5968
+ [ConfigKey.group_imageNoZoom]: {
5969
+ type: GroupConfigType.standard,
5970
+ description: "Image bit tags",
5971
+ tags: [
5972
+ {
5973
+ key: ConfigKey.group_backgroundWallpaper,
5974
+ description: "Background wallpaper tags for images, used to define background properties for images"
5975
+ },
5976
+ {
5977
+ key: ConfigKey.group_resourceBitTags,
5978
+ description: "Resource bit tags for images, used to define additional properties for images"
5979
+ },
5980
+ {
5981
+ key: ConfigKey.group_resourceImageNoZoom,
5982
+ description: "Resource image tags for images, used to attach images to the bit",
5983
+ minCount: 1
5984
+ }
5985
+ ]
5986
+ },
4523
5987
  //
4524
5988
  // Resource groups
4525
5989
  //
@@ -4639,6 +6103,58 @@ var GROUPS = {
4639
6103
  }
4640
6104
  ]
4641
6105
  },
6106
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
6107
+ type: GroupConfigType.standard,
6108
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
6109
+ tags: [
6110
+ {
6111
+ key: ConfigKey.group_resourceCommon,
6112
+ description: "Common resource properties"
6113
+ },
6114
+ {
6115
+ key: ConfigKey.property_src1x,
6116
+ description: "The source URL for the image at 1x resolution",
6117
+ format: TagFormat.plainText
6118
+ },
6119
+ {
6120
+ key: ConfigKey.property_src2x,
6121
+ description: "The source URL for the image at 2x resolution",
6122
+ format: TagFormat.plainText
6123
+ },
6124
+ {
6125
+ key: ConfigKey.property_src3x,
6126
+ description: "The source URL for the image at 3x resolution",
6127
+ format: TagFormat.plainText
6128
+ },
6129
+ {
6130
+ key: ConfigKey.property_src4x,
6131
+ description: "The source URL for the image at 4x resolution",
6132
+ format: TagFormat.plainText
6133
+ },
6134
+ {
6135
+ key: ConfigKey.property_width,
6136
+ description: "The width of the image",
6137
+ format: TagFormat.number
6138
+ },
6139
+ {
6140
+ key: ConfigKey.property_height,
6141
+ description: "The height of the image",
6142
+ format: TagFormat.number
6143
+ },
6144
+ {
6145
+ key: ConfigKey.property_alt,
6146
+ description: "The alternative text for the image",
6147
+ format: TagFormat.plainText
6148
+ },
6149
+ // @zoomDisabled with a true default
6150
+ {
6151
+ key: ConfigKey.property_zoomDisabled,
6152
+ description: "If true, zooming is disabled for the image (defaults to true)",
6153
+ format: TagFormat.boolean,
6154
+ defaultValue: "true"
6155
+ }
6156
+ ]
6157
+ },
4642
6158
  [ConfigKey.group_resourceAudioCommon]: {
4643
6159
  type: GroupConfigType.standard,
4644
6160
  description: "Common properties for audio resources",
@@ -4728,22 +6244,26 @@ var GROUPS = {
4728
6244
  {
4729
6245
  key: ConfigKey.property_src1x,
4730
6246
  description: "The source URL for the video at 1x resolution",
4731
- format: TagFormat.plainText
6247
+ format: TagFormat.plainText,
6248
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4732
6249
  },
4733
6250
  {
4734
6251
  key: ConfigKey.property_src2x,
4735
6252
  description: "The source URL for the video at 2x resolution",
4736
- format: TagFormat.plainText
6253
+ format: TagFormat.plainText,
6254
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4737
6255
  },
4738
6256
  {
4739
6257
  key: ConfigKey.property_src3x,
4740
6258
  description: "The source URL for the video at 3x resolution",
4741
- format: TagFormat.plainText
6259
+ format: TagFormat.plainText,
6260
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4742
6261
  },
4743
6262
  {
4744
6263
  key: ConfigKey.property_src4x,
4745
6264
  description: "The source URL for the video at 4x resolution",
4746
- format: TagFormat.plainText
6265
+ format: TagFormat.plainText,
6266
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4747
6267
  }
4748
6268
  ]
4749
6269
  },
@@ -4756,6 +6276,7 @@ var GROUPS = {
4756
6276
  tags: [
4757
6277
  {
4758
6278
  key: ConfigKey.resource_icon,
6279
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4759
6280
  description: "The icon resource",
4760
6281
  chain: [
4761
6282
  {
@@ -4783,12 +6304,30 @@ var GROUPS = {
4783
6304
  }
4784
6305
  ]
4785
6306
  },
6307
+ [ConfigKey.group_resourceImageNoZoom]: {
6308
+ type: GroupConfigType.resource,
6309
+ description: "Image resource where @zoomDisabled defaults to true",
6310
+ tags: [
6311
+ {
6312
+ key: ConfigKey.resource_image,
6313
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6314
+ description: "The image resource (no-zoom default)",
6315
+ chain: [
6316
+ {
6317
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6318
+ description: "Common image properties with @zoomDisabled defaulting to true"
6319
+ }
6320
+ ]
6321
+ }
6322
+ ]
6323
+ },
4786
6324
  [ConfigKey.group_resourceImagePortrait]: {
4787
6325
  type: GroupConfigType.resource,
4788
6326
  description: "Portrait image resource",
4789
6327
  tags: [
4790
6328
  {
4791
6329
  key: ConfigKey.resource_imagePortrait,
6330
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4792
6331
  description: "The portrait image resource",
4793
6332
  chain: [
4794
6333
  {
@@ -4805,6 +6344,7 @@ var GROUPS = {
4805
6344
  tags: [
4806
6345
  {
4807
6346
  key: ConfigKey.resource_imageLandscape,
6347
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4808
6348
  description: "The landscape image resource",
4809
6349
  chain: [
4810
6350
  {
@@ -4821,6 +6361,7 @@ var GROUPS = {
4821
6361
  tags: [
4822
6362
  {
4823
6363
  key: ConfigKey.resource_imageEmbed,
6364
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4824
6365
  description: "The embedded image resource",
4825
6366
  chain: [
4826
6367
  {
@@ -4994,6 +6535,7 @@ var GROUPS = {
4994
6535
  tags: [
4995
6536
  {
4996
6537
  key: ConfigKey.resource_article,
6538
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
4997
6539
  description: "The article resource",
4998
6540
  chain: [
4999
6541
  {
@@ -5010,6 +6552,7 @@ var GROUPS = {
5010
6552
  tags: [
5011
6553
  {
5012
6554
  key: ConfigKey.resource_articleEmbed,
6555
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
5013
6556
  description: "The embedded article resource",
5014
6557
  chain: [
5015
6558
  {
@@ -5026,6 +6569,7 @@ var GROUPS = {
5026
6569
  tags: [
5027
6570
  {
5028
6571
  key: ConfigKey.resource_articleLink,
6572
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
5029
6573
  description: "The link to the article resource",
5030
6574
  chain: [
5031
6575
  {
@@ -5296,7 +6840,12 @@ var ConfigHydrator = class {
5296
6840
  isDefault: v.isDefault,
5297
6841
  sideJsonKey: v.sideJsonKey,
5298
6842
  sideExportJsonKey: v.sideExportJsonKey,
5299
- hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey")
6843
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6844
+ // PLAN-085: cardinality fields pass through; defaults applied
6845
+ // downstream by the config consumer (Rust treats `0` / undefined
6846
+ // as unbounded).
6847
+ minCount: v.minCount,
6848
+ maxCount: v.maxCount
5300
6849
  };
5301
6850
  }
5302
6851
  return out;
@@ -5511,7 +7060,8 @@ var BITS = {
5511
7060
  { isExample: true, example: "$" }
5512
7061
  ],
5513
7062
  description: "The example text for the bit",
5514
- format: TagFormat.plainText
7063
+ format: TagFormat.plainText,
7064
+ nullable: true
5515
7065
  }
5516
7066
  ],
5517
7067
  rootExampleType: ExampleType.string
@@ -5850,7 +7400,8 @@ var BITS = {
5850
7400
  { isExample: true, example: "$" }
5851
7401
  ],
5852
7402
  description: "The example text for the bit",
5853
- format: TagFormat.plainText
7403
+ format: TagFormat.plainText,
7404
+ nullable: true
5854
7405
  }
5855
7406
  ],
5856
7407
  rootExampleType: ExampleType.string
@@ -6013,7 +7564,8 @@ var BITS = {
6013
7564
  { isExample: true, example: "$" }
6014
7565
  ],
6015
7566
  description: "The example text for the bit",
6016
- format: TagFormat.plainText
7567
+ format: TagFormat.plainText,
7568
+ nullable: true
6017
7569
  }
6018
7570
  ],
6019
7571
  rootExampleType: ExampleType.string
@@ -6164,7 +7716,8 @@ var BITS = {
6164
7716
  { isExample: true, example: "$" }
6165
7717
  ],
6166
7718
  description: "The example text for the bit",
6167
- format: TagFormat.plainText
7719
+ format: TagFormat.plainText,
7720
+ nullable: true
6168
7721
  }
6169
7722
  ],
6170
7723
  rootExampleType: ExampleType.string
@@ -6741,6 +8294,12 @@ var BITS = {
6741
8294
  description: "The progress of the chapter, used for tracking reading progress",
6742
8295
  format: TagFormat.boolean,
6743
8296
  defaultValue: "true"
8297
+ },
8298
+ {
8299
+ key: ConfigKey.property_isCollapsible,
8300
+ description: "If true, the chapter is collapsible",
8301
+ format: TagFormat.boolean,
8302
+ defaultValue: "false"
6744
8303
  }
6745
8304
  ]
6746
8305
  },
@@ -6759,8 +8318,31 @@ var BITS = {
6759
8318
  description: "Tags for gaps in cloze and multiple choice text bits"
6760
8319
  },
6761
8320
  {
6762
- key: ConfigKey.group_trueFalse,
8321
+ key: ConfigKey.group_trueFalseInlineSelect,
6763
8322
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8323
+ },
8324
+ {
8325
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8326
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8327
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8328
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8329
+ key: ConfigKey.property_isCaseSensitive,
8330
+ exportJsonKey: {},
8331
+ description: "If the cloze answers are case sensitive",
8332
+ format: TagFormat.boolean,
8333
+ defaultValue: "true"
8334
+ },
8335
+ {
8336
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8337
+ key: ConfigKey.property_example,
8338
+ exportJsonKey: [
8339
+ { "@keyonly": { isExample: true } },
8340
+ { "@absent": { isExample: true } },
8341
+ { isExample: true, example: "$" }
8342
+ ],
8343
+ description: "The example(s) for the bit",
8344
+ format: TagFormat.bitmarkText,
8345
+ nullable: true
6764
8346
  }
6765
8347
  ]
6766
8348
  },
@@ -6799,18 +8381,56 @@ var BITS = {
6799
8381
  {
6800
8382
  key: ConfigKey.group_gap,
6801
8383
  description: "Tags for gaps in cloze bits"
8384
+ },
8385
+ {
8386
+ // Override inherited @example: cloze is in OLD parser's
8387
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8388
+ // emits `isExample: true`.
8389
+ key: ConfigKey.property_example,
8390
+ exportJsonKey: [
8391
+ { "@keyonly": { isExample: true } },
8392
+ { "@absent": { isExample: true } },
8393
+ { isExample: true, example: "$" }
8394
+ ],
8395
+ description: "The example(s) for the bit",
8396
+ format: TagFormat.bitmarkText,
8397
+ nullable: true
6802
8398
  }
6803
8399
  ]
6804
8400
  },
6805
8401
  [BitType2.clozeInstructionGrouped]: {
6806
8402
  since: "1.3.0",
6807
8403
  baseBitType: BitType2.cloze,
6808
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8404
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8405
+ tags: [
8406
+ {
8407
+ key: ConfigKey.property_quizCountItems,
8408
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8409
+ format: TagFormat.boolean,
8410
+ defaultValue: "true"
8411
+ }
8412
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8413
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8414
+ ]
6809
8415
  },
6810
8416
  [BitType2.clozeSolutionGrouped]: {
6811
8417
  since: "1.3.0",
6812
8418
  baseBitType: BitType2.cloze,
6813
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8419
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8420
+ tags: [
8421
+ {
8422
+ key: ConfigKey.property_quizCountItems,
8423
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8424
+ format: TagFormat.boolean,
8425
+ defaultValue: "true"
8426
+ },
8427
+ {
8428
+ key: ConfigKey.property_quizStrikethroughSolutions,
8429
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8430
+ format: TagFormat.boolean,
8431
+ defaultValue: "true"
8432
+ }
8433
+ ]
6814
8434
  },
6815
8435
  [BitType2.clozeSeveral]: {
6816
8436
  since: "3.5.0",
@@ -6846,6 +8466,17 @@ var BITS = {
6846
8466
  {
6847
8467
  key: ConfigKey.group_quizCommon,
6848
8468
  description: "Common tags for quiz bits"
8469
+ },
8470
+ {
8471
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8472
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8473
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8474
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8475
+ key: ConfigKey.property_isCaseSensitive,
8476
+ exportJsonKey: {},
8477
+ description: "If the cloze answers are case sensitive",
8478
+ format: TagFormat.boolean,
8479
+ defaultValue: "true"
6849
8480
  }
6850
8481
  ],
6851
8482
  cardSet: CardSetConfigKey.clozeList
@@ -7012,6 +8643,7 @@ var BITS = {
7012
8643
  tags: [
7013
8644
  {
7014
8645
  key: ConfigKey.group_person,
8646
+ exportJsonKey: { partner: { name: "$" } },
7015
8647
  description: "Tags for the person in the conversation"
7016
8648
  }
7017
8649
  ]
@@ -7442,7 +9074,12 @@ var BITS = {
7442
9074
  maxCount: Count.infinity
7443
9075
  },
7444
9076
  {
7445
- // Override inherited @example from group_standardTags: string root example.
9077
+ // Override inherited @example from group_standardTags: emits both
9078
+ // `isExample` (bool) AND `example` (paragraph array). Declared as
9079
+ // bitmarkText because BPG `JsonGenerator.toExample` renders the
9080
+ // root-example value through the bit's textFormat (not this tag's
9081
+ // declared format) — `TagFormat.plainText` here would be ignored
9082
+ // at runtime and is misleading.
7446
9083
  key: ConfigKey.property_example,
7447
9084
  exportJsonKey: [
7448
9085
  { "@keyonly": { isExample: true, example: "true" } },
@@ -7450,7 +9087,8 @@ var BITS = {
7450
9087
  { isExample: true, example: "$" }
7451
9088
  ],
7452
9089
  description: "The example text for the essay bit",
7453
- format: TagFormat.plainText
9090
+ format: TagFormat.bitmarkText,
9091
+ nullable: true
7454
9092
  }
7455
9093
  ],
7456
9094
  rootExampleType: ExampleType.string
@@ -7484,7 +9122,8 @@ var BITS = {
7484
9122
  { isExample: true, example: "$" }
7485
9123
  ],
7486
9124
  description: "The example text for the example bit",
7487
- format: TagFormat.plainText
9125
+ format: TagFormat.plainText,
9126
+ nullable: true
7488
9127
  }
7489
9128
  ],
7490
9129
  rootExampleType: ExampleType.string
@@ -7570,7 +9209,19 @@ var BITS = {
7570
9209
  [BitType2.articleAi]: {
7571
9210
  since: "1.3.0",
7572
9211
  baseBitType: BitType2.article,
7573
- description: "Article AI bit, used to create AI-generated articles"
9212
+ description: "Article AI bit, used to create AI-generated articles",
9213
+ tags: [
9214
+ {
9215
+ // Shadows the inherited @aiGenerated from the standard group: this
9216
+ // bit type forces aiGenerated:true regardless of source value or
9217
+ // absence. Literal `true` in the export pattern ignores `$`.
9218
+ key: ConfigKey.property_aiGenerated,
9219
+ description: "AI-generated flag, forced true for article-ai bits",
9220
+ format: TagFormat.boolean,
9221
+ defaultValue: "true",
9222
+ exportJsonKey: { aiGenerated: true }
9223
+ }
9224
+ ]
7574
9225
  },
7575
9226
  [BitType2.articleAttachment]: {
7576
9227
  since: "1.3.0",
@@ -7895,7 +9546,19 @@ var BITS = {
7895
9546
  [BitType2.noteAi]: {
7896
9547
  since: "1.3.0",
7897
9548
  baseBitType: BitType2.note,
7898
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9549
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9550
+ tags: [
9551
+ {
9552
+ // Shadows the inherited @aiGenerated from the standard group: this
9553
+ // bit type forces aiGenerated:true regardless of source value or
9554
+ // absence. Literal `true` in the export pattern ignores `$`.
9555
+ key: ConfigKey.property_aiGenerated,
9556
+ description: "AI-generated flag, forced true for article-ai bits",
9557
+ format: TagFormat.boolean,
9558
+ defaultValue: "true",
9559
+ exportJsonKey: { aiGenerated: true }
9560
+ }
9561
+ ]
7899
9562
  },
7900
9563
  [BitType2.notebookArticle]: {
7901
9564
  since: "1.3.0",
@@ -8020,7 +9683,19 @@ var BITS = {
8020
9683
  [BitType2.summaryAi]: {
8021
9684
  since: "1.3.0",
8022
9685
  baseBitType: BitType2.summary,
8023
- description: "AI-generated summary bit"
9686
+ description: "AI-generated summary bit",
9687
+ tags: [
9688
+ {
9689
+ // Shadows the inherited @aiGenerated from the standard group: this
9690
+ // bit type forces aiGenerated:true regardless of source value or
9691
+ // absence. Literal `true` in the export pattern ignores `$`.
9692
+ key: ConfigKey.property_aiGenerated,
9693
+ description: "AI-generated flag, forced true for article-ai bits",
9694
+ format: TagFormat.boolean,
9695
+ defaultValue: "true",
9696
+ exportJsonKey: { aiGenerated: true }
9697
+ }
9698
+ ]
8024
9699
  },
8025
9700
  [BitType2.videoTranscript]: {
8026
9701
  since: "1.3.0",
@@ -8294,7 +9969,7 @@ var BITS = {
8294
9969
  description: "Product ID for the module product, used to link to a specific product",
8295
9970
  format: TagFormat.plainText,
8296
9971
  minCount: 1,
8297
- maxCount: Count.infinity
9972
+ maxCount: 1
8298
9973
  }
8299
9974
  ]
8300
9975
  },
@@ -8347,10 +10022,13 @@ var BITS = {
8347
10022
  description: "Resource bit tags for logo grave images, used to define additional properties"
8348
10023
  },
8349
10024
  {
8350
- // Image resource
8351
- key: ConfigKey.group_resourceImage,
10025
+ // Image resource — fully-expanded shape so each `&image` emission
10026
+ // produces a complete `{type, image: {src}}` object inside the
10027
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
10028
+ // `images[].image` via the `[]` last-element-merge semantic.
10029
+ key: ConfigKey.group_resourceImageNoZoom,
8352
10030
  description: "Resource image tags for logo grave images, used to attach images",
8353
- exportJsonKey: { images: "$" },
10031
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
8354
10032
  minCount: 1,
8355
10033
  maxCount: Count.infinity
8356
10034
  }
@@ -8640,6 +10318,10 @@ var BITS = {
8640
10318
  tags: [
8641
10319
  {
8642
10320
  key: ConfigKey.property_book,
10321
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10322
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10323
+ // inside the most-recently-appended entry as `reference: $`.
10324
+ exportJsonKey: { book: [{ book: "$" }] },
8643
10325
  description: "Book reference for the page, used to link to a specific book",
8644
10326
  maxCount: Count.infinity,
8645
10327
  chain: [
@@ -8707,7 +10389,9 @@ var BITS = {
8707
10389
  format: TagFormat.plainText
8708
10390
  }
8709
10391
  ],
8710
- cardSet: CardSetConfigKey.exampleBitList
10392
+ // page-footer emits cards under `sections` (vs `listItems` for the
10393
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10394
+ cardSet: CardSetConfigKey.pageFooterSections
8711
10395
  },
8712
10396
  [BitType2.legend]: {
8713
10397
  since: "3.12.0",
@@ -8749,13 +10433,52 @@ var BITS = {
8749
10433
  since: "1.34.0",
8750
10434
  baseBitType: BitType2._standard,
8751
10435
  description: "Definition list bit, used to create lists of definitions in articles or books",
10436
+ tags: [
10437
+ {
10438
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10439
+ key: ConfigKey.property_example,
10440
+ exportJsonKey: [
10441
+ { "@keyonly": { isExample: true } },
10442
+ { "@absent": { isExample: true } },
10443
+ { isExample: true, example: "$" }
10444
+ ],
10445
+ description: "The example(s) for the bit",
10446
+ format: TagFormat.bitmarkText,
10447
+ nullable: true
10448
+ }
10449
+ ],
8752
10450
  cardSet: CardSetConfigKey.definitionList
8753
10451
  },
10452
+ [BitType2.definitionListCollapsible]: {
10453
+ since: "5.25.0",
10454
+ baseBitType: BitType2.definitionList,
10455
+ description: "Collapsible definition list bit, used to create collapsible lists of definitions in articles or books"
10456
+ },
10457
+ [BitType2.standardDefinitionListNormative]: {
10458
+ since: "5.24.0",
10459
+ baseBitType: BitType2.definitionList,
10460
+ description: "Standard normative definition list bit, used to create normative standard definition lists in articles or books"
10461
+ },
10462
+ [BitType2.standardDefinitionListNonNormative]: {
10463
+ since: "5.24.0",
10464
+ baseBitType: BitType2.definitionList,
10465
+ description: "Standard non-normative definition list bit, used to create non-normative standard definition lists in articles or books"
10466
+ },
10467
+ [BitType2.smartStandardDefinitionListNormative]: {
10468
+ since: "5.24.0",
10469
+ baseBitType: BitType2.standardDefinitionListNormative,
10470
+ description: "Smart standard normative definition list bit, used to create normative smart standard definition lists in articles or books"
10471
+ },
10472
+ [BitType2.smartStandardDefinitionListNonNormative]: {
10473
+ since: "5.24.0",
10474
+ baseBitType: BitType2.standardDefinitionListNonNormative,
10475
+ description: "Smart standard non-normative definition list bit, used to create non-normative smart standard definition lists in articles or books"
10476
+ },
8754
10477
  [BitType2.metaSearchDefaultTerms]: {
8755
10478
  since: "3.12.0",
8756
10479
  baseBitType: BitType2._standard,
8757
10480
  description: "Meta search default terms bit, used to define default search terms in articles",
8758
- cardSet: CardSetConfigKey.definitionList
10481
+ cardSet: CardSetConfigKey.definitionListPlain
8759
10482
  },
8760
10483
  [BitType2.metaSearchDefaultTopics]: {
8761
10484
  since: "3.12.0",
@@ -8771,6 +10494,18 @@ var BITS = {
8771
10494
  {
8772
10495
  key: ConfigKey.group_quizCommon,
8773
10496
  description: "Common quiz tags for flashcards"
10497
+ },
10498
+ {
10499
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10500
+ key: ConfigKey.property_example,
10501
+ exportJsonKey: [
10502
+ { "@keyonly": { isExample: true } },
10503
+ { "@absent": { isExample: true } },
10504
+ { isExample: true, example: "$" }
10505
+ ],
10506
+ description: "The example(s) for the bit",
10507
+ format: TagFormat.bitmarkText,
10508
+ nullable: true
8774
10509
  }
8775
10510
  ],
8776
10511
  cardSet: CardSetConfigKey.flashcard
@@ -8778,12 +10513,20 @@ var BITS = {
8778
10513
  [BitType2.flashcard1]: {
8779
10514
  since: "1.3.0",
8780
10515
  baseBitType: BitType2.flashcard,
8781
- description: "Flashcard 1 bit"
10516
+ description: "Flashcard 1 bit",
10517
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10518
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10519
+ // bit type structurally allows only a single card.
10520
+ cardSet: CardSetConfigKey.flashcard1
8782
10521
  },
8783
10522
  [BitType2.qAndACard]: {
8784
10523
  since: "3.25.0",
8785
10524
  baseBitType: BitType2.flashcard1,
8786
- description: "Q&A card bit, used to create question and answer cards in articles or books"
10525
+ description: "Q&A card bit, used to create question and answer cards in articles or books",
10526
+ // PLAN-085: q-and-a-card permits multiple cards (some fixtures carry
10527
+ // two). Override the inherited `flashcard1` (max=1) cardset back to
10528
+ // the unbounded `flashcard` cardset.
10529
+ cardSet: CardSetConfigKey.flashcard
8787
10530
  },
8788
10531
  [BitType2.focusImage]: {
8789
10532
  since: "1.3.0",
@@ -8813,8 +10556,24 @@ var BITS = {
8813
10556
  description: "Common quiz tags for highlighted text"
8814
10557
  },
8815
10558
  {
8816
- key: ConfigKey.group_trueFalse,
8817
- description: "True/False quiz tags for highlighted text"
10559
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10560
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10561
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10562
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10563
+ key: ConfigKey.group_trueFalseInlineHighlight,
10564
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10565
+ },
10566
+ {
10567
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10568
+ key: ConfigKey.property_example,
10569
+ exportJsonKey: [
10570
+ { "@keyonly": { isExample: true } },
10571
+ { "@absent": { isExample: true } },
10572
+ { isExample: true, example: "$" }
10573
+ ],
10574
+ description: "The example(s) for the bit",
10575
+ format: TagFormat.bitmarkText,
10576
+ nullable: true
8818
10577
  }
8819
10578
  ]
8820
10579
  },
@@ -8824,15 +10583,8 @@ var BITS = {
8824
10583
  description: "Image bit, used to create images in articles or books",
8825
10584
  tags: [
8826
10585
  {
8827
- key: ConfigKey.resource_backgroundWallpaper,
8828
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8829
- description: "Background wallpaper for the image, used to set a background for the image",
8830
- chain: [
8831
- {
8832
- key: ConfigKey.group_resourceImageCommon,
8833
- description: "Common resource image tags for images"
8834
- }
8835
- ]
10586
+ key: ConfigKey.group_backgroundWallpaper,
10587
+ description: "Background wallpaper tags for images, used to define background properties for images"
8836
10588
  },
8837
10589
  {
8838
10590
  key: ConfigKey.group_resourceBitTags,
@@ -8935,8 +10687,15 @@ var BITS = {
8935
10687
  },
8936
10688
  [BitType2.imageSeparator]: {
8937
10689
  since: "1.4.15",
8938
- baseBitType: BitType2.image,
8939
- description: "Image separator bit, used to create separators in articles or books"
10690
+ baseBitType: BitType2._standard,
10691
+ description: "Image separator bit, used to create separators in articles or books",
10692
+ tags: [
10693
+ {
10694
+ key: ConfigKey.group_imageNoZoom,
10695
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10696
+ }
10697
+ ],
10698
+ resourceAttachmentAllowed: false
8940
10699
  },
8941
10700
  [BitType2.imageSeparatorAlt]: {
8942
10701
  since: "1.16.0",
@@ -8975,15 +10734,20 @@ var BITS = {
8975
10734
  },
8976
10735
  [BitType2.pageBanner]: {
8977
10736
  since: "1.4.3",
8978
- baseBitType: BitType2.image,
10737
+ baseBitType: BitType2._standard,
8979
10738
  description: "Page banner bit, used to create banners for pages in articles or books",
8980
10739
  tags: [
8981
10740
  {
8982
10741
  key: ConfigKey.property_slug,
8983
10742
  description: "Slug for the page banner, used to identify the banner in the system",
8984
10743
  format: TagFormat.plainText
10744
+ },
10745
+ {
10746
+ key: ConfigKey.group_imageNoZoom,
10747
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
8985
10748
  }
8986
- ]
10749
+ ],
10750
+ resourceAttachmentAllowed: false
8987
10751
  },
8988
10752
  [BitType2.pageHero]: {
8989
10753
  since: "1.11.0",
@@ -8998,7 +10762,7 @@ var BITS = {
8998
10762
  },
8999
10763
  [BitType2.tableImage]: {
9000
10764
  since: "1.5.15",
9001
- baseBitType: BitType2.table,
10765
+ baseBitType: BitType2.tableExtended,
9002
10766
  description: "Table image bit, used to create images in tables in articles or books",
9003
10767
  tags: [
9004
10768
  {
@@ -9007,15 +10771,8 @@ var BITS = {
9007
10771
  format: TagFormat.bitmarkText
9008
10772
  },
9009
10773
  {
9010
- key: ConfigKey.resource_backgroundWallpaper,
9011
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
9012
- description: "Background wallpaper for the image, used to set a background for the image",
9013
- chain: [
9014
- {
9015
- key: ConfigKey.group_resourceImageCommon,
9016
- description: "Common resource image tags for images"
9017
- }
9018
- ]
10774
+ key: ConfigKey.group_backgroundWallpaper,
10775
+ description: "Background wallpaper tags for images, used to define background properties for images"
9019
10776
  },
9020
10777
  {
9021
10778
  key: ConfigKey.group_resourceBitTags,
@@ -9158,10 +10915,10 @@ var BITS = {
9158
10915
  description: "Resource bit tags for logo grave images, used to define additional properties"
9159
10916
  },
9160
10917
  {
9161
- // Image resource
9162
- key: ConfigKey.group_resourceImage,
10918
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10919
+ key: ConfigKey.group_resourceImageNoZoom,
9163
10920
  description: "Resource image tags for logo grave images, used to attach images",
9164
- exportJsonKey: { logos: "$" },
10921
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
9165
10922
  minCount: 1,
9166
10923
  maxCount: Count.infinity
9167
10924
  }
@@ -9171,7 +10928,17 @@ var BITS = {
9171
10928
  [BitType2.prototypeImages]: {
9172
10929
  since: "1.6.1",
9173
10930
  baseBitType: BitType2.imagesLogoGrave,
9174
- description: "Prototype images bit, used to create prototype images in articles or books"
10931
+ description: "Prototype images bit, used to create prototype images in articles or books",
10932
+ tags: [
10933
+ {
10934
+ // Override inherited `logos` wrapper with `images` for this bit type.
10935
+ key: ConfigKey.group_resourceImageNoZoom,
10936
+ description: "Resource image tags for prototype images",
10937
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
10938
+ minCount: 1,
10939
+ maxCount: Count.infinity
10940
+ }
10941
+ ]
9175
10942
  },
9176
10943
  [BitType2.internalLink]: {
9177
10944
  since: "1.3.0",
@@ -9201,6 +10968,18 @@ var BITS = {
9201
10968
  exportJsonKey: {},
9202
10969
  description: "Reasonable number of characters for the interview, used to limit input size",
9203
10970
  format: TagFormat.number
10971
+ },
10972
+ {
10973
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10974
+ key: ConfigKey.property_example,
10975
+ exportJsonKey: [
10976
+ { "@keyonly": { isExample: true } },
10977
+ { "@absent": { isExample: true } },
10978
+ { isExample: true, example: "$" }
10979
+ ],
10980
+ description: "The example(s) for the bit",
10981
+ format: TagFormat.bitmarkText,
10982
+ nullable: true
9204
10983
  }
9205
10984
  ],
9206
10985
  cardSet: CardSetConfigKey.questions
@@ -9559,6 +11338,18 @@ var BITS = {
9559
11338
  {
9560
11339
  key: ConfigKey.group_mark,
9561
11340
  description: "Mark tags, used to define the content of marks"
11341
+ },
11342
+ {
11343
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11344
+ key: ConfigKey.property_example,
11345
+ exportJsonKey: [
11346
+ { "@keyonly": { isExample: true } },
11347
+ { "@absent": { isExample: true } },
11348
+ { isExample: true, example: "$" }
11349
+ ],
11350
+ description: "The example(s) for the bit",
11351
+ format: TagFormat.bitmarkText,
11352
+ nullable: true
9562
11353
  }
9563
11354
  ]
9564
11355
  },
@@ -9578,6 +11369,23 @@ var BITS = {
9578
11369
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9579
11370
  format: TagFormat.boolean,
9580
11371
  defaultValue: "true"
11372
+ },
11373
+ {
11374
+ // Override inherited @example: in `isTopLevelExample` allow-list
11375
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11376
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11377
+ // Bit-level fires emit only `isExample`; the @example value (when
11378
+ // provided) is recorded in the ancestor stack and consumed by
11379
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11380
+ key: ConfigKey.property_example,
11381
+ exportJsonKey: [
11382
+ { "@keyonly": { isExample: true } },
11383
+ { "@absent": { isExample: true } },
11384
+ { isExample: true }
11385
+ ],
11386
+ description: "The example(s) for the bit",
11387
+ format: TagFormat.bitmarkText,
11388
+ nullable: true
9581
11389
  }
9582
11390
  ],
9583
11391
  cardSet: CardSetConfigKey.matchPairs
@@ -9662,6 +11470,18 @@ var BITS = {
9662
11470
  {
9663
11471
  key: ConfigKey.group_trueFalse,
9664
11472
  description: "True/False tags for multiple choice 1 questions"
11473
+ },
11474
+ {
11475
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11476
+ key: ConfigKey.property_example,
11477
+ exportJsonKey: [
11478
+ { "@keyonly": { isExample: true } },
11479
+ { "@absent": { isExample: true } },
11480
+ { isExample: true, example: "$" }
11481
+ ],
11482
+ description: "The example(s) for the bit",
11483
+ format: TagFormat.bitmarkText,
11484
+ nullable: true
9665
11485
  }
9666
11486
  ]
9667
11487
  },
@@ -9683,8 +11503,20 @@ var BITS = {
9683
11503
  {
9684
11504
  key: ConfigKey.group_trueFalse,
9685
11505
  description: "True/False tags for multiple choice questions"
9686
- }
11506
+ },
9687
11507
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11508
+ {
11509
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11510
+ key: ConfigKey.property_example,
11511
+ exportJsonKey: [
11512
+ { "@keyonly": { isExample: true } },
11513
+ { "@absent": { isExample: true } },
11514
+ { isExample: true, example: "$" }
11515
+ ],
11516
+ description: "The example(s) for the bit",
11517
+ format: TagFormat.bitmarkText,
11518
+ nullable: true
11519
+ }
9688
11520
  ],
9689
11521
  cardSet: CardSetConfigKey.quiz
9690
11522
  },
@@ -9704,8 +11536,20 @@ var BITS = {
9704
11536
  description: "Common quiz tags for multiple choice text questions"
9705
11537
  },
9706
11538
  {
9707
- key: ConfigKey.group_trueFalse,
11539
+ key: ConfigKey.group_trueFalseInlineSelect,
9708
11540
  description: "True/False tags for multiple choice text questions"
11541
+ },
11542
+ {
11543
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11544
+ key: ConfigKey.property_example,
11545
+ exportJsonKey: [
11546
+ { "@keyonly": { isExample: true } },
11547
+ { "@absent": { isExample: true } },
11548
+ { isExample: true, example: "$" }
11549
+ ],
11550
+ description: "The example(s) for the bit",
11551
+ format: TagFormat.bitmarkText,
11552
+ nullable: true
9709
11553
  }
9710
11554
  ]
9711
11555
  },
@@ -9730,8 +11574,20 @@ var BITS = {
9730
11574
  description: "Common quiz tags for multiple response 1 questions"
9731
11575
  },
9732
11576
  {
9733
- key: ConfigKey.group_trueFalse,
11577
+ key: ConfigKey.group_trueFalseResponses,
9734
11578
  description: "True/False tags for multiple response 1 questions"
11579
+ },
11580
+ {
11581
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11582
+ key: ConfigKey.property_example,
11583
+ exportJsonKey: [
11584
+ { "@keyonly": { isExample: true } },
11585
+ { "@absent": { isExample: true } },
11586
+ { isExample: true, example: "$" }
11587
+ ],
11588
+ description: "The example(s) for the bit",
11589
+ format: TagFormat.bitmarkText,
11590
+ nullable: true
9735
11591
  }
9736
11592
  ]
9737
11593
  },
@@ -9751,12 +11607,24 @@ var BITS = {
9751
11607
  description: "Common quiz tags for multiple response questions"
9752
11608
  },
9753
11609
  {
9754
- key: ConfigKey.group_trueFalse,
11610
+ key: ConfigKey.group_trueFalseResponses,
9755
11611
  description: "True/False tags for multiple response questions"
9756
- }
11612
+ },
9757
11613
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11614
+ {
11615
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11616
+ key: ConfigKey.property_example,
11617
+ exportJsonKey: [
11618
+ { "@keyonly": { isExample: true } },
11619
+ { "@absent": { isExample: true } },
11620
+ { isExample: true, example: "$" }
11621
+ ],
11622
+ description: "The example(s) for the bit",
11623
+ format: TagFormat.bitmarkText,
11624
+ nullable: true
11625
+ }
9758
11626
  ],
9759
- cardSet: CardSetConfigKey.quiz
11627
+ cardSet: CardSetConfigKey.quizResponses
9760
11628
  },
9761
11629
  [BitType2.coachSelfReflectionMultipleResponse]: {
9762
11630
  since: "1.3.0",
@@ -10006,6 +11874,8 @@ var BITS = {
10006
11874
  },
10007
11875
  {
10008
11876
  key: ConfigKey.resource_platformIcon,
11877
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11878
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
10009
11879
  description: "The platform icon",
10010
11880
  chain: [
10011
11881
  {
@@ -10041,6 +11911,8 @@ var BITS = {
10041
11911
  tags: [
10042
11912
  {
10043
11913
  key: ConfigKey.resource_platformLogo,
11914
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11915
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
10044
11916
  description: "The platform logo",
10045
11917
  chain: [
10046
11918
  {
@@ -10252,7 +12124,8 @@ var BITS = {
10252
12124
  { isExample: true, example: "$" }
10253
12125
  ],
10254
12126
  description: "The example text for the review note bit",
10255
- format: TagFormat.plainText
12127
+ format: TagFormat.plainText,
12128
+ nullable: true
10256
12129
  }
10257
12130
  ],
10258
12131
  rootExampleType: ExampleType.string
@@ -10339,7 +12212,8 @@ var BITS = {
10339
12212
  { isExample: true, example: "$" }
10340
12213
  ],
10341
12214
  description: "The example flag for the bit (boolean)",
10342
- format: TagFormat.plainText
12215
+ format: TagFormat.boolean,
12216
+ nullable: true
10343
12217
  }
10344
12218
  ],
10345
12219
  cardSet: CardSetConfigKey.elements,
@@ -10613,15 +12487,8 @@ var BITS = {
10613
12487
  format: TagFormat.bitmarkText
10614
12488
  },
10615
12489
  {
10616
- key: ConfigKey.resource_backgroundWallpaper,
10617
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
10618
- description: "Background wallpaper for the image, used to set a background for the image",
10619
- chain: [
10620
- {
10621
- key: ConfigKey.group_resourceImageCommon,
10622
- description: "Common resource image tags for images"
10623
- }
10624
- ]
12490
+ key: ConfigKey.group_backgroundWallpaper,
12491
+ description: "Background wallpaper tags for images, used to define background properties for images"
10625
12492
  },
10626
12493
  {
10627
12494
  key: ConfigKey.group_resourceBitTags,
@@ -10998,16 +12865,20 @@ var BITS = {
10998
12865
  format: TagFormat.plainText
10999
12866
  },
11000
12867
  {
11001
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
12868
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12869
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12870
+ // not a `choices[]` array. Format is bitmarkText so the statement
12871
+ // body renders as a ProseMirror tree, matching reference fixtures.
12872
+ exportJsonKey: { statement: "$", isCorrect: true },
11002
12873
  key: ConfigKey.tag_true,
11003
12874
  description: "Tag for the true option in the true/false question",
11004
- format: TagFormat.plainText
12875
+ format: TagFormat.bitmarkText
11005
12876
  },
11006
12877
  {
11007
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
12878
+ exportJsonKey: { statement: "$", isCorrect: false },
11008
12879
  key: ConfigKey.tag_false,
11009
12880
  description: "Tag for the false option in the true/false question",
11010
- format: TagFormat.plainText
12881
+ format: TagFormat.bitmarkText
11011
12882
  },
11012
12883
  {
11013
12884
  // Override inherited @example from group_standardTags: boolean root
@@ -11018,14 +12889,24 @@ var BITS = {
11018
12889
  // sufficient for `[+...]` (correct) statements which dominate the
11019
12890
  // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
11020
12891
  // need either a runtime extension or an explicit `[@example:false]`.
12892
+ //
12893
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12894
+ // (a natural default that gets stripped in optimised mode, matching
12895
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12896
+ // STRING `"false"` which doesn't strip.
11021
12897
  key: ConfigKey.property_example,
11022
12898
  exportJsonKey: [
11023
12899
  { "@keyonly": { isExample: true, example: true } },
11024
12900
  { "@absent": { isExample: true, example: true } },
12901
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12902
+ // is the natural default for boolean, stripped in optimised mode.
12903
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12904
+ { "@value=false": { isExample: true } },
11025
12905
  { isExample: true, example: "$" }
11026
12906
  ],
11027
12907
  description: "The example flag for the bit (boolean)",
11028
- format: TagFormat.plainText
12908
+ format: TagFormat.boolean,
12909
+ nullable: true
11029
12910
  }
11030
12911
  ],
11031
12912
  rootExampleType: ExampleType.boolean
@@ -11049,6 +12930,18 @@ var BITS = {
11049
12930
  key: ConfigKey.property_labelFalse,
11050
12931
  description: "Label for the false option in the true/false question",
11051
12932
  format: TagFormat.plainText
12933
+ },
12934
+ {
12935
+ // Override inherited @example: in `isTopLevelExample` allow-list.
12936
+ key: ConfigKey.property_example,
12937
+ exportJsonKey: [
12938
+ { "@keyonly": { isExample: true } },
12939
+ { "@absent": { isExample: true } },
12940
+ { isExample: true, example: "$" }
12941
+ ],
12942
+ description: "The example(s) for the bit",
12943
+ format: TagFormat.bitmarkText,
12944
+ nullable: true
11052
12945
  }
11053
12946
  ],
11054
12947
  cardSet: CardSetConfigKey.statements
@@ -11238,6 +13131,11 @@ var BITS = {
11238
13131
  baseBitType: BitType2.vendorStripePricingTable,
11239
13132
  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",
11240
13133
  tags: [
13134
+ {
13135
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
13136
+ description: "Descriptive name for the Stripe pricing table",
13137
+ format: TagFormat.plainText
13138
+ },
11241
13139
  {
11242
13140
  key: ConfigKey.property_printOnRequestCustomerId,
11243
13141
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -11714,7 +13612,7 @@ var instance2 = new Config();
11714
13612
  // src/generated/package_info.ts
11715
13613
  var PACKAGE_INFO = {
11716
13614
  "name": "@gmb/bitmark-parser-generator",
11717
- "version": "5.23.0",
13615
+ "version": "5.25.0",
11718
13616
  "author": "Get More Brain Ltd",
11719
13617
  "license": "ISC",
11720
13618
  "description": "A bitmark parser and generator using Peggy.js"
@@ -12360,6 +14258,8 @@ var NodeType = {
12360
14258
  internalPrintPdf: "internalPrintPdf",
12361
14259
  internalPrintPdfValue: "internalPrintPdfValue",
12362
14260
  isCaseSensitive: "isCaseSensitive",
14261
+ isCollapsible: "isCollapsible",
14262
+ isCollapsibleValue: "isCollapsibleValue",
12363
14263
  isCommented: "isCommented",
12364
14264
  isCorrect: "isCorrect",
12365
14265
  isEditable: "isEditable",
@@ -12679,6 +14579,8 @@ var NodeType = {
12679
14579
  stillImageFilmLink: "stillImageFilmLink",
12680
14580
  stripePricingTableId: "stripePricingTableId",
12681
14581
  stripePricingTableIdValue: "stripePricingTableIdValue",
14582
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14583
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12682
14584
  stripePublishableKey: "stripePublishableKey",
12683
14585
  stripePublishableKeyValue: "stripePublishableKeyValue",
12684
14586
  subject: "subject",
@@ -26104,6 +28006,12 @@ var Builder = class extends BaseBuilder {
26104
28006
  data.stripePricingTableId,
26105
28007
  options
26106
28008
  ),
28009
+ stripePricingTableDescriptiveName: this.toAstProperty(
28010
+ bitType,
28011
+ ConfigKey.property_stripePricingTableDescriptiveName,
28012
+ data.stripePricingTableDescriptiveName,
28013
+ options
28014
+ ),
26107
28015
  stripePublishableKey: this.toAstProperty(
26108
28016
  bitType,
26109
28017
  ConfigKey.property_stripePublishableKey,
@@ -26590,6 +28498,12 @@ var Builder = class extends BaseBuilder {
26590
28498
  level: instance6.asNumber(data.level),
26591
28499
  toc: this.toAstProperty(bitType, ConfigKey.property_toc, data.toc, options),
26592
28500
  progress: this.toAstProperty(bitType, ConfigKey.property_progress, data.progress, options),
28501
+ isCollapsible: this.toAstProperty(
28502
+ bitType,
28503
+ ConfigKey.property_isCollapsible,
28504
+ data.isCollapsible,
28505
+ options
28506
+ ),
26593
28507
  anchor: data.anchor,
26594
28508
  reference: data.reference,
26595
28509
  referenceEnd: data.referenceEnd,
@@ -32136,6 +34050,7 @@ var JsonGenerator = class extends AstWalkerGenerator {
32136
34050
  if (instance2.isOfBitType(bitType, BitType2.chapter)) {
32137
34051
  if (bitJson.toc == null) bitJson.toc = true;
32138
34052
  if (bitJson.progress == null) bitJson.progress = true;
34053
+ if (bitJson.isCollapsible == null) bitJson.isCollapsible = false;
32139
34054
  if (bitJson.level == null) bitJson.level = 1;
32140
34055
  if (bitJson.body == null) bitJson.body = this.bodyDefault;
32141
34056
  }
@@ -34331,7 +36246,9 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34331
36246
  const cardSetType = bitConfig.cardSet?.configKey;
34332
36247
  switch (cardSetType) {
34333
36248
  case CardSetConfigKey.flashcard:
36249
+ case CardSetConfigKey.flashcard1:
34334
36250
  case CardSetConfigKey.definitionList:
36251
+ case CardSetConfigKey.definitionListPlain:
34335
36252
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
34336
36253
  break;
34337
36254
  case CardSetConfigKey.elements:
@@ -34344,6 +36261,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34344
36261
  result = parseFeedback(context, bitType, processedCardSet);
34345
36262
  break;
34346
36263
  case CardSetConfigKey.quiz:
36264
+ case CardSetConfigKey.quizResponses:
34347
36265
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
34348
36266
  break;
34349
36267
  case CardSetConfigKey.questions:
@@ -34370,6 +36288,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34370
36288
  break;
34371
36289
  case CardSetConfigKey.clozeList:
34372
36290
  case CardSetConfigKey.exampleBitList:
36291
+ case CardSetConfigKey.pageFooterSections:
34373
36292
  case CardSetConfigKey.bookReferenceList:
34374
36293
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
34375
36294
  break;
@@ -40396,7 +42315,11 @@ var InputFormat = {
40396
42315
  /**
40397
42316
  * Input is plain text
40398
42317
  */
40399
- plainText: "plainText"
42318
+ plainText: "plainText",
42319
+ /**
42320
+ * Input is HTML (used by convertHtmlTable)
42321
+ */
42322
+ html: "html"
40400
42323
  };
40401
42324
  var BitmarkParserGenerator = class {
40402
42325
  constructor() {
@@ -40931,6 +42854,87 @@ var BitmarkParserGenerator = class {
40931
42854
  const res = generator.generate(data);
40932
42855
  return res;
40933
42856
  }
42857
+ /**
42858
+ * Convert between HTML tables and bitmark tables.
42859
+ *
42860
+ * Direction is auto-detected (override with `options.inputFormat`):
42861
+ * - HTML input: every `<table>` is extracted and emitted as a `[.table-extended]` (or `[.table]`)
42862
+ * bit; output as bitmark (default), JSON, AST, or HTML (re-emit).
42863
+ * - bitmark / JSON / AST input: each `table` / `table-extended` bit is rendered as an HTML
42864
+ * `<table>` fragment.
42865
+ *
42866
+ * Cell content is reproduced verbatim except inline conversion to bitmark marks (bold, italic,
42867
+ * superscript, etc.), with lists and images mapped to their bitmark equivalents.
42868
+ *
42869
+ * @param input - HTML, bitmark, JSON or AST string; or a parsed JSON/AST value; or a file path.
42870
+ * @param options - the conversion options
42871
+ * @returns the converted output as a string (or object for JSON/AST), or void when writing a file
42872
+ * @throws Error if any error occurs
42873
+ */
42874
+ convertHtmlTable(input, options) {
42875
+ const opts = Object.assign({}, options);
42876
+ if (env.isBrowser && opts.outputFile) {
42877
+ throw new Error("Cannot write to file in browser environment");
42878
+ }
42879
+ let inStr = input;
42880
+ const inputIsString = typeof input === "string";
42881
+ if (env.isNode && inputIsString && fs.existsSync(inStr)) {
42882
+ inStr = fs.readFileSync(inStr, { encoding: "utf8" });
42883
+ }
42884
+ const isHtmlString = (s) => {
42885
+ const t = s.replace(/^\s+/, "");
42886
+ return t.startsWith("<") && /<table/i.test(t);
42887
+ };
42888
+ let htmlInput;
42889
+ if (opts.inputFormat === InputFormat.html) {
42890
+ htmlInput = true;
42891
+ } else if (opts.inputFormat != null) {
42892
+ htmlInput = false;
42893
+ } else {
42894
+ htmlInput = inputIsString && isHtmlString(inStr);
42895
+ }
42896
+ if (htmlInput) {
42897
+ const parser2 = new HtmlTableParser();
42898
+ const bits = parser2.parse(inStr, {
42899
+ tableFormat: opts.tableFormat ?? "table-extended",
42900
+ keepUnknownTags: opts.keepUnknownTags,
42901
+ onWarning: (m) => console.warn(m)
42902
+ });
42903
+ const outputFormat2 = opts.outputFormat ?? Output.bitmark;
42904
+ if (outputFormat2 === "html") {
42905
+ const json2 = this.convert(bits, { outputFormat: Output.json });
42906
+ return this.outputHtml(new HtmlTableGenerator().generate(json2), opts);
42907
+ }
42908
+ return this.convert(bits, {
42909
+ outputFormat: Enum22(Output).fromValue(outputFormat2),
42910
+ outputFile: opts.outputFile,
42911
+ fileOptions: opts.fileOptions,
42912
+ jsonOptions: opts.jsonOptions,
42913
+ bitmarkOptions: opts.bitmarkOptions
42914
+ });
42915
+ }
42916
+ const outputFormat = opts.outputFormat ?? "html";
42917
+ if (outputFormat !== "html") {
42918
+ throw new Error(
42919
+ "convertHtmlTable: non-HTML input can only be converted to HTML. Use convert() for bitmark/JSON/AST conversions."
42920
+ );
42921
+ }
42922
+ const json = this.convert(inputIsString ? inStr : input, { outputFormat: Output.json });
42923
+ return this.outputHtml(new HtmlTableGenerator().generate(json), opts);
42924
+ }
42925
+ /**
42926
+ * Write an HTML string to the configured output file, or return it.
42927
+ */
42928
+ outputHtml(html, opts) {
42929
+ if (opts.outputFile) {
42930
+ const output = opts.outputFile.toString();
42931
+ const flag = opts.fileOptions?.append ? "a" : "w";
42932
+ fs.ensureDirSync(path.dirname(output));
42933
+ fs.writeFileSync(output, html, { flag });
42934
+ return;
42935
+ }
42936
+ return html;
42937
+ }
40934
42938
  /**
40935
42939
  * Breakscape bitmark text.
40936
42940
  *
@@ -41209,6 +43213,728 @@ var BoundingBoxIndex = class _BoundingBoxIndex {
41209
43213
  }
41210
43214
  };
41211
43215
 
43216
+ // src/generator/html/HtmlTableGenerator.ts
43217
+ var MARK_TO_TAG = {
43218
+ bold: "b",
43219
+ italic: "i",
43220
+ superscript: "sup",
43221
+ subscript: "sub",
43222
+ strike: "s",
43223
+ del: "del",
43224
+ ins: "ins",
43225
+ underline: "u",
43226
+ highlight: "mark",
43227
+ code: "code"
43228
+ };
43229
+ var HtmlTableGenerator2 = class {
43230
+ constructor() {
43231
+ __publicField(this, "indentUnit", " ");
43232
+ }
43233
+ /**
43234
+ * Generate HTML table fragments from a string or JSON value.
43235
+ * @returns the concatenated `<table>` fragments, blank-line separated (empty string if none).
43236
+ */
43237
+ generate(input) {
43238
+ let data = input;
43239
+ if (typeof data === "string") {
43240
+ try {
43241
+ data = JSON.parse(data);
43242
+ } catch {
43243
+ return "";
43244
+ }
43245
+ }
43246
+ const bits = this.collectBits(data);
43247
+ const fragments = [];
43248
+ for (const bit of bits) {
43249
+ const html = this.renderTableBit(bit);
43250
+ if (html) fragments.push(html);
43251
+ }
43252
+ return fragments.join("\n\n");
43253
+ }
43254
+ // --- bit collection -------------------------------------------------------
43255
+ collectBits(data) {
43256
+ const out = [];
43257
+ const visit = (value) => {
43258
+ if (value == null || typeof value !== "object") return;
43259
+ if (Array.isArray(value)) {
43260
+ value.forEach(visit);
43261
+ return;
43262
+ }
43263
+ const obj = value;
43264
+ if ("bit" in obj && obj.bit && typeof obj.bit === "object") {
43265
+ out.push(obj.bit);
43266
+ } else if ("type" in obj) {
43267
+ out.push(obj);
43268
+ }
43269
+ };
43270
+ visit(data);
43271
+ return out;
43272
+ }
43273
+ // --- table rendering ------------------------------------------------------
43274
+ renderTableBit(bit) {
43275
+ const table = bit.table;
43276
+ if (!table) return "";
43277
+ if (bit.type !== "table" && bit.type !== "table-extended") return "";
43278
+ const extended = this.toExtended(table);
43279
+ const lines = [];
43280
+ lines.push("<table>");
43281
+ if (bit.caption != null) {
43282
+ const caption = this.cellContentToHtml(this.asTextAst(bit.caption));
43283
+ if (caption) lines.push(`${this.indentUnit}<caption>${caption}</caption>`);
43284
+ }
43285
+ this.renderSection(lines, "thead", extended.header, "header");
43286
+ this.renderSection(lines, "tbody", extended.body, "body");
43287
+ this.renderSection(lines, "tfoot", extended.footer, "footer");
43288
+ lines.push("</table>");
43289
+ return lines.join("\n");
43290
+ }
43291
+ toExtended(table) {
43292
+ const t = table;
43293
+ if (t.header || t.body || t.footer) return table;
43294
+ return convertBasicToExtendedTableFormat(table);
43295
+ }
43296
+ renderSection(lines, tag, section, key) {
43297
+ if (!section || !section.rows || section.rows.length === 0) return;
43298
+ lines.push(`${this.indentUnit}<${tag}>`);
43299
+ for (const row of section.rows) {
43300
+ lines.push(`${this.indentUnit.repeat(2)}<tr>`);
43301
+ for (const cell of row.cells ?? []) {
43302
+ lines.push(`${this.indentUnit.repeat(3)}${this.renderCell(cell, key)}`);
43303
+ }
43304
+ lines.push(`${this.indentUnit.repeat(2)}</tr>`);
43305
+ }
43306
+ lines.push(`${this.indentUnit}</${tag}>`);
43307
+ }
43308
+ renderCell(cell, _key) {
43309
+ const tag = cell.title === true ? "th" : "td";
43310
+ const attrs = [];
43311
+ if (cell.rowspan && cell.rowspan > 1) attrs.push(`rowspan="${cell.rowspan}"`);
43312
+ if (cell.colspan && cell.colspan > 1) attrs.push(`colspan="${cell.colspan}"`);
43313
+ if (cell.scope) attrs.push(`scope="${cell.scope}"`);
43314
+ if (cell.colwidth && cell.colwidth > 0) attrs.push(`width="${cell.colwidth}"`);
43315
+ const open = attrs.length > 0 ? `<${tag} ${attrs.join(" ")}>` : `<${tag}>`;
43316
+ return `${open}${this.cellContentToHtml(this.asTextAst(cell.content))}</${tag}>`;
43317
+ }
43318
+ // --- cell content (TextAst -> HTML) ---------------------------------------
43319
+ asTextAst(content) {
43320
+ if (content == null) return [];
43321
+ if (typeof content === "string") return [{ type: "text", text: content }];
43322
+ return content;
43323
+ }
43324
+ cellContentToHtml(blocks) {
43325
+ if (blocks.length === 0) return "";
43326
+ if (blocks.length === 1 && blocks[0].type === "paragraph") {
43327
+ return this.inlineToHtml(blocks[0].content ?? []);
43328
+ }
43329
+ return blocks.map((b) => this.blockToHtml(b)).join("");
43330
+ }
43331
+ blockToHtml(node) {
43332
+ switch (node.type) {
43333
+ case "paragraph":
43334
+ return `<p>${this.inlineToHtml(node.content ?? [])}</p>`;
43335
+ case "bulletList":
43336
+ case "noBulletList":
43337
+ case "orderedList":
43338
+ case "orderedListRoman":
43339
+ case "orderedListRomanLower":
43340
+ case "letteredList":
43341
+ case "letteredListLower":
43342
+ case "taskList":
43343
+ return this.listToHtml(node);
43344
+ case "image":
43345
+ case "imageInline":
43346
+ return this.imageToHtml(node);
43347
+ default:
43348
+ return this.inlineToHtml([node]);
43349
+ }
43350
+ }
43351
+ inlineToHtml(nodes) {
43352
+ return nodes.map((n) => this.inlineNodeToHtml(n)).join("");
43353
+ }
43354
+ inlineNodeToHtml(node) {
43355
+ if (node.type === "hardBreak") return "<br>";
43356
+ if (node.type === "image" || node.type === "imageInline") return this.imageToHtml(node);
43357
+ if (node.type === "text") {
43358
+ return this.wrapMarks(this.escapeText(node.text ?? ""), node.marks);
43359
+ }
43360
+ if (node.content) return this.inlineToHtml(node.content);
43361
+ return this.escapeText(node.text ?? "");
43362
+ }
43363
+ wrapMarks(text, marks) {
43364
+ if (!marks || marks.length === 0) return text;
43365
+ let open = "";
43366
+ let close = "";
43367
+ for (const mark of marks) {
43368
+ if (mark.type === "link") {
43369
+ const href = mark.attrs?.href ?? "";
43370
+ open += `<a href="${this.escapeAttr(href)}">`;
43371
+ close = `</a>${close}`;
43372
+ } else {
43373
+ const tag = MARK_TO_TAG[mark.type];
43374
+ if (tag) {
43375
+ open += `<${tag}>`;
43376
+ close = `</${tag}>${close}`;
43377
+ }
43378
+ }
43379
+ }
43380
+ return `${open}${text}${close}`;
43381
+ }
43382
+ // --- lists ----------------------------------------------------------------
43383
+ listToHtml(node) {
43384
+ const { tag, attrs } = this.listTag(node);
43385
+ const items = (node.content ?? []).map((item) => this.listItemToHtml(item, node.type === "taskList")).join("");
43386
+ return `<${tag}${attrs}>${items}</${tag}>`;
43387
+ }
43388
+ listTag(node) {
43389
+ const start = node.attrs?.start;
43390
+ const startAttr = typeof start === "number" && start !== 1 ? ` start="${start}"` : "";
43391
+ switch (node.type) {
43392
+ case "orderedList":
43393
+ return { tag: "ol", attrs: startAttr };
43394
+ case "orderedListRoman":
43395
+ return { tag: "ol", attrs: ` type="I"${startAttr}` };
43396
+ case "orderedListRomanLower":
43397
+ return { tag: "ol", attrs: ` type="i"${startAttr}` };
43398
+ case "letteredList":
43399
+ return { tag: "ol", attrs: ` type="A"${startAttr}` };
43400
+ case "letteredListLower":
43401
+ return { tag: "ol", attrs: ` type="a"${startAttr}` };
43402
+ case "noBulletList":
43403
+ return { tag: "ul", attrs: ' style="list-style:none"' };
43404
+ case "taskList":
43405
+ case "bulletList":
43406
+ default:
43407
+ return { tag: "ul", attrs: "" };
43408
+ }
43409
+ }
43410
+ listItemToHtml(item, task) {
43411
+ const inner = (item.content ?? []).map((b) => {
43412
+ if (b.type === "paragraph") return this.inlineToHtml(b.content ?? []);
43413
+ return this.blockToHtml(b);
43414
+ }).join("");
43415
+ if (task) {
43416
+ const checked = item.attrs?.checked ? " checked" : "";
43417
+ return `<li><input type="checkbox"${checked}>${inner}</li>`;
43418
+ }
43419
+ return `<li>${inner}</li>`;
43420
+ }
43421
+ // --- images ---------------------------------------------------------------
43422
+ imageToHtml(node) {
43423
+ const attrs = node.attrs ?? {};
43424
+ const parts = [];
43425
+ const src = attrs.src;
43426
+ parts.push(`src="${this.escapeAttr(typeof src === "string" ? src : "")}"`);
43427
+ if (typeof attrs.alt === "string") parts.push(`alt="${this.escapeAttr(attrs.alt)}"`);
43428
+ if (typeof attrs.title === "string") parts.push(`title="${this.escapeAttr(attrs.title)}"`);
43429
+ if (attrs.width != null) parts.push(`width="${attrs.width}"`);
43430
+ if (attrs.height != null) parts.push(`height="${attrs.height}"`);
43431
+ if (typeof attrs.class === "string" && attrs.class) {
43432
+ parts.push(`class="${this.escapeAttr(attrs.class)}"`);
43433
+ }
43434
+ return `<img ${parts.join(" ")}>`;
43435
+ }
43436
+ // --- escaping -------------------------------------------------------------
43437
+ escapeText(text) {
43438
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
43439
+ }
43440
+ escapeAttr(text) {
43441
+ return this.escapeText(text).replace(/"/g, "&quot;");
43442
+ }
43443
+ };
43444
+
43445
+ // src/parser/html/HtmlDom.ts
43446
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
43447
+ "area",
43448
+ "base",
43449
+ "br",
43450
+ "col",
43451
+ "embed",
43452
+ "hr",
43453
+ "img",
43454
+ "input",
43455
+ "link",
43456
+ "meta",
43457
+ "param",
43458
+ "source",
43459
+ "track",
43460
+ "wbr"
43461
+ ]);
43462
+ var IMPLIED_CLOSE = {
43463
+ li: /* @__PURE__ */ new Set(["li"]),
43464
+ td: /* @__PURE__ */ new Set(["td", "th"]),
43465
+ th: /* @__PURE__ */ new Set(["td", "th"]),
43466
+ tr: /* @__PURE__ */ new Set(["td", "th", "tr"]),
43467
+ thead: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43468
+ tbody: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43469
+ tfoot: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43470
+ p: /* @__PURE__ */ new Set(["p"]),
43471
+ option: /* @__PURE__ */ new Set(["option"])
43472
+ };
43473
+ var TAG_RE = /<(\/)?([a-zA-Z][a-zA-Z0-9-]*)((?:\s+(?:"[^"]*"|'[^']*'|[^<>"'])*)?)(\/)?>/g;
43474
+ var ATTR_RE = /([a-zA-Z_:][a-zA-Z0-9:._-]*)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s"'>]+))?/g;
43475
+ function parseAttrs(raw) {
43476
+ const attrs = {};
43477
+ if (!raw) return attrs;
43478
+ ATTR_RE.lastIndex = 0;
43479
+ let m;
43480
+ while ((m = ATTR_RE.exec(raw)) !== null) {
43481
+ const name = m[1].toLowerCase();
43482
+ let value = m[2] ?? "";
43483
+ if (value.length >= 2 && (value[0] === '"' || value[0] === "'")) {
43484
+ value = value.slice(1, -1);
43485
+ }
43486
+ attrs[name] = value;
43487
+ }
43488
+ return attrs;
43489
+ }
43490
+ function stripNonContent(html) {
43491
+ 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, "");
43492
+ }
43493
+ function tokenize(html) {
43494
+ const tokens = [];
43495
+ const clean = stripNonContent(html);
43496
+ TAG_RE.lastIndex = 0;
43497
+ let lastIndex = 0;
43498
+ let m;
43499
+ while ((m = TAG_RE.exec(clean)) !== null) {
43500
+ if (m.index > lastIndex) {
43501
+ tokens.push({ kind: "text", text: clean.slice(lastIndex, m.index) });
43502
+ }
43503
+ lastIndex = TAG_RE.lastIndex;
43504
+ const isClose = m[1] === "/";
43505
+ const name = m[2].toLowerCase();
43506
+ if (isClose) {
43507
+ tokens.push({ kind: "close", name });
43508
+ } else {
43509
+ tokens.push({
43510
+ kind: "open",
43511
+ name,
43512
+ attrs: parseAttrs(m[3]),
43513
+ selfClose: m[4] === "/"
43514
+ });
43515
+ }
43516
+ }
43517
+ if (lastIndex < clean.length) {
43518
+ tokens.push({ kind: "text", text: clean.slice(lastIndex) });
43519
+ }
43520
+ return tokens;
43521
+ }
43522
+ function parseHtml(html) {
43523
+ const root = { type: "element", name: "#root", attrs: {}, children: [] };
43524
+ const stack = [root];
43525
+ const top = () => stack[stack.length - 1];
43526
+ for (const token of tokenize(html)) {
43527
+ if (token.kind === "text") {
43528
+ if (token.text) top().children.push({ type: "text", value: token.text });
43529
+ continue;
43530
+ }
43531
+ if (token.kind === "open") {
43532
+ const name = token.name;
43533
+ const closes = IMPLIED_CLOSE[name];
43534
+ if (closes) {
43535
+ while (stack.length > 1 && closes.has(top().name)) stack.pop();
43536
+ }
43537
+ const el = {
43538
+ type: "element",
43539
+ name,
43540
+ attrs: token.attrs ?? {},
43541
+ children: []
43542
+ };
43543
+ top().children.push(el);
43544
+ if (!token.selfClose && !VOID_ELEMENTS.has(name)) {
43545
+ stack.push(el);
43546
+ }
43547
+ continue;
43548
+ }
43549
+ for (let i = stack.length - 1; i > 0; i--) {
43550
+ if (stack[i].name === token.name) {
43551
+ stack.length = i;
43552
+ break;
43553
+ }
43554
+ }
43555
+ }
43556
+ return root.children;
43557
+ }
43558
+ function childElements(el, names) {
43559
+ return el.children.filter((c) => c.type === "element" && names.has(c.name));
43560
+ }
43561
+
43562
+ // src/parser/html/HtmlTableParser.ts
43563
+ var MARK_TAGS = {
43564
+ b: "bold",
43565
+ strong: "bold",
43566
+ i: "italic",
43567
+ em: "italic",
43568
+ sup: "superscript",
43569
+ sub: "subscript",
43570
+ s: "strike",
43571
+ strike: "strike",
43572
+ del: "del",
43573
+ ins: "ins",
43574
+ u: "underline",
43575
+ mark: "highlight",
43576
+ code: "code"
43577
+ };
43578
+ var SECTION_TAGS = /* @__PURE__ */ new Set(["thead", "tbody", "tfoot"]);
43579
+ var ROW_TAGS = /* @__PURE__ */ new Set(["tr"]);
43580
+ var CELL_TAGS = /* @__PURE__ */ new Set(["td", "th"]);
43581
+ var LI_TAGS = /* @__PURE__ */ new Set(["li"]);
43582
+ var VALID_SCOPES = /* @__PURE__ */ new Set(["row", "col", "rowgroup", "colgroup"]);
43583
+ var NAMED_ENTITIES = {
43584
+ amp: "&",
43585
+ lt: "<",
43586
+ gt: ">",
43587
+ quot: '"',
43588
+ apos: "'",
43589
+ nbsp: "\xA0"
43590
+ };
43591
+ function decodeEntities(text) {
43592
+ return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (whole, body) => {
43593
+ if (body[0] === "#") {
43594
+ const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
43595
+ if (Number.isFinite(code) && code >= 0 && code <= 1114111) {
43596
+ try {
43597
+ return String.fromCodePoint(code);
43598
+ } catch {
43599
+ return whole;
43600
+ }
43601
+ }
43602
+ return whole;
43603
+ }
43604
+ return NAMED_ENTITIES[body] ?? whole;
43605
+ });
43606
+ }
43607
+ function intAttr(value) {
43608
+ if (value == null) return void 0;
43609
+ const n = Number.parseInt(value, 10);
43610
+ return Number.isInteger(n) ? n : void 0;
43611
+ }
43612
+ function isElement(node) {
43613
+ return node.type === "element";
43614
+ }
43615
+ var HtmlTableParser2 = class {
43616
+ constructor() {
43617
+ __publicField(this, "keepUnknownTags", false);
43618
+ }
43619
+ /**
43620
+ * Parse an HTML string and return one table bit per top-level `<table>`.
43621
+ */
43622
+ parse(html, options) {
43623
+ this.keepUnknownTags = options?.keepUnknownTags ?? false;
43624
+ const tableFormat = options?.tableFormat ?? "table-extended";
43625
+ const onWarning = options?.onWarning;
43626
+ const tables = this.collectTopLevelTables(parseHtml(html));
43627
+ const bits = [];
43628
+ for (const table of tables) {
43629
+ const bit = this.tableToBit(table, tableFormat, onWarning);
43630
+ if (bit) bits.push(bit);
43631
+ }
43632
+ return bits;
43633
+ }
43634
+ // --- table extraction -----------------------------------------------------
43635
+ /** Collect every `<table>` element, NOT descending into nested tables. */
43636
+ collectTopLevelTables(nodes) {
43637
+ const tables = [];
43638
+ const visit = (node) => {
43639
+ if (!isElement(node)) return;
43640
+ if (node.name === "table") {
43641
+ tables.push(node);
43642
+ return;
43643
+ }
43644
+ node.children.forEach(visit);
43645
+ };
43646
+ nodes.forEach(visit);
43647
+ return tables;
43648
+ }
43649
+ tableToBit(table, tableFormat, onWarning) {
43650
+ const header = [];
43651
+ const body = [];
43652
+ const footer = [];
43653
+ let caption;
43654
+ for (const child of table.children) {
43655
+ if (!isElement(child)) continue;
43656
+ if (child.name === "caption") {
43657
+ const capContent = this.inlineNodes(child.children, []);
43658
+ const trimmed = this.trimInline(capContent);
43659
+ if (trimmed.length > 0) caption = trimmed;
43660
+ } else if (SECTION_TAGS.has(child.name)) {
43661
+ const target = child.name === "thead" ? header : child.name === "tfoot" ? footer : body;
43662
+ for (const tr of childElements(child, ROW_TAGS)) {
43663
+ target.push(this.rowToCells(tr));
43664
+ }
43665
+ } else if (ROW_TAGS.has(child.name)) {
43666
+ body.push(this.rowToCells(child));
43667
+ }
43668
+ }
43669
+ if (header.length === 0 && body.length === 0 && footer.length === 0) {
43670
+ return void 0;
43671
+ }
43672
+ const tableExtended = {};
43673
+ if (header.length > 0) tableExtended.header = { rows: header };
43674
+ if (body.length > 0) tableExtended.body = { rows: body };
43675
+ if (footer.length > 0) tableExtended.footer = { rows: footer };
43676
+ let tableData = tableExtended;
43677
+ if (tableFormat === "table") {
43678
+ if (this.isLossyAsBasic(tableExtended)) {
43679
+ onWarning?.(
43680
+ "Converting an HTML table to [.table] loses rowspan/colspan/scope and multi-section structure; use --tableFormat table-extended to preserve them."
43681
+ );
43682
+ }
43683
+ tableData = convertExtendedToBasicTableFormat(tableExtended);
43684
+ }
43685
+ const bit = {
43686
+ type: tableFormat,
43687
+ format: "bitmark++",
43688
+ table: tableData
43689
+ };
43690
+ if (caption) bit.caption = caption;
43691
+ return bit;
43692
+ }
43693
+ rowToCells(tr) {
43694
+ const cells = [];
43695
+ for (const cell of childElements(tr, CELL_TAGS)) {
43696
+ cells.push(this.cellToJson(cell));
43697
+ }
43698
+ return { cells };
43699
+ }
43700
+ cellToJson(cell) {
43701
+ const json = {
43702
+ content: this.blockNodes(cell.children)
43703
+ };
43704
+ if (cell.name === "th") json.title = true;
43705
+ const rowspan = intAttr(cell.attrs.rowspan);
43706
+ if (rowspan != null && rowspan >= 2) json.rowspan = rowspan;
43707
+ const colspan = intAttr(cell.attrs.colspan);
43708
+ if (colspan != null && colspan >= 2) json.colspan = colspan;
43709
+ const scope = cell.attrs.scope?.toLowerCase();
43710
+ if (scope && VALID_SCOPES.has(scope)) {
43711
+ json.scope = scope;
43712
+ }
43713
+ const width = this.widthFromCell(cell);
43714
+ if (width != null) json.colwidth = width;
43715
+ return json;
43716
+ }
43717
+ widthFromCell(cell) {
43718
+ const attrWidth = intAttr(cell.attrs.width);
43719
+ if (attrWidth != null && attrWidth > 0) return attrWidth;
43720
+ const style = cell.attrs.style;
43721
+ if (style) {
43722
+ const m = style.match(/width\s*:\s*(\d+)\s*px/i);
43723
+ if (m) {
43724
+ const n = Number.parseInt(m[1], 10);
43725
+ if (Number.isInteger(n) && n > 0) return n;
43726
+ }
43727
+ }
43728
+ return void 0;
43729
+ }
43730
+ isLossyAsBasic(table) {
43731
+ if (table.footer) return true;
43732
+ if (table.header && table.header.rows.length > 1) return true;
43733
+ const sections = [table.header, table.body, table.footer];
43734
+ for (const section of sections) {
43735
+ if (!section) continue;
43736
+ for (const row of section.rows) {
43737
+ for (const cell of row.cells) {
43738
+ if (cell.rowspan || cell.colspan || cell.scope) return true;
43739
+ }
43740
+ }
43741
+ }
43742
+ return false;
43743
+ }
43744
+ // --- content conversion (HTML -> TextAst) ---------------------------------
43745
+ /**
43746
+ * Convert a cell/li's child nodes into block-level TextAst nodes (paragraphs, lists, images).
43747
+ */
43748
+ blockNodes(nodes) {
43749
+ const blocks = [];
43750
+ let inline = [];
43751
+ const flush = () => {
43752
+ const trimmed = this.trimInline(inline);
43753
+ inline = [];
43754
+ if (trimmed.length === 0) return;
43755
+ if (trimmed.length === 1 && trimmed[0].type === "imageInline") {
43756
+ blocks.push({ ...trimmed[0], type: "image" });
43757
+ } else {
43758
+ blocks.push({ type: "paragraph", content: trimmed, attrs: {} });
43759
+ }
43760
+ };
43761
+ for (const node of nodes) {
43762
+ if (!isElement(node)) {
43763
+ inline.push(...this.inlineText(node.value, []));
43764
+ continue;
43765
+ }
43766
+ const name = node.name;
43767
+ if (name === "p") {
43768
+ flush();
43769
+ const content = this.trimInline(this.inlineNodes(node.children, []));
43770
+ if (content.length > 0) {
43771
+ blocks.push({ type: "paragraph", content, attrs: {} });
43772
+ }
43773
+ } else if (this.isListTag(name)) {
43774
+ flush();
43775
+ const list = this.listNode(node);
43776
+ if (list) blocks.push(list);
43777
+ } else {
43778
+ inline.push(...this.inlineFromElement(node, []));
43779
+ }
43780
+ }
43781
+ flush();
43782
+ return blocks;
43783
+ }
43784
+ /** Convert child nodes to a flat list of inline TextNodes, applying the given marks. */
43785
+ inlineNodes(nodes, marks) {
43786
+ const out = [];
43787
+ for (const node of nodes) {
43788
+ if (!isElement(node)) {
43789
+ out.push(...this.inlineText(node.value, marks));
43790
+ } else {
43791
+ out.push(...this.inlineFromElement(node, marks));
43792
+ }
43793
+ }
43794
+ return out;
43795
+ }
43796
+ inlineFromElement(el, marks) {
43797
+ const name = el.name;
43798
+ if (name === "br") {
43799
+ return [{ type: "hardBreak" }];
43800
+ }
43801
+ if (name === "img") {
43802
+ return [this.imageNode(el, true)];
43803
+ }
43804
+ const markType = MARK_TAGS[name];
43805
+ if (markType) {
43806
+ return this.inlineNodes(el.children, [...marks, { type: markType }]);
43807
+ }
43808
+ if (name === "a") {
43809
+ const linkMark = {
43810
+ type: "link",
43811
+ attrs: {
43812
+ href: el.attrs.href ?? "",
43813
+ ...el.attrs.target ? { target: el.attrs.target } : {}
43814
+ }
43815
+ };
43816
+ return this.inlineNodes(el.children, [...marks, linkMark]);
43817
+ }
43818
+ if (this.keepUnknownTags) {
43819
+ const open = this.serializeOpenTag(el);
43820
+ const close = `</${name}>`;
43821
+ return [
43822
+ ...this.inlineText(open, marks),
43823
+ ...this.inlineNodes(el.children, marks),
43824
+ ...this.inlineText(close, marks)
43825
+ ];
43826
+ }
43827
+ return this.inlineNodes(el.children, marks);
43828
+ }
43829
+ inlineText(raw, marks) {
43830
+ const text = decodeEntities(raw).replace(/\s+/g, " ");
43831
+ if (text === "") return [];
43832
+ const node = { text, type: "text" };
43833
+ if (marks.length > 0) node.marks = marks.map((m) => ({ ...m }));
43834
+ return [node];
43835
+ }
43836
+ serializeOpenTag(el) {
43837
+ const attrs = Object.entries(el.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${v}"`).join("");
43838
+ return `<${el.name}${attrs}>`;
43839
+ }
43840
+ // --- lists ----------------------------------------------------------------
43841
+ isListTag(name) {
43842
+ return name === "ul" || name === "ol";
43843
+ }
43844
+ listNode(el) {
43845
+ const items = childElements(el, LI_TAGS);
43846
+ if (items.length === 0) return void 0;
43847
+ const isTask = el.name === "ul" && items.some((li) => {
43848
+ const firstEl = li.children.find(isElement);
43849
+ return firstEl?.name === "input" && firstEl.attrs.type?.toLowerCase() === "checkbox";
43850
+ });
43851
+ if (isTask) {
43852
+ return {
43853
+ type: "taskList",
43854
+ content: items.map((li) => this.taskItemNode(li))
43855
+ };
43856
+ }
43857
+ const type = this.listType(el);
43858
+ const node = {
43859
+ type,
43860
+ content: items.map((li) => this.listItemNode(li))
43861
+ };
43862
+ if (el.name === "ol") {
43863
+ const start = intAttr(el.attrs.start);
43864
+ if (start != null) {
43865
+ node.attrs = { start };
43866
+ }
43867
+ }
43868
+ return node;
43869
+ }
43870
+ listType(el) {
43871
+ if (el.name === "ol") {
43872
+ switch (el.attrs.type) {
43873
+ case "i":
43874
+ return "orderedListRomanLower";
43875
+ case "I":
43876
+ return "orderedListRoman";
43877
+ case "a":
43878
+ return "letteredListLower";
43879
+ case "A":
43880
+ return "letteredList";
43881
+ default:
43882
+ return "orderedList";
43883
+ }
43884
+ }
43885
+ const style = el.attrs.style ?? "";
43886
+ if (/list-style(-type)?\s*:\s*none/i.test(style)) return "noBulletList";
43887
+ return "bulletList";
43888
+ }
43889
+ listItemNode(li) {
43890
+ let content = this.blockNodes(li.children);
43891
+ if (content.length === 0) {
43892
+ content = [{ type: "paragraph", content: [], attrs: {} }];
43893
+ }
43894
+ return { type: "listItem", content };
43895
+ }
43896
+ taskItemNode(li) {
43897
+ const checkbox = li.children.find(
43898
+ (c) => isElement(c) && c.name === "input" && c.attrs.type?.toLowerCase() === "checkbox"
43899
+ );
43900
+ const checked = checkbox ? "checked" in checkbox.attrs : false;
43901
+ const rest = li.children.filter((c) => c !== checkbox);
43902
+ let content = this.blockNodes(rest);
43903
+ if (content.length === 0) {
43904
+ content = [{ type: "paragraph", content: [], attrs: {} }];
43905
+ }
43906
+ return { type: "taskItem", attrs: { checked }, content };
43907
+ }
43908
+ // --- images ---------------------------------------------------------------
43909
+ imageNode(el, inline) {
43910
+ const attrs = { src: el.attrs.src ?? "" };
43911
+ attrs.alt = el.attrs.alt ?? null;
43912
+ attrs.title = el.attrs.title ?? null;
43913
+ const width = intAttr(el.attrs.width);
43914
+ if (width != null) attrs.width = width;
43915
+ const height = intAttr(el.attrs.height);
43916
+ if (height != null) attrs.height = height;
43917
+ if (el.attrs.class) attrs.class = el.attrs.class;
43918
+ return { type: inline ? "imageInline" : "image", attrs };
43919
+ }
43920
+ // --- whitespace -----------------------------------------------------------
43921
+ /** Trim leading/trailing whitespace from inline content (edges only). */
43922
+ trimInline(nodes) {
43923
+ const out = nodes.slice();
43924
+ const isWs = (n) => n.type === "text" && /^\s*$/.test(n.text ?? "");
43925
+ while (out.length > 0 && isWs(out[0])) out.shift();
43926
+ if (out.length > 0 && out[0].type === "text") {
43927
+ out[0] = { ...out[0], text: (out[0].text ?? "").replace(/^\s+/, "") };
43928
+ }
43929
+ while (out.length > 0 && isWs(out[out.length - 1])) out.pop();
43930
+ if (out.length > 0 && out[out.length - 1].type === "text") {
43931
+ const last = out.length - 1;
43932
+ out[last] = { ...out[last], text: (out[last].text ?? "").replace(/\s+$/, "") };
43933
+ }
43934
+ return out.filter((n) => !(n.type === "text" && n.text === ""));
43935
+ }
43936
+ };
43937
+
41212
43938
  // src/index.ts
41213
43939
  init();
41214
43940
  export {
@@ -41224,6 +43950,8 @@ export {
41224
43950
  BoundingBoxIndex,
41225
43951
  Builder,
41226
43952
  CardSetVersion,
43953
+ HtmlTableGenerator2 as HtmlTableGenerator,
43954
+ HtmlTableParser2 as HtmlTableParser,
41227
43955
  InfoFormat,
41228
43956
  InfoType,
41229
43957
  Input,