@gmb/bitmark-parser-generator 5.23.0 → 5.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -2,7 +2,7 @@ import process2 from 'process';
2
2
  import { Command, Option, Help, Argument, InvalidArgumentError } from 'commander';
3
3
  import { Enum } from '@ncoderz/superenum';
4
4
  import path3 from 'path';
5
- import fs3 from 'fs-extra';
5
+ import fs4 from 'fs-extra';
6
6
  import 'fs';
7
7
  import os from 'os';
8
8
 
@@ -230,6 +230,7 @@ var BitType = {
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 BitType = {
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 BitType = {
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",
@@ -709,10 +714,13 @@ var TextFormat = {
709
714
  };
710
715
  var CardSetConfigKey = {
711
716
  flashcard: "flashcard",
717
+ flashcard1: "flashcard1",
712
718
  definitionList: "definitionList",
719
+ definitionListPlain: "definitionListPlain",
713
720
  elements: "elements",
714
721
  statements: "statements",
715
722
  quiz: "quiz",
723
+ quizResponses: "quizResponses",
716
724
  feedback: "feedback",
717
725
  questions: "questions",
718
726
  matchPairs: "matchPairs",
@@ -724,6 +732,7 @@ var CardSetConfigKey = {
724
732
  pronunciationTable: "pronunciationTable",
725
733
  botActionResponses: "botActionResponses",
726
734
  exampleBitList: "exampleBitList",
735
+ pageFooterSections: "pageFooterSections",
727
736
  clozeList: "clozeList",
728
737
  ingredients: "ingredients",
729
738
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -1128,12 +1137,17 @@ var groupKeys = {
1128
1137
  group_standardAllBits: "group_standardAllBits",
1129
1138
  group_standardItemLead: "group_standardItemLead",
1130
1139
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1140
+ group_standardInstructionHint: "group_standardInstructionHint",
1131
1141
  group_standardTags: "group_standardTags",
1142
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1132
1143
  group_imageSource: "group_imageSource",
1133
1144
  group_technicalTerm: "group_technicalTerm",
1134
1145
  group_person: "group_person",
1135
1146
  group_gap: "group_gap",
1136
1147
  group_trueFalse: "group_trueFalse",
1148
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1149
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1150
+ group_trueFalseResponses: "group_trueFalseResponses",
1137
1151
  group_markConfig: "group_markConfig",
1138
1152
  group_mark: "group_mark",
1139
1153
  group_bookCommon: "group_bookCommon",
@@ -1141,14 +1155,18 @@ var groupKeys = {
1141
1155
  group_advertisingCommon: "group_advertisingCommon",
1142
1156
  group_platformStylesCommon: "group_platformStylesCommon",
1143
1157
  group_quizCommon: "group_quizCommon",
1158
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1159
+ group_imageNoZoom: "group_imageNoZoom",
1144
1160
  group_resourceBitTags: "group_resourceBitTags",
1145
1161
  group_resourceCommon: "group_resourceCommon",
1146
1162
  group_resourceImageCommon: "group_resourceImageCommon",
1163
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1147
1164
  group_resourceAudioCommon: "group_resourceAudioCommon",
1148
1165
  group_resourceVideoCommon: "group_resourceVideoCommon",
1149
1166
  group_resourceIcon: "group_resourceIcon",
1150
1167
  // Alias for image
1151
1168
  group_resourceImage: "group_resourceImage",
1169
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1152
1170
  group_resourceImageEmbed: "group_resourceImageEmbed",
1153
1171
  group_resourceImageLink: "group_resourceImageLink",
1154
1172
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1297,6 +1315,7 @@ var propertyKeys = {
1297
1315
  property_enforceUpdateOverUserInput: "@enforceUpdateOverUserInput",
1298
1316
  property_extractorExtractionTimestamp: "@extractorExtractionTimestamp",
1299
1317
  property_isCaseSensitive: "@isCaseSensitive",
1318
+ property_isCollapsible: "@isCollapsible",
1300
1319
  property_isInfoOnly: "@isInfoOnly",
1301
1320
  property_isPublic: "@isPublic",
1302
1321
  property_isTemplate: "@isTemplate",
@@ -1422,6 +1441,7 @@ var propertyKeys = {
1422
1441
  property_src4x: "@src4x",
1423
1442
  property_srcAlt: "@srcAlt",
1424
1443
  property_stripePricingTableId: "@stripePricingTableId",
1444
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1425
1445
  property_stripePublishableKey: "@stripePublishableKey",
1426
1446
  property_subject: "@subject",
1427
1447
  property_subtype: "@subtype",
@@ -2164,7 +2184,7 @@ var CARDSETS = {
2164
2184
  variants: [
2165
2185
  {
2166
2186
  jsonKey: "question.text",
2167
- exportJsonKey: { question: { text: "$" } },
2187
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2168
2188
  tags: [
2169
2189
  {
2170
2190
  key: ConfigKey.group_standardTags,
@@ -2180,6 +2200,136 @@ var CARDSETS = {
2180
2200
  description: "Icon resource for the flashcard.",
2181
2201
  jsonKey: "question.icon|resource(type=image, key=image)",
2182
2202
  exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2203
+ },
2204
+ {
2205
+ key: ConfigKey.property_example,
2206
+ exportJsonKey: [
2207
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2208
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2209
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2210
+ ],
2211
+ description: "Example marker for the flashcard question.",
2212
+ format: TagFormat.bitmarkText,
2213
+ nullable: true
2214
+ }
2215
+ ]
2216
+ }
2217
+ ]
2218
+ },
2219
+ {
2220
+ name: "answer",
2221
+ variants: [
2222
+ {
2223
+ jsonKey: "answer.text",
2224
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2225
+ tags: [
2226
+ {
2227
+ key: ConfigKey.group_standardTags,
2228
+ description: "Standard tags for the flashcard."
2229
+ },
2230
+ {
2231
+ exportJsonKey: { title: "$" },
2232
+ key: ConfigKey.tag_title,
2233
+ description: "Title of the flashcard."
2234
+ },
2235
+ {
2236
+ key: ConfigKey.group_resourceIcon,
2237
+ description: "Icon resource for the flashcard.",
2238
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2239
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2240
+ },
2241
+ {
2242
+ key: ConfigKey.property_example,
2243
+ exportJsonKey: [
2244
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2245
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2246
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2247
+ ],
2248
+ description: "Example marker for the flashcard answer.",
2249
+ format: TagFormat.bitmarkText,
2250
+ nullable: true
2251
+ }
2252
+ ]
2253
+ },
2254
+ {
2255
+ jsonKey: "alternativeAnswers[].text",
2256
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2257
+ tags: [
2258
+ {
2259
+ key: ConfigKey.group_standardTags,
2260
+ description: "Standard tags for the flashcard."
2261
+ },
2262
+ {
2263
+ exportJsonKey: { title: "$" },
2264
+ key: ConfigKey.tag_title,
2265
+ description: "Title of the flashcard."
2266
+ },
2267
+ {
2268
+ key: ConfigKey.group_resourceIcon,
2269
+ description: "Icon resource for the flashcard.",
2270
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2271
+ exportJsonKey: {
2272
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2273
+ }
2274
+ }
2275
+ ],
2276
+ repeatCount: Count.infinity
2277
+ }
2278
+ ]
2279
+ }
2280
+ ]
2281
+ },
2282
+ //
2283
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2284
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2285
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2286
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2287
+ // cardset.
2288
+ //
2289
+ [CardSetConfigKey.flashcard1]: {
2290
+ jsonKey: "cards",
2291
+ exportJsonKey: { cards: "$" },
2292
+ sections: {
2293
+ default: {
2294
+ jsonKey: "cards",
2295
+ exportJsonKey: { cards: "$" },
2296
+ isDefault: true,
2297
+ maxCount: 1
2298
+ }
2299
+ },
2300
+ sides: [
2301
+ {
2302
+ name: "question",
2303
+ variants: [
2304
+ {
2305
+ jsonKey: "question.text",
2306
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2307
+ tags: [
2308
+ {
2309
+ key: ConfigKey.group_standardTags,
2310
+ description: "Standard tags for the flashcard."
2311
+ },
2312
+ {
2313
+ exportJsonKey: { title: "$" },
2314
+ key: ConfigKey.tag_title,
2315
+ description: "Title of the flashcard."
2316
+ },
2317
+ {
2318
+ key: ConfigKey.group_resourceIcon,
2319
+ description: "Icon resource for the flashcard.",
2320
+ jsonKey: "question.icon|resource(type=image, key=image)",
2321
+ exportJsonKey: { question: { icon: { type: "image", image: { src: "$" } } } }
2322
+ },
2323
+ {
2324
+ key: ConfigKey.property_example,
2325
+ exportJsonKey: [
2326
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2327
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2328
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2329
+ ],
2330
+ description: "Example marker for the flashcard question.",
2331
+ format: TagFormat.bitmarkText,
2332
+ nullable: true
2183
2333
  }
2184
2334
  ]
2185
2335
  }
@@ -2190,7 +2340,7 @@ var CARDSETS = {
2190
2340
  variants: [
2191
2341
  {
2192
2342
  jsonKey: "answer.text",
2193
- exportJsonKey: { answer: { text: "$" } },
2343
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2194
2344
  tags: [
2195
2345
  {
2196
2346
  key: ConfigKey.group_standardTags,
@@ -2206,6 +2356,17 @@ var CARDSETS = {
2206
2356
  description: "Icon resource for the flashcard.",
2207
2357
  jsonKey: "answer.icon|resource(type=image, key=image)",
2208
2358
  exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2359
+ },
2360
+ {
2361
+ key: ConfigKey.property_example,
2362
+ exportJsonKey: [
2363
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2364
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2365
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2366
+ ],
2367
+ description: "Example marker for the flashcard answer.",
2368
+ format: TagFormat.bitmarkText,
2369
+ nullable: true
2209
2370
  }
2210
2371
  ]
2211
2372
  },
@@ -2249,7 +2410,7 @@ var CARDSETS = {
2249
2410
  variants: [
2250
2411
  {
2251
2412
  jsonKey: "term.text",
2252
- exportJsonKey: { term: { text: "$" } },
2413
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2253
2414
  tags: [
2254
2415
  {
2255
2416
  key: ConfigKey.group_standardTags,
@@ -2268,6 +2429,17 @@ var CARDSETS = {
2268
2429
  format: TagFormat.plainText,
2269
2430
  jsonKey: "term.icon|resource(type=image, key=image)",
2270
2431
  exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2432
+ },
2433
+ {
2434
+ key: ConfigKey.property_example,
2435
+ exportJsonKey: [
2436
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2437
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2438
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2439
+ ],
2440
+ description: "Example marker for the definition term.",
2441
+ format: TagFormat.bitmarkText,
2442
+ nullable: true
2271
2443
  }
2272
2444
  ]
2273
2445
  }
@@ -2278,7 +2450,7 @@ var CARDSETS = {
2278
2450
  variants: [
2279
2451
  {
2280
2452
  jsonKey: "definition.text",
2281
- exportJsonKey: { definition: { text: "$" } },
2453
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2282
2454
  tags: [
2283
2455
  {
2284
2456
  key: ConfigKey.group_standardTags,
@@ -2296,6 +2468,17 @@ var CARDSETS = {
2296
2468
  description: "Icon resource for the definition.",
2297
2469
  jsonKey: "definition.icon|resource(type=image, key=image)",
2298
2470
  exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2471
+ },
2472
+ {
2473
+ key: ConfigKey.property_example,
2474
+ exportJsonKey: [
2475
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2476
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2477
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2478
+ ],
2479
+ description: "Example marker for the definition.",
2480
+ format: TagFormat.bitmarkText,
2481
+ nullable: true
2299
2482
  }
2300
2483
  ]
2301
2484
  },
@@ -2328,6 +2511,126 @@ var CARDSETS = {
2328
2511
  ]
2329
2512
  },
2330
2513
  //
2514
+ // definition-list-plain
2515
+ //
2516
+ // Same shape as `definitionList`, but every variant body is rendered as
2517
+ // plain text (string), not bitmark+ AST. Used by
2518
+ // `meta-search-default-terms` / `meta-search-default-topics`, which need
2519
+ // term/definition text emitted verbatim instead of as a ProseMirror tree.
2520
+ //
2521
+ [CardSetConfigKey.definitionListPlain]: {
2522
+ jsonKey: "definitions",
2523
+ exportJsonKey: { definitions: "$" },
2524
+ sides: [
2525
+ {
2526
+ name: "term",
2527
+ variants: [
2528
+ {
2529
+ jsonKey: "term.text",
2530
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2531
+ format: TextFormat.plainText,
2532
+ tags: [
2533
+ {
2534
+ key: ConfigKey.group_standardTags,
2535
+ description: "Standard tags for the definition."
2536
+ },
2537
+ {
2538
+ key: ConfigKey.tag_title,
2539
+ description: "Title of the definition.",
2540
+ format: TagFormat.plainText,
2541
+ jsonKey: "^heading.forKeys",
2542
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2543
+ },
2544
+ {
2545
+ key: ConfigKey.group_resourceIcon,
2546
+ description: "Icon resource for the definition.",
2547
+ format: TagFormat.plainText,
2548
+ jsonKey: "term.icon|resource(type=image, key=image)",
2549
+ exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2550
+ },
2551
+ {
2552
+ key: ConfigKey.property_example,
2553
+ exportJsonKey: [
2554
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2555
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2556
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2557
+ ],
2558
+ description: "Example marker for the definition term.",
2559
+ format: TagFormat.bitmarkText,
2560
+ nullable: true
2561
+ }
2562
+ ]
2563
+ }
2564
+ ]
2565
+ },
2566
+ {
2567
+ name: "definition",
2568
+ variants: [
2569
+ {
2570
+ jsonKey: "definition.text",
2571
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2572
+ format: TextFormat.plainText,
2573
+ tags: [
2574
+ {
2575
+ key: ConfigKey.group_standardTags,
2576
+ description: "Standard tags for the definition."
2577
+ },
2578
+ {
2579
+ key: ConfigKey.tag_title,
2580
+ description: "Title of the definition.",
2581
+ format: TagFormat.plainText,
2582
+ jsonKey: "^heading.forValues",
2583
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2584
+ },
2585
+ {
2586
+ key: ConfigKey.group_resourceIcon,
2587
+ description: "Icon resource for the definition.",
2588
+ jsonKey: "definition.icon|resource(type=image, key=image)",
2589
+ exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2590
+ },
2591
+ {
2592
+ key: ConfigKey.property_example,
2593
+ exportJsonKey: [
2594
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2595
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2596
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2597
+ ],
2598
+ description: "Example marker for the definition.",
2599
+ format: TagFormat.bitmarkText,
2600
+ nullable: true
2601
+ }
2602
+ ]
2603
+ },
2604
+ {
2605
+ jsonKey: "alternativeDefinitions[].text",
2606
+ exportJsonKey: { alternativeDefinitions: [{ text: "$" }] },
2607
+ format: TextFormat.plainText,
2608
+ tags: [
2609
+ {
2610
+ key: ConfigKey.group_standardTags,
2611
+ description: "Standard tags for the definition."
2612
+ },
2613
+ {
2614
+ exportJsonKey: { title: "$" },
2615
+ key: ConfigKey.tag_title,
2616
+ description: "Title of the definition."
2617
+ },
2618
+ {
2619
+ key: ConfigKey.group_resourceIcon,
2620
+ description: "Icon resource for the definition.",
2621
+ jsonKey: "alternativeDefinitions[].icon|resource(type=image, key=image)",
2622
+ exportJsonKey: {
2623
+ alternativeDefinitions: [{ icon: { type: "image", image: { src: "$" } } }]
2624
+ }
2625
+ }
2626
+ ],
2627
+ repeatCount: Count.infinity
2628
+ }
2629
+ ]
2630
+ }
2631
+ ]
2632
+ },
2633
+ //
2331
2634
  // match-pairs
2332
2635
  //
2333
2636
  [CardSetConfigKey.matchPairs]: {
@@ -2340,11 +2643,51 @@ var CARDSETS = {
2340
2643
  {
2341
2644
  jsonKey: "key",
2342
2645
  exportJsonKey: { key: "$" },
2646
+ format: TextFormat.plainText,
2343
2647
  tags: [
2344
2648
  {
2345
2649
  key: ConfigKey.group_standardTags,
2346
2650
  description: "Standard tags for the match pair."
2347
2651
  },
2652
+ {
2653
+ // Pair-level example: emit `isExample` alongside `example`
2654
+ // (matches OLD parser's per-pair allow-list behaviour).
2655
+ //
2656
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2657
+ // in the cardset) fills each pair's `example` with `V`
2658
+ // once per pair from the keys-side cascade fire (single
2659
+ // parsed_variant per pair — no per-value duplication).
2660
+ //
2661
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2662
+ // at the pair-object level. BPG `parseMatchPairs`
2663
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2664
+ // per PAIR (the `exampleCard` accumulator), placed at the
2665
+ // pair object. Unlike matchMatrix (per-cell example),
2666
+ // matchPairs has no nested per-cell layer, so the pair-key
2667
+ // `[@example:V]` must double-emit as the visible field.
2668
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2669
+ // expects `example: "packen"` at pair-object level from
2670
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2671
+ // draft applied to both keys-side tags; the matchPairs
2672
+ // half was reverted after fixture parity testing.
2673
+ key: ConfigKey.property_example,
2674
+ exportJsonKey: [
2675
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2676
+ {
2677
+ predicates: ["@absent", { $cascade: "*" }],
2678
+ rule: {
2679
+ isExample: true,
2680
+ example: "$cascade",
2681
+ "@bit": { isExample: true }
2682
+ }
2683
+ },
2684
+ { "@absent": { isExample: true } },
2685
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2686
+ ],
2687
+ description: "Example marker / value on the match pair.",
2688
+ format: TagFormat.bitmarkText,
2689
+ nullable: true
2690
+ },
2348
2691
  {
2349
2692
  key: ConfigKey.tag_title,
2350
2693
  description: "Title of the match pair.",
@@ -2353,9 +2696,19 @@ var CARDSETS = {
2353
2696
  exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2354
2697
  },
2355
2698
  {
2699
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2700
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2701
+ // only fires when NO variant in the active card explicitly
2702
+ // carries `[@isCaseSensitive]`. When a value-side variant
2703
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2704
+ // synth is skipped here; the value-side explicit fire owns
2705
+ // the pair-level emission.
2356
2706
  key: ConfigKey.property_isCaseSensitive,
2357
2707
  exportJsonKey: [
2358
- { "@absent": { isCaseSensitive: "$ancestor" } },
2708
+ {
2709
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2710
+ rule: { isCaseSensitive: "$ancestor" }
2711
+ },
2359
2712
  { isCaseSensitive: "$" }
2360
2713
  ],
2361
2714
  description: "Property to indicate if the match is case sensitive.",
@@ -2384,10 +2737,85 @@ var CARDSETS = {
2384
2737
  {
2385
2738
  jsonKey: "values[]",
2386
2739
  exportJsonKey: { values: ["$"] },
2740
+ format: TextFormat.plainText,
2387
2741
  tags: [
2388
2742
  {
2389
- key: ConfigKey.group_standardTags,
2390
- description: "Standard tags for the match pair."
2743
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2744
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2745
+ // for sideIdx > 0; the schema-side equivalent is to use a
2746
+ // group_standardTags variant that omits group_standardItemLead.
2747
+ key: ConfigKey.group_standardTagsNoItemLead,
2748
+ description: "Standard tags for the match pair (values side, no item/lead)."
2749
+ },
2750
+ {
2751
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2752
+ // `@example` from the bit header fills each pair's
2753
+ // `example` with the pair's first value (mirrors BPG's
2754
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2755
+ //
2756
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2757
+ // TAG 415 (keys side) which fires once per pair — keeping
2758
+ // it here would emit `example` twice per pair (one per
2759
+ // values-side parsed_variant) and merge them into a
2760
+ // multi-paragraph array.
2761
+ //
2762
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2763
+ // source `$` value (the current `++`-iteration's value
2764
+ // text). For single-value pairs this matches the old
2765
+ // `$parent.values[0]`; for multi-value pairs with
2766
+ // `[@example]` attached to a non-first value, it correctly
2767
+ // resolves to that value.
2768
+ //
2769
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2770
+ // routes pair-key `[@example:V]` (a per-pair cascade
2771
+ // source, bound via per-card CascadeCounters scope) and
2772
+ // bit-header `[@example:V]` (cardset cascade) into the
2773
+ // pair-level `example` via `$cascade`. The aggregator in
2774
+ // `populate_variants` fires this rule ONCE per pair (BPG
2775
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2776
+ // per-iter pollution.
2777
+ key: ConfigKey.property_example,
2778
+ exportJsonKey: [
2779
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2780
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2781
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2782
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2783
+ // scope opened by `build_card_item`.
2784
+ {
2785
+ predicates: ["@keyonly"],
2786
+ maxEmits: 1,
2787
+ rule: {
2788
+ isExample: true,
2789
+ example: "$parent",
2790
+ "@bit": { isExample: true }
2791
+ }
2792
+ },
2793
+ {
2794
+ predicates: ["@absent", { $cascade: "*" }],
2795
+ rule: {
2796
+ isExample: true,
2797
+ example: "$cascade",
2798
+ "@bit": { isExample: true }
2799
+ }
2800
+ },
2801
+ {
2802
+ "@absent": {
2803
+ isExample: true,
2804
+ example: "$parent",
2805
+ "@bit": { isExample: true }
2806
+ }
2807
+ },
2808
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2809
+ // form so per-`++`-iter values-side fires also first-win.
2810
+ {
2811
+ predicates: [],
2812
+ maxEmits: 1,
2813
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2814
+ }
2815
+ ],
2816
+ description: "Example marker / value on the match pair (values side).",
2817
+ format: TagFormat.bitmarkText,
2818
+ nullable: true
2391
2819
  },
2392
2820
  {
2393
2821
  key: ConfigKey.tag_title,
@@ -2397,9 +2825,13 @@ var CARDSETS = {
2397
2825
  exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2398
2826
  },
2399
2827
  {
2828
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2400
2829
  key: ConfigKey.property_isCaseSensitive,
2401
2830
  exportJsonKey: [
2402
- { "@absent": { isCaseSensitive: "$ancestor" } },
2831
+ {
2832
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2833
+ rule: { isCaseSensitive: "$ancestor" }
2834
+ },
2403
2835
  { isCaseSensitive: "$" }
2404
2836
  ],
2405
2837
  description: "Property to indicate if the match is case sensitive.",
@@ -2425,6 +2857,7 @@ var CARDSETS = {
2425
2857
  {
2426
2858
  jsonKey: "key",
2427
2859
  exportJsonKey: { key: "$" },
2860
+ format: TextFormat.plainText,
2428
2861
  tags: [
2429
2862
  {
2430
2863
  key: ConfigKey.group_standardTags,
@@ -2454,10 +2887,12 @@ var CARDSETS = {
2454
2887
  {
2455
2888
  jsonKey: "values[]",
2456
2889
  exportJsonKey: { values: ["$"] },
2890
+ format: TextFormat.plainText,
2457
2891
  tags: [
2458
2892
  {
2459
- key: ConfigKey.group_standardTags,
2460
- description: "Standard tags for the audio match pair."
2893
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2894
+ key: ConfigKey.group_standardTagsNoItemLead,
2895
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2461
2896
  },
2462
2897
  {
2463
2898
  key: ConfigKey.tag_title,
@@ -2490,6 +2925,7 @@ var CARDSETS = {
2490
2925
  {
2491
2926
  jsonKey: "key",
2492
2927
  exportJsonKey: { key: "$" },
2928
+ format: TextFormat.plainText,
2493
2929
  tags: [
2494
2930
  {
2495
2931
  key: ConfigKey.group_standardTags,
@@ -2519,10 +2955,12 @@ var CARDSETS = {
2519
2955
  {
2520
2956
  jsonKey: "values[]",
2521
2957
  exportJsonKey: { values: ["$"] },
2958
+ format: TextFormat.plainText,
2522
2959
  tags: [
2523
2960
  {
2524
- key: ConfigKey.group_standardTags,
2525
- description: "Standard tags for the image match pair."
2961
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2962
+ key: ConfigKey.group_standardTagsNoItemLead,
2963
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2526
2964
  },
2527
2965
  {
2528
2966
  key: ConfigKey.tag_title,
@@ -2555,20 +2993,49 @@ var CARDSETS = {
2555
2993
  {
2556
2994
  jsonKey: "key",
2557
2995
  exportJsonKey: { key: "$" },
2996
+ format: TextFormat.plainText,
2558
2997
  tags: [
2559
2998
  {
2560
2999
  key: ConfigKey.group_standardItemLeadInstructionHint,
2561
3000
  description: "Standard tags for lead, instruction, and hint."
2562
3001
  },
2563
3002
  {
3003
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
3004
+ // `[@example:V]` is consumed as the per-pair cascade source.
3005
+ // PLAN-077 R14 follow-up: investigation showed the fixture
3006
+ // reference is INCONSISTENT — bits without a cardset-level
3007
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
3008
+ // pair-level `example: V` at pair scope, but bits WITH a
3009
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
3010
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
3011
+ // example. Emitting `example: $` here satisfies bit 3 but
3012
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
3013
+ // 3. Conditional emission would require runtime gating on
3014
+ // the cardset cascade scope state. Left dropped pending a
3015
+ // dedicated plan — accepted divergence on bit 3's pair-
3016
+ // level emission (cells still get the per-cell `example`
3017
+ // correctly via PLAN-077 §4.2 + §4.3).
2564
3018
  key: ConfigKey.property_example,
2565
3019
  exportJsonKey: [
2566
- { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2567
- { "@absent": { isExample: true, "@bit": { isExample: true } } },
2568
- { isExample: true, example: "$", "@bit": { isExample: true } }
3020
+ {
3021
+ "@keyonly": {
3022
+ isExample: true,
3023
+ "@bit": { isExample: true },
3024
+ "@card": { isExample: true }
3025
+ }
3026
+ },
3027
+ {
3028
+ "@absent": {
3029
+ isExample: true,
3030
+ "@bit": { isExample: true },
3031
+ "@card": { isExample: true }
3032
+ }
3033
+ },
3034
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2569
3035
  ],
2570
3036
  description: "Example text for the match matrix.",
2571
- format: TagFormat.plainText
3037
+ format: TagFormat.plainText,
3038
+ nullable: true
2572
3039
  },
2573
3040
  {
2574
3041
  key: ConfigKey.tag_title,
@@ -2596,35 +3063,96 @@ var CARDSETS = {
2596
3063
  variants: [
2597
3064
  {
2598
3065
  jsonKey: "cells[{s}].values[]",
2599
- exportJsonKey: { cells: { $s: { values: ["$"] } } },
3066
+ exportJsonKey: [
3067
+ // PLAN-085 / R14: the positional-blank emission for an
3068
+ // effectively-absent cell side fires ONLY when the active
3069
+ // card has at least one `[#]` variant fire. This matches
3070
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
3071
+ // `[#]` exists anywhere in the title card, BPG returns
3072
+ // undefined and no `heading` is emitted. The phantom
3073
+ // `forValues:['']` placeholder is appropriate only when
3074
+ // the card is being treated as a title-row.
3075
+ {
3076
+ predicates: ["@absent", { "@variantHasTag": "#" }],
3077
+ rule: { "@bit": { heading: { forValues: [""] } } }
3078
+ },
3079
+ { cells: { $s: { values: ["$"] } } }
3080
+ ],
3081
+ format: TextFormat.plainText,
2600
3082
  tags: [
2601
3083
  {
2602
3084
  key: ConfigKey.group_standardItemLeadInstructionHint,
2603
3085
  description: "Standard tags for lead, instruction, and hint."
2604
3086
  },
2605
3087
  {
3088
+ // PLAN-075: bare `$parent` reads the parent variant fire's
3089
+ // source `$` value (the current `++`-iteration's cell text).
3090
+ // Replaces `$parent.values[0]` which pinned to position 0
3091
+ // regardless of which `++`-value `[@example]` was on.
3092
+ //
3093
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
3094
+ // routes pair-key `[@example:V]` (a per-pair cascade
3095
+ // source, bound via per-card CascadeCounters scope) and
3096
+ // bit-header `[@example:V]` (cardset cascade) into the
3097
+ // per-cell `example` via `$cascade`. The aggregator in
3098
+ // `populate_variants` fires this rule ONCE per cell (BPG
3099
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
3100
+ // per-iter pollution.
3101
+ // @card scope-shift writes matrix-row container isExample.
2606
3102
  key: ConfigKey.property_example,
2607
3103
  exportJsonKey: [
2608
3104
  {
2609
3105
  "@keyonly": {
2610
- cells: { $s: { isExample: true, example: "$parent.values[0]" } },
2611
- "@bit": { isExample: true }
3106
+ cells: { $s: { isExample: true, example: "$parent" } },
3107
+ "@bit": { isExample: true },
3108
+ "@card": { isExample: true }
2612
3109
  }
2613
3110
  },
2614
3111
  {
2615
- "@absent": { cells: { $s: { isExample: true, example: "$parent.values[0]" } } }
3112
+ predicates: ["@absent", { $cascade: "*" }],
3113
+ rule: {
3114
+ cells: { $s: { isExample: true, example: "$cascade" } },
3115
+ "@bit": { isExample: true },
3116
+ "@card": { isExample: true }
3117
+ }
2616
3118
  },
2617
- { cells: { $s: { isExample: true, example: "$" } }, "@bit": { isExample: true } }
3119
+ {
3120
+ "@absent": {
3121
+ cells: { $s: { isExample: true, example: "$parent" } },
3122
+ "@bit": { isExample: true },
3123
+ "@card": { isExample: true }
3124
+ }
3125
+ },
3126
+ {
3127
+ cells: { $s: { isExample: true, example: "$" } },
3128
+ "@bit": { isExample: true },
3129
+ "@card": { isExample: true }
3130
+ }
2618
3131
  ],
2619
3132
  description: "Example text for the match matrix.",
2620
- format: TagFormat.plainText
3133
+ format: TagFormat.bitmarkText,
3134
+ nullable: true
2621
3135
  },
2622
3136
  {
3137
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3138
+ // empty-string positional blank into `forValues` so the
3139
+ // array stays aligned with the cell positions (matches BPG
3140
+ // `extractHeadingCard`'s "all value-side headings present"
3141
+ // shape). The `@keyonly` rule is required for two reasons:
3142
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3143
+ // in the unconditional rule has no source for bare); and
3144
+ // 2) the presence of any `@keyonly` rule disables the
3145
+ // optimised-mode `should_suppress_outcome` strip (see
3146
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3147
+ // survives the all-natural-defaults check.
2623
3148
  key: ConfigKey.tag_title,
2624
3149
  description: "Title of the match matrix.",
2625
3150
  format: TagFormat.plainText,
2626
3151
  jsonKey: "^heading.forValues",
2627
- exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
3152
+ exportJsonKey: [
3153
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3154
+ { "@bit": { heading: { forValues: ["$"] } } }
3155
+ ]
2628
3156
  },
2629
3157
  {
2630
3158
  key: ConfigKey.property_isCaseSensitive,
@@ -2673,6 +3201,35 @@ var CARDSETS = {
2673
3201
  {
2674
3202
  key: ConfigKey.group_standardTags,
2675
3203
  description: "Standard tags for the statement."
3204
+ },
3205
+ {
3206
+ // PLAN-072: cascade-fired `@example` from the bit header
3207
+ // fills each statement entry's `example` with the entry's
3208
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3209
+ // for the statements cardset. Per-array-item strip removes
3210
+ // `example: false` from incorrect entries so only correct
3211
+ // entries retain `example: true`.
3212
+ key: ConfigKey.property_example,
3213
+ exportJsonKey: [
3214
+ {
3215
+ "@keyonly": {
3216
+ isExample: true,
3217
+ example: true,
3218
+ "@bit": { isExample: true }
3219
+ }
3220
+ },
3221
+ {
3222
+ "@absent": {
3223
+ isExample: true,
3224
+ example: "$parent.isCorrect",
3225
+ "@bit": { isExample: true }
3226
+ }
3227
+ },
3228
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3229
+ ],
3230
+ 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`.",
3231
+ format: TagFormat.boolean,
3232
+ nullable: true
2676
3233
  }
2677
3234
  ],
2678
3235
  bodyAllowed: false
@@ -2701,6 +3258,71 @@ var CARDSETS = {
2701
3258
  {
2702
3259
  key: ConfigKey.group_standardTags,
2703
3260
  description: "Standard tags for the quiz."
3261
+ },
3262
+ {
3263
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3264
+ // quiz choices is delegated to group_trueFalse's
3265
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3266
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3267
+ // would over-fire on subsequent correct choices and on
3268
+ // incorrect ones — drop it.
3269
+ key: ConfigKey.property_example,
3270
+ exportJsonKey: [
3271
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3272
+ { isExample: true, "@bit": { isExample: true } }
3273
+ ],
3274
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3275
+ format: TagFormat.boolean,
3276
+ nullable: true
3277
+ }
3278
+ ],
3279
+ bodyAllowed: false
3280
+ }
3281
+ ]
3282
+ }
3283
+ ]
3284
+ },
3285
+ //
3286
+ // quiz (responses)
3287
+ //
3288
+ [CardSetConfigKey.quizResponses]: {
3289
+ jsonKey: "quizzes",
3290
+ exportJsonKey: { quizzes: "$" },
3291
+ sides: [
3292
+ {
3293
+ name: "responses",
3294
+ variants: [
3295
+ {
3296
+ jsonKey: null,
3297
+ tags: [
3298
+ {
3299
+ key: ConfigKey.group_trueFalseResponses,
3300
+ description: "Group for true/false questions."
3301
+ },
3302
+ {
3303
+ key: ConfigKey.group_standardTags,
3304
+ description: "Standard tags for the quiz."
3305
+ },
3306
+ {
3307
+ // PLAN-072: cascade-fired `@example` from the bit header
3308
+ // fills each response entry's `example` with the entry's
3309
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3310
+ // for the quiz-responses cardset (statements-style).
3311
+ key: ConfigKey.property_example,
3312
+ exportJsonKey: [
3313
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3314
+ {
3315
+ "@absent": {
3316
+ isExample: true,
3317
+ example: "$parent.isCorrect",
3318
+ "@bit": { isExample: true }
3319
+ }
3320
+ },
3321
+ { isExample: true, "@bit": { isExample: true } }
3322
+ ],
3323
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3324
+ format: TagFormat.boolean,
3325
+ nullable: true
2704
3326
  }
2705
3327
  ],
2706
3328
  bodyAllowed: false
@@ -2726,6 +3348,73 @@ var CARDSETS = {
2726
3348
  key: ConfigKey.group_standardItemLeadInstructionHint,
2727
3349
  description: "Standard tags for lead, instruction, and hint."
2728
3350
  },
3351
+ {
3352
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3353
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3354
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3355
+ // feedback-specific shape.
3356
+ //
3357
+ // Chain @example: `[+ X][@example]` writes
3358
+ // `isExample/example` onto the X choice only. BPG's
3359
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3360
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3361
+ // there is no bubble to bit-level or feedbacks-level
3362
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3363
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3364
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3365
+ // no feedback dispatch, so bit-header `[@example]` does
3366
+ // not flow into feedback choices.
3367
+ key: ConfigKey.tag_true,
3368
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3369
+ description: "Reason-required choice for feedback (`[+]`).",
3370
+ maxCount: Count.infinity,
3371
+ format: TagFormat.plainText,
3372
+ chain: [
3373
+ {
3374
+ key: ConfigKey.property_example,
3375
+ exportJsonKey: [
3376
+ { "@keyonly": { isExample: true, example: true } },
3377
+ { isExample: true, example: "$" }
3378
+ ],
3379
+ description: "Per-choice example marker for feedback `[+]`.",
3380
+ format: TagFormat.boolean,
3381
+ maxCount: 1,
3382
+ nullable: true
3383
+ },
3384
+ {
3385
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3386
+ // choice; same for `[?]` (hint), `[%]` (item),
3387
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3388
+ // Mirrors group_trueFalse's `+` chain shape.
3389
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3390
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3391
+ }
3392
+ ]
3393
+ },
3394
+ {
3395
+ key: ConfigKey.tag_false,
3396
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3397
+ description: "Reason-not-required choice for feedback (`[-]`).",
3398
+ maxCount: Count.infinity,
3399
+ format: TagFormat.plainText,
3400
+ chain: [
3401
+ {
3402
+ key: ConfigKey.property_example,
3403
+ exportJsonKey: [
3404
+ { "@keyonly": { isExample: true, example: true } },
3405
+ { isExample: true, example: "$" }
3406
+ ],
3407
+ description: "Per-choice example marker for feedback `[-]`.",
3408
+ format: TagFormat.boolean,
3409
+ maxCount: 1,
3410
+ nullable: true
3411
+ },
3412
+ {
3413
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3414
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3415
+ }
3416
+ ]
3417
+ },
2729
3418
  {
2730
3419
  key: ConfigKey.group_trueFalse,
2731
3420
  description: "Group for true/false feedback."
@@ -2746,31 +3435,71 @@ var CARDSETS = {
2746
3435
  name: "reason",
2747
3436
  variants: [
2748
3437
  {
3438
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3439
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3440
+ // under `reason.*`; each tag here spells out the full path.
3441
+ // Body is a plain string (BPG `parseFeedback` uses
3442
+ // `cardBodyStr ?? ''` for `reason.text`).
2749
3443
  jsonKey: "reason.text",
2750
3444
  exportJsonKey: { reason: { text: "$" } },
3445
+ format: TextFormat.plainText,
2751
3446
  tags: [
2752
3447
  {
2753
- key: ConfigKey.group_standardItemLeadInstructionHint,
2754
- description: "Standard tags for lead, instruction, and hint."
3448
+ // Instruction lands under reason. Hint/item/lead/etc.
3449
+ // omitted: not exercised by any fixture and BPG's
3450
+ // `parseFeedback` spreads only the tags actually present
3451
+ // — adding the rest can be done lazily as fixtures
3452
+ // require them, with the same `{reason: {…}}` wrapper.
3453
+ key: ConfigKey.tag_instruction,
3454
+ exportJsonKey: { reason: { instruction: "$" } },
3455
+ description: "Instruction for the feedback reason.",
3456
+ format: TagFormat.bitmarkText
2755
3457
  },
2756
3458
  {
2757
3459
  key: ConfigKey.property_reasonableNumOfChars,
2758
3460
  exportJsonKey: [
2759
- { "@absent": { reasonableNumOfChars: "$ancestor" } },
2760
- { reasonableNumOfChars: "$" }
3461
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3462
+ { reason: { reasonableNumOfChars: "$" } }
2761
3463
  ],
2762
3464
  description: "Property for reasonable number of characters.",
2763
3465
  format: TagFormat.number
2764
3466
  },
2765
3467
  {
3468
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3469
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3470
+ // cascade. Local emission only.
3471
+ //
3472
+ // Both `reason.example` and `feedbacks[].example` are
3473
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3474
+ // here is `bitmarkText` so `$` coerces to a paragraph
3475
+ // array at both write sites.
2766
3476
  key: ConfigKey.property_example,
2767
3477
  exportJsonKey: [
2768
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2769
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2770
- { isExample: true, example: "$", "@bit": { isExample: true } }
3478
+ {
3479
+ // Bare `[@example]`: reason.example is TextAst BPG
3480
+ // coerces the boolean default to a paragraph
3481
+ // containing the string "true". feedbacks[].example
3482
+ // is not TextAst, stays a boolean. Two literal writes.
3483
+ "@keyonly": {
3484
+ reason: {
3485
+ isExample: true,
3486
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3487
+ },
3488
+ isExample: true,
3489
+ example: true,
3490
+ "@bit": { isExample: true }
3491
+ }
3492
+ },
3493
+ {
3494
+ reason: { isExample: true, example: "$" },
3495
+ isExample: true,
3496
+ example: "$",
3497
+ "@bit": { isExample: true }
3498
+ }
2771
3499
  ],
2772
- description: "Example text for the feedback.",
2773
- format: TagFormat.plainText
3500
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3501
+ format: TagFormat.bitmarkText,
3502
+ nullable: true
2774
3503
  },
2775
3504
  {
2776
3505
  key: ConfigKey.tag_title,
@@ -2834,6 +3563,39 @@ var CARDSETS = {
2834
3563
  {
2835
3564
  key: ConfigKey.group_standardTags,
2836
3565
  description: "Standard tags for the question."
3566
+ },
3567
+ {
3568
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3569
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3570
+ // does not propagate `example`/`isExample` onto question
3571
+ // entries. Only entry-local `[@example]` fires.
3572
+ //
3573
+ // BPG's `BaseBuilder.toExample` for interview questions
3574
+ // (Builder.ts:2575,2588) uses `data.__sampleSolutionAst`
3575
+ // as `defaultExample` — bare `[@example]` (which sets
3576
+ // `__isDefaultExample=true, example=undefined`) emits the
3577
+ // parsed sample-solution AST in the `example` slot. The
3578
+ // `@keyonly` rule mirrors that runtime behaviour via
3579
+ // `$parent.sampleSolution` — the chain-walker's post-walk
3580
+ // retry resolves this once the sibling `[@sampleSolution]`
3581
+ // chain has merged into the shared scope (interview's
3582
+ // `[@example]` typically precedes `[@sampleSolution]` in
3583
+ // source). Format coercion turns the resolved string
3584
+ // value into a PM tree.
3585
+ key: ConfigKey.property_example,
3586
+ exportJsonKey: [
3587
+ {
3588
+ "@keyonly": {
3589
+ isExample: true,
3590
+ example: "$parent.sampleSolution",
3591
+ "@bit": { isExample: true }
3592
+ }
3593
+ },
3594
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3595
+ ],
3596
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3597
+ format: TagFormat.bitmarkText,
3598
+ nullable: true
2837
3599
  }
2838
3600
  ]
2839
3601
  }
@@ -2860,14 +3622,16 @@ var CARDSETS = {
2860
3622
  description: "Standard tags for lead, instruction, and hint."
2861
3623
  },
2862
3624
  {
3625
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3626
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2863
3627
  key: ConfigKey.property_example,
2864
3628
  exportJsonKey: [
2865
3629
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2866
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2867
3630
  { isExample: true, example: "$", "@bit": { isExample: true } }
2868
3631
  ],
2869
- description: "Example text for the element.",
2870
- format: TagFormat.plainText
3632
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3633
+ format: TagFormat.plainText,
3634
+ nullable: true
2871
3635
  }
2872
3636
  ],
2873
3637
  repeatCount: Count.infinity
@@ -2879,9 +3643,57 @@ var CARDSETS = {
2879
3643
  //
2880
3644
  // table
2881
3645
  //
3646
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3647
+ // output (`{ columns?, data, columnWidths? }`):
3648
+ //
3649
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3650
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3651
+ // no qualifier, so they route to the `table-body` (default) section.
3652
+ //
3653
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3654
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3655
+ // the first header card's cells to `columns` and demote remaining
3656
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3657
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3658
+ // here (no per-cell metadata in basic shape) — declared on the cell
3659
+ // variant below.
3660
+ //
2882
3661
  [CardSetConfigKey.table]: {
2883
- jsonKey: "table.data",
2884
- exportJsonKey: { table: { data: "$" } },
3662
+ jsonKey: null,
3663
+ exportJsonKey: null,
3664
+ sections: {
3665
+ "table-header": {
3666
+ // First header card's row of cells → `columns`. Subsequent header
3667
+ // cards' rows → `data` (per-card append; `[$]` shape because
3668
+ // per-card emission fires once per card, multiple fires deep_merge-
3669
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3670
+ // both ROUTE cards to this section (when no explicit `==== table-
3671
+ // header ====` qualifier) and SELECT the rule body once routed.
3672
+ // First rule = first card with any `[#]` → `columns`; second rule
3673
+ // = subsequent cards in this section → `data` (per-row append).
3674
+ jsonKey: "table.columns",
3675
+ exportJsonKey: [
3676
+ {
3677
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3678
+ rule: { "@bit": { table: { columns: "$" } } }
3679
+ },
3680
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3681
+ { "@bit": { table: { data: ["$"] } } }
3682
+ ]
3683
+ },
3684
+ "table-body": {
3685
+ // Batched emission: `$` is the entire 2D array of body card rows.
3686
+ // This is the default section (no qualifier → cards land here).
3687
+ jsonKey: "table.data",
3688
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3689
+ isDefault: true
3690
+ },
3691
+ "table-footer": {
3692
+ // Batched emission: `$` is the entire 2D array of footer rows.
3693
+ jsonKey: "table.data",
3694
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3695
+ }
3696
+ },
2885
3697
  sides: [
2886
3698
  {
2887
3699
  name: "cell",
@@ -2922,6 +3734,8 @@ var CARDSETS = {
2922
3734
  },
2923
3735
  {
2924
3736
  key: ConfigKey.property_tableColWidth,
3737
+ jsonKey: "^table.columnWidths[{s}]",
3738
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
2925
3739
  description: "Width for the column.",
2926
3740
  format: TagFormat.number
2927
3741
  }
@@ -2940,7 +3754,22 @@ var CARDSETS = {
2940
3754
  sections: {
2941
3755
  "table-header": {
2942
3756
  jsonKey: "table.header.rows",
2943
- exportJsonKey: { table: { header: { rows: "$" } } },
3757
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3758
+ // Cards with no explicit `==== table-header ====` qualifier
3759
+ // route here if FIRST CARD contains any `[#]` tag in any
3760
+ // variant. Explicit-qualifier cards fall through to the
3761
+ // unconditional fallback rule. Both rules use the per-card
3762
+ // append shape `rows: ['$']` — the runtime triggers per-card
3763
+ // emission whenever the pattern carries predicates, and each
3764
+ // fire appends a single-element array (multi-fire deep-merge
3765
+ // concatenates).
3766
+ exportJsonKey: [
3767
+ {
3768
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3769
+ rule: { table: { header: { rows: ["$"] } } }
3770
+ },
3771
+ { table: { header: { rows: ["$"] } } }
3772
+ ],
2944
3773
  sideJsonKey: "cells[{s}]|set(title=true)",
2945
3774
  sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
2946
3775
  },
@@ -2973,13 +3802,13 @@ var CARDSETS = {
2973
3802
  },
2974
3803
  {
2975
3804
  key: ConfigKey.tag_title,
2976
- jsonKey: ".",
2977
- exportJsonKey: {
2978
- "@bit": {
2979
- table: { header: { rows: { cells: { cells: { $s: { content: "$" } } } } } }
2980
- }
2981
- },
2982
- description: "Title of the table cell."
3805
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3806
+ // `content` field. When the card is in the `table-header`
3807
+ // section, the section's sideExportJsonKey wraps each
3808
+ // cell with `title: true` automatically.
3809
+ jsonKey: "content",
3810
+ exportJsonKey: { content: "$" },
3811
+ description: "Title text of the table cell (header rows)."
2983
3812
  },
2984
3813
  {
2985
3814
  key: ConfigKey.property_tableCellType,
@@ -3011,6 +3840,9 @@ var CARDSETS = {
3011
3840
  },
3012
3841
  {
3013
3842
  key: ConfigKey.property_tableColWidth,
3843
+ // PLAN-080: write to the cell's `colwidth` field.
3844
+ jsonKey: "colwidth",
3845
+ exportJsonKey: { colwidth: "$" },
3014
3846
  description: "Width for the column.",
3015
3847
  format: TagFormat.number
3016
3848
  }
@@ -3048,7 +3880,8 @@ var CARDSETS = {
3048
3880
  {
3049
3881
  key: ConfigKey.resource_audio,
3050
3882
  description: "Audio resource for the pronunciation table.",
3051
- jsonKey: "audio"
3883
+ jsonKey: "audio",
3884
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
3052
3885
  }
3053
3886
  ],
3054
3887
  repeatCount: Count.infinity
@@ -3119,14 +3952,17 @@ var CARDSETS = {
3119
3952
  format: TagFormat.plainText
3120
3953
  },
3121
3954
  {
3955
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
3956
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
3957
+ // header cascade.
3122
3958
  key: ConfigKey.property_example,
3123
3959
  exportJsonKey: [
3124
3960
  { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3125
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
3126
3961
  { isExample: true, example: "$", "@bit": { isExample: true } }
3127
3962
  ],
3128
- description: "Example text for the bot action response.",
3129
- format: TagFormat.plainText
3963
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
3964
+ format: TagFormat.plainText,
3965
+ nullable: true
3130
3966
  }
3131
3967
  ]
3132
3968
  }
@@ -3192,6 +4028,38 @@ var CARDSETS = {
3192
4028
  ]
3193
4029
  },
3194
4030
  //
4031
+ // page-footer-sections — mirrors example-bit-list but emits cards under
4032
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
4033
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
4034
+ // routing as pure config so the Rust serializer can pick it up.
4035
+ //
4036
+ [CardSetConfigKey.pageFooterSections]: {
4037
+ jsonKey: "sections",
4038
+ exportJsonKey: { sections: "$" },
4039
+ sides: [
4040
+ {
4041
+ name: "item",
4042
+ variants: [
4043
+ {
4044
+ jsonKey: "body",
4045
+ exportJsonKey: { body: "$" },
4046
+ tags: [
4047
+ {
4048
+ key: ConfigKey.group_standardTags,
4049
+ description: "Standard tags for page-footer section bits."
4050
+ },
4051
+ {
4052
+ exportJsonKey: { title: "$" },
4053
+ key: ConfigKey.tag_title,
4054
+ description: "Title of the page-footer section."
4055
+ }
4056
+ ]
4057
+ }
4058
+ ]
4059
+ }
4060
+ ]
4061
+ },
4062
+ //
3195
4063
  // ingredients
3196
4064
  //
3197
4065
  [CardSetConfigKey.ingredients]: {
@@ -3522,7 +4390,11 @@ var GROUPS = {
3522
4390
  {
3523
4391
  key: ConfigKey.property_groupTag,
3524
4392
  jsonKey: "groupTag.name",
3525
- exportJsonKey: { groupTag: { name: "$" } },
4393
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4394
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4395
+ // same value fold into one entry; chained `@tag` children's
4396
+ // arrays merge as sets.
4397
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3526
4398
  description: "The group tag(s) for the bit",
3527
4399
  format: TagFormat.plainText,
3528
4400
  maxCount: Count.infinity,
@@ -3695,6 +4567,24 @@ var GROUPS = {
3695
4567
  }
3696
4568
  ]
3697
4569
  },
4570
+ [ConfigKey.group_standardInstructionHint]: {
4571
+ type: GroupConfigType.standard,
4572
+ 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).",
4573
+ tags: [
4574
+ {
4575
+ exportJsonKey: { instruction: "$" },
4576
+ key: ConfigKey.tag_instruction,
4577
+ name: "Instruction",
4578
+ description: "The instruction for the bit"
4579
+ },
4580
+ {
4581
+ exportJsonKey: { hint: "$" },
4582
+ key: ConfigKey.tag_hint,
4583
+ name: "Hint",
4584
+ description: "The hint for the bit"
4585
+ }
4586
+ ]
4587
+ },
3698
4588
  [ConfigKey.group_standardTags]: {
3699
4589
  type: GroupConfigType.standard,
3700
4590
  description: "Standard tags which apply to MOST (but not all) bits",
@@ -3708,17 +4598,45 @@ var GROUPS = {
3708
4598
  description: "Item, lead, page number, margin number, instruction and hint tags"
3709
4599
  },
3710
4600
  {
3711
- // Bit-level example tag: bare/synthesized records the marker only;
3712
- // valued [@example:text] emits `isExample: true` and `example` as
3713
- // PM-rendered rich text (bitmark+ format).
4601
+ // Bit-level example tag (default non-`isTopLevelExample` bits):
4602
+ // - bare `[@example]`: consume only (records cascade marker, emits
4603
+ // nothing at bit-level OLD parser scrubs bit-level isExample
4604
+ // for bits NOT in its `isTopLevelExample` allow-list).
4605
+ // - synthesized (descendant cascade): still emits `isExample: true`
4606
+ // at the descendant scope.
4607
+ // - valued `[@example:text]`: emit the example value only; bit-level
4608
+ // `isExample` is left to bubble from descendants.
4609
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4610
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4611
+ // matchMatrix, match, essay, etc.) override this entry with the
4612
+ // legacy emit-isExample shape.
3714
4613
  key: ConfigKey.property_example,
3715
- exportJsonKey: [
3716
- { "@keyonly": { isExample: true } },
3717
- { "@absent": { isExample: true } },
3718
- { isExample: true, example: "$" }
3719
- ],
4614
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3720
4615
  description: "The example(s) for the bit",
3721
- format: TagFormat.bitmarkText
4616
+ format: TagFormat.bitmarkText,
4617
+ nullable: true
4618
+ }
4619
+ ]
4620
+ },
4621
+ [ConfigKey.group_standardTagsNoItemLead]: {
4622
+ type: GroupConfigType.standard,
4623
+ 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).",
4624
+ tags: [
4625
+ {
4626
+ key: ConfigKey.group_standardAllBits,
4627
+ description: "All standard tags which apply to all bits"
4628
+ },
4629
+ {
4630
+ key: ConfigKey.group_standardInstructionHint,
4631
+ description: "Instruction and hint tags only (no item/lead chain)."
4632
+ },
4633
+ {
4634
+ // Mirror of group_standardTags property_example entry.
4635
+ key: ConfigKey.property_example,
4636
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4637
+ description: "The example(s) for the bit",
4638
+ format: TagFormat.bitmarkText,
4639
+ nullable: true
3722
4640
  }
3723
4641
  ]
3724
4642
  },
@@ -3783,8 +4701,8 @@ var GROUPS = {
3783
4701
  tags: [
3784
4702
  {
3785
4703
  key: ConfigKey.property_person,
3786
- jsonKey: "partner.name",
3787
- exportJsonKey: { partner: { name: "$" } },
4704
+ jsonKey: "person.name",
4705
+ exportJsonKey: { person: { name: "$" } },
3788
4706
  description: "A person",
3789
4707
  format: TagFormat.plainText,
3790
4708
  chain: [
@@ -3804,8 +4722,8 @@ var GROUPS = {
3804
4722
  {
3805
4723
  // Deprecated (parter renamed to person)
3806
4724
  key: ConfigKey.property_partner,
3807
- jsonKey: "partner.name",
3808
- exportJsonKey: { partner: { name: "$" } },
4725
+ jsonKey: "person.name",
4726
+ exportJsonKey: { person: { name: "$" } },
3809
4727
  description: "A partner",
3810
4728
  format: TagFormat.plainText,
3811
4729
  chain: [
@@ -3829,7 +4747,9 @@ var GROUPS = {
3829
4747
  description: "Gap chain",
3830
4748
  tags: [
3831
4749
  {
3832
- exportJsonKey: { solutions: [["$"]] },
4750
+ exportJsonKey: {
4751
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4752
+ },
3833
4753
  key: ConfigKey.tag_gap,
3834
4754
  description: "The value of a gap in the content",
3835
4755
  maxCount: Count.infinity,
@@ -3848,27 +4768,37 @@ var GROUPS = {
3848
4768
  description: "Item, lead, page number, margin number, instruction and hint tags"
3849
4769
  },
3850
4770
  {
4771
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4772
+ // `__solutionsAst[length - 1]` — the solution closest to the
4773
+ // `[@example]` tag at parse time. Our chain accumulator is built
4774
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4775
+ // the moment `[@example]` fires, `solutions` only contains the
4776
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4777
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4778
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4779
+ // solutions=["solution"], `[-1]`="solution".
3851
4780
  key: ConfigKey.property_example,
3852
4781
  jsonKey: "example",
3853
4782
  exportJsonKey: [
3854
4783
  {
3855
4784
  "@keyonly": {
3856
4785
  isExample: true,
3857
- example: "$parent.solutions[0]",
4786
+ example: "$parent.solutions[-1]",
3858
4787
  "@bit": { isExample: true }
3859
4788
  }
3860
4789
  },
3861
4790
  {
3862
4791
  "@absent": {
3863
4792
  isExample: true,
3864
- example: "$parent.solutions[0]",
4793
+ example: "$parent.solutions[-1]",
3865
4794
  "@bit": { isExample: true }
3866
4795
  }
3867
4796
  },
3868
4797
  { isExample: true, example: "$", "@bit": { isExample: true } }
3869
4798
  ],
3870
4799
  description: "An example for the gap",
3871
- format: TagFormat.bitmarkText
4800
+ format: TagFormat.bitmarkText,
4801
+ nullable: true
3872
4802
  },
3873
4803
  {
3874
4804
  key: ConfigKey.property_isCaseSensitive,
@@ -3917,6 +4847,99 @@ var GROUPS = {
3917
4847
  description: "Item, lead, page number, margin number, instruction and hint tags"
3918
4848
  },
3919
4849
  {
4850
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4851
+ // `@example` from the bit header marks EVERY choice with
4852
+ // `isExample: true` (matches BPG output), but only the
4853
+ // FIRST correct choice gets `example: true` (BPG
4854
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4855
+ //
4856
+ // Two `@absent` rules, picked first-match-wins:
4857
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4858
+ // `maxEmits:1` — fires once for the first correct.
4859
+ // 2. Plain `@absent` — fires on every remaining entry
4860
+ // (subsequent corrects and all incorrects), emitting
4861
+ // `isExample` only.
4862
+ // The `serialize_cards` cascade-counter scope holds the cap
4863
+ // across the whole cardset.
4864
+ // @card scope-shift writes choice-container isExample (e.g.
4865
+ // `quizzes[i].isExample`) — was previously provided by the
4866
+ // universal `bubble_is_example` post-walk, which was removed
4867
+ // in favour of explicit per-tag config. SYNTAX.md §8.
4868
+ key: ConfigKey.property_example,
4869
+ jsonKey: "example",
4870
+ exportJsonKey: [
4871
+ {
4872
+ "@keyonly": {
4873
+ isExample: true,
4874
+ example: "$parent.isCorrect",
4875
+ "@bit": { isExample: true },
4876
+ "@card": { isExample: true }
4877
+ }
4878
+ },
4879
+ {
4880
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4881
+ maxEmits: 1,
4882
+ rule: {
4883
+ isExample: true,
4884
+ example: true,
4885
+ "@bit": { isExample: true },
4886
+ "@card": { isExample: true }
4887
+ }
4888
+ },
4889
+ {
4890
+ "@absent": {
4891
+ isExample: true,
4892
+ "@bit": { isExample: true },
4893
+ "@card": { isExample: true }
4894
+ }
4895
+ },
4896
+ {
4897
+ isExample: true,
4898
+ example: "$",
4899
+ "@bit": { isExample: true },
4900
+ "@card": { isExample: true }
4901
+ }
4902
+ ],
4903
+ description: "An example for the true/false statement/question",
4904
+ format: TagFormat.boolean,
4905
+ nullable: true
4906
+ }
4907
+ ]
4908
+ },
4909
+ {
4910
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
4911
+ key: ConfigKey.tag_false,
4912
+ description: "False value for a true/false statement/question",
4913
+ maxCount: Count.infinity,
4914
+ format: TagFormat.plainText,
4915
+ chain: [
4916
+ {
4917
+ key: ConfigKey.tag_true,
4918
+ jsonKey: "choices[]|set(isCorrect=true)",
4919
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
4920
+ description: "True values for a true/false statement/question",
4921
+ maxCount: Count.infinity,
4922
+ format: TagFormat.plainText
4923
+ },
4924
+ {
4925
+ key: ConfigKey.tag_false,
4926
+ jsonKey: "choices[]|set(isCorrect=false)",
4927
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
4928
+ description: "False values for a true/false statement/question",
4929
+ maxCount: Count.infinity,
4930
+ format: TagFormat.plainText
4931
+ },
4932
+ {
4933
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4934
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4935
+ },
4936
+ {
4937
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4938
+ // `@example` on a `-` (incorrect choice) emits
4939
+ // `isExample: true` only — no `example` (BPG fixtures show
4940
+ // `isExample` on every choice, but `example` only on the
4941
+ // first correct via firstCorrectOnly cascade).
4942
+ // @card scope-shift writes choice-container isExample.
3920
4943
  key: ConfigKey.property_example,
3921
4944
  jsonKey: "example",
3922
4945
  exportJsonKey: [
@@ -3924,25 +4947,99 @@ var GROUPS = {
3924
4947
  "@keyonly": {
3925
4948
  isExample: true,
3926
4949
  example: "$parent.isCorrect",
3927
- "@bit": { isExample: true }
4950
+ "@bit": { isExample: true },
4951
+ "@card": { isExample: true }
3928
4952
  }
3929
4953
  },
3930
4954
  {
3931
4955
  "@absent": {
4956
+ isExample: true,
4957
+ "@bit": { isExample: true },
4958
+ "@card": { isExample: true }
4959
+ }
4960
+ },
4961
+ {
4962
+ isExample: true,
4963
+ example: "$",
4964
+ "@bit": { isExample: true },
4965
+ "@card": { isExample: true }
4966
+ }
4967
+ ],
4968
+ description: "An example for the true/false statement/question (incorrect entry \u2014 isExample only on cascade)",
4969
+ format: TagFormat.boolean,
4970
+ nullable: true
4971
+ }
4972
+ ]
4973
+ }
4974
+ ]
4975
+ },
4976
+ [ConfigKey.group_trueFalseResponses]: {
4977
+ type: GroupConfigType.standard,
4978
+ description: "True/False Responses chain",
4979
+ tags: [
4980
+ {
4981
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4982
+ key: ConfigKey.tag_true,
4983
+ description: "True value for a true/false statement/question",
4984
+ maxCount: Count.infinity,
4985
+ format: TagFormat.plainText,
4986
+ chain: [
4987
+ {
4988
+ key: ConfigKey.tag_true,
4989
+ jsonKey: "responses[]|set(isCorrect=true)",
4990
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4991
+ description: "True values for a true/false statement/question",
4992
+ maxCount: Count.infinity,
4993
+ format: TagFormat.plainText
4994
+ },
4995
+ {
4996
+ key: ConfigKey.tag_false,
4997
+ jsonKey: "responses[]|set(isCorrect=false)",
4998
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4999
+ description: "False values for a true/false statement/question",
5000
+ maxCount: Count.infinity,
5001
+ format: TagFormat.plainText
5002
+ },
5003
+ {
5004
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5005
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5006
+ },
5007
+ {
5008
+ // @card scope-shift writes response-container isExample.
5009
+ key: ConfigKey.property_example,
5010
+ jsonKey: "example",
5011
+ exportJsonKey: [
5012
+ {
5013
+ "@keyonly": {
3932
5014
  isExample: true,
3933
5015
  example: "$parent.isCorrect",
3934
- "@bit": { isExample: true }
5016
+ "@bit": { isExample: true },
5017
+ "@card": { isExample: true }
3935
5018
  }
3936
5019
  },
3937
- { isExample: true, example: "$", "@bit": { isExample: true } }
5020
+ {
5021
+ "@absent": {
5022
+ isExample: "$parent.isCorrect",
5023
+ example: "$parent.isCorrect",
5024
+ "@bit": { isExample: "$parent.isCorrect" },
5025
+ "@card": { isExample: "$parent.isCorrect" }
5026
+ }
5027
+ },
5028
+ {
5029
+ isExample: true,
5030
+ example: "$",
5031
+ "@bit": { isExample: true },
5032
+ "@card": { isExample: true }
5033
+ }
3938
5034
  ],
3939
5035
  description: "An example for the true/false statement/question",
3940
- format: TagFormat.bitmarkText
5036
+ format: TagFormat.boolean,
5037
+ nullable: true
3941
5038
  }
3942
5039
  ]
3943
5040
  },
3944
5041
  {
3945
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5042
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
3946
5043
  key: ConfigKey.tag_false,
3947
5044
  description: "False value for a true/false statement/question",
3948
5045
  maxCount: Count.infinity,
@@ -3950,16 +5047,16 @@ var GROUPS = {
3950
5047
  chain: [
3951
5048
  {
3952
5049
  key: ConfigKey.tag_true,
3953
- jsonKey: "choices[]|set(isCorrect=true)",
3954
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
5050
+ jsonKey: "responses[]|set(isCorrect=true)",
5051
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
3955
5052
  description: "True values for a true/false statement/question",
3956
5053
  maxCount: Count.infinity,
3957
5054
  format: TagFormat.plainText
3958
5055
  },
3959
5056
  {
3960
5057
  key: ConfigKey.tag_false,
3961
- jsonKey: "choices[]|set(isCorrect=false)",
3962
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
5058
+ jsonKey: "responses[]|set(isCorrect=false)",
5059
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
3963
5060
  description: "False values for a true/false statement/question",
3964
5061
  maxCount: Count.infinity,
3965
5062
  format: TagFormat.plainText
@@ -3969,6 +5066,7 @@ var GROUPS = {
3969
5066
  description: "Item, lead, page number, margin number, instruction and hint tags"
3970
5067
  },
3971
5068
  {
5069
+ // @card scope-shift writes response-container isExample.
3972
5070
  key: ConfigKey.property_example,
3973
5071
  jsonKey: "example",
3974
5072
  exportJsonKey: [
@@ -3976,20 +5074,333 @@ var GROUPS = {
3976
5074
  "@keyonly": {
3977
5075
  isExample: true,
3978
5076
  example: "$parent.isCorrect",
3979
- "@bit": { isExample: true }
5077
+ "@bit": { isExample: true },
5078
+ "@card": { isExample: true }
3980
5079
  }
3981
5080
  },
3982
5081
  {
3983
5082
  "@absent": {
3984
5083
  isExample: true,
3985
5084
  example: "$parent.isCorrect",
3986
- "@bit": { isExample: true }
5085
+ "@bit": { isExample: true },
5086
+ "@card": { isExample: true }
3987
5087
  }
3988
5088
  },
3989
- { isExample: true, example: "$", "@bit": { isExample: true } }
5089
+ {
5090
+ isExample: true,
5091
+ example: "$",
5092
+ "@bit": { isExample: true },
5093
+ "@card": { isExample: true }
5094
+ }
3990
5095
  ],
3991
5096
  description: "An example for the true/false statement/question",
3992
- format: TagFormat.bitmarkText
5097
+ format: TagFormat.boolean,
5098
+ nullable: true
5099
+ }
5100
+ ]
5101
+ }
5102
+ ]
5103
+ },
5104
+ [ConfigKey.group_trueFalseInlineSelect]: {
5105
+ type: GroupConfigType.standard,
5106
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
5107
+ tags: [
5108
+ {
5109
+ exportJsonKey: {
5110
+ "@body-inline": {
5111
+ type: "select",
5112
+ attrs: { options: [{ text: "$", isCorrect: true }] }
5113
+ }
5114
+ },
5115
+ key: ConfigKey.tag_true,
5116
+ description: "True value for an inline select body bit",
5117
+ maxCount: Count.infinity,
5118
+ format: TagFormat.plainText,
5119
+ chain: [
5120
+ {
5121
+ key: ConfigKey.tag_true,
5122
+ exportJsonKey: {},
5123
+ description: "True values for an inline select body bit",
5124
+ maxCount: Count.infinity,
5125
+ format: TagFormat.plainText
5126
+ },
5127
+ {
5128
+ key: ConfigKey.tag_false,
5129
+ exportJsonKey: {},
5130
+ description: "False values for an inline select body bit",
5131
+ maxCount: Count.infinity,
5132
+ format: TagFormat.plainText
5133
+ },
5134
+ {
5135
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5136
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5137
+ },
5138
+ {
5139
+ // PLAN-083: cascade-fired `@example` from bit header fires
5140
+ // `isExample:true, example:true` on the FIRST CORRECT option
5141
+ // only (BPG `fillBooleanExample(select.options,
5142
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
5143
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5144
+ // OR — any cascade ⇒ bit marked) and
5145
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5146
+ // on the select node).
5147
+ key: ConfigKey.property_example,
5148
+ jsonKey: "example",
5149
+ exportJsonKey: [
5150
+ {
5151
+ "@keyonly": {
5152
+ isExample: true,
5153
+ example: "$parent.isCorrect",
5154
+ "@bit": { isExample: true },
5155
+ "@body-inline": { attrs: { isExample: true } }
5156
+ }
5157
+ },
5158
+ {
5159
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5160
+ maxEmits: 1,
5161
+ rule: {
5162
+ isExample: true,
5163
+ example: true,
5164
+ "@bit": { isExample: true },
5165
+ "@body-inline": { attrs: { isExample: true } }
5166
+ }
5167
+ },
5168
+ {
5169
+ isExample: true,
5170
+ example: "$",
5171
+ "@bit": { isExample: true },
5172
+ "@body-inline": { attrs: { isExample: true } }
5173
+ }
5174
+ ],
5175
+ description: "An example for the inline select option (first correct only)",
5176
+ format: TagFormat.boolean,
5177
+ nullable: true
5178
+ }
5179
+ ]
5180
+ },
5181
+ {
5182
+ exportJsonKey: {
5183
+ "@body-inline": {
5184
+ type: "select",
5185
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5186
+ }
5187
+ },
5188
+ key: ConfigKey.tag_false,
5189
+ description: "False value for an inline select body bit",
5190
+ maxCount: Count.infinity,
5191
+ format: TagFormat.plainText,
5192
+ chain: [
5193
+ {
5194
+ key: ConfigKey.tag_true,
5195
+ exportJsonKey: {},
5196
+ description: "True values for an inline select body bit",
5197
+ maxCount: Count.infinity,
5198
+ format: TagFormat.plainText
5199
+ },
5200
+ {
5201
+ key: ConfigKey.tag_false,
5202
+ exportJsonKey: {},
5203
+ description: "False values for an inline select body bit",
5204
+ maxCount: Count.infinity,
5205
+ format: TagFormat.plainText
5206
+ },
5207
+ {
5208
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5209
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5210
+ },
5211
+ {
5212
+ // PLAN-083: identical to the `+` lead's chain @example —
5213
+ // cascade reaches the first correct option regardless of
5214
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5215
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5216
+ key: ConfigKey.property_example,
5217
+ jsonKey: "example",
5218
+ exportJsonKey: [
5219
+ {
5220
+ "@keyonly": {
5221
+ isExample: true,
5222
+ example: "$parent.isCorrect",
5223
+ "@bit": { isExample: true },
5224
+ "@body-inline": { attrs: { isExample: true } }
5225
+ }
5226
+ },
5227
+ {
5228
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5229
+ maxEmits: 1,
5230
+ rule: {
5231
+ isExample: true,
5232
+ example: true,
5233
+ "@bit": { isExample: true },
5234
+ "@body-inline": { attrs: { isExample: true } }
5235
+ }
5236
+ },
5237
+ {
5238
+ isExample: true,
5239
+ example: "$",
5240
+ "@bit": { isExample: true },
5241
+ "@body-inline": { attrs: { isExample: true } }
5242
+ }
5243
+ ],
5244
+ description: "An example for the inline select option (first correct only)",
5245
+ format: TagFormat.boolean,
5246
+ nullable: true
5247
+ }
5248
+ ]
5249
+ }
5250
+ ]
5251
+ },
5252
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5253
+ type: GroupConfigType.standard,
5254
+ 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.',
5255
+ tags: [
5256
+ {
5257
+ exportJsonKey: {
5258
+ "@body-inline": {
5259
+ type: "highlight",
5260
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5261
+ }
5262
+ },
5263
+ key: ConfigKey.tag_true,
5264
+ description: "Highlighted span in the body (correct)",
5265
+ maxCount: Count.infinity,
5266
+ format: TagFormat.plainText,
5267
+ chain: [
5268
+ {
5269
+ key: ConfigKey.tag_true,
5270
+ exportJsonKey: {},
5271
+ description: "Highlighted span values (correct)",
5272
+ maxCount: Count.infinity,
5273
+ format: TagFormat.plainText
5274
+ },
5275
+ {
5276
+ key: ConfigKey.tag_false,
5277
+ exportJsonKey: {},
5278
+ description: "Highlighted span values (incorrect)",
5279
+ maxCount: Count.infinity,
5280
+ format: TagFormat.plainText
5281
+ },
5282
+ {
5283
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5284
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5285
+ },
5286
+ {
5287
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5288
+ // `@example` from the bit header fires `example: true` +
5289
+ // `isExample: true` on the FIRST correct highlight only
5290
+ // (BPG `fillBooleanExample(highlight.texts,
5291
+ // firstCorrectOnly=true)`). Incorrect highlights get
5292
+ // nothing (unlike choices, the highlight-text fixture
5293
+ // shows no `isExample` on incorrect entries).
5294
+ key: ConfigKey.property_example,
5295
+ jsonKey: "example",
5296
+ exportJsonKey: [
5297
+ {
5298
+ "@keyonly": {
5299
+ isExample: true,
5300
+ example: "$parent.isCorrect",
5301
+ "@bit": { isExample: true },
5302
+ "@body-inline": { attrs: { isExample: true } }
5303
+ }
5304
+ },
5305
+ {
5306
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5307
+ maxEmits: 1,
5308
+ rule: {
5309
+ isExample: true,
5310
+ example: true,
5311
+ "@bit": { isExample: true },
5312
+ "@body-inline": { attrs: { isExample: true } }
5313
+ }
5314
+ },
5315
+ {
5316
+ isExample: true,
5317
+ example: "$",
5318
+ "@bit": { isExample: true },
5319
+ "@body-inline": { attrs: { isExample: true } }
5320
+ }
5321
+ ],
5322
+ description: "An example for the highlighted span (first correct only)",
5323
+ format: TagFormat.boolean,
5324
+ nullable: true
5325
+ }
5326
+ ]
5327
+ },
5328
+ {
5329
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5330
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5331
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5332
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5333
+ // through and nothing fires. The natural-default strip removes
5334
+ // `isCorrect: false` from the final texts entries (matches fixture).
5335
+ exportJsonKey: {
5336
+ "@body-inline": {
5337
+ type: "highlight",
5338
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5339
+ }
5340
+ },
5341
+ key: ConfigKey.tag_false,
5342
+ description: "Highlighted span in the body (incorrect)",
5343
+ maxCount: Count.infinity,
5344
+ format: TagFormat.plainText,
5345
+ chain: [
5346
+ {
5347
+ key: ConfigKey.tag_true,
5348
+ exportJsonKey: {},
5349
+ description: "Highlighted span values (correct)",
5350
+ maxCount: Count.infinity,
5351
+ format: TagFormat.plainText
5352
+ },
5353
+ {
5354
+ key: ConfigKey.tag_false,
5355
+ exportJsonKey: {},
5356
+ description: "Highlighted span values (incorrect)",
5357
+ maxCount: Count.infinity,
5358
+ format: TagFormat.plainText
5359
+ },
5360
+ {
5361
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5362
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5363
+ },
5364
+ {
5365
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5366
+ // `@example` on a `-` (incorrect highlight) emits nothing
5367
+ // (highlight-text fixture has no `isExample` on incorrect
5368
+ // entries; firstCorrectOnly cascade skips them entirely).
5369
+ // No `@absent` rule.
5370
+ // PLAN-083: identical to the `+` lead's chain @example —
5371
+ // cascade reaches the first correct option regardless of
5372
+ // which lead (+/-) opens the chain.
5373
+ key: ConfigKey.property_example,
5374
+ jsonKey: "example",
5375
+ exportJsonKey: [
5376
+ {
5377
+ "@keyonly": {
5378
+ isExample: true,
5379
+ example: "$parent.isCorrect",
5380
+ "@bit": { isExample: true },
5381
+ "@body-inline": { attrs: { isExample: true } }
5382
+ }
5383
+ },
5384
+ {
5385
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5386
+ maxEmits: 1,
5387
+ rule: {
5388
+ isExample: true,
5389
+ example: true,
5390
+ "@bit": { isExample: true },
5391
+ "@body-inline": { attrs: { isExample: true } }
5392
+ }
5393
+ },
5394
+ {
5395
+ isExample: true,
5396
+ example: "$",
5397
+ "@bit": { isExample: true },
5398
+ "@body-inline": { attrs: { isExample: true } }
5399
+ }
5400
+ ],
5401
+ description: "An example for the highlighted span (first correct only)",
5402
+ format: TagFormat.boolean,
5403
+ nullable: true
3993
5404
  }
3994
5405
  ]
3995
5406
  }
@@ -4002,7 +5413,11 @@ var GROUPS = {
4002
5413
  {
4003
5414
  key: ConfigKey.property_mark,
4004
5415
  jsonKey: "marks.mark",
4005
- exportJsonKey: { marks: { mark: ["$"] } },
5416
+ // `emphasis: 'underline'` is the per-entry default for mark
5417
+ // configs — mirrors the hardcoded default in BPG
5418
+ // `MarkConfigChainContentProcessor.ts:55-58`. Chained
5419
+ // `[@emphasis:…]` overrides via deep-merge into `marks[-1]`.
5420
+ exportJsonKey: { marks: [{ mark: "$", emphasis: "underline" }] },
4006
5421
  description: "The mark configuration",
4007
5422
  format: TagFormat.plainText,
4008
5423
  maxCount: Count.infinity,
@@ -4026,7 +5441,9 @@ var GROUPS = {
4026
5441
  description: "Mark chain",
4027
5442
  tags: [
4028
5443
  {
4029
- exportJsonKey: { solution: ["$"] },
5444
+ exportJsonKey: {
5445
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5446
+ },
4030
5447
  key: ConfigKey.tag_mark,
4031
5448
  description: "The marked content",
4032
5449
  maxCount: Count.infinity,
@@ -4035,19 +5452,30 @@ var GROUPS = {
4035
5452
  {
4036
5453
  key: ConfigKey.property_mark,
4037
5454
  jsonKey: "mark",
5455
+ exportJsonKey: { mark: "$" },
4038
5456
  description: "The mark configuration",
4039
5457
  format: TagFormat.plainText
4040
5458
  },
4041
5459
  {
4042
5460
  key: ConfigKey.property_example,
4043
5461
  jsonKey: "example",
4044
- exportJsonKey: [
4045
- { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
4046
- { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
4047
- { isExample: true, example: "$", "@bit": { isExample: true } }
4048
- ],
5462
+ // Per-mark `@example` is a boolean toggle. BPG's runtime
5463
+ // `ExampleTagContentProcessor.handleDefaultOnlyExample`
5464
+ // (lines 147-161) normalises ANY `[@example]` or
5465
+ // `[@example:V]` on a mark body-bit to
5466
+ // `__isDefaultExample=true, example=undefined`; the
5467
+ // downstream `toExample(_, _, defaultExample=true)` in
5468
+ // `BaseBuilder.ts:80-87` then emits `example: true`
5469
+ // regardless of source. Declare a single unconditional
5470
+ // boolean-shape rule so the exported schema matches that
5471
+ // runtime semantic instead of the misleading `example: $`
5472
+ // shape (which would only fire if `handleDefaultOnlyExample`
5473
+ // were removed). `@bit: {isExample: true}` cascades the
5474
+ // marker — but NOT the `example` value — to bit root.
5475
+ exportJsonKey: { isExample: true, example: true, "@bit": { isExample: true } },
4049
5476
  description: "An example for the marked content",
4050
- format: TagFormat.bitmarkText
5477
+ format: TagFormat.boolean,
5478
+ nullable: true
4051
5479
  }
4052
5480
  ]
4053
5481
  }
@@ -4476,6 +5904,42 @@ var GROUPS = {
4476
5904
  }
4477
5905
  ]
4478
5906
  },
5907
+ [ConfigKey.group_backgroundWallpaper]: {
5908
+ type: GroupConfigType.standard,
5909
+ description: "Background wallpaper tags",
5910
+ tags: [
5911
+ {
5912
+ key: ConfigKey.resource_backgroundWallpaper,
5913
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
5914
+ description: "Background wallpaper for the image, used to set a background for the image",
5915
+ chain: [
5916
+ {
5917
+ key: ConfigKey.group_resourceImageCommon,
5918
+ description: "Common resource image tags for images"
5919
+ }
5920
+ ]
5921
+ }
5922
+ ]
5923
+ },
5924
+ [ConfigKey.group_imageNoZoom]: {
5925
+ type: GroupConfigType.standard,
5926
+ description: "Image bit tags",
5927
+ tags: [
5928
+ {
5929
+ key: ConfigKey.group_backgroundWallpaper,
5930
+ description: "Background wallpaper tags for images, used to define background properties for images"
5931
+ },
5932
+ {
5933
+ key: ConfigKey.group_resourceBitTags,
5934
+ description: "Resource bit tags for images, used to define additional properties for images"
5935
+ },
5936
+ {
5937
+ key: ConfigKey.group_resourceImageNoZoom,
5938
+ description: "Resource image tags for images, used to attach images to the bit",
5939
+ minCount: 1
5940
+ }
5941
+ ]
5942
+ },
4479
5943
  //
4480
5944
  // Resource groups
4481
5945
  //
@@ -4595,6 +6059,58 @@ var GROUPS = {
4595
6059
  }
4596
6060
  ]
4597
6061
  },
6062
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
6063
+ type: GroupConfigType.standard,
6064
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
6065
+ tags: [
6066
+ {
6067
+ key: ConfigKey.group_resourceCommon,
6068
+ description: "Common resource properties"
6069
+ },
6070
+ {
6071
+ key: ConfigKey.property_src1x,
6072
+ description: "The source URL for the image at 1x resolution",
6073
+ format: TagFormat.plainText
6074
+ },
6075
+ {
6076
+ key: ConfigKey.property_src2x,
6077
+ description: "The source URL for the image at 2x resolution",
6078
+ format: TagFormat.plainText
6079
+ },
6080
+ {
6081
+ key: ConfigKey.property_src3x,
6082
+ description: "The source URL for the image at 3x resolution",
6083
+ format: TagFormat.plainText
6084
+ },
6085
+ {
6086
+ key: ConfigKey.property_src4x,
6087
+ description: "The source URL for the image at 4x resolution",
6088
+ format: TagFormat.plainText
6089
+ },
6090
+ {
6091
+ key: ConfigKey.property_width,
6092
+ description: "The width of the image",
6093
+ format: TagFormat.number
6094
+ },
6095
+ {
6096
+ key: ConfigKey.property_height,
6097
+ description: "The height of the image",
6098
+ format: TagFormat.number
6099
+ },
6100
+ {
6101
+ key: ConfigKey.property_alt,
6102
+ description: "The alternative text for the image",
6103
+ format: TagFormat.plainText
6104
+ },
6105
+ // @zoomDisabled with a true default
6106
+ {
6107
+ key: ConfigKey.property_zoomDisabled,
6108
+ description: "If true, zooming is disabled for the image (defaults to true)",
6109
+ format: TagFormat.boolean,
6110
+ defaultValue: "true"
6111
+ }
6112
+ ]
6113
+ },
4598
6114
  [ConfigKey.group_resourceAudioCommon]: {
4599
6115
  type: GroupConfigType.standard,
4600
6116
  description: "Common properties for audio resources",
@@ -4684,22 +6200,26 @@ var GROUPS = {
4684
6200
  {
4685
6201
  key: ConfigKey.property_src1x,
4686
6202
  description: "The source URL for the video at 1x resolution",
4687
- format: TagFormat.plainText
6203
+ format: TagFormat.plainText,
6204
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4688
6205
  },
4689
6206
  {
4690
6207
  key: ConfigKey.property_src2x,
4691
6208
  description: "The source URL for the video at 2x resolution",
4692
- format: TagFormat.plainText
6209
+ format: TagFormat.plainText,
6210
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4693
6211
  },
4694
6212
  {
4695
6213
  key: ConfigKey.property_src3x,
4696
6214
  description: "The source URL for the video at 3x resolution",
4697
- format: TagFormat.plainText
6215
+ format: TagFormat.plainText,
6216
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4698
6217
  },
4699
6218
  {
4700
6219
  key: ConfigKey.property_src4x,
4701
6220
  description: "The source URL for the video at 4x resolution",
4702
- format: TagFormat.plainText
6221
+ format: TagFormat.plainText,
6222
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4703
6223
  }
4704
6224
  ]
4705
6225
  },
@@ -4712,6 +6232,7 @@ var GROUPS = {
4712
6232
  tags: [
4713
6233
  {
4714
6234
  key: ConfigKey.resource_icon,
6235
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4715
6236
  description: "The icon resource",
4716
6237
  chain: [
4717
6238
  {
@@ -4739,12 +6260,30 @@ var GROUPS = {
4739
6260
  }
4740
6261
  ]
4741
6262
  },
6263
+ [ConfigKey.group_resourceImageNoZoom]: {
6264
+ type: GroupConfigType.resource,
6265
+ description: "Image resource where @zoomDisabled defaults to true",
6266
+ tags: [
6267
+ {
6268
+ key: ConfigKey.resource_image,
6269
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6270
+ description: "The image resource (no-zoom default)",
6271
+ chain: [
6272
+ {
6273
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6274
+ description: "Common image properties with @zoomDisabled defaulting to true"
6275
+ }
6276
+ ]
6277
+ }
6278
+ ]
6279
+ },
4742
6280
  [ConfigKey.group_resourceImagePortrait]: {
4743
6281
  type: GroupConfigType.resource,
4744
6282
  description: "Portrait image resource",
4745
6283
  tags: [
4746
6284
  {
4747
6285
  key: ConfigKey.resource_imagePortrait,
6286
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4748
6287
  description: "The portrait image resource",
4749
6288
  chain: [
4750
6289
  {
@@ -4761,6 +6300,7 @@ var GROUPS = {
4761
6300
  tags: [
4762
6301
  {
4763
6302
  key: ConfigKey.resource_imageLandscape,
6303
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4764
6304
  description: "The landscape image resource",
4765
6305
  chain: [
4766
6306
  {
@@ -4777,6 +6317,7 @@ var GROUPS = {
4777
6317
  tags: [
4778
6318
  {
4779
6319
  key: ConfigKey.resource_imageEmbed,
6320
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4780
6321
  description: "The embedded image resource",
4781
6322
  chain: [
4782
6323
  {
@@ -4950,6 +6491,7 @@ var GROUPS = {
4950
6491
  tags: [
4951
6492
  {
4952
6493
  key: ConfigKey.resource_article,
6494
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
4953
6495
  description: "The article resource",
4954
6496
  chain: [
4955
6497
  {
@@ -4966,6 +6508,7 @@ var GROUPS = {
4966
6508
  tags: [
4967
6509
  {
4968
6510
  key: ConfigKey.resource_articleEmbed,
6511
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
4969
6512
  description: "The embedded article resource",
4970
6513
  chain: [
4971
6514
  {
@@ -4982,6 +6525,7 @@ var GROUPS = {
4982
6525
  tags: [
4983
6526
  {
4984
6527
  key: ConfigKey.resource_articleLink,
6528
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
4985
6529
  description: "The link to the article resource",
4986
6530
  chain: [
4987
6531
  {
@@ -5252,7 +6796,12 @@ var ConfigHydrator = class {
5252
6796
  isDefault: v.isDefault,
5253
6797
  sideJsonKey: v.sideJsonKey,
5254
6798
  sideExportJsonKey: v.sideExportJsonKey,
5255
- hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey")
6799
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6800
+ // PLAN-085: cardinality fields pass through; defaults applied
6801
+ // downstream by the config consumer (Rust treats `0` / undefined
6802
+ // as unbounded).
6803
+ minCount: v.minCount,
6804
+ maxCount: v.maxCount
5256
6805
  };
5257
6806
  }
5258
6807
  return out;
@@ -5467,7 +7016,8 @@ var BITS = {
5467
7016
  { isExample: true, example: "$" }
5468
7017
  ],
5469
7018
  description: "The example text for the bit",
5470
- format: TagFormat.plainText
7019
+ format: TagFormat.plainText,
7020
+ nullable: true
5471
7021
  }
5472
7022
  ],
5473
7023
  rootExampleType: ExampleType.string
@@ -5806,7 +7356,8 @@ var BITS = {
5806
7356
  { isExample: true, example: "$" }
5807
7357
  ],
5808
7358
  description: "The example text for the bit",
5809
- format: TagFormat.plainText
7359
+ format: TagFormat.plainText,
7360
+ nullable: true
5810
7361
  }
5811
7362
  ],
5812
7363
  rootExampleType: ExampleType.string
@@ -5969,7 +7520,8 @@ var BITS = {
5969
7520
  { isExample: true, example: "$" }
5970
7521
  ],
5971
7522
  description: "The example text for the bit",
5972
- format: TagFormat.plainText
7523
+ format: TagFormat.plainText,
7524
+ nullable: true
5973
7525
  }
5974
7526
  ],
5975
7527
  rootExampleType: ExampleType.string
@@ -6120,7 +7672,8 @@ var BITS = {
6120
7672
  { isExample: true, example: "$" }
6121
7673
  ],
6122
7674
  description: "The example text for the bit",
6123
- format: TagFormat.plainText
7675
+ format: TagFormat.plainText,
7676
+ nullable: true
6124
7677
  }
6125
7678
  ],
6126
7679
  rootExampleType: ExampleType.string
@@ -6697,6 +8250,12 @@ var BITS = {
6697
8250
  description: "The progress of the chapter, used for tracking reading progress",
6698
8251
  format: TagFormat.boolean,
6699
8252
  defaultValue: "true"
8253
+ },
8254
+ {
8255
+ key: ConfigKey.property_isCollapsible,
8256
+ description: "If true, the chapter is collapsible",
8257
+ format: TagFormat.boolean,
8258
+ defaultValue: "false"
6700
8259
  }
6701
8260
  ]
6702
8261
  },
@@ -6715,8 +8274,31 @@ var BITS = {
6715
8274
  description: "Tags for gaps in cloze and multiple choice text bits"
6716
8275
  },
6717
8276
  {
6718
- key: ConfigKey.group_trueFalse,
8277
+ key: ConfigKey.group_trueFalseInlineSelect,
6719
8278
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8279
+ },
8280
+ {
8281
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8282
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8283
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8284
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8285
+ key: ConfigKey.property_isCaseSensitive,
8286
+ exportJsonKey: {},
8287
+ description: "If the cloze answers are case sensitive",
8288
+ format: TagFormat.boolean,
8289
+ defaultValue: "true"
8290
+ },
8291
+ {
8292
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8293
+ key: ConfigKey.property_example,
8294
+ exportJsonKey: [
8295
+ { "@keyonly": { isExample: true } },
8296
+ { "@absent": { isExample: true } },
8297
+ { isExample: true, example: "$" }
8298
+ ],
8299
+ description: "The example(s) for the bit",
8300
+ format: TagFormat.bitmarkText,
8301
+ nullable: true
6720
8302
  }
6721
8303
  ]
6722
8304
  },
@@ -6755,18 +8337,56 @@ var BITS = {
6755
8337
  {
6756
8338
  key: ConfigKey.group_gap,
6757
8339
  description: "Tags for gaps in cloze bits"
8340
+ },
8341
+ {
8342
+ // Override inherited @example: cloze is in OLD parser's
8343
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8344
+ // emits `isExample: true`.
8345
+ key: ConfigKey.property_example,
8346
+ exportJsonKey: [
8347
+ { "@keyonly": { isExample: true } },
8348
+ { "@absent": { isExample: true } },
8349
+ { isExample: true, example: "$" }
8350
+ ],
8351
+ description: "The example(s) for the bit",
8352
+ format: TagFormat.bitmarkText,
8353
+ nullable: true
6758
8354
  }
6759
8355
  ]
6760
8356
  },
6761
8357
  [BitType.clozeInstructionGrouped]: {
6762
8358
  since: "1.3.0",
6763
8359
  baseBitType: BitType.cloze,
6764
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8360
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8361
+ tags: [
8362
+ {
8363
+ key: ConfigKey.property_quizCountItems,
8364
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8365
+ format: TagFormat.boolean,
8366
+ defaultValue: "true"
8367
+ }
8368
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8369
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8370
+ ]
6765
8371
  },
6766
8372
  [BitType.clozeSolutionGrouped]: {
6767
8373
  since: "1.3.0",
6768
8374
  baseBitType: BitType.cloze,
6769
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8375
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8376
+ tags: [
8377
+ {
8378
+ key: ConfigKey.property_quizCountItems,
8379
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8380
+ format: TagFormat.boolean,
8381
+ defaultValue: "true"
8382
+ },
8383
+ {
8384
+ key: ConfigKey.property_quizStrikethroughSolutions,
8385
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8386
+ format: TagFormat.boolean,
8387
+ defaultValue: "true"
8388
+ }
8389
+ ]
6770
8390
  },
6771
8391
  [BitType.clozeSeveral]: {
6772
8392
  since: "3.5.0",
@@ -6802,6 +8422,17 @@ var BITS = {
6802
8422
  {
6803
8423
  key: ConfigKey.group_quizCommon,
6804
8424
  description: "Common tags for quiz bits"
8425
+ },
8426
+ {
8427
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8428
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8429
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8430
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8431
+ key: ConfigKey.property_isCaseSensitive,
8432
+ exportJsonKey: {},
8433
+ description: "If the cloze answers are case sensitive",
8434
+ format: TagFormat.boolean,
8435
+ defaultValue: "true"
6805
8436
  }
6806
8437
  ],
6807
8438
  cardSet: CardSetConfigKey.clozeList
@@ -6968,6 +8599,7 @@ var BITS = {
6968
8599
  tags: [
6969
8600
  {
6970
8601
  key: ConfigKey.group_person,
8602
+ exportJsonKey: { partner: { name: "$" } },
6971
8603
  description: "Tags for the person in the conversation"
6972
8604
  }
6973
8605
  ]
@@ -7398,7 +9030,12 @@ var BITS = {
7398
9030
  maxCount: Count.infinity
7399
9031
  },
7400
9032
  {
7401
- // Override inherited @example from group_standardTags: string root example.
9033
+ // Override inherited @example from group_standardTags: emits both
9034
+ // `isExample` (bool) AND `example` (paragraph array). Declared as
9035
+ // bitmarkText because BPG `JsonGenerator.toExample` renders the
9036
+ // root-example value through the bit's textFormat (not this tag's
9037
+ // declared format) — `TagFormat.plainText` here would be ignored
9038
+ // at runtime and is misleading.
7402
9039
  key: ConfigKey.property_example,
7403
9040
  exportJsonKey: [
7404
9041
  { "@keyonly": { isExample: true, example: "true" } },
@@ -7406,7 +9043,8 @@ var BITS = {
7406
9043
  { isExample: true, example: "$" }
7407
9044
  ],
7408
9045
  description: "The example text for the essay bit",
7409
- format: TagFormat.plainText
9046
+ format: TagFormat.bitmarkText,
9047
+ nullable: true
7410
9048
  }
7411
9049
  ],
7412
9050
  rootExampleType: ExampleType.string
@@ -7440,7 +9078,8 @@ var BITS = {
7440
9078
  { isExample: true, example: "$" }
7441
9079
  ],
7442
9080
  description: "The example text for the example bit",
7443
- format: TagFormat.plainText
9081
+ format: TagFormat.plainText,
9082
+ nullable: true
7444
9083
  }
7445
9084
  ],
7446
9085
  rootExampleType: ExampleType.string
@@ -7526,7 +9165,19 @@ var BITS = {
7526
9165
  [BitType.articleAi]: {
7527
9166
  since: "1.3.0",
7528
9167
  baseBitType: BitType.article,
7529
- description: "Article AI bit, used to create AI-generated articles"
9168
+ description: "Article AI bit, used to create AI-generated articles",
9169
+ tags: [
9170
+ {
9171
+ // Shadows the inherited @aiGenerated from the standard group: this
9172
+ // bit type forces aiGenerated:true regardless of source value or
9173
+ // absence. Literal `true` in the export pattern ignores `$`.
9174
+ key: ConfigKey.property_aiGenerated,
9175
+ description: "AI-generated flag, forced true for article-ai bits",
9176
+ format: TagFormat.boolean,
9177
+ defaultValue: "true",
9178
+ exportJsonKey: { aiGenerated: true }
9179
+ }
9180
+ ]
7530
9181
  },
7531
9182
  [BitType.articleAttachment]: {
7532
9183
  since: "1.3.0",
@@ -7851,7 +9502,19 @@ var BITS = {
7851
9502
  [BitType.noteAi]: {
7852
9503
  since: "1.3.0",
7853
9504
  baseBitType: BitType.note,
7854
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9505
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9506
+ tags: [
9507
+ {
9508
+ // Shadows the inherited @aiGenerated from the standard group: this
9509
+ // bit type forces aiGenerated:true regardless of source value or
9510
+ // absence. Literal `true` in the export pattern ignores `$`.
9511
+ key: ConfigKey.property_aiGenerated,
9512
+ description: "AI-generated flag, forced true for article-ai bits",
9513
+ format: TagFormat.boolean,
9514
+ defaultValue: "true",
9515
+ exportJsonKey: { aiGenerated: true }
9516
+ }
9517
+ ]
7855
9518
  },
7856
9519
  [BitType.notebookArticle]: {
7857
9520
  since: "1.3.0",
@@ -7976,7 +9639,19 @@ var BITS = {
7976
9639
  [BitType.summaryAi]: {
7977
9640
  since: "1.3.0",
7978
9641
  baseBitType: BitType.summary,
7979
- description: "AI-generated summary bit"
9642
+ description: "AI-generated summary bit",
9643
+ tags: [
9644
+ {
9645
+ // Shadows the inherited @aiGenerated from the standard group: this
9646
+ // bit type forces aiGenerated:true regardless of source value or
9647
+ // absence. Literal `true` in the export pattern ignores `$`.
9648
+ key: ConfigKey.property_aiGenerated,
9649
+ description: "AI-generated flag, forced true for article-ai bits",
9650
+ format: TagFormat.boolean,
9651
+ defaultValue: "true",
9652
+ exportJsonKey: { aiGenerated: true }
9653
+ }
9654
+ ]
7980
9655
  },
7981
9656
  [BitType.videoTranscript]: {
7982
9657
  since: "1.3.0",
@@ -8250,7 +9925,7 @@ var BITS = {
8250
9925
  description: "Product ID for the module product, used to link to a specific product",
8251
9926
  format: TagFormat.plainText,
8252
9927
  minCount: 1,
8253
- maxCount: Count.infinity
9928
+ maxCount: 1
8254
9929
  }
8255
9930
  ]
8256
9931
  },
@@ -8303,10 +9978,13 @@ var BITS = {
8303
9978
  description: "Resource bit tags for logo grave images, used to define additional properties"
8304
9979
  },
8305
9980
  {
8306
- // Image resource
8307
- key: ConfigKey.group_resourceImage,
9981
+ // Image resource — fully-expanded shape so each `&image` emission
9982
+ // produces a complete `{type, image: {src}}` object inside the
9983
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
9984
+ // `images[].image` via the `[]` last-element-merge semantic.
9985
+ key: ConfigKey.group_resourceImageNoZoom,
8308
9986
  description: "Resource image tags for logo grave images, used to attach images",
8309
- exportJsonKey: { images: "$" },
9987
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
8310
9988
  minCount: 1,
8311
9989
  maxCount: Count.infinity
8312
9990
  }
@@ -8596,6 +10274,10 @@ var BITS = {
8596
10274
  tags: [
8597
10275
  {
8598
10276
  key: ConfigKey.property_book,
10277
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10278
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10279
+ // inside the most-recently-appended entry as `reference: $`.
10280
+ exportJsonKey: { book: [{ book: "$" }] },
8599
10281
  description: "Book reference for the page, used to link to a specific book",
8600
10282
  maxCount: Count.infinity,
8601
10283
  chain: [
@@ -8663,7 +10345,9 @@ var BITS = {
8663
10345
  format: TagFormat.plainText
8664
10346
  }
8665
10347
  ],
8666
- cardSet: CardSetConfigKey.exampleBitList
10348
+ // page-footer emits cards under `sections` (vs `listItems` for the
10349
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10350
+ cardSet: CardSetConfigKey.pageFooterSections
8667
10351
  },
8668
10352
  [BitType.legend]: {
8669
10353
  since: "3.12.0",
@@ -8705,13 +10389,52 @@ var BITS = {
8705
10389
  since: "1.34.0",
8706
10390
  baseBitType: BitType._standard,
8707
10391
  description: "Definition list bit, used to create lists of definitions in articles or books",
10392
+ tags: [
10393
+ {
10394
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10395
+ key: ConfigKey.property_example,
10396
+ exportJsonKey: [
10397
+ { "@keyonly": { isExample: true } },
10398
+ { "@absent": { isExample: true } },
10399
+ { isExample: true, example: "$" }
10400
+ ],
10401
+ description: "The example(s) for the bit",
10402
+ format: TagFormat.bitmarkText,
10403
+ nullable: true
10404
+ }
10405
+ ],
8708
10406
  cardSet: CardSetConfigKey.definitionList
8709
10407
  },
10408
+ [BitType.definitionListCollapsible]: {
10409
+ since: "5.25.0",
10410
+ baseBitType: BitType.definitionList,
10411
+ description: "Collapsible definition list bit, used to create collapsible lists of definitions in articles or books"
10412
+ },
10413
+ [BitType.standardDefinitionListNormative]: {
10414
+ since: "5.24.0",
10415
+ baseBitType: BitType.definitionList,
10416
+ description: "Standard normative definition list bit, used to create normative standard definition lists in articles or books"
10417
+ },
10418
+ [BitType.standardDefinitionListNonNormative]: {
10419
+ since: "5.24.0",
10420
+ baseBitType: BitType.definitionList,
10421
+ description: "Standard non-normative definition list bit, used to create non-normative standard definition lists in articles or books"
10422
+ },
10423
+ [BitType.smartStandardDefinitionListNormative]: {
10424
+ since: "5.24.0",
10425
+ baseBitType: BitType.standardDefinitionListNormative,
10426
+ description: "Smart standard normative definition list bit, used to create normative smart standard definition lists in articles or books"
10427
+ },
10428
+ [BitType.smartStandardDefinitionListNonNormative]: {
10429
+ since: "5.24.0",
10430
+ baseBitType: BitType.standardDefinitionListNonNormative,
10431
+ description: "Smart standard non-normative definition list bit, used to create non-normative smart standard definition lists in articles or books"
10432
+ },
8710
10433
  [BitType.metaSearchDefaultTerms]: {
8711
10434
  since: "3.12.0",
8712
10435
  baseBitType: BitType._standard,
8713
10436
  description: "Meta search default terms bit, used to define default search terms in articles",
8714
- cardSet: CardSetConfigKey.definitionList
10437
+ cardSet: CardSetConfigKey.definitionListPlain
8715
10438
  },
8716
10439
  [BitType.metaSearchDefaultTopics]: {
8717
10440
  since: "3.12.0",
@@ -8727,6 +10450,18 @@ var BITS = {
8727
10450
  {
8728
10451
  key: ConfigKey.group_quizCommon,
8729
10452
  description: "Common quiz tags for flashcards"
10453
+ },
10454
+ {
10455
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10456
+ key: ConfigKey.property_example,
10457
+ exportJsonKey: [
10458
+ { "@keyonly": { isExample: true } },
10459
+ { "@absent": { isExample: true } },
10460
+ { isExample: true, example: "$" }
10461
+ ],
10462
+ description: "The example(s) for the bit",
10463
+ format: TagFormat.bitmarkText,
10464
+ nullable: true
8730
10465
  }
8731
10466
  ],
8732
10467
  cardSet: CardSetConfigKey.flashcard
@@ -8734,12 +10469,20 @@ var BITS = {
8734
10469
  [BitType.flashcard1]: {
8735
10470
  since: "1.3.0",
8736
10471
  baseBitType: BitType.flashcard,
8737
- description: "Flashcard 1 bit"
10472
+ description: "Flashcard 1 bit",
10473
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10474
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10475
+ // bit type structurally allows only a single card.
10476
+ cardSet: CardSetConfigKey.flashcard1
8738
10477
  },
8739
10478
  [BitType.qAndACard]: {
8740
10479
  since: "3.25.0",
8741
10480
  baseBitType: BitType.flashcard1,
8742
- description: "Q&A card bit, used to create question and answer cards in articles or books"
10481
+ description: "Q&A card bit, used to create question and answer cards in articles or books",
10482
+ // PLAN-085: q-and-a-card permits multiple cards (some fixtures carry
10483
+ // two). Override the inherited `flashcard1` (max=1) cardset back to
10484
+ // the unbounded `flashcard` cardset.
10485
+ cardSet: CardSetConfigKey.flashcard
8743
10486
  },
8744
10487
  [BitType.focusImage]: {
8745
10488
  since: "1.3.0",
@@ -8769,8 +10512,24 @@ var BITS = {
8769
10512
  description: "Common quiz tags for highlighted text"
8770
10513
  },
8771
10514
  {
8772
- key: ConfigKey.group_trueFalse,
8773
- description: "True/False quiz tags for highlighted text"
10515
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10516
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10517
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10518
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10519
+ key: ConfigKey.group_trueFalseInlineHighlight,
10520
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10521
+ },
10522
+ {
10523
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10524
+ key: ConfigKey.property_example,
10525
+ exportJsonKey: [
10526
+ { "@keyonly": { isExample: true } },
10527
+ { "@absent": { isExample: true } },
10528
+ { isExample: true, example: "$" }
10529
+ ],
10530
+ description: "The example(s) for the bit",
10531
+ format: TagFormat.bitmarkText,
10532
+ nullable: true
8774
10533
  }
8775
10534
  ]
8776
10535
  },
@@ -8780,15 +10539,8 @@ var BITS = {
8780
10539
  description: "Image bit, used to create images in articles or books",
8781
10540
  tags: [
8782
10541
  {
8783
- key: ConfigKey.resource_backgroundWallpaper,
8784
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8785
- description: "Background wallpaper for the image, used to set a background for the image",
8786
- chain: [
8787
- {
8788
- key: ConfigKey.group_resourceImageCommon,
8789
- description: "Common resource image tags for images"
8790
- }
8791
- ]
10542
+ key: ConfigKey.group_backgroundWallpaper,
10543
+ description: "Background wallpaper tags for images, used to define background properties for images"
8792
10544
  },
8793
10545
  {
8794
10546
  key: ConfigKey.group_resourceBitTags,
@@ -8891,8 +10643,15 @@ var BITS = {
8891
10643
  },
8892
10644
  [BitType.imageSeparator]: {
8893
10645
  since: "1.4.15",
8894
- baseBitType: BitType.image,
8895
- description: "Image separator bit, used to create separators in articles or books"
10646
+ baseBitType: BitType._standard,
10647
+ description: "Image separator bit, used to create separators in articles or books",
10648
+ tags: [
10649
+ {
10650
+ key: ConfigKey.group_imageNoZoom,
10651
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10652
+ }
10653
+ ],
10654
+ resourceAttachmentAllowed: false
8896
10655
  },
8897
10656
  [BitType.imageSeparatorAlt]: {
8898
10657
  since: "1.16.0",
@@ -8931,15 +10690,20 @@ var BITS = {
8931
10690
  },
8932
10691
  [BitType.pageBanner]: {
8933
10692
  since: "1.4.3",
8934
- baseBitType: BitType.image,
10693
+ baseBitType: BitType._standard,
8935
10694
  description: "Page banner bit, used to create banners for pages in articles or books",
8936
10695
  tags: [
8937
10696
  {
8938
10697
  key: ConfigKey.property_slug,
8939
10698
  description: "Slug for the page banner, used to identify the banner in the system",
8940
10699
  format: TagFormat.plainText
10700
+ },
10701
+ {
10702
+ key: ConfigKey.group_imageNoZoom,
10703
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
8941
10704
  }
8942
- ]
10705
+ ],
10706
+ resourceAttachmentAllowed: false
8943
10707
  },
8944
10708
  [BitType.pageHero]: {
8945
10709
  since: "1.11.0",
@@ -8954,7 +10718,7 @@ var BITS = {
8954
10718
  },
8955
10719
  [BitType.tableImage]: {
8956
10720
  since: "1.5.15",
8957
- baseBitType: BitType.table,
10721
+ baseBitType: BitType.tableExtended,
8958
10722
  description: "Table image bit, used to create images in tables in articles or books",
8959
10723
  tags: [
8960
10724
  {
@@ -8963,15 +10727,8 @@ var BITS = {
8963
10727
  format: TagFormat.bitmarkText
8964
10728
  },
8965
10729
  {
8966
- key: ConfigKey.resource_backgroundWallpaper,
8967
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
8968
- description: "Background wallpaper for the image, used to set a background for the image",
8969
- chain: [
8970
- {
8971
- key: ConfigKey.group_resourceImageCommon,
8972
- description: "Common resource image tags for images"
8973
- }
8974
- ]
10730
+ key: ConfigKey.group_backgroundWallpaper,
10731
+ description: "Background wallpaper tags for images, used to define background properties for images"
8975
10732
  },
8976
10733
  {
8977
10734
  key: ConfigKey.group_resourceBitTags,
@@ -9114,10 +10871,10 @@ var BITS = {
9114
10871
  description: "Resource bit tags for logo grave images, used to define additional properties"
9115
10872
  },
9116
10873
  {
9117
- // Image resource
9118
- key: ConfigKey.group_resourceImage,
10874
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10875
+ key: ConfigKey.group_resourceImageNoZoom,
9119
10876
  description: "Resource image tags for logo grave images, used to attach images",
9120
- exportJsonKey: { logos: "$" },
10877
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
9121
10878
  minCount: 1,
9122
10879
  maxCount: Count.infinity
9123
10880
  }
@@ -9127,7 +10884,17 @@ var BITS = {
9127
10884
  [BitType.prototypeImages]: {
9128
10885
  since: "1.6.1",
9129
10886
  baseBitType: BitType.imagesLogoGrave,
9130
- description: "Prototype images bit, used to create prototype images in articles or books"
10887
+ description: "Prototype images bit, used to create prototype images in articles or books",
10888
+ tags: [
10889
+ {
10890
+ // Override inherited `logos` wrapper with `images` for this bit type.
10891
+ key: ConfigKey.group_resourceImageNoZoom,
10892
+ description: "Resource image tags for prototype images",
10893
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
10894
+ minCount: 1,
10895
+ maxCount: Count.infinity
10896
+ }
10897
+ ]
9131
10898
  },
9132
10899
  [BitType.internalLink]: {
9133
10900
  since: "1.3.0",
@@ -9157,6 +10924,18 @@ var BITS = {
9157
10924
  exportJsonKey: {},
9158
10925
  description: "Reasonable number of characters for the interview, used to limit input size",
9159
10926
  format: TagFormat.number
10927
+ },
10928
+ {
10929
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10930
+ key: ConfigKey.property_example,
10931
+ exportJsonKey: [
10932
+ { "@keyonly": { isExample: true } },
10933
+ { "@absent": { isExample: true } },
10934
+ { isExample: true, example: "$" }
10935
+ ],
10936
+ description: "The example(s) for the bit",
10937
+ format: TagFormat.bitmarkText,
10938
+ nullable: true
9160
10939
  }
9161
10940
  ],
9162
10941
  cardSet: CardSetConfigKey.questions
@@ -9515,6 +11294,18 @@ var BITS = {
9515
11294
  {
9516
11295
  key: ConfigKey.group_mark,
9517
11296
  description: "Mark tags, used to define the content of marks"
11297
+ },
11298
+ {
11299
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11300
+ key: ConfigKey.property_example,
11301
+ exportJsonKey: [
11302
+ { "@keyonly": { isExample: true } },
11303
+ { "@absent": { isExample: true } },
11304
+ { isExample: true, example: "$" }
11305
+ ],
11306
+ description: "The example(s) for the bit",
11307
+ format: TagFormat.bitmarkText,
11308
+ nullable: true
9518
11309
  }
9519
11310
  ]
9520
11311
  },
@@ -9534,6 +11325,23 @@ var BITS = {
9534
11325
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9535
11326
  format: TagFormat.boolean,
9536
11327
  defaultValue: "true"
11328
+ },
11329
+ {
11330
+ // Override inherited @example: in `isTopLevelExample` allow-list
11331
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11332
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11333
+ // Bit-level fires emit only `isExample`; the @example value (when
11334
+ // provided) is recorded in the ancestor stack and consumed by
11335
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11336
+ key: ConfigKey.property_example,
11337
+ exportJsonKey: [
11338
+ { "@keyonly": { isExample: true } },
11339
+ { "@absent": { isExample: true } },
11340
+ { isExample: true }
11341
+ ],
11342
+ description: "The example(s) for the bit",
11343
+ format: TagFormat.bitmarkText,
11344
+ nullable: true
9537
11345
  }
9538
11346
  ],
9539
11347
  cardSet: CardSetConfigKey.matchPairs
@@ -9618,6 +11426,18 @@ var BITS = {
9618
11426
  {
9619
11427
  key: ConfigKey.group_trueFalse,
9620
11428
  description: "True/False tags for multiple choice 1 questions"
11429
+ },
11430
+ {
11431
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11432
+ key: ConfigKey.property_example,
11433
+ exportJsonKey: [
11434
+ { "@keyonly": { isExample: true } },
11435
+ { "@absent": { isExample: true } },
11436
+ { isExample: true, example: "$" }
11437
+ ],
11438
+ description: "The example(s) for the bit",
11439
+ format: TagFormat.bitmarkText,
11440
+ nullable: true
9621
11441
  }
9622
11442
  ]
9623
11443
  },
@@ -9639,8 +11459,20 @@ var BITS = {
9639
11459
  {
9640
11460
  key: ConfigKey.group_trueFalse,
9641
11461
  description: "True/False tags for multiple choice questions"
9642
- }
11462
+ },
9643
11463
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11464
+ {
11465
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11466
+ key: ConfigKey.property_example,
11467
+ exportJsonKey: [
11468
+ { "@keyonly": { isExample: true } },
11469
+ { "@absent": { isExample: true } },
11470
+ { isExample: true, example: "$" }
11471
+ ],
11472
+ description: "The example(s) for the bit",
11473
+ format: TagFormat.bitmarkText,
11474
+ nullable: true
11475
+ }
9644
11476
  ],
9645
11477
  cardSet: CardSetConfigKey.quiz
9646
11478
  },
@@ -9660,8 +11492,20 @@ var BITS = {
9660
11492
  description: "Common quiz tags for multiple choice text questions"
9661
11493
  },
9662
11494
  {
9663
- key: ConfigKey.group_trueFalse,
11495
+ key: ConfigKey.group_trueFalseInlineSelect,
9664
11496
  description: "True/False tags for multiple choice text questions"
11497
+ },
11498
+ {
11499
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11500
+ key: ConfigKey.property_example,
11501
+ exportJsonKey: [
11502
+ { "@keyonly": { isExample: true } },
11503
+ { "@absent": { isExample: true } },
11504
+ { isExample: true, example: "$" }
11505
+ ],
11506
+ description: "The example(s) for the bit",
11507
+ format: TagFormat.bitmarkText,
11508
+ nullable: true
9665
11509
  }
9666
11510
  ]
9667
11511
  },
@@ -9686,8 +11530,20 @@ var BITS = {
9686
11530
  description: "Common quiz tags for multiple response 1 questions"
9687
11531
  },
9688
11532
  {
9689
- key: ConfigKey.group_trueFalse,
11533
+ key: ConfigKey.group_trueFalseResponses,
9690
11534
  description: "True/False tags for multiple response 1 questions"
11535
+ },
11536
+ {
11537
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11538
+ key: ConfigKey.property_example,
11539
+ exportJsonKey: [
11540
+ { "@keyonly": { isExample: true } },
11541
+ { "@absent": { isExample: true } },
11542
+ { isExample: true, example: "$" }
11543
+ ],
11544
+ description: "The example(s) for the bit",
11545
+ format: TagFormat.bitmarkText,
11546
+ nullable: true
9691
11547
  }
9692
11548
  ]
9693
11549
  },
@@ -9707,12 +11563,24 @@ var BITS = {
9707
11563
  description: "Common quiz tags for multiple response questions"
9708
11564
  },
9709
11565
  {
9710
- key: ConfigKey.group_trueFalse,
11566
+ key: ConfigKey.group_trueFalseResponses,
9711
11567
  description: "True/False tags for multiple response questions"
9712
- }
11568
+ },
9713
11569
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11570
+ {
11571
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11572
+ key: ConfigKey.property_example,
11573
+ exportJsonKey: [
11574
+ { "@keyonly": { isExample: true } },
11575
+ { "@absent": { isExample: true } },
11576
+ { isExample: true, example: "$" }
11577
+ ],
11578
+ description: "The example(s) for the bit",
11579
+ format: TagFormat.bitmarkText,
11580
+ nullable: true
11581
+ }
9714
11582
  ],
9715
- cardSet: CardSetConfigKey.quiz
11583
+ cardSet: CardSetConfigKey.quizResponses
9716
11584
  },
9717
11585
  [BitType.coachSelfReflectionMultipleResponse]: {
9718
11586
  since: "1.3.0",
@@ -9962,6 +11830,8 @@ var BITS = {
9962
11830
  },
9963
11831
  {
9964
11832
  key: ConfigKey.resource_platformIcon,
11833
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11834
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
9965
11835
  description: "The platform icon",
9966
11836
  chain: [
9967
11837
  {
@@ -9997,6 +11867,8 @@ var BITS = {
9997
11867
  tags: [
9998
11868
  {
9999
11869
  key: ConfigKey.resource_platformLogo,
11870
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11871
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
10000
11872
  description: "The platform logo",
10001
11873
  chain: [
10002
11874
  {
@@ -10208,7 +12080,8 @@ var BITS = {
10208
12080
  { isExample: true, example: "$" }
10209
12081
  ],
10210
12082
  description: "The example text for the review note bit",
10211
- format: TagFormat.plainText
12083
+ format: TagFormat.plainText,
12084
+ nullable: true
10212
12085
  }
10213
12086
  ],
10214
12087
  rootExampleType: ExampleType.string
@@ -10295,7 +12168,8 @@ var BITS = {
10295
12168
  { isExample: true, example: "$" }
10296
12169
  ],
10297
12170
  description: "The example flag for the bit (boolean)",
10298
- format: TagFormat.plainText
12171
+ format: TagFormat.boolean,
12172
+ nullable: true
10299
12173
  }
10300
12174
  ],
10301
12175
  cardSet: CardSetConfigKey.elements,
@@ -10569,15 +12443,8 @@ var BITS = {
10569
12443
  format: TagFormat.bitmarkText
10570
12444
  },
10571
12445
  {
10572
- key: ConfigKey.resource_backgroundWallpaper,
10573
- exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
10574
- description: "Background wallpaper for the image, used to set a background for the image",
10575
- chain: [
10576
- {
10577
- key: ConfigKey.group_resourceImageCommon,
10578
- description: "Common resource image tags for images"
10579
- }
10580
- ]
12446
+ key: ConfigKey.group_backgroundWallpaper,
12447
+ description: "Background wallpaper tags for images, used to define background properties for images"
10581
12448
  },
10582
12449
  {
10583
12450
  key: ConfigKey.group_resourceBitTags,
@@ -10954,16 +12821,20 @@ var BITS = {
10954
12821
  format: TagFormat.plainText
10955
12822
  },
10956
12823
  {
10957
- exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
12824
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12825
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12826
+ // not a `choices[]` array. Format is bitmarkText so the statement
12827
+ // body renders as a ProseMirror tree, matching reference fixtures.
12828
+ exportJsonKey: { statement: "$", isCorrect: true },
10958
12829
  key: ConfigKey.tag_true,
10959
12830
  description: "Tag for the true option in the true/false question",
10960
- format: TagFormat.plainText
12831
+ format: TagFormat.bitmarkText
10961
12832
  },
10962
12833
  {
10963
- exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
12834
+ exportJsonKey: { statement: "$", isCorrect: false },
10964
12835
  key: ConfigKey.tag_false,
10965
12836
  description: "Tag for the false option in the true/false question",
10966
- format: TagFormat.plainText
12837
+ format: TagFormat.bitmarkText
10967
12838
  },
10968
12839
  {
10969
12840
  // Override inherited @example from group_standardTags: boolean root
@@ -10974,14 +12845,24 @@ var BITS = {
10974
12845
  // sufficient for `[+...]` (correct) statements which dominate the
10975
12846
  // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
10976
12847
  // need either a runtime extension or an explicit `[@example:false]`.
12848
+ //
12849
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12850
+ // (a natural default that gets stripped in optimised mode, matching
12851
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12852
+ // STRING `"false"` which doesn't strip.
10977
12853
  key: ConfigKey.property_example,
10978
12854
  exportJsonKey: [
10979
12855
  { "@keyonly": { isExample: true, example: true } },
10980
12856
  { "@absent": { isExample: true, example: true } },
12857
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12858
+ // is the natural default for boolean, stripped in optimised mode.
12859
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12860
+ { "@value=false": { isExample: true } },
10981
12861
  { isExample: true, example: "$" }
10982
12862
  ],
10983
12863
  description: "The example flag for the bit (boolean)",
10984
- format: TagFormat.plainText
12864
+ format: TagFormat.boolean,
12865
+ nullable: true
10985
12866
  }
10986
12867
  ],
10987
12868
  rootExampleType: ExampleType.boolean
@@ -11005,6 +12886,18 @@ var BITS = {
11005
12886
  key: ConfigKey.property_labelFalse,
11006
12887
  description: "Label for the false option in the true/false question",
11007
12888
  format: TagFormat.plainText
12889
+ },
12890
+ {
12891
+ // Override inherited @example: in `isTopLevelExample` allow-list.
12892
+ key: ConfigKey.property_example,
12893
+ exportJsonKey: [
12894
+ { "@keyonly": { isExample: true } },
12895
+ { "@absent": { isExample: true } },
12896
+ { isExample: true, example: "$" }
12897
+ ],
12898
+ description: "The example(s) for the bit",
12899
+ format: TagFormat.bitmarkText,
12900
+ nullable: true
11008
12901
  }
11009
12902
  ],
11010
12903
  cardSet: CardSetConfigKey.statements
@@ -11194,6 +13087,11 @@ var BITS = {
11194
13087
  baseBitType: BitType.vendorStripePricingTable,
11195
13088
  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",
11196
13089
  tags: [
13090
+ {
13091
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
13092
+ description: "Descriptive name for the Stripe pricing table",
13093
+ format: TagFormat.plainText
13094
+ },
11197
13095
  {
11198
13096
  key: ConfigKey.property_printOnRequestCustomerId,
11199
13097
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -11670,7 +13568,7 @@ var instance2 = new Config();
11670
13568
  // src/generated/package_info.ts
11671
13569
  var PACKAGE_INFO = {
11672
13570
  "name": "@gmb/bitmark-parser-generator",
11673
- "version": "5.23.0",
13571
+ "version": "5.25.0",
11674
13572
  "license": "ISC"};
11675
13573
  var Environment = {
11676
13574
  unknown: "",
@@ -12284,6 +14182,8 @@ var NodeType = {
12284
14182
  internalPrintPdf: "internalPrintPdf",
12285
14183
  internalPrintPdfValue: "internalPrintPdfValue",
12286
14184
  isCaseSensitive: "isCaseSensitive",
14185
+ isCollapsible: "isCollapsible",
14186
+ isCollapsibleValue: "isCollapsibleValue",
12287
14187
  isCommented: "isCommented",
12288
14188
  isCorrect: "isCorrect",
12289
14189
  isEditable: "isEditable",
@@ -12603,6 +14503,8 @@ var NodeType = {
12603
14503
  stillImageFilmLink: "stillImageFilmLink",
12604
14504
  stripePricingTableId: "stripePricingTableId",
12605
14505
  stripePricingTableIdValue: "stripePricingTableIdValue",
14506
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14507
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12606
14508
  stripePublishableKey: "stripePublishableKey",
12607
14509
  stripePublishableKeyValue: "stripePublishableKeyValue",
12608
14510
  subject: "subject",
@@ -25844,6 +27746,12 @@ var Builder = class extends BaseBuilder {
25844
27746
  data.stripePricingTableId,
25845
27747
  options
25846
27748
  ),
27749
+ stripePricingTableDescriptiveName: this.toAstProperty(
27750
+ bitType,
27751
+ ConfigKey.property_stripePricingTableDescriptiveName,
27752
+ data.stripePricingTableDescriptiveName,
27753
+ options
27754
+ ),
25847
27755
  stripePublishableKey: this.toAstProperty(
25848
27756
  bitType,
25849
27757
  ConfigKey.property_stripePublishableKey,
@@ -26330,6 +28238,12 @@ var Builder = class extends BaseBuilder {
26330
28238
  level: instance6.asNumber(data.level),
26331
28239
  toc: this.toAstProperty(bitType, ConfigKey.property_toc, data.toc, options),
26332
28240
  progress: this.toAstProperty(bitType, ConfigKey.property_progress, data.progress, options),
28241
+ isCollapsible: this.toAstProperty(
28242
+ bitType,
28243
+ ConfigKey.property_isCollapsible,
28244
+ data.isCollapsible,
28245
+ options
28246
+ ),
26333
28247
  anchor: data.anchor,
26334
28248
  reference: data.reference,
26335
28249
  referenceEnd: data.referenceEnd,
@@ -31851,6 +33765,7 @@ var JsonGenerator = class extends AstWalkerGenerator {
31851
33765
  if (instance2.isOfBitType(bitType, BitType.chapter)) {
31852
33766
  if (bitJson.toc == null) bitJson.toc = true;
31853
33767
  if (bitJson.progress == null) bitJson.progress = true;
33768
+ if (bitJson.isCollapsible == null) bitJson.isCollapsible = false;
31854
33769
  if (bitJson.level == null) bitJson.level = 1;
31855
33770
  if (bitJson.body == null) bitJson.body = this.bodyDefault;
31856
33771
  }
@@ -33996,7 +35911,9 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
33996
35911
  const cardSetType = bitConfig.cardSet?.configKey;
33997
35912
  switch (cardSetType) {
33998
35913
  case CardSetConfigKey.flashcard:
35914
+ case CardSetConfigKey.flashcard1:
33999
35915
  case CardSetConfigKey.definitionList:
35916
+ case CardSetConfigKey.definitionListPlain:
34000
35917
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
34001
35918
  break;
34002
35919
  case CardSetConfigKey.elements:
@@ -34009,6 +35926,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34009
35926
  result = parseFeedback(context, bitType, processedCardSet);
34010
35927
  break;
34011
35928
  case CardSetConfigKey.quiz:
35929
+ case CardSetConfigKey.quizResponses:
34012
35930
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
34013
35931
  break;
34014
35932
  case CardSetConfigKey.questions:
@@ -34035,6 +35953,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
34035
35953
  break;
34036
35954
  case CardSetConfigKey.clozeList:
34037
35955
  case CardSetConfigKey.exampleBitList:
35956
+ case CardSetConfigKey.pageFooterSections:
34038
35957
  case CardSetConfigKey.bookReferenceList:
34039
35958
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
34040
35959
  break;
@@ -40068,8 +41987,8 @@ var FileWriter = class extends StreamWriter {
40068
41987
  async open() {
40069
41988
  return new Promise((resolve, reject) => {
40070
41989
  const flags = this._append ? "a" : "w";
40071
- fs3.ensureDirSync(path3.dirname(this._path.toString()));
40072
- const stream = fs3.createWriteStream(this._path, {
41990
+ fs4.ensureDirSync(path3.dirname(this._path.toString()));
41991
+ const stream = fs4.createWriteStream(this._path, {
40073
41992
  flags,
40074
41993
  encoding: "utf8"
40075
41994
  });
@@ -40143,8 +42062,8 @@ var SyncFileWriter = class {
40143
42062
  }
40144
42063
  openSync() {
40145
42064
  const flag = this._append ? "a" : "w";
40146
- fs3.ensureDirSync(path3.dirname(this._path.toString()));
40147
- fs3.writeFileSync(this._path, "", {
42065
+ fs4.ensureDirSync(path3.dirname(this._path.toString()));
42066
+ fs4.writeFileSync(this._path, "", {
40148
42067
  flag,
40149
42068
  encoding: this._encoding
40150
42069
  });
@@ -40161,7 +42080,7 @@ var SyncFileWriter = class {
40161
42080
  } else {
40162
42081
  line = this.endOfLineString;
40163
42082
  }
40164
- fs3.appendFileSync(this._path, line, {
42083
+ fs4.appendFileSync(this._path, line, {
40165
42084
  encoding: this._encoding
40166
42085
  });
40167
42086
  this.lastWrite = line;
@@ -40179,7 +42098,7 @@ var SyncFileWriter = class {
40179
42098
  }
40180
42099
  str += this.endOfLineString;
40181
42100
  }
40182
- fs3.appendFileSync(this._path, str, {
42101
+ fs4.appendFileSync(this._path, str, {
40183
42102
  encoding: this._encoding
40184
42103
  });
40185
42104
  this.lastWrite = str;
@@ -40189,7 +42108,7 @@ var SyncFileWriter = class {
40189
42108
  if (!this._open) return this;
40190
42109
  if (value == null) return this;
40191
42110
  if (value) this.lastWrite = value;
40192
- fs3.appendFileSync(this._path, value, {
42111
+ fs4.appendFileSync(this._path, value, {
40193
42112
  encoding: this._encoding
40194
42113
  });
40195
42114
  return this;
@@ -40238,6 +42157,233 @@ var BitmarkFileGenerator = class {
40238
42157
  this.generator.generateSync(_ast);
40239
42158
  }
40240
42159
  };
42160
+
42161
+ // src/generator/html/HtmlTableGenerator.ts
42162
+ var MARK_TO_TAG = {
42163
+ bold: "b",
42164
+ italic: "i",
42165
+ superscript: "sup",
42166
+ subscript: "sub",
42167
+ strike: "s",
42168
+ del: "del",
42169
+ ins: "ins",
42170
+ underline: "u",
42171
+ highlight: "mark",
42172
+ code: "code"
42173
+ };
42174
+ var HtmlTableGenerator = class {
42175
+ indentUnit = " ";
42176
+ /**
42177
+ * Generate HTML table fragments from a string or JSON value.
42178
+ * @returns the concatenated `<table>` fragments, blank-line separated (empty string if none).
42179
+ */
42180
+ generate(input) {
42181
+ let data = input;
42182
+ if (typeof data === "string") {
42183
+ try {
42184
+ data = JSON.parse(data);
42185
+ } catch {
42186
+ return "";
42187
+ }
42188
+ }
42189
+ const bits = this.collectBits(data);
42190
+ const fragments = [];
42191
+ for (const bit of bits) {
42192
+ const html = this.renderTableBit(bit);
42193
+ if (html) fragments.push(html);
42194
+ }
42195
+ return fragments.join("\n\n");
42196
+ }
42197
+ // --- bit collection -------------------------------------------------------
42198
+ collectBits(data) {
42199
+ const out = [];
42200
+ const visit = (value) => {
42201
+ if (value == null || typeof value !== "object") return;
42202
+ if (Array.isArray(value)) {
42203
+ value.forEach(visit);
42204
+ return;
42205
+ }
42206
+ const obj = value;
42207
+ if ("bit" in obj && obj.bit && typeof obj.bit === "object") {
42208
+ out.push(obj.bit);
42209
+ } else if ("type" in obj) {
42210
+ out.push(obj);
42211
+ }
42212
+ };
42213
+ visit(data);
42214
+ return out;
42215
+ }
42216
+ // --- table rendering ------------------------------------------------------
42217
+ renderTableBit(bit) {
42218
+ const table = bit.table;
42219
+ if (!table) return "";
42220
+ if (bit.type !== "table" && bit.type !== "table-extended") return "";
42221
+ const extended = this.toExtended(table);
42222
+ const lines = [];
42223
+ lines.push("<table>");
42224
+ if (bit.caption != null) {
42225
+ const caption = this.cellContentToHtml(this.asTextAst(bit.caption));
42226
+ if (caption) lines.push(`${this.indentUnit}<caption>${caption}</caption>`);
42227
+ }
42228
+ this.renderSection(lines, "thead", extended.header, "header");
42229
+ this.renderSection(lines, "tbody", extended.body, "body");
42230
+ this.renderSection(lines, "tfoot", extended.footer, "footer");
42231
+ lines.push("</table>");
42232
+ return lines.join("\n");
42233
+ }
42234
+ toExtended(table) {
42235
+ const t = table;
42236
+ if (t.header || t.body || t.footer) return table;
42237
+ return convertBasicToExtendedTableFormat(table);
42238
+ }
42239
+ renderSection(lines, tag, section, key) {
42240
+ if (!section || !section.rows || section.rows.length === 0) return;
42241
+ lines.push(`${this.indentUnit}<${tag}>`);
42242
+ for (const row of section.rows) {
42243
+ lines.push(`${this.indentUnit.repeat(2)}<tr>`);
42244
+ for (const cell of row.cells ?? []) {
42245
+ lines.push(`${this.indentUnit.repeat(3)}${this.renderCell(cell, key)}`);
42246
+ }
42247
+ lines.push(`${this.indentUnit.repeat(2)}</tr>`);
42248
+ }
42249
+ lines.push(`${this.indentUnit}</${tag}>`);
42250
+ }
42251
+ renderCell(cell, _key) {
42252
+ const tag = cell.title === true ? "th" : "td";
42253
+ const attrs = [];
42254
+ if (cell.rowspan && cell.rowspan > 1) attrs.push(`rowspan="${cell.rowspan}"`);
42255
+ if (cell.colspan && cell.colspan > 1) attrs.push(`colspan="${cell.colspan}"`);
42256
+ if (cell.scope) attrs.push(`scope="${cell.scope}"`);
42257
+ if (cell.colwidth && cell.colwidth > 0) attrs.push(`width="${cell.colwidth}"`);
42258
+ const open = attrs.length > 0 ? `<${tag} ${attrs.join(" ")}>` : `<${tag}>`;
42259
+ return `${open}${this.cellContentToHtml(this.asTextAst(cell.content))}</${tag}>`;
42260
+ }
42261
+ // --- cell content (TextAst -> HTML) ---------------------------------------
42262
+ asTextAst(content) {
42263
+ if (content == null) return [];
42264
+ if (typeof content === "string") return [{ type: "text", text: content }];
42265
+ return content;
42266
+ }
42267
+ cellContentToHtml(blocks) {
42268
+ if (blocks.length === 0) return "";
42269
+ if (blocks.length === 1 && blocks[0].type === "paragraph") {
42270
+ return this.inlineToHtml(blocks[0].content ?? []);
42271
+ }
42272
+ return blocks.map((b) => this.blockToHtml(b)).join("");
42273
+ }
42274
+ blockToHtml(node) {
42275
+ switch (node.type) {
42276
+ case "paragraph":
42277
+ return `<p>${this.inlineToHtml(node.content ?? [])}</p>`;
42278
+ case "bulletList":
42279
+ case "noBulletList":
42280
+ case "orderedList":
42281
+ case "orderedListRoman":
42282
+ case "orderedListRomanLower":
42283
+ case "letteredList":
42284
+ case "letteredListLower":
42285
+ case "taskList":
42286
+ return this.listToHtml(node);
42287
+ case "image":
42288
+ case "imageInline":
42289
+ return this.imageToHtml(node);
42290
+ default:
42291
+ return this.inlineToHtml([node]);
42292
+ }
42293
+ }
42294
+ inlineToHtml(nodes) {
42295
+ return nodes.map((n) => this.inlineNodeToHtml(n)).join("");
42296
+ }
42297
+ inlineNodeToHtml(node) {
42298
+ if (node.type === "hardBreak") return "<br>";
42299
+ if (node.type === "image" || node.type === "imageInline") return this.imageToHtml(node);
42300
+ if (node.type === "text") {
42301
+ return this.wrapMarks(this.escapeText(node.text ?? ""), node.marks);
42302
+ }
42303
+ if (node.content) return this.inlineToHtml(node.content);
42304
+ return this.escapeText(node.text ?? "");
42305
+ }
42306
+ wrapMarks(text, marks) {
42307
+ if (!marks || marks.length === 0) return text;
42308
+ let open = "";
42309
+ let close = "";
42310
+ for (const mark of marks) {
42311
+ if (mark.type === "link") {
42312
+ const href = mark.attrs?.href ?? "";
42313
+ open += `<a href="${this.escapeAttr(href)}">`;
42314
+ close = `</a>${close}`;
42315
+ } else {
42316
+ const tag = MARK_TO_TAG[mark.type];
42317
+ if (tag) {
42318
+ open += `<${tag}>`;
42319
+ close = `</${tag}>${close}`;
42320
+ }
42321
+ }
42322
+ }
42323
+ return `${open}${text}${close}`;
42324
+ }
42325
+ // --- lists ----------------------------------------------------------------
42326
+ listToHtml(node) {
42327
+ const { tag, attrs } = this.listTag(node);
42328
+ const items = (node.content ?? []).map((item) => this.listItemToHtml(item, node.type === "taskList")).join("");
42329
+ return `<${tag}${attrs}>${items}</${tag}>`;
42330
+ }
42331
+ listTag(node) {
42332
+ const start = node.attrs?.start;
42333
+ const startAttr = typeof start === "number" && start !== 1 ? ` start="${start}"` : "";
42334
+ switch (node.type) {
42335
+ case "orderedList":
42336
+ return { tag: "ol", attrs: startAttr };
42337
+ case "orderedListRoman":
42338
+ return { tag: "ol", attrs: ` type="I"${startAttr}` };
42339
+ case "orderedListRomanLower":
42340
+ return { tag: "ol", attrs: ` type="i"${startAttr}` };
42341
+ case "letteredList":
42342
+ return { tag: "ol", attrs: ` type="A"${startAttr}` };
42343
+ case "letteredListLower":
42344
+ return { tag: "ol", attrs: ` type="a"${startAttr}` };
42345
+ case "noBulletList":
42346
+ return { tag: "ul", attrs: ' style="list-style:none"' };
42347
+ case "taskList":
42348
+ case "bulletList":
42349
+ default:
42350
+ return { tag: "ul", attrs: "" };
42351
+ }
42352
+ }
42353
+ listItemToHtml(item, task) {
42354
+ const inner = (item.content ?? []).map((b) => {
42355
+ if (b.type === "paragraph") return this.inlineToHtml(b.content ?? []);
42356
+ return this.blockToHtml(b);
42357
+ }).join("");
42358
+ if (task) {
42359
+ const checked = item.attrs?.checked ? " checked" : "";
42360
+ return `<li><input type="checkbox"${checked}>${inner}</li>`;
42361
+ }
42362
+ return `<li>${inner}</li>`;
42363
+ }
42364
+ // --- images ---------------------------------------------------------------
42365
+ imageToHtml(node) {
42366
+ const attrs = node.attrs ?? {};
42367
+ const parts = [];
42368
+ const src = attrs.src;
42369
+ parts.push(`src="${this.escapeAttr(typeof src === "string" ? src : "")}"`);
42370
+ if (typeof attrs.alt === "string") parts.push(`alt="${this.escapeAttr(attrs.alt)}"`);
42371
+ if (typeof attrs.title === "string") parts.push(`title="${this.escapeAttr(attrs.title)}"`);
42372
+ if (attrs.width != null) parts.push(`width="${attrs.width}"`);
42373
+ if (attrs.height != null) parts.push(`height="${attrs.height}"`);
42374
+ if (typeof attrs.class === "string" && attrs.class) {
42375
+ parts.push(`class="${this.escapeAttr(attrs.class)}"`);
42376
+ }
42377
+ return `<img ${parts.join(" ")}>`;
42378
+ }
42379
+ // --- escaping -------------------------------------------------------------
42380
+ escapeText(text) {
42381
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
42382
+ }
42383
+ escapeAttr(text) {
42384
+ return this.escapeText(text).replace(/"/g, "&quot;");
42385
+ }
42386
+ };
40241
42387
  var JsonFileGenerator = class {
40242
42388
  generator;
40243
42389
  async;
@@ -40574,20 +42720,20 @@ var ConfigBuilder = class {
40574
42720
  const outputFolderBits = path3.join(outputFolder, "bits");
40575
42721
  const outputFolderGroups = path3.join(outputFolder, "partials");
40576
42722
  const outputFolderCards = path3.join(outputFolder, "cards");
40577
- fs3.ensureDirSync(outputFolderBits);
40578
- fs3.ensureDirSync(outputFolderGroups);
40579
- fs3.ensureDirSync(outputFolderCards);
40580
- const bitsFiles = fs3.readdirSync(outputFolderBits).filter((f) => f.endsWith(".jsonc"));
42723
+ fs4.ensureDirSync(outputFolderBits);
42724
+ fs4.ensureDirSync(outputFolderGroups);
42725
+ fs4.ensureDirSync(outputFolderCards);
42726
+ const bitsFiles = fs4.readdirSync(outputFolderBits).filter((f) => f.endsWith(".jsonc"));
40581
42727
  for (const file of bitsFiles) {
40582
- fs3.removeSync(path3.join(outputFolderBits, file));
42728
+ fs4.removeSync(path3.join(outputFolderBits, file));
40583
42729
  }
40584
- const partialsFiles = fs3.readdirSync(outputFolderGroups).filter((f) => f.endsWith(".jsonc"));
42730
+ const partialsFiles = fs4.readdirSync(outputFolderGroups).filter((f) => f.endsWith(".jsonc"));
40585
42731
  for (const file of partialsFiles) {
40586
- fs3.removeSync(path3.join(outputFolderGroups, file));
42732
+ fs4.removeSync(path3.join(outputFolderGroups, file));
40587
42733
  }
40588
- const cardsFiles = fs3.readdirSync(outputFolderCards).filter((f) => f.endsWith(".jsonc"));
42734
+ const cardsFiles = fs4.readdirSync(outputFolderCards).filter((f) => f.endsWith(".jsonc"));
40589
42735
  for (const file of cardsFiles) {
40590
- fs3.removeSync(path3.join(outputFolderCards, file));
42736
+ fs4.removeSync(path3.join(outputFolderCards, file));
40591
42737
  }
40592
42738
  const fileWrites = [];
40593
42739
  const tagEntriesTypeOrder = [
@@ -40729,6 +42875,13 @@ var ConfigBuilder = class {
40729
42875
  const cardSetConfig = CARDSETS[cardSetKey];
40730
42876
  if (!cardSetConfig) return void 0;
40731
42877
  const normalizedKey = normalizeCardKey(cardSetKey);
42878
+ const variantTagEntriesTypeOrder = [
42879
+ BitTagConfigKeyType.group,
42880
+ BitTagConfigKeyType.tag,
42881
+ BitTagConfigKeyType.property,
42882
+ BitTagConfigKeyType.resource,
42883
+ BitTagConfigKeyType.unknown
42884
+ ];
40732
42885
  const sides = cardSetConfig.sides.map((side, sideIdx) => {
40733
42886
  const sidePath = `cardSets.${normalizedKey} / sides.${side.name ?? `[${sideIdx}]`}`;
40734
42887
  const variants = side.variants.map((variant, variantIdx) => {
@@ -40739,7 +42892,7 @@ var ConfigBuilder = class {
40739
42892
  const tagB = b[1];
40740
42893
  const typeA = typeFromConfigKey(tagA.key);
40741
42894
  const typeB = typeFromConfigKey(tagB.key);
40742
- const typeOrder = tagEntriesTypeOrder.indexOf(typeA) - tagEntriesTypeOrder.indexOf(typeB);
42895
+ const typeOrder = variantTagEntriesTypeOrder.indexOf(typeA) - variantTagEntriesTypeOrder.indexOf(typeB);
40743
42896
  return typeOrder;
40744
42897
  });
40745
42898
  for (const [, variantTag] of variantTagEntries) {
@@ -40779,6 +42932,10 @@ var ConfigBuilder = class {
40779
42932
  name: sectionName,
40780
42933
  ...section.isDefault ? { isDefault: true } : {},
40781
42934
  ...cardJsonKeyField(section, "jsonKey", "exportJsonKey", sectionPath),
42935
+ // PLAN-085: per-section cardinality. Emit only when non-default
42936
+ // (treat `0` / undefined as "unbounded — omit").
42937
+ ...section.minCount != null && section.minCount !== 0 ? { min: section.minCount } : {},
42938
+ ...section.maxCount != null && section.maxCount !== 0 ? { max: section.maxCount } : {},
40782
42939
  sides: cardSides
40783
42940
  };
40784
42941
  });
@@ -40848,10 +43005,20 @@ var ConfigBuilder = class {
40848
43005
  return typeOrder;
40849
43006
  });
40850
43007
  const isInheritedFrom = bitGroupConfigKeys.has(b.bitType);
43008
+ const indexByKey = /* @__PURE__ */ new Map();
43009
+ const pushOrReplace = (entry) => {
43010
+ const existing = indexByKey.get(entry.key);
43011
+ if (existing !== void 0) {
43012
+ tags2[existing] = entry;
43013
+ } else {
43014
+ indexByKey.set(entry.key, tags2.length);
43015
+ tags2.push(entry);
43016
+ }
43017
+ };
40851
43018
  if (isInheritedFrom) {
40852
43019
  const resolvedGroups = resolveGroupReferences(`group-${b.bitType}`);
40853
43020
  for (const ref of resolvedGroups) {
40854
- tags2.push({
43021
+ pushOrReplace({
40855
43022
  type: "group",
40856
43023
  key: ref.key,
40857
43024
  ...ref.hasExportJsonKey ? { jsonKey: ref.exportJsonKey } : {},
@@ -40864,7 +43031,7 @@ var ConfigBuilder = class {
40864
43031
  if (b.baseBitType) {
40865
43032
  const resolvedGroups = resolveGroupReferences(`group-${b.baseBitType}`);
40866
43033
  for (const ref of resolvedGroups) {
40867
- tags2.push({
43034
+ pushOrReplace({
40868
43035
  type: "group",
40869
43036
  key: ref.key,
40870
43037
  ...ref.hasExportJsonKey ? { jsonKey: ref.exportJsonKey } : {},
@@ -40875,7 +43042,9 @@ var ConfigBuilder = class {
40875
43042
  }
40876
43043
  }
40877
43044
  for (const [_tagKey, tag] of tagEntries) {
40878
- tags2.push(...processTagEntries(tag, []));
43045
+ for (const entry of processTagEntries(tag, [])) {
43046
+ pushOrReplace(entry);
43047
+ }
40879
43048
  }
40880
43049
  }
40881
43050
  let cardRef;
@@ -40909,7 +43078,7 @@ var ConfigBuilder = class {
40909
43078
  };
40910
43079
  const output = path3.join(outputFolderBits, `${b.bitType}.jsonc`);
40911
43080
  const str = JSON.stringify(bitJson, null, 2);
40912
- fileWrites.push(fs3.writeFile(output, str));
43081
+ fileWrites.push(fs4.writeFile(output, str));
40913
43082
  }
40914
43083
  const writeGroupConfigs = (groupConfigs2) => {
40915
43084
  for (const g of groupConfigs2) {
@@ -40968,7 +43137,7 @@ var ConfigBuilder = class {
40968
43137
  };
40969
43138
  const output = path3.join(outputFolderGroups, `${groupKey}.jsonc`);
40970
43139
  const str = JSON.stringify(bitJson, null, 2);
40971
- fs3.writeFileSync(output, str);
43140
+ fs4.writeFileSync(output, str);
40972
43141
  }
40973
43142
  };
40974
43143
  writeGroupConfigs(groupConfigs);
@@ -40978,7 +43147,7 @@ var ConfigBuilder = class {
40978
43147
  if (!cardJson) continue;
40979
43148
  const output = path3.join(outputFolderCards, `${cardJson.name}.jsonc`);
40980
43149
  const str = JSON.stringify(cardJson, null, 2);
40981
- fileWrites.push(fs3.writeFile(output, str));
43150
+ fileWrites.push(fs4.writeFile(output, str));
40982
43151
  }
40983
43152
  };
40984
43153
  const writeBitsAsGroupConfigs = (bitsAsGroupConfigs) => {
@@ -41027,7 +43196,7 @@ var ConfigBuilder = class {
41027
43196
  };
41028
43197
  const output = path3.join(outputFolderGroups, `${groupKey}.jsonc`);
41029
43198
  const str = JSON.stringify(bitJson, null, 2);
41030
- fs3.writeFileSync(output, str);
43199
+ fs4.writeFileSync(output, str);
41031
43200
  }
41032
43201
  };
41033
43202
  writeBitsAsGroupConfigs(bitGroupConfigs);
@@ -41066,15 +43235,15 @@ var ConfigBuilder = class {
41066
43235
  const outputFolderGroups = path3.join(outputFolder, "partials");
41067
43236
  const outputFolderCards = path3.join(outputFolder, "cards");
41068
43237
  const availableGroups = /* @__PURE__ */ new Set();
41069
- if (fs3.existsSync(outputFolderGroups)) {
41070
- const groupFiles = fs3.readdirSync(outputFolderGroups).filter((f) => f.endsWith(".jsonc"));
43238
+ if (fs4.existsSync(outputFolderGroups)) {
43239
+ const groupFiles = fs4.readdirSync(outputFolderGroups).filter((f) => f.endsWith(".jsonc"));
41071
43240
  for (const file of groupFiles) {
41072
43241
  const groupName = file.replace(".jsonc", "");
41073
43242
  availableGroups.add(groupName);
41074
43243
  }
41075
43244
  }
41076
43245
  const availableCards = /* @__PURE__ */ new Set();
41077
- const cardFiles = fs3.existsSync(outputFolderCards) ? fs3.readdirSync(outputFolderCards).filter((f) => f.endsWith(".jsonc")) : [];
43246
+ const cardFiles = fs4.existsSync(outputFolderCards) ? fs4.readdirSync(outputFolderCards).filter((f) => f.endsWith(".jsonc")) : [];
41078
43247
  for (const file of cardFiles) {
41079
43248
  const cardName = file.replace(".jsonc", "");
41080
43249
  availableCards.add(cardName);
@@ -41093,12 +43262,12 @@ var ConfigBuilder = class {
41093
43262
  }
41094
43263
  }
41095
43264
  };
41096
- if (fs3.existsSync(outputFolderBits)) {
41097
- const bitFiles = fs3.readdirSync(outputFolderBits).filter((f) => f.endsWith(".jsonc"));
43265
+ if (fs4.existsSync(outputFolderBits)) {
43266
+ const bitFiles = fs4.readdirSync(outputFolderBits).filter((f) => f.endsWith(".jsonc"));
41098
43267
  for (const file of bitFiles) {
41099
43268
  const filePath = path3.join(outputFolderBits, file);
41100
43269
  try {
41101
- const content = fs3.readFileSync(filePath, "utf-8");
43270
+ const content = fs4.readFileSync(filePath, "utf-8");
41102
43271
  const config = JSON.parse(content);
41103
43272
  if (config.tags && Array.isArray(config.tags)) {
41104
43273
  checkTags(config.tags, `bits/${file}`, 15);
@@ -41129,12 +43298,12 @@ var ConfigBuilder = class {
41129
43298
  }
41130
43299
  }
41131
43300
  }
41132
- if (fs3.existsSync(outputFolderGroups)) {
41133
- const groupFiles = fs3.readdirSync(outputFolderGroups).filter((f) => f.endsWith(".jsonc"));
43301
+ if (fs4.existsSync(outputFolderGroups)) {
43302
+ const groupFiles = fs4.readdirSync(outputFolderGroups).filter((f) => f.endsWith(".jsonc"));
41134
43303
  for (const file of groupFiles) {
41135
43304
  const filePath = path3.join(outputFolderGroups, file);
41136
43305
  try {
41137
- const content = fs3.readFileSync(filePath, "utf-8");
43306
+ const content = fs4.readFileSync(filePath, "utf-8");
41138
43307
  const config = JSON.parse(content);
41139
43308
  if (config.tags && Array.isArray(config.tags)) {
41140
43309
  checkTags(config.tags, `partials/${file}`, 15);
@@ -41148,7 +43317,7 @@ var ConfigBuilder = class {
41148
43317
  for (const file of cardFiles) {
41149
43318
  const filePath = path3.join(outputFolderCards, file);
41150
43319
  try {
41151
- const content = fs3.readFileSync(filePath, "utf-8");
43320
+ const content = fs4.readFileSync(filePath, "utf-8");
41152
43321
  const config = JSON.parse(content);
41153
43322
  const expectedKey = file.replace(".jsonc", "");
41154
43323
  if (config.key && config.key !== expectedKey) {
@@ -41184,6 +43353,497 @@ var ConfigBuilder = class {
41184
43353
  }
41185
43354
  };
41186
43355
 
43356
+ // src/parser/html/HtmlDom.ts
43357
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
43358
+ "area",
43359
+ "base",
43360
+ "br",
43361
+ "col",
43362
+ "embed",
43363
+ "hr",
43364
+ "img",
43365
+ "input",
43366
+ "link",
43367
+ "meta",
43368
+ "param",
43369
+ "source",
43370
+ "track",
43371
+ "wbr"
43372
+ ]);
43373
+ var IMPLIED_CLOSE = {
43374
+ li: /* @__PURE__ */ new Set(["li"]),
43375
+ td: /* @__PURE__ */ new Set(["td", "th"]),
43376
+ th: /* @__PURE__ */ new Set(["td", "th"]),
43377
+ tr: /* @__PURE__ */ new Set(["td", "th", "tr"]),
43378
+ thead: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43379
+ tbody: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43380
+ tfoot: /* @__PURE__ */ new Set(["td", "th", "tr", "thead", "tbody", "tfoot"]),
43381
+ p: /* @__PURE__ */ new Set(["p"]),
43382
+ option: /* @__PURE__ */ new Set(["option"])
43383
+ };
43384
+ var TAG_RE = /<(\/)?([a-zA-Z][a-zA-Z0-9-]*)((?:\s+(?:"[^"]*"|'[^']*'|[^<>"'])*)?)(\/)?>/g;
43385
+ var ATTR_RE = /([a-zA-Z_:][a-zA-Z0-9:._-]*)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s"'>]+))?/g;
43386
+ function parseAttrs(raw) {
43387
+ const attrs = {};
43388
+ if (!raw) return attrs;
43389
+ ATTR_RE.lastIndex = 0;
43390
+ let m;
43391
+ while ((m = ATTR_RE.exec(raw)) !== null) {
43392
+ const name = m[1].toLowerCase();
43393
+ let value = m[2] ?? "";
43394
+ if (value.length >= 2 && (value[0] === '"' || value[0] === "'")) {
43395
+ value = value.slice(1, -1);
43396
+ }
43397
+ attrs[name] = value;
43398
+ }
43399
+ return attrs;
43400
+ }
43401
+ function stripNonContent(html) {
43402
+ 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, "");
43403
+ }
43404
+ function tokenize(html) {
43405
+ const tokens = [];
43406
+ const clean = stripNonContent(html);
43407
+ TAG_RE.lastIndex = 0;
43408
+ let lastIndex = 0;
43409
+ let m;
43410
+ while ((m = TAG_RE.exec(clean)) !== null) {
43411
+ if (m.index > lastIndex) {
43412
+ tokens.push({ kind: "text", text: clean.slice(lastIndex, m.index) });
43413
+ }
43414
+ lastIndex = TAG_RE.lastIndex;
43415
+ const isClose = m[1] === "/";
43416
+ const name = m[2].toLowerCase();
43417
+ if (isClose) {
43418
+ tokens.push({ kind: "close", name });
43419
+ } else {
43420
+ tokens.push({
43421
+ kind: "open",
43422
+ name,
43423
+ attrs: parseAttrs(m[3]),
43424
+ selfClose: m[4] === "/"
43425
+ });
43426
+ }
43427
+ }
43428
+ if (lastIndex < clean.length) {
43429
+ tokens.push({ kind: "text", text: clean.slice(lastIndex) });
43430
+ }
43431
+ return tokens;
43432
+ }
43433
+ function parseHtml(html) {
43434
+ const root = { type: "element", name: "#root", attrs: {}, children: [] };
43435
+ const stack = [root];
43436
+ const top = () => stack[stack.length - 1];
43437
+ for (const token of tokenize(html)) {
43438
+ if (token.kind === "text") {
43439
+ if (token.text) top().children.push({ type: "text", value: token.text });
43440
+ continue;
43441
+ }
43442
+ if (token.kind === "open") {
43443
+ const name = token.name;
43444
+ const closes = IMPLIED_CLOSE[name];
43445
+ if (closes) {
43446
+ while (stack.length > 1 && closes.has(top().name)) stack.pop();
43447
+ }
43448
+ const el = {
43449
+ type: "element",
43450
+ name,
43451
+ attrs: token.attrs ?? {},
43452
+ children: []
43453
+ };
43454
+ top().children.push(el);
43455
+ if (!token.selfClose && !VOID_ELEMENTS.has(name)) {
43456
+ stack.push(el);
43457
+ }
43458
+ continue;
43459
+ }
43460
+ for (let i = stack.length - 1; i > 0; i--) {
43461
+ if (stack[i].name === token.name) {
43462
+ stack.length = i;
43463
+ break;
43464
+ }
43465
+ }
43466
+ }
43467
+ return root.children;
43468
+ }
43469
+ function childElements(el, names) {
43470
+ return el.children.filter((c) => c.type === "element" && names.has(c.name));
43471
+ }
43472
+
43473
+ // src/parser/html/HtmlTableParser.ts
43474
+ var MARK_TAGS = {
43475
+ b: "bold",
43476
+ strong: "bold",
43477
+ i: "italic",
43478
+ em: "italic",
43479
+ sup: "superscript",
43480
+ sub: "subscript",
43481
+ s: "strike",
43482
+ strike: "strike",
43483
+ del: "del",
43484
+ ins: "ins",
43485
+ u: "underline",
43486
+ mark: "highlight",
43487
+ code: "code"
43488
+ };
43489
+ var SECTION_TAGS = /* @__PURE__ */ new Set(["thead", "tbody", "tfoot"]);
43490
+ var ROW_TAGS = /* @__PURE__ */ new Set(["tr"]);
43491
+ var CELL_TAGS = /* @__PURE__ */ new Set(["td", "th"]);
43492
+ var LI_TAGS = /* @__PURE__ */ new Set(["li"]);
43493
+ var VALID_SCOPES = /* @__PURE__ */ new Set(["row", "col", "rowgroup", "colgroup"]);
43494
+ var NAMED_ENTITIES = {
43495
+ amp: "&",
43496
+ lt: "<",
43497
+ gt: ">",
43498
+ quot: '"',
43499
+ apos: "'",
43500
+ nbsp: "\xA0"
43501
+ };
43502
+ function decodeEntities(text) {
43503
+ return text.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (whole, body) => {
43504
+ if (body[0] === "#") {
43505
+ const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
43506
+ if (Number.isFinite(code) && code >= 0 && code <= 1114111) {
43507
+ try {
43508
+ return String.fromCodePoint(code);
43509
+ } catch {
43510
+ return whole;
43511
+ }
43512
+ }
43513
+ return whole;
43514
+ }
43515
+ return NAMED_ENTITIES[body] ?? whole;
43516
+ });
43517
+ }
43518
+ function intAttr(value) {
43519
+ if (value == null) return void 0;
43520
+ const n = Number.parseInt(value, 10);
43521
+ return Number.isInteger(n) ? n : void 0;
43522
+ }
43523
+ function isElement(node) {
43524
+ return node.type === "element";
43525
+ }
43526
+ var HtmlTableParser = class {
43527
+ keepUnknownTags = false;
43528
+ /**
43529
+ * Parse an HTML string and return one table bit per top-level `<table>`.
43530
+ */
43531
+ parse(html, options) {
43532
+ this.keepUnknownTags = options?.keepUnknownTags ?? false;
43533
+ const tableFormat = options?.tableFormat ?? "table-extended";
43534
+ const onWarning = options?.onWarning;
43535
+ const tables = this.collectTopLevelTables(parseHtml(html));
43536
+ const bits = [];
43537
+ for (const table of tables) {
43538
+ const bit = this.tableToBit(table, tableFormat, onWarning);
43539
+ if (bit) bits.push(bit);
43540
+ }
43541
+ return bits;
43542
+ }
43543
+ // --- table extraction -----------------------------------------------------
43544
+ /** Collect every `<table>` element, NOT descending into nested tables. */
43545
+ collectTopLevelTables(nodes) {
43546
+ const tables = [];
43547
+ const visit = (node) => {
43548
+ if (!isElement(node)) return;
43549
+ if (node.name === "table") {
43550
+ tables.push(node);
43551
+ return;
43552
+ }
43553
+ node.children.forEach(visit);
43554
+ };
43555
+ nodes.forEach(visit);
43556
+ return tables;
43557
+ }
43558
+ tableToBit(table, tableFormat, onWarning) {
43559
+ const header = [];
43560
+ const body = [];
43561
+ const footer = [];
43562
+ let caption;
43563
+ for (const child of table.children) {
43564
+ if (!isElement(child)) continue;
43565
+ if (child.name === "caption") {
43566
+ const capContent = this.inlineNodes(child.children, []);
43567
+ const trimmed = this.trimInline(capContent);
43568
+ if (trimmed.length > 0) caption = trimmed;
43569
+ } else if (SECTION_TAGS.has(child.name)) {
43570
+ const target = child.name === "thead" ? header : child.name === "tfoot" ? footer : body;
43571
+ for (const tr of childElements(child, ROW_TAGS)) {
43572
+ target.push(this.rowToCells(tr));
43573
+ }
43574
+ } else if (ROW_TAGS.has(child.name)) {
43575
+ body.push(this.rowToCells(child));
43576
+ }
43577
+ }
43578
+ if (header.length === 0 && body.length === 0 && footer.length === 0) {
43579
+ return void 0;
43580
+ }
43581
+ const tableExtended = {};
43582
+ if (header.length > 0) tableExtended.header = { rows: header };
43583
+ if (body.length > 0) tableExtended.body = { rows: body };
43584
+ if (footer.length > 0) tableExtended.footer = { rows: footer };
43585
+ let tableData = tableExtended;
43586
+ if (tableFormat === "table") {
43587
+ if (this.isLossyAsBasic(tableExtended)) {
43588
+ onWarning?.(
43589
+ "Converting an HTML table to [.table] loses rowspan/colspan/scope and multi-section structure; use --tableFormat table-extended to preserve them."
43590
+ );
43591
+ }
43592
+ tableData = convertExtendedToBasicTableFormat(tableExtended);
43593
+ }
43594
+ const bit = {
43595
+ type: tableFormat,
43596
+ format: "bitmark++",
43597
+ table: tableData
43598
+ };
43599
+ if (caption) bit.caption = caption;
43600
+ return bit;
43601
+ }
43602
+ rowToCells(tr) {
43603
+ const cells = [];
43604
+ for (const cell of childElements(tr, CELL_TAGS)) {
43605
+ cells.push(this.cellToJson(cell));
43606
+ }
43607
+ return { cells };
43608
+ }
43609
+ cellToJson(cell) {
43610
+ const json = {
43611
+ content: this.blockNodes(cell.children)
43612
+ };
43613
+ if (cell.name === "th") json.title = true;
43614
+ const rowspan = intAttr(cell.attrs.rowspan);
43615
+ if (rowspan != null && rowspan >= 2) json.rowspan = rowspan;
43616
+ const colspan = intAttr(cell.attrs.colspan);
43617
+ if (colspan != null && colspan >= 2) json.colspan = colspan;
43618
+ const scope = cell.attrs.scope?.toLowerCase();
43619
+ if (scope && VALID_SCOPES.has(scope)) {
43620
+ json.scope = scope;
43621
+ }
43622
+ const width = this.widthFromCell(cell);
43623
+ if (width != null) json.colwidth = width;
43624
+ return json;
43625
+ }
43626
+ widthFromCell(cell) {
43627
+ const attrWidth = intAttr(cell.attrs.width);
43628
+ if (attrWidth != null && attrWidth > 0) return attrWidth;
43629
+ const style = cell.attrs.style;
43630
+ if (style) {
43631
+ const m = style.match(/width\s*:\s*(\d+)\s*px/i);
43632
+ if (m) {
43633
+ const n = Number.parseInt(m[1], 10);
43634
+ if (Number.isInteger(n) && n > 0) return n;
43635
+ }
43636
+ }
43637
+ return void 0;
43638
+ }
43639
+ isLossyAsBasic(table) {
43640
+ if (table.footer) return true;
43641
+ if (table.header && table.header.rows.length > 1) return true;
43642
+ const sections = [table.header, table.body, table.footer];
43643
+ for (const section of sections) {
43644
+ if (!section) continue;
43645
+ for (const row of section.rows) {
43646
+ for (const cell of row.cells) {
43647
+ if (cell.rowspan || cell.colspan || cell.scope) return true;
43648
+ }
43649
+ }
43650
+ }
43651
+ return false;
43652
+ }
43653
+ // --- content conversion (HTML -> TextAst) ---------------------------------
43654
+ /**
43655
+ * Convert a cell/li's child nodes into block-level TextAst nodes (paragraphs, lists, images).
43656
+ */
43657
+ blockNodes(nodes) {
43658
+ const blocks = [];
43659
+ let inline = [];
43660
+ const flush = () => {
43661
+ const trimmed = this.trimInline(inline);
43662
+ inline = [];
43663
+ if (trimmed.length === 0) return;
43664
+ if (trimmed.length === 1 && trimmed[0].type === "imageInline") {
43665
+ blocks.push({ ...trimmed[0], type: "image" });
43666
+ } else {
43667
+ blocks.push({ type: "paragraph", content: trimmed, attrs: {} });
43668
+ }
43669
+ };
43670
+ for (const node of nodes) {
43671
+ if (!isElement(node)) {
43672
+ inline.push(...this.inlineText(node.value, []));
43673
+ continue;
43674
+ }
43675
+ const name = node.name;
43676
+ if (name === "p") {
43677
+ flush();
43678
+ const content = this.trimInline(this.inlineNodes(node.children, []));
43679
+ if (content.length > 0) {
43680
+ blocks.push({ type: "paragraph", content, attrs: {} });
43681
+ }
43682
+ } else if (this.isListTag(name)) {
43683
+ flush();
43684
+ const list = this.listNode(node);
43685
+ if (list) blocks.push(list);
43686
+ } else {
43687
+ inline.push(...this.inlineFromElement(node, []));
43688
+ }
43689
+ }
43690
+ flush();
43691
+ return blocks;
43692
+ }
43693
+ /** Convert child nodes to a flat list of inline TextNodes, applying the given marks. */
43694
+ inlineNodes(nodes, marks) {
43695
+ const out = [];
43696
+ for (const node of nodes) {
43697
+ if (!isElement(node)) {
43698
+ out.push(...this.inlineText(node.value, marks));
43699
+ } else {
43700
+ out.push(...this.inlineFromElement(node, marks));
43701
+ }
43702
+ }
43703
+ return out;
43704
+ }
43705
+ inlineFromElement(el, marks) {
43706
+ const name = el.name;
43707
+ if (name === "br") {
43708
+ return [{ type: "hardBreak" }];
43709
+ }
43710
+ if (name === "img") {
43711
+ return [this.imageNode(el, true)];
43712
+ }
43713
+ const markType = MARK_TAGS[name];
43714
+ if (markType) {
43715
+ return this.inlineNodes(el.children, [...marks, { type: markType }]);
43716
+ }
43717
+ if (name === "a") {
43718
+ const linkMark = {
43719
+ type: "link",
43720
+ attrs: {
43721
+ href: el.attrs.href ?? "",
43722
+ ...el.attrs.target ? { target: el.attrs.target } : {}
43723
+ }
43724
+ };
43725
+ return this.inlineNodes(el.children, [...marks, linkMark]);
43726
+ }
43727
+ if (this.keepUnknownTags) {
43728
+ const open = this.serializeOpenTag(el);
43729
+ const close = `</${name}>`;
43730
+ return [
43731
+ ...this.inlineText(open, marks),
43732
+ ...this.inlineNodes(el.children, marks),
43733
+ ...this.inlineText(close, marks)
43734
+ ];
43735
+ }
43736
+ return this.inlineNodes(el.children, marks);
43737
+ }
43738
+ inlineText(raw, marks) {
43739
+ const text = decodeEntities(raw).replace(/\s+/g, " ");
43740
+ if (text === "") return [];
43741
+ const node = { text, type: "text" };
43742
+ if (marks.length > 0) node.marks = marks.map((m) => ({ ...m }));
43743
+ return [node];
43744
+ }
43745
+ serializeOpenTag(el) {
43746
+ const attrs = Object.entries(el.attrs).map(([k, v]) => v === "" ? ` ${k}` : ` ${k}="${v}"`).join("");
43747
+ return `<${el.name}${attrs}>`;
43748
+ }
43749
+ // --- lists ----------------------------------------------------------------
43750
+ isListTag(name) {
43751
+ return name === "ul" || name === "ol";
43752
+ }
43753
+ listNode(el) {
43754
+ const items = childElements(el, LI_TAGS);
43755
+ if (items.length === 0) return void 0;
43756
+ const isTask = el.name === "ul" && items.some((li) => {
43757
+ const firstEl = li.children.find(isElement);
43758
+ return firstEl?.name === "input" && firstEl.attrs.type?.toLowerCase() === "checkbox";
43759
+ });
43760
+ if (isTask) {
43761
+ return {
43762
+ type: "taskList",
43763
+ content: items.map((li) => this.taskItemNode(li))
43764
+ };
43765
+ }
43766
+ const type = this.listType(el);
43767
+ const node = {
43768
+ type,
43769
+ content: items.map((li) => this.listItemNode(li))
43770
+ };
43771
+ if (el.name === "ol") {
43772
+ const start = intAttr(el.attrs.start);
43773
+ if (start != null) {
43774
+ node.attrs = { start };
43775
+ }
43776
+ }
43777
+ return node;
43778
+ }
43779
+ listType(el) {
43780
+ if (el.name === "ol") {
43781
+ switch (el.attrs.type) {
43782
+ case "i":
43783
+ return "orderedListRomanLower";
43784
+ case "I":
43785
+ return "orderedListRoman";
43786
+ case "a":
43787
+ return "letteredListLower";
43788
+ case "A":
43789
+ return "letteredList";
43790
+ default:
43791
+ return "orderedList";
43792
+ }
43793
+ }
43794
+ const style = el.attrs.style ?? "";
43795
+ if (/list-style(-type)?\s*:\s*none/i.test(style)) return "noBulletList";
43796
+ return "bulletList";
43797
+ }
43798
+ listItemNode(li) {
43799
+ let content = this.blockNodes(li.children);
43800
+ if (content.length === 0) {
43801
+ content = [{ type: "paragraph", content: [], attrs: {} }];
43802
+ }
43803
+ return { type: "listItem", content };
43804
+ }
43805
+ taskItemNode(li) {
43806
+ const checkbox = li.children.find(
43807
+ (c) => isElement(c) && c.name === "input" && c.attrs.type?.toLowerCase() === "checkbox"
43808
+ );
43809
+ const checked = checkbox ? "checked" in checkbox.attrs : false;
43810
+ const rest = li.children.filter((c) => c !== checkbox);
43811
+ let content = this.blockNodes(rest);
43812
+ if (content.length === 0) {
43813
+ content = [{ type: "paragraph", content: [], attrs: {} }];
43814
+ }
43815
+ return { type: "taskItem", attrs: { checked }, content };
43816
+ }
43817
+ // --- images ---------------------------------------------------------------
43818
+ imageNode(el, inline) {
43819
+ const attrs = { src: el.attrs.src ?? "" };
43820
+ attrs.alt = el.attrs.alt ?? null;
43821
+ attrs.title = el.attrs.title ?? null;
43822
+ const width = intAttr(el.attrs.width);
43823
+ if (width != null) attrs.width = width;
43824
+ const height = intAttr(el.attrs.height);
43825
+ if (height != null) attrs.height = height;
43826
+ if (el.attrs.class) attrs.class = el.attrs.class;
43827
+ return { type: inline ? "imageInline" : "image", attrs };
43828
+ }
43829
+ // --- whitespace -----------------------------------------------------------
43830
+ /** Trim leading/trailing whitespace from inline content (edges only). */
43831
+ trimInline(nodes) {
43832
+ const out = nodes.slice();
43833
+ const isWs = (n) => n.type === "text" && /^\s*$/.test(n.text ?? "");
43834
+ while (out.length > 0 && isWs(out[0])) out.shift();
43835
+ if (out.length > 0 && out[0].type === "text") {
43836
+ out[0] = { ...out[0], text: (out[0].text ?? "").replace(/^\s+/, "") };
43837
+ }
43838
+ while (out.length > 0 && isWs(out[out.length - 1])) out.pop();
43839
+ if (out.length > 0 && out[out.length - 1].type === "text") {
43840
+ const last = out.length - 1;
43841
+ out[last] = { ...out[last], text: (out[last].text ?? "").replace(/\s+$/, "") };
43842
+ }
43843
+ return out.filter((n) => !(n.type === "text" && n.text === ""));
43844
+ }
43845
+ };
43846
+
41187
43847
  // src/BitmarkParserGenerator.ts
41188
43848
  var Input = {
41189
43849
  /**
@@ -41221,7 +43881,11 @@ var InputFormat = {
41221
43881
  /**
41222
43882
  * Input is plain text
41223
43883
  */
41224
- plainText: "plainText"
43884
+ plainText: "plainText",
43885
+ /**
43886
+ * Input is HTML (used by convertHtmlTable)
43887
+ */
43888
+ html: "html"
41225
43889
  };
41226
43890
  var BitmarkParserGenerator = class {
41227
43891
  ast = new Ast();
@@ -41342,8 +44006,8 @@ var BitmarkParserGenerator = class {
41342
44006
  }
41343
44007
  if (!opts.inputFormat || opts.inputFormat === Input.file) {
41344
44008
  if (env.isNode) {
41345
- if (inputIsString && fs3.existsSync(inStr)) {
41346
- inStr = fs3.readFileSync(inStr, {
44009
+ if (inputIsString && fs4.existsSync(inStr)) {
44010
+ inStr = fs4.readFileSync(inStr, {
41347
44011
  encoding: "utf8"
41348
44012
  });
41349
44013
  }
@@ -41389,7 +44053,7 @@ var BitmarkParserGenerator = class {
41389
44053
  const textGenerator = new PlainTextGenerator();
41390
44054
  const str = textGenerator.generate(json);
41391
44055
  if (opts.outputFile) {
41392
- fs3.writeFileSync(opts.outputFile, str, {
44056
+ fs4.writeFileSync(opts.outputFile, str, {
41393
44057
  encoding: "utf8"
41394
44058
  });
41395
44059
  } else {
@@ -41424,7 +44088,7 @@ var BitmarkParserGenerator = class {
41424
44088
  const textGenerator = new PlainTextGenerator();
41425
44089
  const str = textGenerator.generate(json);
41426
44090
  if (opts.outputFile) {
41427
- fs3.writeFileSync(opts.outputFile, str, {
44091
+ fs4.writeFileSync(opts.outputFile, str, {
41428
44092
  encoding: "utf8"
41429
44093
  });
41430
44094
  } else {
@@ -41449,7 +44113,7 @@ var BitmarkParserGenerator = class {
41449
44113
  const textGenerator = new PlainTextGenerator();
41450
44114
  const str = textGenerator.generate(json);
41451
44115
  if (opts.outputFile) {
41452
- fs3.writeFileSync(opts.outputFile, str, {
44116
+ fs4.writeFileSync(opts.outputFile, str, {
41453
44117
  encoding: "utf8"
41454
44118
  });
41455
44119
  } else {
@@ -41533,8 +44197,8 @@ var BitmarkParserGenerator = class {
41533
44197
  }
41534
44198
  if (!opts.inputFormat || opts.inputFormat === Input.file) {
41535
44199
  if (env.isNode) {
41536
- if (inputIsString && fs3.existsSync(inStr)) {
41537
- inStr = fs3.readFileSync(inStr, {
44200
+ if (inputIsString && fs4.existsSync(inStr)) {
44201
+ inStr = fs4.readFileSync(inStr, {
41538
44202
  encoding: "utf8"
41539
44203
  });
41540
44204
  }
@@ -41592,8 +44256,8 @@ var BitmarkParserGenerator = class {
41592
44256
  const opts = Object.assign({}, options);
41593
44257
  if (!opts.inputFormat || opts.inputFormat === Input.file) {
41594
44258
  if (env.isNode) {
41595
- if (inputIsString && fs3.existsSync(inStr)) {
41596
- inStr = fs3.readFileSync(inStr, {
44259
+ if (inputIsString && fs4.existsSync(inStr)) {
44260
+ inStr = fs4.readFileSync(inStr, {
41597
44261
  encoding: "utf8"
41598
44262
  });
41599
44263
  }
@@ -41653,8 +44317,8 @@ var BitmarkParserGenerator = class {
41653
44317
  }
41654
44318
  if (!opts.inputFormat || opts.inputFormat === Input.file) {
41655
44319
  if (env.isNode) {
41656
- if (inputIsString && fs3.existsSync(inStr)) {
41657
- inStr = fs3.readFileSync(inStr, {
44320
+ if (inputIsString && fs4.existsSync(inStr)) {
44321
+ inStr = fs4.readFileSync(inStr, {
41658
44322
  encoding: "utf8"
41659
44323
  });
41660
44324
  }
@@ -41677,8 +44341,8 @@ var BitmarkParserGenerator = class {
41677
44341
  strRes = this.jsonStringifyPrettify(preRes, jsonOptions, true);
41678
44342
  }
41679
44343
  const flag = fileOptions.append ? "a" : "w";
41680
- fs3.ensureDirSync(path3.dirname(output));
41681
- fs3.writeFileSync(output, strRes, {
44344
+ fs4.ensureDirSync(path3.dirname(output));
44345
+ fs4.writeFileSync(output, strRes, {
41682
44346
  flag
41683
44347
  });
41684
44348
  } else {
@@ -41738,6 +44402,87 @@ var BitmarkParserGenerator = class {
41738
44402
  const res = generator.generate(data);
41739
44403
  return res;
41740
44404
  }
44405
+ /**
44406
+ * Convert between HTML tables and bitmark tables.
44407
+ *
44408
+ * Direction is auto-detected (override with `options.inputFormat`):
44409
+ * - HTML input: every `<table>` is extracted and emitted as a `[.table-extended]` (or `[.table]`)
44410
+ * bit; output as bitmark (default), JSON, AST, or HTML (re-emit).
44411
+ * - bitmark / JSON / AST input: each `table` / `table-extended` bit is rendered as an HTML
44412
+ * `<table>` fragment.
44413
+ *
44414
+ * Cell content is reproduced verbatim except inline conversion to bitmark marks (bold, italic,
44415
+ * superscript, etc.), with lists and images mapped to their bitmark equivalents.
44416
+ *
44417
+ * @param input - HTML, bitmark, JSON or AST string; or a parsed JSON/AST value; or a file path.
44418
+ * @param options - the conversion options
44419
+ * @returns the converted output as a string (or object for JSON/AST), or void when writing a file
44420
+ * @throws Error if any error occurs
44421
+ */
44422
+ convertHtmlTable(input, options) {
44423
+ const opts = Object.assign({}, options);
44424
+ if (env.isBrowser && opts.outputFile) {
44425
+ throw new Error("Cannot write to file in browser environment");
44426
+ }
44427
+ let inStr = input;
44428
+ const inputIsString = typeof input === "string";
44429
+ if (env.isNode && inputIsString && fs4.existsSync(inStr)) {
44430
+ inStr = fs4.readFileSync(inStr, { encoding: "utf8" });
44431
+ }
44432
+ const isHtmlString = (s) => {
44433
+ const t = s.replace(/^\s+/, "");
44434
+ return t.startsWith("<") && /<table/i.test(t);
44435
+ };
44436
+ let htmlInput;
44437
+ if (opts.inputFormat === InputFormat.html) {
44438
+ htmlInput = true;
44439
+ } else if (opts.inputFormat != null) {
44440
+ htmlInput = false;
44441
+ } else {
44442
+ htmlInput = inputIsString && isHtmlString(inStr);
44443
+ }
44444
+ if (htmlInput) {
44445
+ const parser2 = new HtmlTableParser();
44446
+ const bits = parser2.parse(inStr, {
44447
+ tableFormat: opts.tableFormat ?? "table-extended",
44448
+ keepUnknownTags: opts.keepUnknownTags,
44449
+ onWarning: (m) => console.warn(m)
44450
+ });
44451
+ const outputFormat2 = opts.outputFormat ?? Output.bitmark;
44452
+ if (outputFormat2 === "html") {
44453
+ const json2 = this.convert(bits, { outputFormat: Output.json });
44454
+ return this.outputHtml(new HtmlTableGenerator().generate(json2), opts);
44455
+ }
44456
+ return this.convert(bits, {
44457
+ outputFormat: Enum(Output).fromValue(outputFormat2),
44458
+ outputFile: opts.outputFile,
44459
+ fileOptions: opts.fileOptions,
44460
+ jsonOptions: opts.jsonOptions,
44461
+ bitmarkOptions: opts.bitmarkOptions
44462
+ });
44463
+ }
44464
+ const outputFormat = opts.outputFormat ?? "html";
44465
+ if (outputFormat !== "html") {
44466
+ throw new Error(
44467
+ "convertHtmlTable: non-HTML input can only be converted to HTML. Use convert() for bitmark/JSON/AST conversions."
44468
+ );
44469
+ }
44470
+ const json = this.convert(inputIsString ? inStr : input, { outputFormat: Output.json });
44471
+ return this.outputHtml(new HtmlTableGenerator().generate(json), opts);
44472
+ }
44473
+ /**
44474
+ * Write an HTML string to the configured output file, or return it.
44475
+ */
44476
+ outputHtml(html, opts) {
44477
+ if (opts.outputFile) {
44478
+ const output = opts.outputFile.toString();
44479
+ const flag = opts.fileOptions?.append ? "a" : "w";
44480
+ fs4.ensureDirSync(path3.dirname(output));
44481
+ fs4.writeFileSync(output, html, { flag });
44482
+ return;
44483
+ }
44484
+ return html;
44485
+ }
41741
44486
  /**
41742
44487
  * Breakscape bitmark text.
41743
44488
  *
@@ -41769,8 +44514,8 @@ var BitmarkParserGenerator = class {
41769
44514
  }
41770
44515
  if (!opts.inputFormat || opts.inputFormat === Input.file) {
41771
44516
  if (env.isNode) {
41772
- if (inputIsString && fs3.existsSync(inStr)) {
41773
- inStr = fs3.readFileSync(inStr, {
44517
+ if (inputIsString && fs4.existsSync(inStr)) {
44518
+ inStr = fs4.readFileSync(inStr, {
41774
44519
  encoding: "utf8"
41775
44520
  });
41776
44521
  }
@@ -41783,8 +44528,8 @@ var BitmarkParserGenerator = class {
41783
44528
  if (opts.outputFile) {
41784
44529
  const output = opts.outputFile.toString();
41785
44530
  const flag = fileOptions.append ? "a" : "w";
41786
- fs3.ensureDirSync(path3.dirname(output));
41787
- fs3.writeFileSync(output, res, {
44531
+ fs4.ensureDirSync(path3.dirname(output));
44532
+ fs4.writeFileSync(output, res, {
41788
44533
  flag
41789
44534
  });
41790
44535
  } else {
@@ -41822,8 +44567,8 @@ var BitmarkParserGenerator = class {
41822
44567
  }
41823
44568
  if (!opts.inputFormat || opts.inputFormat === Input.file) {
41824
44569
  if (env.isNode) {
41825
- if (inputIsString && fs3.existsSync(inStr)) {
41826
- inStr = fs3.readFileSync(inStr, {
44570
+ if (inputIsString && fs4.existsSync(inStr)) {
44571
+ inStr = fs4.readFileSync(inStr, {
41827
44572
  encoding: "utf8"
41828
44573
  });
41829
44574
  }
@@ -41836,8 +44581,8 @@ var BitmarkParserGenerator = class {
41836
44581
  if (opts.outputFile) {
41837
44582
  const output = opts.outputFile.toString();
41838
44583
  const flag = fileOptions.append ? "a" : "w";
41839
- fs3.ensureDirSync(path3.dirname(output));
41840
- fs3.writeFileSync(output, res, {
44584
+ fs4.ensureDirSync(path3.dirname(output));
44585
+ fs4.writeFileSync(output, res, {
41841
44586
  flag
41842
44587
  });
41843
44588
  } else {
@@ -41892,8 +44637,8 @@ var BitmarkParserGenerator = class {
41892
44637
  init();
41893
44638
  async function readInput(input) {
41894
44639
  if (input !== void 0) {
41895
- if (await fs3.pathExists(input)) {
41896
- return fs3.readFile(input, "utf8");
44640
+ if (await fs4.pathExists(input)) {
44641
+ return fs4.readFile(input, "utf8");
41897
44642
  }
41898
44643
  return input;
41899
44644
  }
@@ -41906,8 +44651,8 @@ async function readInput(input) {
41906
44651
  async function writeOutput(content, outputFile, append = false) {
41907
44652
  const text = typeof content === "string" ? content : JSON.stringify(content);
41908
44653
  if (outputFile) {
41909
- await fs3.ensureDir(path3.dirname(outputFile));
41910
- await fs3.writeFile(outputFile, text, { flag: append ? "a" : "w" });
44654
+ await fs4.ensureDir(path3.dirname(outputFile));
44655
+ await fs4.writeFile(outputFile, text, { flag: append ? "a" : "w" });
41911
44656
  } else {
41912
44657
  console.log(text);
41913
44658
  }
@@ -42056,6 +44801,70 @@ Examples:
42056
44801
  );
42057
44802
  return cmd;
42058
44803
  }
44804
+ var OUTPUT_FORMAT_CHOICES2 = ["html", "bitmark", "json", "ast"];
44805
+ var INPUT_FORMAT_CHOICES = ["html", "bitmark", "json"];
44806
+ var TABLE_FORMAT_CHOICES = ["table", "table-extended"];
44807
+ function createConvertHtmlTableCommand() {
44808
+ const bpg = new BitmarkParserGenerator();
44809
+ const cmd = new Command("convertHtmlTable").description("Convert between HTML tables and bitmark tables").argument(
44810
+ "[input]",
44811
+ "file to read, or HTML / bitmark / json string. If not specified, input will be from <stdin>"
44812
+ ).addOption(
44813
+ new Option(
44814
+ "-f, --format <format>",
44815
+ "output format (default: bitmark for HTML input, html for bitmark/json input)"
44816
+ ).choices(OUTPUT_FORMAT_CHOICES2)
44817
+ ).addOption(
44818
+ new Option("--inputFormat <format>", "force input format (auto-detected by default)").choices(
44819
+ INPUT_FORMAT_CHOICES
44820
+ )
44821
+ ).addOption(
44822
+ new Option("--tableFormat <format>", "bit type to emit for HTML input").choices(TABLE_FORMAT_CHOICES).default("table-extended")
44823
+ ).option("--keepUnknownTags", "keep unmappable HTML tags as literal text (HTML input only)").option("-p, --pretty", "prettify the JSON output with indent").option(
44824
+ "--indent <indent>",
44825
+ "prettify indent (default: 2 when --pretty is set)",
44826
+ (v) => Number.parseInt(v, 10)
44827
+ ).option("-a, --append", "append to the output file (default is to overwrite)").option("-o, --output <file>", "output file. If not specified, output will be to <stdout>").action(async (input, options) => {
44828
+ try {
44829
+ const dataIn = await readInput(input);
44830
+ const prettify = options.pretty ? options.indent ?? 2 : void 0;
44831
+ const inputFormat = options.inputFormat === "html" ? InputFormat.html : options.inputFormat != null ? InputFormat.bitmark : void 0;
44832
+ const result = bpg.convertHtmlTable(dataIn, {
44833
+ inputFormat,
44834
+ outputFormat: options.format,
44835
+ tableFormat: Enum({ table: "table", "table-extended": "table-extended" }).fromValue(
44836
+ options.tableFormat
44837
+ ),
44838
+ keepUnknownTags: options.keepUnknownTags,
44839
+ jsonOptions: {
44840
+ prettify
44841
+ }
44842
+ });
44843
+ let outputValue = result ?? "";
44844
+ if (typeof outputValue !== "string") {
44845
+ outputValue = formatJson(outputValue, options.pretty, options.indent);
44846
+ }
44847
+ await writeOutput(outputValue, options.output, options.append);
44848
+ } catch (error) {
44849
+ console.error("Error:", error instanceof Error ? error.message : String(error));
44850
+ process.exit(1);
44851
+ }
44852
+ }).addHelpText(
44853
+ "after",
44854
+ `
44855
+ Examples:
44856
+ $ bitmark-parser convertHtmlTable '<table><tr><th>Name</th></tr><tr><td>John</td></tr></table>'
44857
+
44858
+ $ bitmark-parser convertHtmlTable input.html -o output.bitmark
44859
+
44860
+ $ bitmark-parser convertHtmlTable input.html -f json -p -o output.json
44861
+
44862
+ $ bitmark-parser convertHtmlTable input.bitmark -o output.html
44863
+
44864
+ $ bitmark-parser convertHtmlTable --tableFormat table input.html`
44865
+ );
44866
+ return cmd;
44867
+ }
42059
44868
  var TEXT_FORMAT_CHOICES = (() => {
42060
44869
  const choices = enumChoices(BodyTextFormat);
42061
44870
  const preferred = BodyTextFormat.bitmarkPlusPlus;
@@ -42112,7 +44921,7 @@ Examples:
42112
44921
  );
42113
44922
  return cmd;
42114
44923
  }
42115
- var INPUT_FORMAT_CHOICES = enumChoices(InputFormat);
44924
+ var INPUT_FORMAT_CHOICES2 = enumChoices(InputFormat);
42116
44925
  function createExtractPlainTextCommand() {
42117
44926
  const bpg = new BitmarkParserGenerator();
42118
44927
  const cmd = new Command("extractPlainText").description("Extract plain text from bitmark, bitmark text, or JSON").argument(
@@ -42122,7 +44931,7 @@ function createExtractPlainTextCommand() {
42122
44931
  new Option(
42123
44932
  "-f, --inputFormat <format>",
42124
44933
  "force input format (auto-detected by default)"
42125
- ).choices([...INPUT_FORMAT_CHOICES])
44934
+ ).choices([...INPUT_FORMAT_CHOICES2])
42126
44935
  ).option("-a, --append", "append to the output file (default is to overwrite)").option("-o, --output <file>", "output file. If not specified, output will be to <stdout>").action(async (input, options) => {
42127
44936
  try {
42128
44937
  const dataIn = await readInput(input);
@@ -42274,6 +45083,7 @@ async function asyncInit() {
42274
45083
  program.outputHelp();
42275
45084
  });
42276
45085
  program.addCommand(createConvertCommand());
45086
+ program.addCommand(createConvertHtmlTableCommand());
42277
45087
  program.addCommand(createConvertTextCommand());
42278
45088
  program.addCommand(createExtractPlainTextCommand());
42279
45089
  program.addCommand(createBreakscapeCommand());