@gmb/bitmark-parser-generator 5.22.0 → 5.24.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.
@@ -278,6 +278,7 @@ var BitType2 = {
278
278
  footNote: "foot-note",
279
279
  formFreeText: "form-free-text",
280
280
  formula: "formula",
281
+ formulaImage: "formula-image",
281
282
  gapText: "gap-text",
282
283
  gapTextInstructionGrouped: "gap-text-instruction-grouped",
283
284
  glossaryTerm: "glossary-term",
@@ -702,8 +703,10 @@ var TagFormat = {
702
703
  // If the value is treated as a boolean
703
704
  invertedBoolean: "invertedBoolean",
704
705
  // If the value is treated as a boolean with the value inverted (e.g. isLongAnswer ==> isShortAnswer = false)
705
- enumeration: "enumeration"
706
+ enumeration: "enumeration",
706
707
  // If the value is treated as an enumeration (the value is a string that must be one of a predefined set of values)
708
+ coordinates: "coordinates"
709
+ // Comma-separated list of 4 numbers (e.g. bounding-box x, y, x1, y1)
707
710
  };
708
711
 
709
712
  // src/model/enum/TextFormat.ts
@@ -726,10 +729,12 @@ var TextFormat2 = {
726
729
  import "@ncoderz/superenum";
727
730
  var CardSetConfigKey = {
728
731
  flashcard: "flashcard",
732
+ flashcard1: "flashcard1",
729
733
  definitionList: "definitionList",
730
734
  elements: "elements",
731
735
  statements: "statements",
732
736
  quiz: "quiz",
737
+ quizResponses: "quizResponses",
733
738
  feedback: "feedback",
734
739
  questions: "questions",
735
740
  matchPairs: "matchPairs",
@@ -741,6 +746,7 @@ var CardSetConfigKey = {
741
746
  pronunciationTable: "pronunciationTable",
742
747
  botActionResponses: "botActionResponses",
743
748
  exampleBitList: "exampleBitList",
749
+ pageFooterSections: "pageFooterSections",
744
750
  clozeList: "clozeList",
745
751
  ingredients: "ingredients",
746
752
  // DEPRECATED - TO BE REMOVED IN THE FUTURE
@@ -788,7 +794,11 @@ var AbstractTagConfig = class {
788
794
  // Default: 0
789
795
  __publicField(this, "chain");
790
796
  __publicField(this, "jsonKey");
791
- // If the json key is different from the tag
797
+ // Legacy string-form jsonKey
798
+ __publicField(this, "exportJsonKey");
799
+ // New JSON-pattern jsonKey (export-only)
800
+ __publicField(this, "hasExportJsonKey");
801
+ // True iff exportJsonKey was explicitly set
792
802
  __publicField(this, "deprecated");
793
803
  this.type = params.type;
794
804
  this.configKey = params.configKey;
@@ -797,6 +807,8 @@ var AbstractTagConfig = class {
797
807
  this.minCount = params.minCount;
798
808
  this.chain = params.chain;
799
809
  this.jsonKey = params.jsonKey;
810
+ this.exportJsonKey = params.exportJsonKey;
811
+ this.hasExportJsonKey = params.hasExportJsonKey;
800
812
  this.deprecated = params.deprecated;
801
813
  }
802
814
  };
@@ -806,7 +818,6 @@ var MarkupTagConfig = class extends AbstractTagConfig {
806
818
  constructor(params) {
807
819
  super({
808
820
  type: BitTagConfigKeyType.tag,
809
- jsonKey: void 0,
810
821
  ...params
811
822
  });
812
823
  __publicField(this, "type", BitTagConfigKeyType.tag);
@@ -918,8 +929,8 @@ var ResourceTagConfig = class extends AbstractTagConfig {
918
929
 
919
930
  // src/model/config/CardVariantConfig.ts
920
931
  var CardVariantConfig = class {
921
- // JSON path for body text
922
- constructor(tags2, bodyAllowed, bodyRequired, repeatCount, jsonKey) {
932
+ // Body text format. Default: bitmark++
933
+ constructor(tags2, bodyAllowed, bodyRequired, repeatCount, jsonKey, exportJsonKey, hasExportJsonKey = false, format) {
923
934
  __publicField(this, "tags");
924
935
  __publicField(this, "bodyAllowed");
925
936
  // Default: true
@@ -929,11 +940,19 @@ var CardVariantConfig = class {
929
940
  // Default: 1
930
941
  // JSON mapping fields
931
942
  __publicField(this, "jsonKey");
943
+ // JSON path for body text (legacy)
944
+ __publicField(this, "exportJsonKey");
945
+ // New JSON-pattern jsonKey
946
+ __publicField(this, "hasExportJsonKey");
947
+ __publicField(this, "format");
932
948
  this.tags = tags2;
933
949
  this.bodyAllowed = bodyAllowed == null ? true : bodyAllowed;
934
950
  this.bodyRequired = bodyRequired;
935
951
  this.repeatCount = repeatCount;
936
952
  this.jsonKey = jsonKey;
953
+ this.exportJsonKey = exportJsonKey;
954
+ this.hasExportJsonKey = hasExportJsonKey;
955
+ this.format = format;
937
956
  }
938
957
  toString(options) {
939
958
  const opts = Object.assign({}, options);
@@ -957,14 +976,18 @@ ${tag.toString(opts)}`;
957
976
 
958
977
  // src/model/config/CardSideConfig.ts
959
978
  var CardSideConfig = class {
960
- constructor(name, repeat, variants, jsonKey) {
979
+ constructor(name, repeat, variants, jsonKey, exportJsonKey, hasExportJsonKey = false) {
961
980
  __publicField(this, "name");
962
981
  __publicField(this, "repeat");
963
982
  __publicField(this, "jsonKey");
983
+ __publicField(this, "exportJsonKey");
984
+ __publicField(this, "hasExportJsonKey");
964
985
  __publicField(this, "variants");
965
986
  this.name = name;
966
987
  this.repeat = repeat;
967
988
  this.jsonKey = jsonKey;
989
+ this.exportJsonKey = exportJsonKey;
990
+ this.hasExportJsonKey = hasExportJsonKey;
968
991
  this.variants = variants;
969
992
  }
970
993
  toString(options) {
@@ -986,13 +1009,17 @@ var CardSideConfig = class {
986
1009
 
987
1010
  // src/model/config/CardSetConfig.ts
988
1011
  var CardSetConfig = class {
989
- constructor(configKey, jsonKey, sections, sides) {
1012
+ constructor(configKey, jsonKey, exportJsonKey, hasExportJsonKey, sections, sides) {
990
1013
  __publicField(this, "configKey");
991
1014
  __publicField(this, "jsonKey");
1015
+ __publicField(this, "exportJsonKey");
1016
+ __publicField(this, "hasExportJsonKey");
992
1017
  __publicField(this, "sections");
993
1018
  __publicField(this, "sides");
994
1019
  this.configKey = configKey;
995
1020
  this.jsonKey = jsonKey;
1021
+ this.exportJsonKey = exportJsonKey;
1022
+ this.hasExportJsonKey = hasExportJsonKey;
996
1023
  this.sections = sections;
997
1024
  this.sides = sides;
998
1025
  }
@@ -1136,12 +1163,17 @@ var groupKeys = {
1136
1163
  group_standardAllBits: "group_standardAllBits",
1137
1164
  group_standardItemLead: "group_standardItemLead",
1138
1165
  group_standardItemLeadInstructionHint: "group_standardItemLeadInstructionHint",
1166
+ group_standardInstructionHint: "group_standardInstructionHint",
1139
1167
  group_standardTags: "group_standardTags",
1168
+ group_standardTagsNoItemLead: "group_standardTagsNoItemLead",
1140
1169
  group_imageSource: "group_imageSource",
1141
1170
  group_technicalTerm: "group_technicalTerm",
1142
1171
  group_person: "group_person",
1143
1172
  group_gap: "group_gap",
1144
1173
  group_trueFalse: "group_trueFalse",
1174
+ group_trueFalseInlineHighlight: "group_trueFalseInlineHighlight",
1175
+ group_trueFalseInlineSelect: "group_trueFalseInlineSelect",
1176
+ group_trueFalseResponses: "group_trueFalseResponses",
1145
1177
  group_markConfig: "group_markConfig",
1146
1178
  group_mark: "group_mark",
1147
1179
  group_bookCommon: "group_bookCommon",
@@ -1149,14 +1181,18 @@ var groupKeys = {
1149
1181
  group_advertisingCommon: "group_advertisingCommon",
1150
1182
  group_platformStylesCommon: "group_platformStylesCommon",
1151
1183
  group_quizCommon: "group_quizCommon",
1184
+ group_backgroundWallpaper: "group_backgroundWallpaper",
1185
+ group_imageNoZoom: "group_imageNoZoom",
1152
1186
  group_resourceBitTags: "group_resourceBitTags",
1153
1187
  group_resourceCommon: "group_resourceCommon",
1154
1188
  group_resourceImageCommon: "group_resourceImageCommon",
1189
+ group_resourceImageCommonNoZoom: "group_resourceImageCommonNoZoom",
1155
1190
  group_resourceAudioCommon: "group_resourceAudioCommon",
1156
1191
  group_resourceVideoCommon: "group_resourceVideoCommon",
1157
1192
  group_resourceIcon: "group_resourceIcon",
1158
1193
  // Alias for image
1159
1194
  group_resourceImage: "group_resourceImage",
1195
+ group_resourceImageNoZoom: "group_resourceImageNoZoom",
1160
1196
  group_resourceImageEmbed: "group_resourceImageEmbed",
1161
1197
  group_resourceImageLink: "group_resourceImageLink",
1162
1198
  group_resourceImageResponsive: "group_resourceImageResponsive",
@@ -1287,6 +1323,7 @@ var propertyKeys = {
1287
1323
  property_formula: "@formula",
1288
1324
  property_fullName: "@fullName",
1289
1325
  property_groupTag: "@groupTag",
1326
+ property_gUri: "@gUri",
1290
1327
  property_handInAcceptFileType: "@handInAcceptFileType",
1291
1328
  property_handInInstruction: "@handInInstruction",
1292
1329
  property_handInRequirement: "@handInRequirement",
@@ -1422,6 +1459,7 @@ var propertyKeys = {
1422
1459
  property_siteName: "@siteName",
1423
1460
  property_size: "@size",
1424
1461
  property_slug: "@slug",
1462
+ property_sourceBB: "@sourceBB",
1425
1463
  property_sourceDocument: "@sourceDocument",
1426
1464
  property_sourceRL: "@sourceRL",
1427
1465
  property_spaceId: "@spaceId",
@@ -1431,6 +1469,7 @@ var propertyKeys = {
1431
1469
  property_src4x: "@src4x",
1432
1470
  property_srcAlt: "@srcAlt",
1433
1471
  property_stripePricingTableId: "@stripePricingTableId",
1472
+ property_stripePricingTableDescriptiveName: "@stripePricingTableDescriptiveName",
1434
1473
  property_stripePublishableKey: "@stripePublishableKey",
1435
1474
  property_subject: "@subject",
1436
1475
  property_subtype: "@subtype",
@@ -2175,25 +2214,159 @@ var CARDSETS = {
2175
2214
  //
2176
2215
  [CardSetConfigKey.flashcard]: {
2177
2216
  jsonKey: "cards",
2217
+ exportJsonKey: { cards: "$" },
2218
+ sides: [
2219
+ {
2220
+ name: "question",
2221
+ variants: [
2222
+ {
2223
+ jsonKey: "question.text",
2224
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { 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: "question.icon|resource(type=image, key=image)",
2239
+ exportJsonKey: { question: { 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 question.",
2249
+ format: TagFormat.bitmarkText,
2250
+ nullable: true
2251
+ }
2252
+ ]
2253
+ }
2254
+ ]
2255
+ },
2256
+ {
2257
+ name: "answer",
2258
+ variants: [
2259
+ {
2260
+ jsonKey: "answer.text",
2261
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2262
+ tags: [
2263
+ {
2264
+ key: ConfigKey.group_standardTags,
2265
+ description: "Standard tags for the flashcard."
2266
+ },
2267
+ {
2268
+ exportJsonKey: { title: "$" },
2269
+ key: ConfigKey.tag_title,
2270
+ description: "Title of the flashcard."
2271
+ },
2272
+ {
2273
+ key: ConfigKey.group_resourceIcon,
2274
+ description: "Icon resource for the flashcard.",
2275
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2276
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2277
+ },
2278
+ {
2279
+ key: ConfigKey.property_example,
2280
+ exportJsonKey: [
2281
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2282
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2283
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2284
+ ],
2285
+ description: "Example marker for the flashcard answer.",
2286
+ format: TagFormat.bitmarkText,
2287
+ nullable: true
2288
+ }
2289
+ ]
2290
+ },
2291
+ {
2292
+ jsonKey: "alternativeAnswers[].text",
2293
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2294
+ tags: [
2295
+ {
2296
+ key: ConfigKey.group_standardTags,
2297
+ description: "Standard tags for the flashcard."
2298
+ },
2299
+ {
2300
+ exportJsonKey: { title: "$" },
2301
+ key: ConfigKey.tag_title,
2302
+ description: "Title of the flashcard."
2303
+ },
2304
+ {
2305
+ key: ConfigKey.group_resourceIcon,
2306
+ description: "Icon resource for the flashcard.",
2307
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2308
+ exportJsonKey: {
2309
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2310
+ }
2311
+ }
2312
+ ],
2313
+ repeatCount: Count.infinity
2314
+ }
2315
+ ]
2316
+ }
2317
+ ]
2318
+ },
2319
+ //
2320
+ // flashcard1 — single-card variant of flashcard. Same shape, but capped
2321
+ // at one card via `sections.default.maxCount = 1` (PLAN-085). Used by
2322
+ // the `flashcard-1` bit type; the multi-card `flashcard` and
2323
+ // `q-and-a-card` bit types continue to use the unbounded `flashcard`
2324
+ // cardset.
2325
+ //
2326
+ [CardSetConfigKey.flashcard1]: {
2327
+ jsonKey: "cards",
2328
+ exportJsonKey: { cards: "$" },
2329
+ sections: {
2330
+ default: {
2331
+ jsonKey: "cards",
2332
+ exportJsonKey: { cards: "$" },
2333
+ isDefault: true,
2334
+ maxCount: 1
2335
+ }
2336
+ },
2178
2337
  sides: [
2179
2338
  {
2180
2339
  name: "question",
2181
2340
  variants: [
2182
2341
  {
2183
2342
  jsonKey: "question.text",
2343
+ exportJsonKey: [{ "@absent": { question: {} } }, { question: { text: "$" } }],
2184
2344
  tags: [
2185
2345
  {
2186
2346
  key: ConfigKey.group_standardTags,
2187
2347
  description: "Standard tags for the flashcard."
2188
2348
  },
2189
2349
  {
2350
+ exportJsonKey: { title: "$" },
2190
2351
  key: ConfigKey.tag_title,
2191
2352
  description: "Title of the flashcard."
2192
2353
  },
2193
2354
  {
2194
2355
  key: ConfigKey.group_resourceIcon,
2195
2356
  description: "Icon resource for the flashcard.",
2196
- jsonKey: "question.icon|resource(type=image, key=image)"
2357
+ jsonKey: "question.icon|resource(type=image, key=image)",
2358
+ exportJsonKey: { question: { 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 question.",
2368
+ format: TagFormat.bitmarkText,
2369
+ nullable: true
2197
2370
  }
2198
2371
  ]
2199
2372
  }
@@ -2204,37 +2377,56 @@ var CARDSETS = {
2204
2377
  variants: [
2205
2378
  {
2206
2379
  jsonKey: "answer.text",
2380
+ exportJsonKey: [{ "@absent": { answer: {} } }, { answer: { text: "$" } }],
2207
2381
  tags: [
2208
2382
  {
2209
2383
  key: ConfigKey.group_standardTags,
2210
2384
  description: "Standard tags for the flashcard."
2211
2385
  },
2212
2386
  {
2387
+ exportJsonKey: { title: "$" },
2213
2388
  key: ConfigKey.tag_title,
2214
2389
  description: "Title of the flashcard."
2215
2390
  },
2216
2391
  {
2217
2392
  key: ConfigKey.group_resourceIcon,
2218
2393
  description: "Icon resource for the flashcard.",
2219
- jsonKey: "answer.icon|resource(type=image, key=image)"
2394
+ jsonKey: "answer.icon|resource(type=image, key=image)",
2395
+ exportJsonKey: { answer: { icon: { type: "image", image: { src: "$" } } } }
2396
+ },
2397
+ {
2398
+ key: ConfigKey.property_example,
2399
+ exportJsonKey: [
2400
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2401
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2402
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2403
+ ],
2404
+ description: "Example marker for the flashcard answer.",
2405
+ format: TagFormat.bitmarkText,
2406
+ nullable: true
2220
2407
  }
2221
2408
  ]
2222
2409
  },
2223
2410
  {
2224
2411
  jsonKey: "alternativeAnswers[].text",
2412
+ exportJsonKey: { alternativeAnswers: [{ text: "$" }] },
2225
2413
  tags: [
2226
2414
  {
2227
2415
  key: ConfigKey.group_standardTags,
2228
2416
  description: "Standard tags for the flashcard."
2229
2417
  },
2230
2418
  {
2419
+ exportJsonKey: { title: "$" },
2231
2420
  key: ConfigKey.tag_title,
2232
2421
  description: "Title of the flashcard."
2233
2422
  },
2234
2423
  {
2235
2424
  key: ConfigKey.group_resourceIcon,
2236
2425
  description: "Icon resource for the flashcard.",
2237
- jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)"
2426
+ jsonKey: "alternativeAnswers[].icon|resource(type=image, key=image)",
2427
+ exportJsonKey: {
2428
+ alternativeAnswers: [{ icon: { type: "image", image: { src: "$" } } }]
2429
+ }
2238
2430
  }
2239
2431
  ],
2240
2432
  repeatCount: Count.infinity
@@ -2248,12 +2440,14 @@ var CARDSETS = {
2248
2440
  //
2249
2441
  [CardSetConfigKey.definitionList]: {
2250
2442
  jsonKey: "definitions",
2443
+ exportJsonKey: { definitions: "$" },
2251
2444
  sides: [
2252
2445
  {
2253
2446
  name: "term",
2254
2447
  variants: [
2255
2448
  {
2256
2449
  jsonKey: "term.text",
2450
+ exportJsonKey: [{ "@absent": { term: {} } }, { term: { text: "$" } }],
2257
2451
  tags: [
2258
2452
  {
2259
2453
  key: ConfigKey.group_standardTags,
@@ -2263,13 +2457,26 @@ var CARDSETS = {
2263
2457
  key: ConfigKey.tag_title,
2264
2458
  description: "Title of the definition.",
2265
2459
  format: TagFormat.plainText,
2266
- jsonKey: "^heading.forKeys"
2460
+ jsonKey: "^heading.forKeys",
2461
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2267
2462
  },
2268
2463
  {
2269
2464
  key: ConfigKey.group_resourceIcon,
2270
2465
  description: "Icon resource for the definition.",
2271
2466
  format: TagFormat.plainText,
2272
- jsonKey: "term.icon|resource(type=image, key=image)"
2467
+ jsonKey: "term.icon|resource(type=image, key=image)",
2468
+ exportJsonKey: { term: { icon: { type: "image", image: { src: "$" } } } }
2469
+ },
2470
+ {
2471
+ key: ConfigKey.property_example,
2472
+ exportJsonKey: [
2473
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2474
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2475
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2476
+ ],
2477
+ description: "Example marker for the definition term.",
2478
+ format: TagFormat.bitmarkText,
2479
+ nullable: true
2273
2480
  }
2274
2481
  ]
2275
2482
  }
@@ -2280,6 +2487,7 @@ var CARDSETS = {
2280
2487
  variants: [
2281
2488
  {
2282
2489
  jsonKey: "definition.text",
2490
+ exportJsonKey: [{ "@absent": { definition: {} } }, { definition: { text: "$" } }],
2283
2491
  tags: [
2284
2492
  {
2285
2493
  key: ConfigKey.group_standardTags,
@@ -2289,30 +2497,48 @@ var CARDSETS = {
2289
2497
  key: ConfigKey.tag_title,
2290
2498
  description: "Title of the definition.",
2291
2499
  format: TagFormat.plainText,
2292
- jsonKey: "^heading.forValues"
2500
+ jsonKey: "^heading.forValues",
2501
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2293
2502
  },
2294
2503
  {
2295
2504
  key: ConfigKey.group_resourceIcon,
2296
2505
  description: "Icon resource for the definition.",
2297
- jsonKey: "definition.icon|resource(type=image, key=image)"
2506
+ jsonKey: "definition.icon|resource(type=image, key=image)",
2507
+ exportJsonKey: { definition: { icon: { type: "image", image: { src: "$" } } } }
2508
+ },
2509
+ {
2510
+ key: ConfigKey.property_example,
2511
+ exportJsonKey: [
2512
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
2513
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
2514
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2515
+ ],
2516
+ description: "Example marker for the definition.",
2517
+ format: TagFormat.bitmarkText,
2518
+ nullable: true
2298
2519
  }
2299
2520
  ]
2300
2521
  },
2301
2522
  {
2302
2523
  jsonKey: "alternativeDefinitions[].text",
2524
+ exportJsonKey: { alternativeDefinitions: [{ text: "$" }] },
2303
2525
  tags: [
2304
2526
  {
2305
2527
  key: ConfigKey.group_standardTags,
2306
2528
  description: "Standard tags for the definition."
2307
2529
  },
2308
2530
  {
2531
+ exportJsonKey: { title: "$" },
2309
2532
  key: ConfigKey.tag_title,
2310
2533
  description: "Title of the definition."
2311
2534
  },
2312
2535
  {
2313
2536
  key: ConfigKey.group_resourceIcon,
2314
2537
  description: "Icon resource for the definition.",
2315
- jsonKey: "alternativeDefinitions[].icon|resource(type=image, key=image)"
2538
+ jsonKey: "alternativeDefinitions[].icon|resource(type=image, key=image)",
2539
+ exportJsonKey: {
2540
+ alternativeDefinitions: [{ icon: { type: "image", image: { src: "$" } } }]
2541
+ }
2316
2542
  }
2317
2543
  ],
2318
2544
  repeatCount: Count.infinity
@@ -2326,37 +2552,96 @@ var CARDSETS = {
2326
2552
  //
2327
2553
  [CardSetConfigKey.matchPairs]: {
2328
2554
  jsonKey: "pairs",
2555
+ exportJsonKey: { pairs: "$" },
2329
2556
  sides: [
2330
2557
  {
2331
2558
  name: "key",
2332
2559
  variants: [
2333
2560
  {
2334
2561
  jsonKey: "key",
2562
+ exportJsonKey: { key: "$" },
2563
+ format: TextFormat2.plainText,
2335
2564
  tags: [
2336
2565
  {
2337
2566
  key: ConfigKey.group_standardTags,
2338
2567
  description: "Standard tags for the match pair."
2339
2568
  },
2569
+ {
2570
+ // Pair-level example: emit `isExample` alongside `example`
2571
+ // (matches OLD parser's per-pair allow-list behaviour).
2572
+ //
2573
+ // PLAN-074: a valued cascade source (`[@example:V]` anywhere
2574
+ // in the cardset) fills each pair's `example` with `V`
2575
+ // once per pair from the keys-side cascade fire (single
2576
+ // parsed_variant per pair — no per-value duplication).
2577
+ //
2578
+ // PLAN-076 NOTE: matchPairs (TAG 415) KEEPS `example: '$'`
2579
+ // at the pair-object level. BPG `parseMatchPairs`
2580
+ // (`CardContentProcessor.ts:714-723`) emits ONE example
2581
+ // per PAIR (the `exampleCard` accumulator), placed at the
2582
+ // pair object. Unlike matchMatrix (per-cell example),
2583
+ // matchPairs has no nested per-cell layer, so the pair-key
2584
+ // `[@example:V]` must double-emit as the visible field.
2585
+ // Confirmed via `chaining` fixture: `match-solution-grouped`
2586
+ // expects `example: "packen"` at pair-object level from
2587
+ // `die Koffer[%1][@example:packen]`. Original PLAN-076
2588
+ // draft applied to both keys-side tags; the matchPairs
2589
+ // half was reverted after fixture parity testing.
2590
+ key: ConfigKey.property_example,
2591
+ exportJsonKey: [
2592
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
2593
+ {
2594
+ predicates: ["@absent", { $cascade: "*" }],
2595
+ rule: {
2596
+ isExample: true,
2597
+ example: "$cascade",
2598
+ "@bit": { isExample: true }
2599
+ }
2600
+ },
2601
+ { "@absent": { isExample: true } },
2602
+ { isExample: true, example: "$", "@bit": { isExample: true } }
2603
+ ],
2604
+ description: "Example marker / value on the match pair.",
2605
+ format: TagFormat.bitmarkText,
2606
+ nullable: true
2607
+ },
2340
2608
  {
2341
2609
  key: ConfigKey.tag_title,
2342
2610
  description: "Title of the match pair.",
2343
2611
  format: TagFormat.plainText,
2344
- jsonKey: "^heading.forKeys"
2612
+ jsonKey: "^heading.forKeys",
2613
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2345
2614
  },
2346
2615
  {
2616
+ // PLAN-084: gate the key-side `@absent → $ancestor` cascade
2617
+ // synth with `@variantLacksTag=isCaseSensitive` so the rule
2618
+ // only fires when NO variant in the active card explicitly
2619
+ // carries `[@isCaseSensitive]`. When a value-side variant
2620
+ // carries the tag (e.g. `[@isCaseSensitive:false]`), the
2621
+ // synth is skipped here; the value-side explicit fire owns
2622
+ // the pair-level emission.
2347
2623
  key: ConfigKey.property_isCaseSensitive,
2624
+ exportJsonKey: [
2625
+ {
2626
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2627
+ rule: { isCaseSensitive: "$ancestor" }
2628
+ },
2629
+ { isCaseSensitive: "$" }
2630
+ ],
2348
2631
  description: "Property to indicate if the match is case sensitive.",
2349
2632
  format: TagFormat.boolean
2350
2633
  },
2351
2634
  {
2352
2635
  key: ConfigKey.resource_audio,
2353
2636
  description: "Audio resource for the match pair.",
2354
- jsonKey: "keyAudio"
2637
+ jsonKey: "keyAudio",
2638
+ exportJsonKey: { keyAudio: "$" }
2355
2639
  },
2356
2640
  {
2357
2641
  key: ConfigKey.resource_image,
2358
2642
  description: "Image resource for the match pair.",
2359
- jsonKey: "keyImage"
2643
+ jsonKey: "keyImage",
2644
+ exportJsonKey: { keyImage: "$" }
2360
2645
  }
2361
2646
  ]
2362
2647
  }
@@ -2368,19 +2653,104 @@ var CARDSETS = {
2368
2653
  variants: [
2369
2654
  {
2370
2655
  jsonKey: "values[]",
2656
+ exportJsonKey: { values: ["$"] },
2657
+ format: TextFormat2.plainText,
2371
2658
  tags: [
2372
2659
  {
2373
- key: ConfigKey.group_standardTags,
2374
- description: "Standard tags for the match pair."
2660
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2661
+ // BPG `parseMatchPairs` does `delete tags.item; delete tags.lead;`
2662
+ // for sideIdx > 0; the schema-side equivalent is to use a
2663
+ // group_standardTags variant that omits group_standardItemLead.
2664
+ key: ConfigKey.group_standardTagsNoItemLead,
2665
+ description: "Standard tags for the match pair (values side, no item/lead)."
2666
+ },
2667
+ {
2668
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.6: cascade-fired
2669
+ // `@example` from the bit header fills each pair's
2670
+ // `example` with the pair's first value (mirrors BPG's
2671
+ // `fillStringExample(pairs, __defaultExample=values[0])`).
2672
+ //
2673
+ // PLAN-074: the cascade-source-VALUE branch is handled by
2674
+ // TAG 415 (keys side) which fires once per pair — keeping
2675
+ // it here would emit `example` twice per pair (one per
2676
+ // values-side parsed_variant) and merge them into a
2677
+ // multi-paragraph array.
2678
+ //
2679
+ // PLAN-075: bare `$parent` reads the parent variant fire's
2680
+ // source `$` value (the current `++`-iteration's value
2681
+ // text). For single-value pairs this matches the old
2682
+ // `$parent.values[0]`; for multi-value pairs with
2683
+ // `[@example]` attached to a non-first value, it correctly
2684
+ // resolves to that value.
2685
+ //
2686
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
2687
+ // routes pair-key `[@example:V]` (a per-pair cascade
2688
+ // source, bound via per-card CascadeCounters scope) and
2689
+ // bit-header `[@example:V]` (cardset cascade) into the
2690
+ // pair-level `example` via `$cascade`. The aggregator in
2691
+ // `populate_variants` fires this rule ONCE per pair (BPG
2692
+ // `parseMatchPairs` `exampleCard` precedence), avoiding
2693
+ // per-iter pollution.
2694
+ key: ConfigKey.property_example,
2695
+ exportJsonKey: [
2696
+ // PLAN-084: bare `[@example]` on a values-side variant fires
2697
+ // once per pair (first-wins, mirrors BPG `parseMatchPairs`
2698
+ // `exampleCard` + `pushDownTree` set-if-null semantic).
2699
+ // `maxEmits: 1` enforced inside the per-pair cascade-counter
2700
+ // scope opened by `build_card_item`.
2701
+ {
2702
+ predicates: ["@keyonly"],
2703
+ maxEmits: 1,
2704
+ rule: {
2705
+ isExample: true,
2706
+ example: "$parent",
2707
+ "@bit": { isExample: true }
2708
+ }
2709
+ },
2710
+ {
2711
+ predicates: ["@absent", { $cascade: "*" }],
2712
+ rule: {
2713
+ isExample: true,
2714
+ example: "$cascade",
2715
+ "@bit": { isExample: true }
2716
+ }
2717
+ },
2718
+ {
2719
+ "@absent": {
2720
+ isExample: true,
2721
+ example: "$parent",
2722
+ "@bit": { isExample: true }
2723
+ }
2724
+ },
2725
+ // PLAN-084: same cap applies to the explicit-value `[@example:V]`
2726
+ // form so per-`++`-iter values-side fires also first-win.
2727
+ {
2728
+ predicates: [],
2729
+ maxEmits: 1,
2730
+ rule: { isExample: true, example: "$", "@bit": { isExample: true } }
2731
+ }
2732
+ ],
2733
+ description: "Example marker / value on the match pair (values side).",
2734
+ format: TagFormat.bitmarkText,
2735
+ nullable: true
2375
2736
  },
2376
2737
  {
2377
2738
  key: ConfigKey.tag_title,
2378
2739
  description: "Title of the match pair.",
2379
2740
  format: TagFormat.plainText,
2380
- jsonKey: "^heading.forValues"
2741
+ jsonKey: "^heading.forValues",
2742
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2381
2743
  },
2382
2744
  {
2745
+ // PLAN-084: symmetric @variantLacksTag gating with the key-side.
2383
2746
  key: ConfigKey.property_isCaseSensitive,
2747
+ exportJsonKey: [
2748
+ {
2749
+ predicates: ["@absent", { "@variantLacksTag": "@isCaseSensitive" }],
2750
+ rule: { isCaseSensitive: "$ancestor" }
2751
+ },
2752
+ { isCaseSensitive: "$" }
2753
+ ],
2384
2754
  description: "Property to indicate if the match is case sensitive.",
2385
2755
  format: TagFormat.boolean
2386
2756
  }
@@ -2396,12 +2766,15 @@ var CARDSETS = {
2396
2766
  //
2397
2767
  [CardSetConfigKey.matchAudioPairs]: {
2398
2768
  jsonKey: "pairs",
2769
+ exportJsonKey: { pairs: "$" },
2399
2770
  sides: [
2400
2771
  {
2401
2772
  name: "key",
2402
2773
  variants: [
2403
2774
  {
2404
2775
  jsonKey: "key",
2776
+ exportJsonKey: { key: "$" },
2777
+ format: TextFormat2.plainText,
2405
2778
  tags: [
2406
2779
  {
2407
2780
  key: ConfigKey.group_standardTags,
@@ -2411,12 +2784,14 @@ var CARDSETS = {
2411
2784
  key: ConfigKey.tag_title,
2412
2785
  description: "Title of the audio match pair.",
2413
2786
  format: TagFormat.plainText,
2414
- jsonKey: "^heading.forKeys"
2787
+ jsonKey: "^heading.forKeys",
2788
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2415
2789
  },
2416
2790
  {
2417
2791
  key: ConfigKey.resource_audio,
2418
2792
  description: "Audio resource for the match pair.",
2419
- jsonKey: "keyAudio"
2793
+ jsonKey: "keyAudio",
2794
+ exportJsonKey: { keyAudio: "$" }
2420
2795
  }
2421
2796
  ]
2422
2797
  }
@@ -2428,16 +2803,20 @@ var CARDSETS = {
2428
2803
  variants: [
2429
2804
  {
2430
2805
  jsonKey: "values[]",
2806
+ exportJsonKey: { values: ["$"] },
2807
+ format: TextFormat2.plainText,
2431
2808
  tags: [
2432
2809
  {
2433
- key: ConfigKey.group_standardTags,
2434
- description: "Standard tags for the audio match pair."
2810
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2811
+ key: ConfigKey.group_standardTagsNoItemLead,
2812
+ description: "Standard tags for the audio match pair (values side, no item/lead)."
2435
2813
  },
2436
2814
  {
2437
2815
  key: ConfigKey.tag_title,
2438
2816
  description: "Title of the audio match pair.",
2439
2817
  format: TagFormat.plainText,
2440
- jsonKey: "^heading.forValues"
2818
+ jsonKey: "^heading.forValues",
2819
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2441
2820
  },
2442
2821
  {
2443
2822
  key: ConfigKey.resource_audio,
@@ -2455,12 +2834,15 @@ var CARDSETS = {
2455
2834
  //
2456
2835
  [CardSetConfigKey.matchImagePairs]: {
2457
2836
  jsonKey: "pairs",
2837
+ exportJsonKey: { pairs: "$" },
2458
2838
  sides: [
2459
2839
  {
2460
2840
  name: "key",
2461
2841
  variants: [
2462
2842
  {
2463
2843
  jsonKey: "key",
2844
+ exportJsonKey: { key: "$" },
2845
+ format: TextFormat2.plainText,
2464
2846
  tags: [
2465
2847
  {
2466
2848
  key: ConfigKey.group_standardTags,
@@ -2470,12 +2852,14 @@ var CARDSETS = {
2470
2852
  key: ConfigKey.tag_title,
2471
2853
  description: "Title of the image match pair.",
2472
2854
  format: TagFormat.plainText,
2473
- jsonKey: "^heading.forKeys"
2855
+ jsonKey: "^heading.forKeys",
2856
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2474
2857
  },
2475
2858
  {
2476
2859
  key: ConfigKey.resource_image,
2477
2860
  description: "Image resource for the match pair.",
2478
- jsonKey: "keyImage"
2861
+ jsonKey: "keyImage",
2862
+ exportJsonKey: { keyImage: "$" }
2479
2863
  }
2480
2864
  ]
2481
2865
  }
@@ -2487,16 +2871,20 @@ var CARDSETS = {
2487
2871
  variants: [
2488
2872
  {
2489
2873
  jsonKey: "values[]",
2874
+ exportJsonKey: { values: ["$"] },
2875
+ format: TextFormat2.plainText,
2490
2876
  tags: [
2491
2877
  {
2492
- key: ConfigKey.group_standardTags,
2493
- description: "Standard tags for the image match pair."
2878
+ // PLAN-084: value-side excludes item/lead/pageNumber/marginNumber.
2879
+ key: ConfigKey.group_standardTagsNoItemLead,
2880
+ description: "Standard tags for the image match pair (values side, no item/lead)."
2494
2881
  },
2495
2882
  {
2496
2883
  key: ConfigKey.tag_title,
2497
2884
  description: "Title of the image match pair.",
2498
2885
  format: TagFormat.plainText,
2499
- jsonKey: "^heading.forValues"
2886
+ jsonKey: "^heading.forValues",
2887
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2500
2888
  },
2501
2889
  {
2502
2890
  key: ConfigKey.resource_image,
@@ -2514,30 +2902,71 @@ var CARDSETS = {
2514
2902
  //
2515
2903
  [CardSetConfigKey.matchMatrix]: {
2516
2904
  jsonKey: "matrix",
2905
+ exportJsonKey: { matrix: "$" },
2517
2906
  sides: [
2518
2907
  {
2519
2908
  name: "key",
2520
2909
  variants: [
2521
2910
  {
2522
2911
  jsonKey: "key",
2912
+ exportJsonKey: { key: "$" },
2913
+ format: TextFormat2.plainText,
2523
2914
  tags: [
2524
2915
  {
2525
2916
  key: ConfigKey.group_standardItemLeadInstructionHint,
2526
2917
  description: "Standard tags for lead, instruction, and hint."
2527
2918
  },
2528
2919
  {
2920
+ // PLAN-076: drop `example: '$'` from valued rule — pair-key
2921
+ // `[@example:V]` is consumed as the per-pair cascade source.
2922
+ // PLAN-077 R14 follow-up: investigation showed the fixture
2923
+ // reference is INCONSISTENT — bits without a cardset-level
2924
+ // cascade (e.g. bit 3 `[@example:Y]should be...`) expect
2925
+ // pair-level `example: V` at pair scope, but bits WITH a
2926
+ // cardset cascade (e.g. bit 8 `[@example:99]` in title-row
2927
+ // PLUS pair-key `[@example:Y]`) expect NO pair-level
2928
+ // example. Emitting `example: $` here satisfies bit 3 but
2929
+ // breaks bit 8; dropping it satisfies bit 8 but breaks bit
2930
+ // 3. Conditional emission would require runtime gating on
2931
+ // the cardset cascade scope state. Left dropped pending a
2932
+ // dedicated plan — accepted divergence on bit 3's pair-
2933
+ // level emission (cells still get the per-cell `example`
2934
+ // correctly via PLAN-077 §4.2 + §4.3).
2529
2935
  key: ConfigKey.property_example,
2936
+ exportJsonKey: [
2937
+ {
2938
+ "@keyonly": {
2939
+ isExample: true,
2940
+ "@bit": { isExample: true },
2941
+ "@card": { isExample: true }
2942
+ }
2943
+ },
2944
+ {
2945
+ "@absent": {
2946
+ isExample: true,
2947
+ "@bit": { isExample: true },
2948
+ "@card": { isExample: true }
2949
+ }
2950
+ },
2951
+ { isExample: true, "@bit": { isExample: true }, "@card": { isExample: true } }
2952
+ ],
2530
2953
  description: "Example text for the match matrix.",
2531
- format: TagFormat.plainText
2954
+ format: TagFormat.plainText,
2955
+ nullable: true
2532
2956
  },
2533
2957
  {
2534
2958
  key: ConfigKey.tag_title,
2535
2959
  description: "Title of the match matrix.",
2536
2960
  format: TagFormat.plainText,
2537
- jsonKey: "^heading.forKeys"
2961
+ jsonKey: "^heading.forKeys",
2962
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2538
2963
  },
2539
2964
  {
2540
2965
  key: ConfigKey.property_isCaseSensitive,
2966
+ exportJsonKey: [
2967
+ { "@absent": { isCaseSensitive: "$ancestor" } },
2968
+ { isCaseSensitive: "$" }
2969
+ ],
2541
2970
  description: "Property to indicate if the match is case sensitive.",
2542
2971
  format: TagFormat.boolean
2543
2972
  }
@@ -2551,24 +2980,103 @@ var CARDSETS = {
2551
2980
  variants: [
2552
2981
  {
2553
2982
  jsonKey: "cells[{s}].values[]",
2983
+ exportJsonKey: [
2984
+ // PLAN-085 / R14: the positional-blank emission for an
2985
+ // effectively-absent cell side fires ONLY when the active
2986
+ // card has at least one `[#]` variant fire. This matches
2987
+ // BPG `extractHeadingCard`'s `isHeading` gate: when no
2988
+ // `[#]` exists anywhere in the title card, BPG returns
2989
+ // undefined and no `heading` is emitted. The phantom
2990
+ // `forValues:['']` placeholder is appropriate only when
2991
+ // the card is being treated as a title-row.
2992
+ {
2993
+ predicates: ["@absent", { "@variantHasTag": "#" }],
2994
+ rule: { "@bit": { heading: { forValues: [""] } } }
2995
+ },
2996
+ { cells: { $s: { values: ["$"] } } }
2997
+ ],
2998
+ format: TextFormat2.plainText,
2554
2999
  tags: [
2555
3000
  {
2556
3001
  key: ConfigKey.group_standardItemLeadInstructionHint,
2557
3002
  description: "Standard tags for lead, instruction, and hint."
2558
3003
  },
2559
3004
  {
3005
+ // PLAN-075: bare `$parent` reads the parent variant fire's
3006
+ // source `$` value (the current `++`-iteration's cell text).
3007
+ // Replaces `$parent.values[0]` which pinned to position 0
3008
+ // regardless of which `++`-value `[@example]` was on.
3009
+ //
3010
+ // PLAN-077: dual `[@absent, { '$cascade': '*' }]` rule
3011
+ // routes pair-key `[@example:V]` (a per-pair cascade
3012
+ // source, bound via per-card CascadeCounters scope) and
3013
+ // bit-header `[@example:V]` (cardset cascade) into the
3014
+ // per-cell `example` via `$cascade`. The aggregator in
3015
+ // `populate_variants` fires this rule ONCE per cell (BPG
3016
+ // `parseMatchMatrix` `exampleSide` precedence), avoiding
3017
+ // per-iter pollution.
3018
+ // @card scope-shift writes matrix-row container isExample.
2560
3019
  key: ConfigKey.property_example,
3020
+ exportJsonKey: [
3021
+ {
3022
+ "@keyonly": {
3023
+ cells: { $s: { isExample: true, example: "$parent" } },
3024
+ "@bit": { isExample: true },
3025
+ "@card": { isExample: true }
3026
+ }
3027
+ },
3028
+ {
3029
+ predicates: ["@absent", { $cascade: "*" }],
3030
+ rule: {
3031
+ cells: { $s: { isExample: true, example: "$cascade" } },
3032
+ "@bit": { isExample: true },
3033
+ "@card": { isExample: true }
3034
+ }
3035
+ },
3036
+ {
3037
+ "@absent": {
3038
+ cells: { $s: { isExample: true, example: "$parent" } },
3039
+ "@bit": { isExample: true },
3040
+ "@card": { isExample: true }
3041
+ }
3042
+ },
3043
+ {
3044
+ cells: { $s: { isExample: true, example: "$" } },
3045
+ "@bit": { isExample: true },
3046
+ "@card": { isExample: true }
3047
+ }
3048
+ ],
2561
3049
  description: "Example text for the match matrix.",
2562
- format: TagFormat.plainText
3050
+ format: TagFormat.bitmarkText,
3051
+ nullable: true
2563
3052
  },
2564
3053
  {
3054
+ // PLAN-085 / R14: bare `[#]` on a value-side emits the
3055
+ // empty-string positional blank into `forValues` so the
3056
+ // array stays aligned with the cell positions (matches BPG
3057
+ // `extractHeadingCard`'s "all value-side headings present"
3058
+ // shape). The `@keyonly` rule is required for two reasons:
3059
+ // 1) it provides an emission for bare `[#]` (the `$` sigil
3060
+ // in the unconditional rule has no source for bare); and
3061
+ // 2) the presence of any `@keyonly` rule disables the
3062
+ // optimised-mode `should_suppress_outcome` strip (see
3063
+ // `forward_emit.rs`) so the resulting `forValues: [""]`
3064
+ // survives the all-natural-defaults check.
2565
3065
  key: ConfigKey.tag_title,
2566
3066
  description: "Title of the match matrix.",
2567
3067
  format: TagFormat.plainText,
2568
- jsonKey: "^heading.forValues"
3068
+ jsonKey: "^heading.forValues",
3069
+ exportJsonKey: [
3070
+ { "@keyonly": { "@bit": { heading: { forValues: [""] } } } },
3071
+ { "@bit": { heading: { forValues: ["$"] } } }
3072
+ ]
2569
3073
  },
2570
3074
  {
2571
3075
  key: ConfigKey.property_isCaseSensitive,
3076
+ exportJsonKey: [
3077
+ { "@absent": { isCaseSensitive: "$ancestor" } },
3078
+ { isCaseSensitive: "$" }
3079
+ ],
2572
3080
  description: "Property to indicate if the match is case sensitive.",
2573
3081
  format: TagFormat.boolean
2574
3082
  }
@@ -2584,28 +3092,61 @@ var CARDSETS = {
2584
3092
  //
2585
3093
  [CardSetConfigKey.statements]: {
2586
3094
  jsonKey: "statements",
3095
+ exportJsonKey: { statements: "$" },
2587
3096
  sides: [
2588
3097
  {
2589
3098
  name: "statement",
2590
3099
  variants: [
2591
3100
  {
2592
3101
  jsonKey: "statement",
3102
+ exportJsonKey: { statement: "$" },
2593
3103
  tags: [
2594
3104
  {
2595
3105
  key: ConfigKey.tag_true,
2596
3106
  description: "Tag for true statements.",
2597
3107
  jsonKey: "statement|set(isCorrect=true)",
3108
+ exportJsonKey: { statement: "$", isCorrect: true },
2598
3109
  maxCount: 1
2599
3110
  },
2600
3111
  {
2601
3112
  key: ConfigKey.tag_false,
2602
3113
  description: "Tag for false statements.",
2603
3114
  jsonKey: "statement|set(isCorrect=false)",
3115
+ exportJsonKey: { statement: "$", isCorrect: false },
2604
3116
  maxCount: 1
2605
3117
  },
2606
3118
  {
2607
3119
  key: ConfigKey.group_standardTags,
2608
3120
  description: "Standard tags for the statement."
3121
+ },
3122
+ {
3123
+ // PLAN-072: cascade-fired `@example` from the bit header
3124
+ // fills each statement entry's `example` with the entry's
3125
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3126
+ // for the statements cardset. Per-array-item strip removes
3127
+ // `example: false` from incorrect entries so only correct
3128
+ // entries retain `example: true`.
3129
+ key: ConfigKey.property_example,
3130
+ exportJsonKey: [
3131
+ {
3132
+ "@keyonly": {
3133
+ isExample: true,
3134
+ example: true,
3135
+ "@bit": { isExample: true }
3136
+ }
3137
+ },
3138
+ {
3139
+ "@absent": {
3140
+ isExample: true,
3141
+ example: "$parent.isCorrect",
3142
+ "@bit": { isExample: true }
3143
+ }
3144
+ },
3145
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3146
+ ],
3147
+ 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`.",
3148
+ format: TagFormat.boolean,
3149
+ nullable: true
2609
3150
  }
2610
3151
  ],
2611
3152
  bodyAllowed: false
@@ -2619,6 +3160,7 @@ var CARDSETS = {
2619
3160
  //
2620
3161
  [CardSetConfigKey.quiz]: {
2621
3162
  jsonKey: "quizzes",
3163
+ exportJsonKey: { quizzes: "$" },
2622
3164
  sides: [
2623
3165
  {
2624
3166
  name: "choices",
@@ -2633,6 +3175,71 @@ var CARDSETS = {
2633
3175
  {
2634
3176
  key: ConfigKey.group_standardTags,
2635
3177
  description: "Standard tags for the quiz."
3178
+ },
3179
+ {
3180
+ // ALIGN-EXAMPLE-CASCADE §3.2: variant-level cascade for
3181
+ // quiz choices is delegated to group_trueFalse's
3182
+ // chain-child `@example` (`+` chain with maxEmits:1 +
3183
+ // `@parent.isCorrect=true`). Variant-level `@absent`
3184
+ // would over-fire on subsequent correct choices and on
3185
+ // incorrect ones — drop it.
3186
+ key: ConfigKey.property_example,
3187
+ exportJsonKey: [
3188
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3189
+ { isExample: true, "@bit": { isExample: true } }
3190
+ ],
3191
+ description: "Example marker on the quiz choice (entry-local only; cascade handled by group_trueFalse chain-children).",
3192
+ format: TagFormat.boolean,
3193
+ nullable: true
3194
+ }
3195
+ ],
3196
+ bodyAllowed: false
3197
+ }
3198
+ ]
3199
+ }
3200
+ ]
3201
+ },
3202
+ //
3203
+ // quiz (responses)
3204
+ //
3205
+ [CardSetConfigKey.quizResponses]: {
3206
+ jsonKey: "quizzes",
3207
+ exportJsonKey: { quizzes: "$" },
3208
+ sides: [
3209
+ {
3210
+ name: "responses",
3211
+ variants: [
3212
+ {
3213
+ jsonKey: null,
3214
+ tags: [
3215
+ {
3216
+ key: ConfigKey.group_trueFalseResponses,
3217
+ description: "Group for true/false questions."
3218
+ },
3219
+ {
3220
+ key: ConfigKey.group_standardTags,
3221
+ description: "Standard tags for the quiz."
3222
+ },
3223
+ {
3224
+ // PLAN-072: cascade-fired `@example` from the bit header
3225
+ // fills each response entry's `example` with the entry's
3226
+ // own `isCorrect`, mirroring BPG's `pushExampleDownTree`
3227
+ // for the quiz-responses cardset (statements-style).
3228
+ key: ConfigKey.property_example,
3229
+ exportJsonKey: [
3230
+ { "@keyonly": { isExample: true, "@bit": { isExample: true } } },
3231
+ {
3232
+ "@absent": {
3233
+ isExample: true,
3234
+ example: "$parent.isCorrect",
3235
+ "@bit": { isExample: true }
3236
+ }
3237
+ },
3238
+ { isExample: true, "@bit": { isExample: true } }
3239
+ ],
3240
+ description: "Example marker on the quiz response; cascade from bit-header fills `example` with the entry\u2019s own `isCorrect`.",
3241
+ format: TagFormat.boolean,
3242
+ nullable: true
2636
3243
  }
2637
3244
  ],
2638
3245
  bodyAllowed: false
@@ -2646,6 +3253,7 @@ var CARDSETS = {
2646
3253
  //
2647
3254
  [CardSetConfigKey.feedback]: {
2648
3255
  jsonKey: "feedbacks",
3256
+ exportJsonKey: { feedbacks: "$" },
2649
3257
  sides: [
2650
3258
  {
2651
3259
  name: "choices",
@@ -2657,6 +3265,73 @@ var CARDSETS = {
2657
3265
  key: ConfigKey.group_standardItemLeadInstructionHint,
2658
3266
  description: "Standard tags for lead, instruction, and hint."
2659
3267
  },
3268
+ {
3269
+ // Feedback choices: `[+]` / `[-]` map to choice + `requireReason`
3270
+ // (NOT `isCorrect` like trueFalse). Shadow the inherited
3271
+ // `tag_true` / `tag_false` from `group_trueFalse` to emit the
3272
+ // feedback-specific shape.
3273
+ //
3274
+ // Chain @example: `[+ X][@example]` writes
3275
+ // `isExample/example` onto the X choice only. BPG's
3276
+ // `setIsExampleFlags` (Builder.ts:4041-4172) does NOT
3277
+ // iterate `cardNode.feedbacks` — so unlike multipleChoice,
3278
+ // there is no bubble to bit-level or feedbacks-level
3279
+ // isExample. Pattern omits `@bit` scope-shift accordingly.
3280
+ // No `@absent`/`@parent.isCorrect` cascade rules either:
3281
+ // BPG's `pushExampleDownTree` (Builder.ts:3777-3849) has
3282
+ // no feedback dispatch, so bit-header `[@example]` does
3283
+ // not flow into feedback choices.
3284
+ key: ConfigKey.tag_true,
3285
+ exportJsonKey: { choices: [{ choice: "$", requireReason: true }] },
3286
+ description: "Reason-required choice for feedback (`[+]`).",
3287
+ maxCount: Count.infinity,
3288
+ format: TagFormat.plainText,
3289
+ chain: [
3290
+ {
3291
+ key: ConfigKey.property_example,
3292
+ exportJsonKey: [
3293
+ { "@keyonly": { isExample: true, example: true } },
3294
+ { isExample: true, example: "$" }
3295
+ ],
3296
+ description: "Per-choice example marker for feedback `[+]`.",
3297
+ format: TagFormat.boolean,
3298
+ maxCount: 1,
3299
+ nullable: true
3300
+ },
3301
+ {
3302
+ // `[+ X][! foo]` chains `!` (instruction) onto the
3303
+ // choice; same for `[?]` (hint), `[%]` (item),
3304
+ // `[%2]`/`[%3]`/`[%4]` (lead/pageNumber/marginNumber).
3305
+ // Mirrors group_trueFalse's `+` chain shape.
3306
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3307
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3308
+ }
3309
+ ]
3310
+ },
3311
+ {
3312
+ key: ConfigKey.tag_false,
3313
+ exportJsonKey: { choices: [{ choice: "$", requireReason: false }] },
3314
+ description: "Reason-not-required choice for feedback (`[-]`).",
3315
+ maxCount: Count.infinity,
3316
+ format: TagFormat.plainText,
3317
+ chain: [
3318
+ {
3319
+ key: ConfigKey.property_example,
3320
+ exportJsonKey: [
3321
+ { "@keyonly": { isExample: true, example: true } },
3322
+ { isExample: true, example: "$" }
3323
+ ],
3324
+ description: "Per-choice example marker for feedback `[-]`.",
3325
+ format: TagFormat.boolean,
3326
+ maxCount: 1,
3327
+ nullable: true
3328
+ },
3329
+ {
3330
+ key: ConfigKey.group_standardItemLeadInstructionHint,
3331
+ description: "Item, lead, page number, margin number, instruction and hint chained onto the feedback choice."
3332
+ }
3333
+ ]
3334
+ },
2660
3335
  {
2661
3336
  key: ConfigKey.group_trueFalse,
2662
3337
  description: "Group for true/false feedback."
@@ -2665,7 +3340,8 @@ var CARDSETS = {
2665
3340
  key: ConfigKey.tag_title,
2666
3341
  description: "Title of the feedback.",
2667
3342
  format: TagFormat.plainText,
2668
- jsonKey: "^heading.forKeys"
3343
+ jsonKey: "^heading.forKeys",
3344
+ exportJsonKey: { "@bit": { heading: { forKeys: "$" } } }
2669
3345
  }
2670
3346
  ],
2671
3347
  bodyAllowed: false
@@ -2676,27 +3352,78 @@ var CARDSETS = {
2676
3352
  name: "reason",
2677
3353
  variants: [
2678
3354
  {
3355
+ // PLAN: feedback.reason — Object-only variant pattern. Per
3356
+ // SYNTAX.md §8.1, sibling tag emissions do NOT auto-wrap
3357
+ // under `reason.*`; each tag here spells out the full path.
3358
+ // Body is a plain string (BPG `parseFeedback` uses
3359
+ // `cardBodyStr ?? ''` for `reason.text`).
2679
3360
  jsonKey: "reason.text",
3361
+ exportJsonKey: { reason: { text: "$" } },
3362
+ format: TextFormat2.plainText,
2680
3363
  tags: [
2681
3364
  {
2682
- key: ConfigKey.group_standardItemLeadInstructionHint,
2683
- description: "Standard tags for lead, instruction, and hint."
3365
+ // Instruction lands under reason. Hint/item/lead/etc.
3366
+ // omitted: not exercised by any fixture and BPG's
3367
+ // `parseFeedback` spreads only the tags actually present
3368
+ // — adding the rest can be done lazily as fixtures
3369
+ // require them, with the same `{reason: {…}}` wrapper.
3370
+ key: ConfigKey.tag_instruction,
3371
+ exportJsonKey: { reason: { instruction: "$" } },
3372
+ description: "Instruction for the feedback reason.",
3373
+ format: TagFormat.bitmarkText
2684
3374
  },
2685
3375
  {
2686
3376
  key: ConfigKey.property_reasonableNumOfChars,
3377
+ exportJsonKey: [
3378
+ { "@absent": { reason: { reasonableNumOfChars: "$ancestor" } } },
3379
+ { reason: { reasonableNumOfChars: "$" } }
3380
+ ],
2687
3381
  description: "Property for reasonable number of characters.",
2688
3382
  format: TagFormat.number
2689
3383
  },
2690
3384
  {
3385
+ // ALIGN-EXAMPLE-CASCADE §3.9: `feedback.reason` is NOT in
3386
+ // BPG's `pushExampleDownTree` dispatch — no bit-header
3387
+ // cascade. Local emission only.
3388
+ //
3389
+ // Both `reason.example` and `feedbacks[].example` are
3390
+ // `TextAst` per BPG's schema — paragraph arrays. Format
3391
+ // here is `bitmarkText` so `$` coerces to a paragraph
3392
+ // array at both write sites.
2691
3393
  key: ConfigKey.property_example,
2692
- description: "Example text for the feedback.",
2693
- format: TagFormat.plainText
3394
+ exportJsonKey: [
3395
+ {
3396
+ // Bare `[@example]`: reason.example is TextAst — BPG
3397
+ // coerces the boolean default to a paragraph
3398
+ // containing the string "true". feedbacks[].example
3399
+ // is not TextAst, stays a boolean. Two literal writes.
3400
+ "@keyonly": {
3401
+ reason: {
3402
+ isExample: true,
3403
+ example: [{ type: "paragraph", content: [{ text: "true", type: "text" }] }]
3404
+ },
3405
+ isExample: true,
3406
+ example: true,
3407
+ "@bit": { isExample: true }
3408
+ }
3409
+ },
3410
+ {
3411
+ reason: { isExample: true, example: "$" },
3412
+ isExample: true,
3413
+ example: "$",
3414
+ "@bit": { isExample: true }
3415
+ }
3416
+ ],
3417
+ description: "Example text for the feedback reason (entry-local only; no bit-header cascade).",
3418
+ format: TagFormat.bitmarkText,
3419
+ nullable: true
2694
3420
  },
2695
3421
  {
2696
3422
  key: ConfigKey.tag_title,
2697
3423
  description: "Title of the feedback.",
2698
3424
  format: TagFormat.plainText,
2699
- jsonKey: "^heading.forValues"
3425
+ jsonKey: "^heading.forValues",
3426
+ exportJsonKey: { "@bit": { heading: { forValues: "$" } } }
2700
3427
  }
2701
3428
  ],
2702
3429
  bodyAllowed: true
@@ -2710,21 +3437,29 @@ var CARDSETS = {
2710
3437
  //
2711
3438
  [CardSetConfigKey.questions]: {
2712
3439
  jsonKey: "questions",
3440
+ exportJsonKey: { questions: "$" },
2713
3441
  sides: [
2714
3442
  {
2715
3443
  name: "question",
2716
3444
  variants: [
2717
3445
  {
2718
3446
  jsonKey: "question",
3447
+ exportJsonKey: { question: "$" },
2719
3448
  tags: [
2720
3449
  {
2721
3450
  key: ConfigKey.property_reasonableNumOfChars,
3451
+ exportJsonKey: [
3452
+ { "@absent": { reasonableNumOfChars: "$ancestor" } },
3453
+ { reasonableNumOfChars: "$" }
3454
+ ],
2722
3455
  description: "Property for reasonable number of characters.",
2723
3456
  format: TagFormat.number
2724
3457
  },
2725
3458
  {
3459
+ exportJsonKey: { sampleSolution: "$" },
2726
3460
  key: ConfigKey.tag_sampleSolution,
2727
- description: "Sample solution for the question."
3461
+ description: "Sample solution for the question.",
3462
+ format: TagFormat.plainText
2728
3463
  },
2729
3464
  {
2730
3465
  key: ConfigKey.property_sampleSolution,
@@ -2745,6 +3480,22 @@ var CARDSETS = {
2745
3480
  {
2746
3481
  key: ConfigKey.group_standardTags,
2747
3482
  description: "Standard tags for the question."
3483
+ },
3484
+ {
3485
+ // ALIGN-EXAMPLE-CASCADE §3.10: `questions` is NOT in BPG's
3486
+ // `pushExampleDownTree` dispatch — bit-header `[@example]`
3487
+ // does not propagate `example`/`isExample` onto question
3488
+ // entries. Only entry-local `[@example]` fires (handled by
3489
+ // `@keyonly` and the unconditional fallback). No `@absent`
3490
+ // rule.
3491
+ key: ConfigKey.property_example,
3492
+ exportJsonKey: [
3493
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3494
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3495
+ ],
3496
+ description: "Example marker for the question (entry-local only; no bit-header cascade).",
3497
+ format: TagFormat.bitmarkText,
3498
+ nullable: true
2748
3499
  }
2749
3500
  ]
2750
3501
  }
@@ -2764,15 +3515,23 @@ var CARDSETS = {
2764
3515
  variants: [
2765
3516
  {
2766
3517
  jsonKey: "elements[]",
3518
+ exportJsonKey: { elements: ["$"] },
2767
3519
  tags: [
2768
3520
  {
2769
3521
  key: ConfigKey.group_standardItemLeadInstructionHint,
2770
3522
  description: "Standard tags for lead, instruction, and hint."
2771
3523
  },
2772
3524
  {
3525
+ // ALIGN-EXAMPLE-CASCADE §3.11: `elements` is NOT in BPG's
3526
+ // `pushExampleDownTree` dispatch — no bit-header cascade.
2773
3527
  key: ConfigKey.property_example,
2774
- description: "Example text for the element.",
2775
- format: TagFormat.plainText
3528
+ exportJsonKey: [
3529
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3530
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3531
+ ],
3532
+ description: "Example text for the element (entry-local only; no bit-header cascade).",
3533
+ format: TagFormat.plainText,
3534
+ nullable: true
2776
3535
  }
2777
3536
  ],
2778
3537
  repeatCount: Count.infinity
@@ -2784,8 +3543,57 @@ var CARDSETS = {
2784
3543
  //
2785
3544
  // table
2786
3545
  //
3546
+ // PLAN-079: supports BOTH source formats and collapses to the same basic
3547
+ // output (`{ columns?, data, columnWidths? }`):
3548
+ //
3549
+ // 1. Basic format — first card carries `[#X]` markers (fire `tag_title`
3550
+ // → `^table.columns[]`); subsequent cards are body rows. Cards have
3551
+ // no qualifier, so they route to the `table-body` (default) section.
3552
+ //
3553
+ // 2. Extended format — `==== table-header ====` / `==== table-footer ====`
3554
+ // qualifiers. The `table-header` section uses `@cardIndex=0` to route
3555
+ // the first header card's cells to `columns` and demote remaining
3556
+ // header cards (rows) into `data`. Per-cell `[@tableRowSpan]` /
3557
+ // `[@tableScope]` / `[@tableCellType]` tags are valid-but-unmapped
3558
+ // here (no per-cell metadata in basic shape) — declared on the cell
3559
+ // variant below.
3560
+ //
2787
3561
  [CardSetConfigKey.table]: {
2788
- jsonKey: "table.data",
3562
+ jsonKey: null,
3563
+ exportJsonKey: null,
3564
+ sections: {
3565
+ "table-header": {
3566
+ // First header card's row of cells → `columns`. Subsequent header
3567
+ // cards' rows → `data` (per-card append; `[$]` shape because
3568
+ // per-card emission fires once per card, multiple fires deep_merge-
3569
+ // append). PLAN-079 + PLAN-080: predicates inside exportJsonKey
3570
+ // both ROUTE cards to this section (when no explicit `==== table-
3571
+ // header ====` qualifier) and SELECT the rule body once routed.
3572
+ // First rule = first card with any `[#]` → `columns`; second rule
3573
+ // = subsequent cards in this section → `data` (per-row append).
3574
+ jsonKey: "table.columns",
3575
+ exportJsonKey: [
3576
+ {
3577
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3578
+ rule: { "@bit": { table: { columns: "$" } } }
3579
+ },
3580
+ { "@cardIndex=0": { "@bit": { table: { columns: "$" } } } },
3581
+ { "@bit": { table: { data: ["$"] } } }
3582
+ ]
3583
+ },
3584
+ "table-body": {
3585
+ // Batched emission: `$` is the entire 2D array of body card rows.
3586
+ // This is the default section (no qualifier → cards land here).
3587
+ jsonKey: "table.data",
3588
+ exportJsonKey: { "@bit": { table: { data: "$" } } },
3589
+ isDefault: true
3590
+ },
3591
+ "table-footer": {
3592
+ // Batched emission: `$` is the entire 2D array of footer rows.
3593
+ jsonKey: "table.data",
3594
+ exportJsonKey: { "@bit": { table: { data: "$" } } }
3595
+ }
3596
+ },
2789
3597
  sides: [
2790
3598
  {
2791
3599
  name: "cell",
@@ -2801,6 +3609,7 @@ var CARDSETS = {
2801
3609
  {
2802
3610
  key: ConfigKey.tag_title,
2803
3611
  jsonKey: "^table.columns[]",
3612
+ exportJsonKey: { "@bit": { table: { columns: ["$"] } } },
2804
3613
  description: "Title of the table."
2805
3614
  },
2806
3615
  {
@@ -2825,6 +3634,8 @@ var CARDSETS = {
2825
3634
  },
2826
3635
  {
2827
3636
  key: ConfigKey.property_tableColWidth,
3637
+ jsonKey: "^table.columnWidths[{s}]",
3638
+ exportJsonKey: { "@bit": { table: { columnWidths: { $s: "$" } } } },
2828
3639
  description: "Width for the column.",
2829
3640
  format: TagFormat.number
2830
3641
  }
@@ -2841,18 +3652,49 @@ var CARDSETS = {
2841
3652
  [CardSetConfigKey.tableExtended]: {
2842
3653
  jsonKey: null,
2843
3654
  sections: {
2844
- "table-header": { jsonKey: "table.header.rows", sideJsonKey: "cells[{s}]|set(title=true)" },
2845
- "table-body": { jsonKey: "table.body.rows", isDefault: true },
2846
- "table-footer": { jsonKey: "table.footer.rows", sideJsonKey: "cells[{s}]|set(title=true)" }
3655
+ "table-header": {
3656
+ jsonKey: "table.header.rows",
3657
+ // PLAN-080: section dispatch via predicates inside exportJsonKey.
3658
+ // Cards with no explicit `==== table-header ====` qualifier
3659
+ // route here if FIRST CARD contains any `[#]` tag in any
3660
+ // variant. Explicit-qualifier cards fall through to the
3661
+ // unconditional fallback rule. Both rules use the per-card
3662
+ // append shape `rows: ['$']` — the runtime triggers per-card
3663
+ // emission whenever the pattern carries predicates, and each
3664
+ // fire appends a single-element array (multi-fire deep-merge
3665
+ // concatenates).
3666
+ exportJsonKey: [
3667
+ {
3668
+ predicates: [{ "@cardIndex": 0 }, { "@variantHasTag": "#" }],
3669
+ rule: { table: { header: { rows: ["$"] } } }
3670
+ },
3671
+ { table: { header: { rows: ["$"] } } }
3672
+ ],
3673
+ sideJsonKey: "cells[{s}]|set(title=true)",
3674
+ sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
3675
+ },
3676
+ "table-body": {
3677
+ jsonKey: "table.body.rows",
3678
+ exportJsonKey: { table: { body: { rows: "$" } } },
3679
+ isDefault: true
3680
+ },
3681
+ "table-footer": {
3682
+ jsonKey: "table.footer.rows",
3683
+ exportJsonKey: { table: { footer: { rows: "$" } } },
3684
+ sideJsonKey: "cells[{s}]|set(title=true)",
3685
+ sideExportJsonKey: { cells: { $s: { title: true, $: "$" } } }
3686
+ }
2847
3687
  },
2848
3688
  sides: [
2849
3689
  {
2850
3690
  name: "cell",
2851
3691
  repeat: true,
2852
3692
  jsonKey: "cells[{s}]",
3693
+ exportJsonKey: { cells: { $s: "$" } },
2853
3694
  variants: [
2854
3695
  {
2855
3696
  jsonKey: "content",
3697
+ exportJsonKey: { content: "$" },
2856
3698
  tags: [
2857
3699
  {
2858
3700
  key: ConfigKey.group_standardItemLeadInstructionHint,
@@ -2860,35 +3702,47 @@ var CARDSETS = {
2860
3702
  },
2861
3703
  {
2862
3704
  key: ConfigKey.tag_title,
2863
- jsonKey: ".",
2864
- description: "Title of the table cell."
3705
+ // PLAN-080: the `[#]` tag writes its text to the cell's
3706
+ // `content` field. When the card is in the `table-header`
3707
+ // section, the section's sideExportJsonKey wraps each
3708
+ // cell with `title: true` automatically.
3709
+ jsonKey: "content",
3710
+ exportJsonKey: { content: "$" },
3711
+ description: "Title text of the table cell (header rows)."
2865
3712
  },
2866
3713
  {
2867
3714
  key: ConfigKey.property_tableCellType,
2868
3715
  jsonKey: "title|bool(th)",
3716
+ exportJsonKey: [{ "@value=th": { title: true } }],
2869
3717
  description: "Table cell type (th/td).",
2870
3718
  format: TagFormat.plainText
2871
3719
  },
2872
3720
  {
2873
3721
  key: ConfigKey.property_tableRowSpan,
2874
3722
  jsonKey: "rowspan",
3723
+ exportJsonKey: { rowspan: "$" },
2875
3724
  description: "Number of rows the cell spans.",
2876
3725
  format: TagFormat.number
2877
3726
  },
2878
3727
  {
2879
3728
  key: ConfigKey.property_tableColSpan,
2880
3729
  jsonKey: "colspan",
3730
+ exportJsonKey: { colspan: "$" },
2881
3731
  description: "Number of columns the cell spans.",
2882
3732
  format: TagFormat.number
2883
3733
  },
2884
3734
  {
2885
3735
  key: ConfigKey.property_tableScope,
2886
3736
  jsonKey: "scope",
3737
+ exportJsonKey: { scope: "$" },
2887
3738
  description: "Scope attribute for header cells.",
2888
3739
  format: TagFormat.plainText
2889
3740
  },
2890
3741
  {
2891
3742
  key: ConfigKey.property_tableColWidth,
3743
+ // PLAN-080: write to the cell's `colwidth` field.
3744
+ jsonKey: "colwidth",
3745
+ exportJsonKey: { colwidth: "$" },
2892
3746
  description: "Width for the column.",
2893
3747
  format: TagFormat.number
2894
3748
  }
@@ -2904,6 +3758,7 @@ var CARDSETS = {
2904
3758
  //
2905
3759
  [CardSetConfigKey.pronunciationTable]: {
2906
3760
  jsonKey: "pronunciationTable.data",
3761
+ exportJsonKey: { pronunciationTable: { data: "$" } },
2907
3762
  sides: [
2908
3763
  {
2909
3764
  name: "cell",
@@ -2911,19 +3766,22 @@ var CARDSETS = {
2911
3766
  variants: [
2912
3767
  {
2913
3768
  jsonKey: "body",
3769
+ exportJsonKey: { body: "$" },
2914
3770
  tags: [
2915
3771
  {
2916
3772
  key: ConfigKey.group_standardItemLeadInstructionHint,
2917
3773
  description: "Standard tags for lead, instruction, and hint."
2918
3774
  },
2919
3775
  {
3776
+ exportJsonKey: { title: "$" },
2920
3777
  key: ConfigKey.tag_title,
2921
3778
  description: "Title of the pronunciation table."
2922
3779
  },
2923
3780
  {
2924
3781
  key: ConfigKey.resource_audio,
2925
3782
  description: "Audio resource for the pronunciation table.",
2926
- jsonKey: "audio"
3783
+ jsonKey: "audio",
3784
+ exportJsonKey: { audio: { type: "audio", audio: { src: "$" } } }
2927
3785
  }
2928
3786
  ],
2929
3787
  repeatCount: Count.infinity
@@ -2937,12 +3795,15 @@ var CARDSETS = {
2937
3795
  //
2938
3796
  [CardSetConfigKey.botActionResponses]: {
2939
3797
  jsonKey: "responses",
3798
+ exportJsonKey: { responses: "$" },
2940
3799
  sides: [
2941
3800
  {
2942
3801
  name: "response",
2943
3802
  variants: [
2944
3803
  {
2945
3804
  jsonKey: "feedback",
3805
+ exportJsonKey: { feedback: "$" },
3806
+ format: TextFormat2.plainText,
2946
3807
  tags: [
2947
3808
  {
2948
3809
  key: ConfigKey.property_reaction,
@@ -2950,18 +3811,58 @@ var CARDSETS = {
2950
3811
  format: TagFormat.plainText
2951
3812
  },
2952
3813
  {
2953
- key: ConfigKey.group_standardItemLead,
2954
- description: "Item, lead, page number, and margin number for the bot action response."
3814
+ key: ConfigKey.tag_item,
3815
+ jsonKey: "item",
3816
+ exportJsonKey: { item: "$" },
3817
+ format: TagFormat.plainText,
3818
+ description: "The item for the bit",
3819
+ chain: [
3820
+ {
3821
+ key: ConfigKey.tag_item,
3822
+ jsonKey: "lead",
3823
+ exportJsonKey: { lead: "$" },
3824
+ description: "The lead for the bit",
3825
+ maxCount: 1,
3826
+ chain: [
3827
+ {
3828
+ key: ConfigKey.tag_item,
3829
+ jsonKey: "pageNumber",
3830
+ exportJsonKey: { pageNumber: "$" },
3831
+ description: "The page number for the bit",
3832
+ maxCount: 1,
3833
+ chain: [
3834
+ {
3835
+ key: ConfigKey.tag_item,
3836
+ jsonKey: "marginNumber",
3837
+ exportJsonKey: { marginNumber: "$" },
3838
+ description: "The margin number for the bit",
3839
+ maxCount: 1
3840
+ }
3841
+ ]
3842
+ }
3843
+ ]
3844
+ }
3845
+ ]
2955
3846
  },
2956
3847
  {
2957
3848
  key: ConfigKey.tag_instruction,
2958
3849
  description: "The response label for the bot action.",
2959
- jsonKey: "response"
3850
+ jsonKey: "response",
3851
+ exportJsonKey: { response: "$" },
3852
+ format: TagFormat.plainText
2960
3853
  },
2961
3854
  {
3855
+ // ALIGN-EXAMPLE-CASCADE §3.12: `bot-action-responses` is
3856
+ // NOT in BPG's `pushExampleDownTree` dispatch — no bit-
3857
+ // header cascade.
2962
3858
  key: ConfigKey.property_example,
2963
- description: "Example text for the bot action response.",
2964
- format: TagFormat.plainText
3859
+ exportJsonKey: [
3860
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
3861
+ { isExample: true, example: "$", "@bit": { isExample: true } }
3862
+ ],
3863
+ description: "Example text for the bot action response (entry-local only; no bit-header cascade).",
3864
+ format: TagFormat.plainText,
3865
+ nullable: true
2965
3866
  }
2966
3867
  ]
2967
3868
  }
@@ -2974,12 +3875,14 @@ var CARDSETS = {
2974
3875
  //
2975
3876
  [CardSetConfigKey.clozeList]: {
2976
3877
  jsonKey: "listItems",
3878
+ exportJsonKey: { listItems: "$" },
2977
3879
  sides: [
2978
3880
  {
2979
3881
  name: "content",
2980
3882
  variants: [
2981
3883
  {
2982
3884
  jsonKey: "body",
3885
+ exportJsonKey: { body: "$" },
2983
3886
  tags: [
2984
3887
  {
2985
3888
  key: ConfigKey.group_standardTags,
@@ -3000,18 +3903,21 @@ var CARDSETS = {
3000
3903
  //
3001
3904
  [CardSetConfigKey.exampleBitList]: {
3002
3905
  jsonKey: "listItems",
3906
+ exportJsonKey: { listItems: "$" },
3003
3907
  sides: [
3004
3908
  {
3005
3909
  name: "item",
3006
3910
  variants: [
3007
3911
  {
3008
3912
  jsonKey: "body",
3913
+ exportJsonKey: { body: "$" },
3009
3914
  tags: [
3010
3915
  {
3011
3916
  key: ConfigKey.group_standardTags,
3012
3917
  description: "Standard tags for example bits."
3013
3918
  },
3014
3919
  {
3920
+ exportJsonKey: { title: "$" },
3015
3921
  key: ConfigKey.tag_title,
3016
3922
  description: "Title of the example bit."
3017
3923
  }
@@ -3022,18 +3928,53 @@ var CARDSETS = {
3022
3928
  ]
3023
3929
  },
3024
3930
  //
3931
+ // page-footer-sections — mirrors example-bit-list but emits cards under
3932
+ // `sections` instead of `listItems`. BPG's JsonGenerator special-cases
3933
+ // BitType.pageFooter in code; this dedicated cardSet expresses the same
3934
+ // routing as pure config so the Rust serializer can pick it up.
3935
+ //
3936
+ [CardSetConfigKey.pageFooterSections]: {
3937
+ jsonKey: "sections",
3938
+ exportJsonKey: { sections: "$" },
3939
+ sides: [
3940
+ {
3941
+ name: "item",
3942
+ variants: [
3943
+ {
3944
+ jsonKey: "body",
3945
+ exportJsonKey: { body: "$" },
3946
+ tags: [
3947
+ {
3948
+ key: ConfigKey.group_standardTags,
3949
+ description: "Standard tags for page-footer section bits."
3950
+ },
3951
+ {
3952
+ exportJsonKey: { title: "$" },
3953
+ key: ConfigKey.tag_title,
3954
+ description: "Title of the page-footer section."
3955
+ }
3956
+ ]
3957
+ }
3958
+ ]
3959
+ }
3960
+ ]
3961
+ },
3962
+ //
3025
3963
  // ingredients
3026
3964
  //
3027
3965
  [CardSetConfigKey.ingredients]: {
3028
3966
  jsonKey: "ingredients",
3967
+ exportJsonKey: { ingredients: "$" },
3029
3968
  sides: [
3030
3969
  {
3031
3970
  name: "ingredient",
3032
3971
  variants: [
3033
3972
  {
3034
3973
  jsonKey: "ingredient",
3974
+ exportJsonKey: { ingredient: "$" },
3035
3975
  tags: [
3036
3976
  {
3977
+ exportJsonKey: { title: "$" },
3037
3978
  key: ConfigKey.tag_title,
3038
3979
  description: "Title of the ingredient."
3039
3980
  },
@@ -3041,6 +3982,7 @@ var CARDSETS = {
3041
3982
  key: ConfigKey.tag_true,
3042
3983
  description: "Checked state for the ingredient.",
3043
3984
  jsonKey: "|set(checked=true)",
3985
+ exportJsonKey: { checked: true, $: "$" },
3044
3986
  maxCount: 1
3045
3987
  // nullable: true,
3046
3988
  },
@@ -3048,6 +3990,7 @@ var CARDSETS = {
3048
3990
  key: ConfigKey.tag_false,
3049
3991
  description: "Unchecked state for the ingredient.",
3050
3992
  jsonKey: "|set(checked=false)",
3993
+ exportJsonKey: { checked: false, $: "$" },
3051
3994
  maxCount: 1
3052
3995
  // nullable: true,
3053
3996
  },
@@ -3059,9 +4002,11 @@ var CARDSETS = {
3059
4002
  key: ConfigKey.tag_instruction,
3060
4003
  description: "The quantity of the ingredient.",
3061
4004
  jsonKey: "quantity",
4005
+ exportJsonKey: { quantity: "$" },
3062
4006
  format: TagFormat.number
3063
4007
  },
3064
4008
  {
4009
+ exportJsonKey: { hint: "$" },
3065
4010
  key: ConfigKey.tag_hint,
3066
4011
  description: "The hint for the ingredient."
3067
4012
  },
@@ -3097,6 +4042,7 @@ var CARDSETS = {
3097
4042
  //
3098
4043
  [CardSetConfigKey.bookReferenceList]: {
3099
4044
  jsonKey: "bookReferences",
4045
+ exportJsonKey: { bookReferences: "$" },
3100
4046
  sides: [
3101
4047
  {
3102
4048
  name: "reference",
@@ -3156,6 +4102,11 @@ var GROUPS = {
3156
4102
  format: TagFormat.plainText,
3157
4103
  maxCount: Count.infinity
3158
4104
  },
4105
+ {
4106
+ key: ConfigKey.property_gUri,
4107
+ description: "Global URI for the bit",
4108
+ format: TagFormat.plainText
4109
+ },
3159
4110
  {
3160
4111
  key: ConfigKey.property_customerId,
3161
4112
  description: "The customer-specific identifier for the bit",
@@ -3339,12 +4290,18 @@ var GROUPS = {
3339
4290
  {
3340
4291
  key: ConfigKey.property_groupTag,
3341
4292
  jsonKey: "groupTag.name",
4293
+ // PLAN-063: starter-array shape with `@id` declares entity-merge
4294
+ // semantics — repeated `[@groupTag: name]` occurrences with the
4295
+ // same value fold into one entry; chained `@tag` children's
4296
+ // arrays merge as sets.
4297
+ exportJsonKey: { groupTag: [{ "@id": "name", name: "$" }] },
3342
4298
  description: "The group tag(s) for the bit",
3343
4299
  format: TagFormat.plainText,
3344
4300
  maxCount: Count.infinity,
3345
4301
  chain: [
3346
4302
  {
3347
4303
  key: ConfigKey.property_tag,
4304
+ exportJsonKey: { tags: ["$"] },
3348
4305
  description: "The tag(s) for the group",
3349
4306
  format: TagFormat.plainText,
3350
4307
  maxCount: Count.infinity
@@ -3375,6 +4332,12 @@ var GROUPS = {
3375
4332
  format: TagFormat.plainText,
3376
4333
  maxCount: Count.infinity
3377
4334
  },
4335
+ {
4336
+ key: ConfigKey.property_sourceBB,
4337
+ description: "The source bounding box(es) for the bit (x, y, x1, y1)",
4338
+ format: TagFormat.coordinates,
4339
+ maxCount: Count.infinity
4340
+ },
3378
4341
  {
3379
4342
  key: ConfigKey.property_levelCEFRp,
3380
4343
  description: "The CEFRp level for the bit",
@@ -3412,6 +4375,7 @@ var GROUPS = {
3412
4375
  maxCount: Count.infinity
3413
4376
  },
3414
4377
  {
4378
+ exportJsonKey: { anchor: "$" },
3415
4379
  key: ConfigKey.tag_anchor,
3416
4380
  name: "Anchor",
3417
4381
  format: TagFormat.plainText,
@@ -3449,23 +4413,27 @@ var GROUPS = {
3449
4413
  {
3450
4414
  key: ConfigKey.tag_item,
3451
4415
  jsonKey: "item",
4416
+ exportJsonKey: { item: "$" },
3452
4417
  description: "The item for the bit",
3453
4418
  chain: [
3454
4419
  {
3455
4420
  key: ConfigKey.tag_item,
3456
4421
  jsonKey: "lead",
4422
+ exportJsonKey: { lead: "$" },
3457
4423
  description: "The lead for the bit",
3458
4424
  maxCount: 1,
3459
4425
  chain: [
3460
4426
  {
3461
4427
  key: ConfigKey.tag_item,
3462
4428
  jsonKey: "pageNumber",
4429
+ exportJsonKey: { pageNumber: "$" },
3463
4430
  description: "The page number for the bit",
3464
4431
  maxCount: 1,
3465
4432
  chain: [
3466
4433
  {
3467
4434
  key: ConfigKey.tag_item,
3468
4435
  jsonKey: "marginNumber",
4436
+ exportJsonKey: { marginNumber: "$" },
3469
4437
  description: "The margin number for the bit",
3470
4438
  maxCount: 1
3471
4439
  }
@@ -3486,11 +4454,31 @@ var GROUPS = {
3486
4454
  description: "Item, lead, page number, and margin number tags"
3487
4455
  },
3488
4456
  {
4457
+ exportJsonKey: { instruction: "$" },
4458
+ key: ConfigKey.tag_instruction,
4459
+ name: "Instruction",
4460
+ description: "The instruction for the bit"
4461
+ },
4462
+ {
4463
+ exportJsonKey: { hint: "$" },
4464
+ key: ConfigKey.tag_hint,
4465
+ name: "Hint",
4466
+ description: "The hint for the bit"
4467
+ }
4468
+ ]
4469
+ },
4470
+ [ConfigKey.group_standardInstructionHint]: {
4471
+ type: GroupConfigType.standard,
4472
+ 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).",
4473
+ tags: [
4474
+ {
4475
+ exportJsonKey: { instruction: "$" },
3489
4476
  key: ConfigKey.tag_instruction,
3490
4477
  name: "Instruction",
3491
4478
  description: "The instruction for the bit"
3492
4479
  },
3493
4480
  {
4481
+ exportJsonKey: { hint: "$" },
3494
4482
  key: ConfigKey.tag_hint,
3495
4483
  name: "Hint",
3496
4484
  description: "The hint for the bit"
@@ -3510,9 +4498,45 @@ var GROUPS = {
3510
4498
  description: "Item, lead, page number, margin number, instruction and hint tags"
3511
4499
  },
3512
4500
  {
4501
+ // Bit-level example tag (default — non-`isTopLevelExample` bits):
4502
+ // - bare `[@example]`: consume only (records cascade marker, emits
4503
+ // nothing at bit-level — OLD parser scrubs bit-level isExample
4504
+ // for bits NOT in its `isTopLevelExample` allow-list).
4505
+ // - synthesized (descendant cascade): still emits `isExample: true`
4506
+ // at the descendant scope.
4507
+ // - valued `[@example:text]`: emit the example value only; bit-level
4508
+ // `isExample` is left to bubble from descendants.
4509
+ // Bits in the OLD parser's allow-list (flashcard, definitionList,
4510
+ // sequence, multipleChoice, trueFalse, cloze*, mark, interview,
4511
+ // matchMatrix, match, essay, etc.) override this entry with the
4512
+ // legacy emit-isExample shape.
3513
4513
  key: ConfigKey.property_example,
4514
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
3514
4515
  description: "The example(s) for the bit",
3515
- format: TagFormat.plainText
4516
+ format: TagFormat.bitmarkText,
4517
+ nullable: true
4518
+ }
4519
+ ]
4520
+ },
4521
+ [ConfigKey.group_standardTagsNoItemLead]: {
4522
+ type: GroupConfigType.standard,
4523
+ 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).",
4524
+ tags: [
4525
+ {
4526
+ key: ConfigKey.group_standardAllBits,
4527
+ description: "All standard tags which apply to all bits"
4528
+ },
4529
+ {
4530
+ key: ConfigKey.group_standardInstructionHint,
4531
+ description: "Instruction and hint tags only (no item/lead chain)."
4532
+ },
4533
+ {
4534
+ // Mirror of group_standardTags property_example entry.
4535
+ key: ConfigKey.property_example,
4536
+ exportJsonKey: [{ "@keyonly": {} }, { "@absent": { isExample: true } }, { example: "$" }],
4537
+ description: "The example(s) for the bit",
4538
+ format: TagFormat.bitmarkText,
4539
+ nullable: true
3516
4540
  }
3517
4541
  ]
3518
4542
  },
@@ -3523,6 +4547,7 @@ var GROUPS = {
3523
4547
  {
3524
4548
  key: ConfigKey.property_imageSource,
3525
4549
  jsonKey: "imageSource.url",
4550
+ exportJsonKey: { imageSource: { url: "$" } },
3526
4551
  description: "The source of an image",
3527
4552
  format: TagFormat.plainText,
3528
4553
  chain: [
@@ -3557,6 +4582,7 @@ var GROUPS = {
3557
4582
  {
3558
4583
  key: ConfigKey.property_technicalTerm,
3559
4584
  jsonKey: "technicalTerm.technicalTerm",
4585
+ exportJsonKey: { technicalTerm: { technicalTerm: "$" } },
3560
4586
  description: "The technical term",
3561
4587
  format: TagFormat.plainText,
3562
4588
  chain: [
@@ -3575,7 +4601,8 @@ var GROUPS = {
3575
4601
  tags: [
3576
4602
  {
3577
4603
  key: ConfigKey.property_person,
3578
- jsonKey: "partner.name",
4604
+ jsonKey: "person.name",
4605
+ exportJsonKey: { person: { name: "$" } },
3579
4606
  description: "A person",
3580
4607
  format: TagFormat.plainText,
3581
4608
  chain: [
@@ -3587,6 +4614,7 @@ var GROUPS = {
3587
4614
  {
3588
4615
  key: ConfigKey.group_resourceImage,
3589
4616
  jsonKey: "avatarImage|resource(type=image, key=image)",
4617
+ exportJsonKey: { avatarImage: { type: "image", image: { src: "$" } } },
3590
4618
  description: "The image of the person"
3591
4619
  }
3592
4620
  ]
@@ -3594,7 +4622,8 @@ var GROUPS = {
3594
4622
  {
3595
4623
  // Deprecated (parter renamed to person)
3596
4624
  key: ConfigKey.property_partner,
3597
- jsonKey: "partner.name",
4625
+ jsonKey: "person.name",
4626
+ exportJsonKey: { person: { name: "$" } },
3598
4627
  description: "A partner",
3599
4628
  format: TagFormat.plainText,
3600
4629
  chain: [
@@ -3606,6 +4635,7 @@ var GROUPS = {
3606
4635
  {
3607
4636
  key: ConfigKey.group_resourceImage,
3608
4637
  jsonKey: "avatarImage|resource(type=image, key=image)",
4638
+ exportJsonKey: { avatarImage: { type: "image", image: { src: "$" } } },
3609
4639
  description: "The image of the partner"
3610
4640
  }
3611
4641
  ]
@@ -3617,29 +4647,66 @@ var GROUPS = {
3617
4647
  description: "Gap chain",
3618
4648
  tags: [
3619
4649
  {
4650
+ exportJsonKey: {
4651
+ "@body-inline": { type: "gap", attrs: { solutions: ["$"] } }
4652
+ },
3620
4653
  key: ConfigKey.tag_gap,
3621
4654
  description: "The value of a gap in the content",
3622
4655
  maxCount: Count.infinity,
4656
+ format: TagFormat.plainText,
3623
4657
  chain: [
3624
4658
  {
3625
4659
  key: ConfigKey.tag_gap,
3626
4660
  jsonKey: "solutions[]",
4661
+ exportJsonKey: { solutions: ["$"] },
3627
4662
  description: "Alternative values for the gaps in the content",
3628
- maxCount: Count.infinity
4663
+ maxCount: Count.infinity,
4664
+ format: TagFormat.plainText
3629
4665
  },
3630
4666
  {
3631
4667
  key: ConfigKey.group_standardItemLeadInstructionHint,
3632
4668
  description: "Item, lead, page number, margin number, instruction and hint tags"
3633
4669
  },
3634
4670
  {
4671
+ // BPG ExampleTagContentProcessor.ts:91-95 reads
4672
+ // `__solutionsAst[length - 1]` — the solution closest to the
4673
+ // `[@example]` tag at parse time. Our chain accumulator is built
4674
+ // incrementally (scope.rs `parent.merge` after each tag), so at
4675
+ // the moment `[@example]` fires, `solutions` only contains the
4676
+ // preceding `[_x]` entries — `[-1]` resolves to the closest one.
4677
+ // E.g. `[_cloze][_gap][@example]` → solutions=["cloze","gap"],
4678
+ // `[-1]`="gap"; `[_solution][@example][_problem]` →
4679
+ // solutions=["solution"], `[-1]`="solution".
3635
4680
  key: ConfigKey.property_example,
3636
4681
  jsonKey: "example",
4682
+ exportJsonKey: [
4683
+ {
4684
+ "@keyonly": {
4685
+ isExample: true,
4686
+ example: "$parent.solutions[-1]",
4687
+ "@bit": { isExample: true }
4688
+ }
4689
+ },
4690
+ {
4691
+ "@absent": {
4692
+ isExample: true,
4693
+ example: "$parent.solutions[-1]",
4694
+ "@bit": { isExample: true }
4695
+ }
4696
+ },
4697
+ { isExample: true, example: "$", "@bit": { isExample: true } }
4698
+ ],
3637
4699
  description: "An example for the gap",
3638
- format: TagFormat.plainText
4700
+ format: TagFormat.bitmarkText,
4701
+ nullable: true
3639
4702
  },
3640
4703
  {
3641
4704
  key: ConfigKey.property_isCaseSensitive,
3642
4705
  jsonKey: "isCaseSensitive",
4706
+ exportJsonKey: [
4707
+ { "@absent": { isCaseSensitive: "$ancestor" } },
4708
+ { isCaseSensitive: "$" }
4709
+ ],
3643
4710
  description: "If true, the gap text is case sensitive",
3644
4711
  format: TagFormat.boolean,
3645
4712
  defaultValue: "true"
@@ -3653,60 +4720,587 @@ var GROUPS = {
3653
4720
  description: "True/False chain",
3654
4721
  tags: [
3655
4722
  {
4723
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
3656
4724
  key: ConfigKey.tag_true,
3657
4725
  description: "True value for a true/false statement/question",
3658
4726
  maxCount: Count.infinity,
4727
+ format: TagFormat.plainText,
3659
4728
  chain: [
3660
4729
  {
3661
4730
  key: ConfigKey.tag_true,
3662
4731
  jsonKey: "choices[]|set(isCorrect=true)",
4732
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
3663
4733
  description: "True values for a true/false statement/question",
3664
- maxCount: Count.infinity
4734
+ maxCount: Count.infinity,
4735
+ format: TagFormat.plainText
3665
4736
  },
3666
4737
  {
3667
4738
  key: ConfigKey.tag_false,
3668
4739
  jsonKey: "choices[]|set(isCorrect=false)",
4740
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
3669
4741
  description: "False values for a true/false statement/question",
3670
- maxCount: Count.infinity
4742
+ maxCount: Count.infinity,
4743
+ format: TagFormat.plainText
3671
4744
  },
3672
4745
  {
3673
4746
  key: ConfigKey.group_standardItemLeadInstructionHint,
3674
4747
  description: "Item, lead, page number, margin number, instruction and hint tags"
3675
4748
  },
3676
4749
  {
4750
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4751
+ // `@example` from the bit header marks EVERY choice with
4752
+ // `isExample: true` (matches BPG output), but only the
4753
+ // FIRST correct choice gets `example: true` (BPG
4754
+ // `fillBooleanExample(..., firstCorrectOnly=true)`).
4755
+ //
4756
+ // Two `@absent` rules, picked first-match-wins:
4757
+ // 1. Verbose form gated on `@parent.isCorrect=true` with
4758
+ // `maxEmits:1` — fires once for the first correct.
4759
+ // 2. Plain `@absent` — fires on every remaining entry
4760
+ // (subsequent corrects and all incorrects), emitting
4761
+ // `isExample` only.
4762
+ // The `serialize_cards` cascade-counter scope holds the cap
4763
+ // across the whole cardset.
4764
+ // @card scope-shift writes choice-container isExample (e.g.
4765
+ // `quizzes[i].isExample`) — was previously provided by the
4766
+ // universal `bubble_is_example` post-walk, which was removed
4767
+ // in favour of explicit per-tag config. SYNTAX.md §8.
3677
4768
  key: ConfigKey.property_example,
3678
4769
  jsonKey: "example",
4770
+ exportJsonKey: [
4771
+ {
4772
+ "@keyonly": {
4773
+ isExample: true,
4774
+ example: "$parent.isCorrect",
4775
+ "@bit": { isExample: true },
4776
+ "@card": { isExample: true }
4777
+ }
4778
+ },
4779
+ {
4780
+ predicates: ["@absent", { "@parent.isCorrect": true }],
4781
+ maxEmits: 1,
4782
+ rule: {
4783
+ isExample: true,
4784
+ example: true,
4785
+ "@bit": { isExample: true },
4786
+ "@card": { isExample: true }
4787
+ }
4788
+ },
4789
+ {
4790
+ "@absent": {
4791
+ isExample: true,
4792
+ "@bit": { isExample: true },
4793
+ "@card": { isExample: true }
4794
+ }
4795
+ },
4796
+ {
4797
+ isExample: true,
4798
+ example: "$",
4799
+ "@bit": { isExample: true },
4800
+ "@card": { isExample: true }
4801
+ }
4802
+ ],
3679
4803
  description: "An example for the true/false statement/question",
3680
- format: TagFormat.plainText
4804
+ format: TagFormat.boolean,
4805
+ nullable: true
3681
4806
  }
3682
4807
  ]
3683
4808
  },
3684
4809
  {
4810
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
3685
4811
  key: ConfigKey.tag_false,
3686
4812
  description: "False value for a true/false statement/question",
3687
4813
  maxCount: Count.infinity,
4814
+ format: TagFormat.plainText,
3688
4815
  chain: [
3689
4816
  {
3690
4817
  key: ConfigKey.tag_true,
3691
4818
  jsonKey: "choices[]|set(isCorrect=true)",
4819
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: true }] },
3692
4820
  description: "True values for a true/false statement/question",
3693
- maxCount: Count.infinity
4821
+ maxCount: Count.infinity,
4822
+ format: TagFormat.plainText
3694
4823
  },
3695
4824
  {
3696
4825
  key: ConfigKey.tag_false,
3697
4826
  jsonKey: "choices[]|set(isCorrect=false)",
4827
+ exportJsonKey: { choices: [{ choice: "$", isCorrect: false }] },
3698
4828
  description: "False values for a true/false statement/question",
3699
- maxCount: Count.infinity
4829
+ maxCount: Count.infinity,
4830
+ format: TagFormat.plainText
4831
+ },
4832
+ {
4833
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4834
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4835
+ },
4836
+ {
4837
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.2: cascade-fired
4838
+ // `@example` on a `-` (incorrect choice) emits
4839
+ // `isExample: true` only — no `example` (BPG fixtures show
4840
+ // `isExample` on every choice, but `example` only on the
4841
+ // first correct via firstCorrectOnly cascade).
4842
+ // @card scope-shift writes choice-container isExample.
4843
+ key: ConfigKey.property_example,
4844
+ jsonKey: "example",
4845
+ exportJsonKey: [
4846
+ {
4847
+ "@keyonly": {
4848
+ isExample: true,
4849
+ example: "$parent.isCorrect",
4850
+ "@bit": { isExample: true },
4851
+ "@card": { isExample: true }
4852
+ }
4853
+ },
4854
+ {
4855
+ "@absent": {
4856
+ isExample: true,
4857
+ "@bit": { isExample: true },
4858
+ "@card": { isExample: true }
4859
+ }
4860
+ },
4861
+ {
4862
+ isExample: true,
4863
+ example: "$",
4864
+ "@bit": { isExample: true },
4865
+ "@card": { isExample: true }
4866
+ }
4867
+ ],
4868
+ description: "An example for the true/false statement/question (incorrect entry \u2014 isExample only on cascade)",
4869
+ format: TagFormat.boolean,
4870
+ nullable: true
4871
+ }
4872
+ ]
4873
+ }
4874
+ ]
4875
+ },
4876
+ [ConfigKey.group_trueFalseResponses]: {
4877
+ type: GroupConfigType.standard,
4878
+ description: "True/False Responses chain",
4879
+ tags: [
4880
+ {
4881
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4882
+ key: ConfigKey.tag_true,
4883
+ description: "True value for a true/false statement/question",
4884
+ maxCount: Count.infinity,
4885
+ format: TagFormat.plainText,
4886
+ chain: [
4887
+ {
4888
+ key: ConfigKey.tag_true,
4889
+ jsonKey: "responses[]|set(isCorrect=true)",
4890
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4891
+ description: "True values for a true/false statement/question",
4892
+ maxCount: Count.infinity,
4893
+ format: TagFormat.plainText
4894
+ },
4895
+ {
4896
+ key: ConfigKey.tag_false,
4897
+ jsonKey: "responses[]|set(isCorrect=false)",
4898
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4899
+ description: "False values for a true/false statement/question",
4900
+ maxCount: Count.infinity,
4901
+ format: TagFormat.plainText
4902
+ },
4903
+ {
4904
+ key: ConfigKey.group_standardItemLeadInstructionHint,
4905
+ description: "Item, lead, page number, margin number, instruction and hint tags"
4906
+ },
4907
+ {
4908
+ // @card scope-shift writes response-container isExample.
4909
+ key: ConfigKey.property_example,
4910
+ jsonKey: "example",
4911
+ exportJsonKey: [
4912
+ {
4913
+ "@keyonly": {
4914
+ isExample: true,
4915
+ example: "$parent.isCorrect",
4916
+ "@bit": { isExample: true },
4917
+ "@card": { isExample: true }
4918
+ }
4919
+ },
4920
+ {
4921
+ "@absent": {
4922
+ isExample: "$parent.isCorrect",
4923
+ example: "$parent.isCorrect",
4924
+ "@bit": { isExample: "$parent.isCorrect" },
4925
+ "@card": { isExample: "$parent.isCorrect" }
4926
+ }
4927
+ },
4928
+ {
4929
+ isExample: true,
4930
+ example: "$",
4931
+ "@bit": { isExample: true },
4932
+ "@card": { isExample: true }
4933
+ }
4934
+ ],
4935
+ description: "An example for the true/false statement/question",
4936
+ format: TagFormat.boolean,
4937
+ nullable: true
4938
+ }
4939
+ ]
4940
+ },
4941
+ {
4942
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4943
+ key: ConfigKey.tag_false,
4944
+ description: "False value for a true/false statement/question",
4945
+ maxCount: Count.infinity,
4946
+ format: TagFormat.plainText,
4947
+ chain: [
4948
+ {
4949
+ key: ConfigKey.tag_true,
4950
+ jsonKey: "responses[]|set(isCorrect=true)",
4951
+ exportJsonKey: { responses: [{ response: "$", isCorrect: true }] },
4952
+ description: "True values for a true/false statement/question",
4953
+ maxCount: Count.infinity,
4954
+ format: TagFormat.plainText
4955
+ },
4956
+ {
4957
+ key: ConfigKey.tag_false,
4958
+ jsonKey: "responses[]|set(isCorrect=false)",
4959
+ exportJsonKey: { responses: [{ response: "$", isCorrect: false }] },
4960
+ description: "False values for a true/false statement/question",
4961
+ maxCount: Count.infinity,
4962
+ format: TagFormat.plainText
3700
4963
  },
3701
4964
  {
3702
4965
  key: ConfigKey.group_standardItemLeadInstructionHint,
3703
4966
  description: "Item, lead, page number, margin number, instruction and hint tags"
3704
4967
  },
3705
4968
  {
4969
+ // @card scope-shift writes response-container isExample.
3706
4970
  key: ConfigKey.property_example,
3707
4971
  jsonKey: "example",
4972
+ exportJsonKey: [
4973
+ {
4974
+ "@keyonly": {
4975
+ isExample: true,
4976
+ example: "$parent.isCorrect",
4977
+ "@bit": { isExample: true },
4978
+ "@card": { isExample: true }
4979
+ }
4980
+ },
4981
+ {
4982
+ "@absent": {
4983
+ isExample: true,
4984
+ example: "$parent.isCorrect",
4985
+ "@bit": { isExample: true },
4986
+ "@card": { isExample: true }
4987
+ }
4988
+ },
4989
+ {
4990
+ isExample: true,
4991
+ example: "$",
4992
+ "@bit": { isExample: true },
4993
+ "@card": { isExample: true }
4994
+ }
4995
+ ],
3708
4996
  description: "An example for the true/false statement/question",
4997
+ format: TagFormat.boolean,
4998
+ nullable: true
4999
+ }
5000
+ ]
5001
+ }
5002
+ ]
5003
+ },
5004
+ [ConfigKey.group_trueFalseInlineSelect]: {
5005
+ type: GroupConfigType.standard,
5006
+ description: "True/False chain composed inline (select body-bit). +/- tags are consume-only at bit level.",
5007
+ tags: [
5008
+ {
5009
+ exportJsonKey: {
5010
+ "@body-inline": {
5011
+ type: "select",
5012
+ attrs: { options: [{ text: "$", isCorrect: true }] }
5013
+ }
5014
+ },
5015
+ key: ConfigKey.tag_true,
5016
+ description: "True value for an inline select body bit",
5017
+ maxCount: Count.infinity,
5018
+ format: TagFormat.plainText,
5019
+ chain: [
5020
+ {
5021
+ key: ConfigKey.tag_true,
5022
+ exportJsonKey: {},
5023
+ description: "True values for an inline select body bit",
5024
+ maxCount: Count.infinity,
5025
+ format: TagFormat.plainText
5026
+ },
5027
+ {
5028
+ key: ConfigKey.tag_false,
5029
+ exportJsonKey: {},
5030
+ description: "False values for an inline select body bit",
5031
+ maxCount: Count.infinity,
5032
+ format: TagFormat.plainText
5033
+ },
5034
+ {
5035
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5036
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5037
+ },
5038
+ {
5039
+ // PLAN-083: cascade-fired `@example` from bit header fires
5040
+ // `isExample:true, example:true` on the FIRST CORRECT option
5041
+ // only (BPG `fillBooleanExample(select.options,
5042
+ // firstCorrectOnly=true)`). Mirrors group_trueFalseInlineHighlight.
5043
+ // Every rule also bubbles `@bit:{isExample:true}` (idempotent
5044
+ // OR — any cascade ⇒ bit marked) and
5045
+ // `@body-inline:{attrs:{isExample:true}}` (node-level marker
5046
+ // on the select node).
5047
+ key: ConfigKey.property_example,
5048
+ jsonKey: "example",
5049
+ exportJsonKey: [
5050
+ {
5051
+ "@keyonly": {
5052
+ isExample: true,
5053
+ example: "$parent.isCorrect",
5054
+ "@bit": { isExample: true },
5055
+ "@body-inline": { attrs: { isExample: true } }
5056
+ }
5057
+ },
5058
+ {
5059
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5060
+ maxEmits: 1,
5061
+ rule: {
5062
+ isExample: true,
5063
+ example: true,
5064
+ "@bit": { isExample: true },
5065
+ "@body-inline": { attrs: { isExample: true } }
5066
+ }
5067
+ },
5068
+ {
5069
+ isExample: true,
5070
+ example: "$",
5071
+ "@bit": { isExample: true },
5072
+ "@body-inline": { attrs: { isExample: true } }
5073
+ }
5074
+ ],
5075
+ description: "An example for the inline select option (first correct only)",
5076
+ format: TagFormat.boolean,
5077
+ nullable: true
5078
+ }
5079
+ ]
5080
+ },
5081
+ {
5082
+ exportJsonKey: {
5083
+ "@body-inline": {
5084
+ type: "select",
5085
+ attrs: { options: [{ text: "$", isCorrect: false }] }
5086
+ }
5087
+ },
5088
+ key: ConfigKey.tag_false,
5089
+ description: "False value for an inline select body bit",
5090
+ maxCount: Count.infinity,
5091
+ format: TagFormat.plainText,
5092
+ chain: [
5093
+ {
5094
+ key: ConfigKey.tag_true,
5095
+ exportJsonKey: {},
5096
+ description: "True values for an inline select body bit",
5097
+ maxCount: Count.infinity,
5098
+ format: TagFormat.plainText
5099
+ },
5100
+ {
5101
+ key: ConfigKey.tag_false,
5102
+ exportJsonKey: {},
5103
+ description: "False values for an inline select body bit",
5104
+ maxCount: Count.infinity,
5105
+ format: TagFormat.plainText
5106
+ },
5107
+ {
5108
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5109
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5110
+ },
5111
+ {
5112
+ // PLAN-083: identical to the `+` lead's chain @example —
5113
+ // cascade reaches the first correct option regardless of
5114
+ // which lead (+/-) opens the chain. Every firing rule bubbles
5115
+ // `@bit:{isExample:true}` and `@body-inline:{attrs:{isExample:true}}`.
5116
+ key: ConfigKey.property_example,
5117
+ jsonKey: "example",
5118
+ exportJsonKey: [
5119
+ {
5120
+ "@keyonly": {
5121
+ isExample: true,
5122
+ example: "$parent.isCorrect",
5123
+ "@bit": { isExample: true },
5124
+ "@body-inline": { attrs: { isExample: true } }
5125
+ }
5126
+ },
5127
+ {
5128
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5129
+ maxEmits: 1,
5130
+ rule: {
5131
+ isExample: true,
5132
+ example: true,
5133
+ "@bit": { isExample: true },
5134
+ "@body-inline": { attrs: { isExample: true } }
5135
+ }
5136
+ },
5137
+ {
5138
+ isExample: true,
5139
+ example: "$",
5140
+ "@bit": { isExample: true },
5141
+ "@body-inline": { attrs: { isExample: true } }
5142
+ }
5143
+ ],
5144
+ description: "An example for the inline select option (first correct only)",
5145
+ format: TagFormat.boolean,
5146
+ nullable: true
5147
+ }
5148
+ ]
5149
+ }
5150
+ ]
5151
+ },
5152
+ [ConfigKey.group_trueFalseInlineHighlight]: {
5153
+ type: GroupConfigType.standard,
5154
+ 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.',
5155
+ tags: [
5156
+ {
5157
+ exportJsonKey: {
5158
+ "@body-inline": {
5159
+ type: "highlight",
5160
+ attrs: { texts: [{ text: "$", isCorrect: true }] }
5161
+ }
5162
+ },
5163
+ key: ConfigKey.tag_true,
5164
+ description: "Highlighted span in the body (correct)",
5165
+ maxCount: Count.infinity,
5166
+ format: TagFormat.plainText,
5167
+ chain: [
5168
+ {
5169
+ key: ConfigKey.tag_true,
5170
+ exportJsonKey: {},
5171
+ description: "Highlighted span values (correct)",
5172
+ maxCount: Count.infinity,
5173
+ format: TagFormat.plainText
5174
+ },
5175
+ {
5176
+ key: ConfigKey.tag_false,
5177
+ exportJsonKey: {},
5178
+ description: "Highlighted span values (incorrect)",
5179
+ maxCount: Count.infinity,
5180
+ format: TagFormat.plainText
5181
+ },
5182
+ {
5183
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5184
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5185
+ },
5186
+ {
5187
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5188
+ // `@example` from the bit header fires `example: true` +
5189
+ // `isExample: true` on the FIRST correct highlight only
5190
+ // (BPG `fillBooleanExample(highlight.texts,
5191
+ // firstCorrectOnly=true)`). Incorrect highlights get
5192
+ // nothing (unlike choices, the highlight-text fixture
5193
+ // shows no `isExample` on incorrect entries).
5194
+ key: ConfigKey.property_example,
5195
+ jsonKey: "example",
5196
+ exportJsonKey: [
5197
+ {
5198
+ "@keyonly": {
5199
+ isExample: true,
5200
+ example: "$parent.isCorrect",
5201
+ "@bit": { isExample: true },
5202
+ "@body-inline": { attrs: { isExample: true } }
5203
+ }
5204
+ },
5205
+ {
5206
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5207
+ maxEmits: 1,
5208
+ rule: {
5209
+ isExample: true,
5210
+ example: true,
5211
+ "@bit": { isExample: true },
5212
+ "@body-inline": { attrs: { isExample: true } }
5213
+ }
5214
+ },
5215
+ {
5216
+ isExample: true,
5217
+ example: "$",
5218
+ "@bit": { isExample: true },
5219
+ "@body-inline": { attrs: { isExample: true } }
5220
+ }
5221
+ ],
5222
+ description: "An example for the highlighted span (first correct only)",
5223
+ format: TagFormat.boolean,
5224
+ nullable: true
5225
+ }
5226
+ ]
5227
+ },
5228
+ {
5229
+ // PLAN-083 follow-up (2026-05-22): mirror the select `-` lead shape
5230
+ // by writing `isCorrect: false`. Without it, `$parent.isCorrect` in
5231
+ // chain-child @example rules resolves to ParentPathMissing for `-`
5232
+ // entries (e.g. `[-fish][@example]`), so the @keyonly rule falls
5233
+ // through and nothing fires. The natural-default strip removes
5234
+ // `isCorrect: false` from the final texts entries (matches fixture).
5235
+ exportJsonKey: {
5236
+ "@body-inline": {
5237
+ type: "highlight",
5238
+ attrs: { texts: [{ text: "$", isCorrect: false }] }
5239
+ }
5240
+ },
5241
+ key: ConfigKey.tag_false,
5242
+ description: "Highlighted span in the body (incorrect)",
5243
+ maxCount: Count.infinity,
5244
+ format: TagFormat.plainText,
5245
+ chain: [
5246
+ {
5247
+ key: ConfigKey.tag_true,
5248
+ exportJsonKey: {},
5249
+ description: "Highlighted span values (correct)",
5250
+ maxCount: Count.infinity,
3709
5251
  format: TagFormat.plainText
5252
+ },
5253
+ {
5254
+ key: ConfigKey.tag_false,
5255
+ exportJsonKey: {},
5256
+ description: "Highlighted span values (incorrect)",
5257
+ maxCount: Count.infinity,
5258
+ format: TagFormat.plainText
5259
+ },
5260
+ {
5261
+ key: ConfigKey.group_standardItemLeadInstructionHint,
5262
+ description: "Item, lead, page number, margin number, instruction and hint tags"
5263
+ },
5264
+ {
5265
+ // PLAN-072 + ALIGN-EXAMPLE-CASCADE §3.13: cascade-fired
5266
+ // `@example` on a `-` (incorrect highlight) emits nothing
5267
+ // (highlight-text fixture has no `isExample` on incorrect
5268
+ // entries; firstCorrectOnly cascade skips them entirely).
5269
+ // No `@absent` rule.
5270
+ // PLAN-083: identical to the `+` lead's chain @example —
5271
+ // cascade reaches the first correct option regardless of
5272
+ // which lead (+/-) opens the chain.
5273
+ key: ConfigKey.property_example,
5274
+ jsonKey: "example",
5275
+ exportJsonKey: [
5276
+ {
5277
+ "@keyonly": {
5278
+ isExample: true,
5279
+ example: "$parent.isCorrect",
5280
+ "@bit": { isExample: true },
5281
+ "@body-inline": { attrs: { isExample: true } }
5282
+ }
5283
+ },
5284
+ {
5285
+ predicates: ["@absent", { "@parent.isCorrect": true }],
5286
+ maxEmits: 1,
5287
+ rule: {
5288
+ isExample: true,
5289
+ example: true,
5290
+ "@bit": { isExample: true },
5291
+ "@body-inline": { attrs: { isExample: true } }
5292
+ }
5293
+ },
5294
+ {
5295
+ isExample: true,
5296
+ example: "$",
5297
+ "@bit": { isExample: true },
5298
+ "@body-inline": { attrs: { isExample: true } }
5299
+ }
5300
+ ],
5301
+ description: "An example for the highlighted span (first correct only)",
5302
+ format: TagFormat.boolean,
5303
+ nullable: true
3710
5304
  }
3711
5305
  ]
3712
5306
  }
@@ -3719,6 +5313,7 @@ var GROUPS = {
3719
5313
  {
3720
5314
  key: ConfigKey.property_mark,
3721
5315
  jsonKey: "marks.mark",
5316
+ exportJsonKey: { marks: [{ mark: "$" }] },
3722
5317
  description: "The mark configuration",
3723
5318
  format: TagFormat.plainText,
3724
5319
  maxCount: Count.infinity,
@@ -3742,21 +5337,32 @@ var GROUPS = {
3742
5337
  description: "Mark chain",
3743
5338
  tags: [
3744
5339
  {
5340
+ exportJsonKey: {
5341
+ "@body-inline": { type: "mark", attrs: { solution: "$" } }
5342
+ },
3745
5343
  key: ConfigKey.tag_mark,
3746
5344
  description: "The marked content",
3747
5345
  maxCount: Count.infinity,
5346
+ format: TagFormat.plainText,
3748
5347
  chain: [
3749
5348
  {
3750
5349
  key: ConfigKey.property_mark,
3751
5350
  jsonKey: "mark",
5351
+ exportJsonKey: { mark: "$" },
3752
5352
  description: "The mark configuration",
3753
5353
  format: TagFormat.plainText
3754
5354
  },
3755
5355
  {
3756
5356
  key: ConfigKey.property_example,
3757
5357
  jsonKey: "example",
5358
+ exportJsonKey: [
5359
+ { "@keyonly": { isExample: true, example: true, "@bit": { isExample: true } } },
5360
+ { "@absent": { isExample: true, example: true, "@bit": { isExample: true } } },
5361
+ { isExample: true, example: "$", "@bit": { isExample: true } }
5362
+ ],
3758
5363
  description: "An example for the marked content",
3759
- format: TagFormat.plainText
5364
+ format: TagFormat.bitmarkText,
5365
+ nullable: true
3760
5366
  }
3761
5367
  ]
3762
5368
  }
@@ -3845,7 +5451,8 @@ var GROUPS = {
3845
5451
  name: "Title",
3846
5452
  description: "The title of the book",
3847
5453
  maxCount: 2,
3848
- jsonKey: "title|multi(count=2, key=subtitle)"
5454
+ jsonKey: "title|multi(count=2, key=subtitle)",
5455
+ exportJsonKey: [{ "@level=1": { title: "$" } }, { "@level=2": { subtitle: "$" } }]
3849
5456
  },
3850
5457
  {
3851
5458
  key: ConfigKey.property_subtype,
@@ -3861,6 +5468,7 @@ var GROUPS = {
3861
5468
  {
3862
5469
  key: ConfigKey.resource_coverImage,
3863
5470
  jsonKey: "coverImage|resource(type=image, key=image)",
5471
+ exportJsonKey: { coverImage: { type: "image", image: { src: "$" } } },
3864
5472
  description: "The cover image of the book",
3865
5473
  chain: [
3866
5474
  {
@@ -3878,6 +5486,7 @@ var GROUPS = {
3878
5486
  {
3879
5487
  key: ConfigKey.resource_backgroundImage,
3880
5488
  jsonKey: "backgroundImage|resource(type=image, key=image)",
5489
+ exportJsonKey: { backgroundImage: { type: "image", image: { src: "$" } } },
3881
5490
  description: "The background image of the book",
3882
5491
  chain: [
3883
5492
  {
@@ -4037,10 +5646,21 @@ var GROUPS = {
4037
5646
  format: TagFormat.plainText,
4038
5647
  chain: [
4039
5648
  {
5649
+ exportJsonKey: { reference: "$" },
4040
5650
  key: ConfigKey.tag_reference,
4041
5651
  format: TagFormat.plainText,
4042
- description: "The reference(s) for the book(s) in the learning path",
4043
- maxCount: 2
5652
+ description: "The reference for the book(s) in the learning path",
5653
+ maxCount: 1,
5654
+ chain: [
5655
+ {
5656
+ key: ConfigKey.tag_reference,
5657
+ jsonKey: "referenceEnd",
5658
+ exportJsonKey: { referenceEnd: "$" },
5659
+ format: TagFormat.plainText,
5660
+ description: "The referenceEnd for the book(s) in the learning path",
5661
+ maxCount: 1
5662
+ }
5663
+ ]
4044
5664
  }
4045
5665
  ]
4046
5666
  }
@@ -4171,6 +5791,42 @@ var GROUPS = {
4171
5791
  }
4172
5792
  ]
4173
5793
  },
5794
+ [ConfigKey.group_backgroundWallpaper]: {
5795
+ type: GroupConfigType.standard,
5796
+ description: "Background wallpaper tags",
5797
+ tags: [
5798
+ {
5799
+ key: ConfigKey.resource_backgroundWallpaper,
5800
+ exportJsonKey: { backgroundWallpaper: { type: "image", image: { src: "$" } } },
5801
+ description: "Background wallpaper for the image, used to set a background for the image",
5802
+ chain: [
5803
+ {
5804
+ key: ConfigKey.group_resourceImageCommon,
5805
+ description: "Common resource image tags for images"
5806
+ }
5807
+ ]
5808
+ }
5809
+ ]
5810
+ },
5811
+ [ConfigKey.group_imageNoZoom]: {
5812
+ type: GroupConfigType.standard,
5813
+ description: "Image bit tags",
5814
+ tags: [
5815
+ {
5816
+ key: ConfigKey.group_backgroundWallpaper,
5817
+ description: "Background wallpaper tags for images, used to define background properties for images"
5818
+ },
5819
+ {
5820
+ key: ConfigKey.group_resourceBitTags,
5821
+ description: "Resource bit tags for images, used to define additional properties for images"
5822
+ },
5823
+ {
5824
+ key: ConfigKey.group_resourceImageNoZoom,
5825
+ description: "Resource image tags for images, used to attach images to the bit",
5826
+ minCount: 1
5827
+ }
5828
+ ]
5829
+ },
4174
5830
  //
4175
5831
  // Resource groups
4176
5832
  //
@@ -4180,6 +5836,8 @@ var GROUPS = {
4180
5836
  tags: [
4181
5837
  {
4182
5838
  key: ConfigKey.resource_imagePlaceholder,
5839
+ jsonKey: "imagePlaceholder|resource(type=image, key=image)",
5840
+ exportJsonKey: { imagePlaceholder: { type: "image", image: { src: "$" } } },
4183
5841
  description: "Placeholder image for the resource",
4184
5842
  chain: [
4185
5843
  {
@@ -4288,6 +5946,58 @@ var GROUPS = {
4288
5946
  }
4289
5947
  ]
4290
5948
  },
5949
+ [ConfigKey.group_resourceImageCommonNoZoom]: {
5950
+ type: GroupConfigType.standard,
5951
+ description: "Common properties for image resources where @zoomDisabled defaults to true",
5952
+ tags: [
5953
+ {
5954
+ key: ConfigKey.group_resourceCommon,
5955
+ description: "Common resource properties"
5956
+ },
5957
+ {
5958
+ key: ConfigKey.property_src1x,
5959
+ description: "The source URL for the image at 1x resolution",
5960
+ format: TagFormat.plainText
5961
+ },
5962
+ {
5963
+ key: ConfigKey.property_src2x,
5964
+ description: "The source URL for the image at 2x resolution",
5965
+ format: TagFormat.plainText
5966
+ },
5967
+ {
5968
+ key: ConfigKey.property_src3x,
5969
+ description: "The source URL for the image at 3x resolution",
5970
+ format: TagFormat.plainText
5971
+ },
5972
+ {
5973
+ key: ConfigKey.property_src4x,
5974
+ description: "The source URL for the image at 4x resolution",
5975
+ format: TagFormat.plainText
5976
+ },
5977
+ {
5978
+ key: ConfigKey.property_width,
5979
+ description: "The width of the image",
5980
+ format: TagFormat.number
5981
+ },
5982
+ {
5983
+ key: ConfigKey.property_height,
5984
+ description: "The height of the image",
5985
+ format: TagFormat.number
5986
+ },
5987
+ {
5988
+ key: ConfigKey.property_alt,
5989
+ description: "The alternative text for the image",
5990
+ format: TagFormat.plainText
5991
+ },
5992
+ // @zoomDisabled with a true default
5993
+ {
5994
+ key: ConfigKey.property_zoomDisabled,
5995
+ description: "If true, zooming is disabled for the image (defaults to true)",
5996
+ format: TagFormat.boolean,
5997
+ defaultValue: "true"
5998
+ }
5999
+ ]
6000
+ },
4291
6001
  [ConfigKey.group_resourceAudioCommon]: {
4292
6002
  type: GroupConfigType.standard,
4293
6003
  description: "Common properties for audio resources",
@@ -4364,6 +6074,7 @@ var GROUPS = {
4364
6074
  {
4365
6075
  key: ConfigKey.property_posterImage,
4366
6076
  jsonKey: "posterImage.src",
6077
+ exportJsonKey: { posterImage: { src: "$" } },
4367
6078
  description: "The poster image for the video",
4368
6079
  format: TagFormat.plainText,
4369
6080
  chain: [
@@ -4376,22 +6087,26 @@ var GROUPS = {
4376
6087
  {
4377
6088
  key: ConfigKey.property_src1x,
4378
6089
  description: "The source URL for the video at 1x resolution",
4379
- format: TagFormat.plainText
6090
+ format: TagFormat.plainText,
6091
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4380
6092
  },
4381
6093
  {
4382
6094
  key: ConfigKey.property_src2x,
4383
6095
  description: "The source URL for the video at 2x resolution",
4384
- format: TagFormat.plainText
6096
+ format: TagFormat.plainText,
6097
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4385
6098
  },
4386
6099
  {
4387
6100
  key: ConfigKey.property_src3x,
4388
6101
  description: "The source URL for the video at 3x resolution",
4389
- format: TagFormat.plainText
6102
+ format: TagFormat.plainText,
6103
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4390
6104
  },
4391
6105
  {
4392
6106
  key: ConfigKey.property_src4x,
4393
6107
  description: "The source URL for the video at 4x resolution",
4394
- format: TagFormat.plainText
6108
+ format: TagFormat.plainText,
6109
+ exportJsonKey: { thumbnails: [{ src: "$" }] }
4395
6110
  }
4396
6111
  ]
4397
6112
  },
@@ -4404,6 +6119,7 @@ var GROUPS = {
4404
6119
  tags: [
4405
6120
  {
4406
6121
  key: ConfigKey.resource_icon,
6122
+ exportJsonKey: { resource: { type: "icon", icon: { src: "$" } } },
4407
6123
  description: "The icon resource",
4408
6124
  chain: [
4409
6125
  {
@@ -4420,6 +6136,7 @@ var GROUPS = {
4420
6136
  tags: [
4421
6137
  {
4422
6138
  key: ConfigKey.resource_image,
6139
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
4423
6140
  description: "The image resource",
4424
6141
  chain: [
4425
6142
  {
@@ -4430,12 +6147,30 @@ var GROUPS = {
4430
6147
  }
4431
6148
  ]
4432
6149
  },
6150
+ [ConfigKey.group_resourceImageNoZoom]: {
6151
+ type: GroupConfigType.resource,
6152
+ description: "Image resource where @zoomDisabled defaults to true",
6153
+ tags: [
6154
+ {
6155
+ key: ConfigKey.resource_image,
6156
+ exportJsonKey: { resource: { type: "image", image: { src: "$" } } },
6157
+ description: "The image resource (no-zoom default)",
6158
+ chain: [
6159
+ {
6160
+ key: ConfigKey.group_resourceImageCommonNoZoom,
6161
+ description: "Common image properties with @zoomDisabled defaulting to true"
6162
+ }
6163
+ ]
6164
+ }
6165
+ ]
6166
+ },
4433
6167
  [ConfigKey.group_resourceImagePortrait]: {
4434
6168
  type: GroupConfigType.resource,
4435
6169
  description: "Portrait image resource",
4436
6170
  tags: [
4437
6171
  {
4438
6172
  key: ConfigKey.resource_imagePortrait,
6173
+ exportJsonKey: { resource: { type: "image-portrait", imagePortrait: { src: "$" } } },
4439
6174
  description: "The portrait image resource",
4440
6175
  chain: [
4441
6176
  {
@@ -4452,6 +6187,7 @@ var GROUPS = {
4452
6187
  tags: [
4453
6188
  {
4454
6189
  key: ConfigKey.resource_imageLandscape,
6190
+ exportJsonKey: { resource: { type: "image-landscape", imageLandscape: { src: "$" } } },
4455
6191
  description: "The landscape image resource",
4456
6192
  chain: [
4457
6193
  {
@@ -4468,6 +6204,7 @@ var GROUPS = {
4468
6204
  tags: [
4469
6205
  {
4470
6206
  key: ConfigKey.resource_imageEmbed,
6207
+ exportJsonKey: { resource: { type: "image-embed", imageEmbed: { src: "$" } } },
4471
6208
  description: "The embedded image resource",
4472
6209
  chain: [
4473
6210
  {
@@ -4484,6 +6221,7 @@ var GROUPS = {
4484
6221
  tags: [
4485
6222
  {
4486
6223
  key: ConfigKey.resource_imageLink,
6224
+ exportJsonKey: { resource: { type: "image-link", imageLink: { url: "$" } } },
4487
6225
  description: "The link to the image resource",
4488
6226
  chain: [
4489
6227
  {
@@ -4500,6 +6238,7 @@ var GROUPS = {
4500
6238
  tags: [
4501
6239
  {
4502
6240
  key: ConfigKey.resource_audio,
6241
+ exportJsonKey: { resource: { type: "audio", audio: { src: "$" } } },
4503
6242
  description: "The audio resource",
4504
6243
  chain: [
4505
6244
  {
@@ -4516,6 +6255,7 @@ var GROUPS = {
4516
6255
  tags: [
4517
6256
  {
4518
6257
  key: ConfigKey.resource_audioEmbed,
6258
+ exportJsonKey: { resource: { type: "audio-embed", audioEmbed: { src: "$" } } },
4519
6259
  description: "The embedded audio resource",
4520
6260
  chain: [
4521
6261
  {
@@ -4532,6 +6272,7 @@ var GROUPS = {
4532
6272
  tags: [
4533
6273
  {
4534
6274
  key: ConfigKey.resource_audioLink,
6275
+ exportJsonKey: { resource: { type: "audio-link", audioLink: { url: "$" } } },
4535
6276
  description: "The link to the audio resource",
4536
6277
  chain: [
4537
6278
  {
@@ -4548,6 +6289,7 @@ var GROUPS = {
4548
6289
  tags: [
4549
6290
  {
4550
6291
  key: ConfigKey.resource_video,
6292
+ exportJsonKey: { resource: { type: "video", video: { src: "$" } } },
4551
6293
  description: "The video resource",
4552
6294
  chain: [
4553
6295
  {
@@ -4564,6 +6306,7 @@ var GROUPS = {
4564
6306
  tags: [
4565
6307
  {
4566
6308
  key: ConfigKey.resource_videoEmbed,
6309
+ exportJsonKey: { resource: { type: "video-embed", videoEmbed: { url: "$" } } },
4567
6310
  description: "The embedded video resource",
4568
6311
  chain: [
4569
6312
  {
@@ -4580,6 +6323,7 @@ var GROUPS = {
4580
6323
  tags: [
4581
6324
  {
4582
6325
  key: ConfigKey.resource_videoLink,
6326
+ exportJsonKey: { resource: { type: "video-link", videoLink: { url: "$" } } },
4583
6327
  description: "The link to the video resource",
4584
6328
  chain: [
4585
6329
  {
@@ -4596,6 +6340,9 @@ var GROUPS = {
4596
6340
  tags: [
4597
6341
  {
4598
6342
  key: ConfigKey.resource_stillImageFilmEmbed,
6343
+ exportJsonKey: {
6344
+ resource: { type: "still-image-film-embed", stillImageFilmEmbed: { url: "$" } }
6345
+ },
4599
6346
  description: "The embedded still image film resource",
4600
6347
  chain: [
4601
6348
  {
@@ -4612,6 +6359,9 @@ var GROUPS = {
4612
6359
  tags: [
4613
6360
  {
4614
6361
  key: ConfigKey.resource_stillImageFilmLink,
6362
+ exportJsonKey: {
6363
+ resource: { type: "still-image-film-link", stillImageFilmLink: { url: "$" } }
6364
+ },
4615
6365
  description: "The link to the still image film resource",
4616
6366
  chain: [
4617
6367
  {
@@ -4628,6 +6378,7 @@ var GROUPS = {
4628
6378
  tags: [
4629
6379
  {
4630
6380
  key: ConfigKey.resource_article,
6381
+ exportJsonKey: { resource: { type: "article", article: { body: "$" } } },
4631
6382
  description: "The article resource",
4632
6383
  chain: [
4633
6384
  {
@@ -4644,6 +6395,7 @@ var GROUPS = {
4644
6395
  tags: [
4645
6396
  {
4646
6397
  key: ConfigKey.resource_articleEmbed,
6398
+ exportJsonKey: { resource: { type: "article-embed", articleEmbed: { url: "$" } } },
4647
6399
  description: "The embedded article resource",
4648
6400
  chain: [
4649
6401
  {
@@ -4660,6 +6412,7 @@ var GROUPS = {
4660
6412
  tags: [
4661
6413
  {
4662
6414
  key: ConfigKey.resource_articleLink,
6415
+ exportJsonKey: { resource: { type: "article-link", articleLink: { url: "$" } } },
4663
6416
  description: "The link to the article resource",
4664
6417
  chain: [
4665
6418
  {
@@ -4676,6 +6429,7 @@ var GROUPS = {
4676
6429
  tags: [
4677
6430
  {
4678
6431
  key: ConfigKey.resource_document,
6432
+ exportJsonKey: { resource: { type: "document", document: { url: "$" } } },
4679
6433
  description: "The document resource",
4680
6434
  chain: [
4681
6435
  {
@@ -4692,6 +6446,7 @@ var GROUPS = {
4692
6446
  tags: [
4693
6447
  {
4694
6448
  key: ConfigKey.resource_documentEmbed,
6449
+ exportJsonKey: { resource: { type: "document-embed", documentEmbed: { url: "$" } } },
4695
6450
  description: "The embedded document resource",
4696
6451
  chain: [
4697
6452
  {
@@ -4708,6 +6463,7 @@ var GROUPS = {
4708
6463
  tags: [
4709
6464
  {
4710
6465
  key: ConfigKey.resource_documentLink,
6466
+ exportJsonKey: { resource: { type: "document-link", documentLink: { url: "$" } } },
4711
6467
  description: "The link to the document resource",
4712
6468
  chain: [
4713
6469
  {
@@ -4724,6 +6480,7 @@ var GROUPS = {
4724
6480
  tags: [
4725
6481
  {
4726
6482
  key: ConfigKey.resource_documentDownload,
6483
+ exportJsonKey: { resource: { type: "document-download", documentDownload: { url: "$" } } },
4727
6484
  description: "The downloadable document resource",
4728
6485
  chain: [
4729
6486
  {
@@ -4740,6 +6497,7 @@ var GROUPS = {
4740
6497
  tags: [
4741
6498
  {
4742
6499
  key: ConfigKey.resource_appLink,
6500
+ exportJsonKey: { resource: { type: "app-link", appLink: { url: "$" } } },
4743
6501
  description: "The link to the app resource",
4744
6502
  chain: [
4745
6503
  {
@@ -4756,6 +6514,7 @@ var GROUPS = {
4756
6514
  tags: [
4757
6515
  {
4758
6516
  key: ConfigKey.resource_websiteLink,
6517
+ exportJsonKey: { resource: { type: "website-link", websiteLink: { url: "$" } } },
4759
6518
  description: "The link to the website resource",
4760
6519
  chain: [
4761
6520
  {
@@ -4814,6 +6573,7 @@ var GROUPS = {
4814
6573
  {
4815
6574
  key: ConfigKey.resource_previewImage,
4816
6575
  jsonKey: "previewImage|resource(type=image, key=image)",
6576
+ exportJsonKey: { previewImage: { type: "image", image: { src: "$" } } },
4817
6577
  description: "The preview image resource",
4818
6578
  chain: [
4819
6579
  {
@@ -4831,6 +6591,7 @@ var GROUPS = {
4831
6591
  {
4832
6592
  key: ConfigKey.resource_previewVideo,
4833
6593
  jsonKey: "previewVideo|resource(type=video, key=video)",
6594
+ exportJsonKey: { previewVideo: { type: "video", video: { src: "$" } } },
4834
6595
  description: "The preview video resource",
4835
6596
  chain: [
4836
6597
  {
@@ -4895,20 +6656,53 @@ var ConfigHydrator = class {
4895
6656
  _side.name,
4896
6657
  _side.repeat ?? false,
4897
6658
  variantsOfSide,
4898
- _side.jsonKey
6659
+ _side.jsonKey,
6660
+ _side.exportJsonKey,
6661
+ Object.prototype.hasOwnProperty.call(_side, "exportJsonKey")
4899
6662
  );
4900
6663
  sides.push(sideConfig);
4901
6664
  }
4902
6665
  const cardSetConfig = new CardSetConfig(
4903
6666
  _cardSet,
4904
6667
  _cardSetConfig.jsonKey,
4905
- _cardSetConfig.sections,
6668
+ _cardSetConfig.exportJsonKey,
6669
+ Object.prototype.hasOwnProperty.call(_cardSetConfig, "exportJsonKey"),
6670
+ this.hydrateCardSections(_cardSetConfig.sections),
4906
6671
  sides
4907
6672
  );
4908
6673
  return cardSetConfig;
4909
6674
  }
6675
+ hydrateCardSections(sections) {
6676
+ if (!sections) return void 0;
6677
+ const out = {};
6678
+ for (const [k, v] of Object.entries(sections)) {
6679
+ out[k] = {
6680
+ jsonKey: v.jsonKey,
6681
+ exportJsonKey: v.exportJsonKey,
6682
+ hasExportJsonKey: Object.prototype.hasOwnProperty.call(v, "exportJsonKey"),
6683
+ isDefault: v.isDefault,
6684
+ sideJsonKey: v.sideJsonKey,
6685
+ sideExportJsonKey: v.sideExportJsonKey,
6686
+ hasSideExportJsonKey: Object.prototype.hasOwnProperty.call(v, "sideExportJsonKey"),
6687
+ // PLAN-085: cardinality fields pass through; defaults applied
6688
+ // downstream by the config consumer (Rust treats `0` / undefined
6689
+ // as unbounded).
6690
+ minCount: v.minCount,
6691
+ maxCount: v.maxCount
6692
+ };
6693
+ }
6694
+ return out;
6695
+ }
4910
6696
  hydrateTagConfig(_tag) {
4911
- const { key: _configKey, maxCount, minCount, chain: _chain, deprecated } = _tag;
6697
+ const {
6698
+ key: _configKey,
6699
+ maxCount,
6700
+ minCount,
6701
+ chain: _chain,
6702
+ deprecated,
6703
+ jsonKey,
6704
+ exportJsonKey
6705
+ } = _tag;
4912
6706
  const configKey = Enum2(ConfigKey).fromValue(_configKey) || ConfigKey._unknown;
4913
6707
  if (!configKey) throw new Error(`No tag key found for config key '${configKey}'`);
4914
6708
  const tag = Enum2(Tag).fromValue(configKey);
@@ -4923,6 +6717,9 @@ var ConfigHydrator = class {
4923
6717
  maxCount: maxCount ?? MAX_COUNT_DEFAULT,
4924
6718
  minCount: minCount ?? MIN_COUNT_DEFAULT,
4925
6719
  chain,
6720
+ jsonKey,
6721
+ exportJsonKey,
6722
+ hasExportJsonKey: Object.prototype.hasOwnProperty.call(_tag, "exportJsonKey"),
4926
6723
  deprecated
4927
6724
  });
4928
6725
  return {
@@ -4941,7 +6738,8 @@ var ConfigHydrator = class {
4941
6738
  format,
4942
6739
  values,
4943
6740
  defaultValue,
4944
- jsonKey
6741
+ jsonKey,
6742
+ exportJsonKey
4945
6743
  } = _tag;
4946
6744
  const configKey = Enum2(ConfigKey).fromValue(_configKey) || ConfigKey._unknown;
4947
6745
  if (!configKey) throw new Error(`No property key found for config key '${configKey}'`);
@@ -4957,6 +6755,8 @@ var ConfigHydrator = class {
4957
6755
  minCount: minCount ?? MIN_COUNT_DEFAULT,
4958
6756
  chain,
4959
6757
  jsonKey,
6758
+ exportJsonKey,
6759
+ hasExportJsonKey: Object.prototype.hasOwnProperty.call(_tag, "exportJsonKey"),
4960
6760
  format,
4961
6761
  values,
4962
6762
  defaultValue,
@@ -4969,7 +6769,15 @@ var ConfigHydrator = class {
4969
6769
  };
4970
6770
  }
4971
6771
  hydrateResourceTagConfig(_tag) {
4972
- const { key: _configKey, maxCount, minCount, chain: _chain, deprecated, jsonKey } = _tag;
6772
+ const {
6773
+ key: _configKey,
6774
+ maxCount,
6775
+ minCount,
6776
+ chain: _chain,
6777
+ deprecated,
6778
+ jsonKey,
6779
+ exportJsonKey
6780
+ } = _tag;
4973
6781
  const configKey = Enum2(ConfigKey).fromValue(_configKey) || ConfigKey._unknown;
4974
6782
  if (!configKey) throw new Error(`No resource key found for config key '${configKey}'`);
4975
6783
  const tag = _configKey.substring(1);
@@ -4984,6 +6792,8 @@ var ConfigHydrator = class {
4984
6792
  minCount: minCount ?? MIN_COUNT_DEFAULT,
4985
6793
  chain,
4986
6794
  jsonKey,
6795
+ exportJsonKey,
6796
+ hasExportJsonKey: Object.prototype.hasOwnProperty.call(_tag, "exportJsonKey"),
4987
6797
  deprecated
4988
6798
  });
4989
6799
  return {
@@ -5015,14 +6825,25 @@ var ConfigHydrator = class {
5015
6825
  };
5016
6826
  }
5017
6827
  hydrateCardVariantConfig(_variant) {
5018
- const { tags: _tags, bodyAllowed, bodyRequired, repeatCount, jsonKey } = _variant;
6828
+ const {
6829
+ tags: _tags,
6830
+ bodyAllowed,
6831
+ bodyRequired,
6832
+ repeatCount,
6833
+ jsonKey,
6834
+ exportJsonKey,
6835
+ format
6836
+ } = _variant;
5019
6837
  const tags2 = this.hydrateTagsConfig(_tags);
5020
6838
  const cardSetConfig = new CardVariantConfig(
5021
6839
  tags2.tags,
5022
6840
  bodyAllowed,
5023
6841
  bodyRequired,
5024
6842
  repeatCount,
5025
- jsonKey
6843
+ jsonKey,
6844
+ exportJsonKey,
6845
+ Object.prototype.hasOwnProperty.call(_variant, "exportJsonKey"),
6846
+ format
5026
6847
  );
5027
6848
  return cardSetConfig;
5028
6849
  }
@@ -5060,6 +6881,7 @@ var BITS = {
5060
6881
  description: "Common tags for quiz bits"
5061
6882
  },
5062
6883
  {
6884
+ exportJsonKey: { title: "$" },
5063
6885
  key: ConfigKey.tag_title,
5064
6886
  description: "The title of the flashcard quiz"
5065
6887
  },
@@ -5068,6 +6890,21 @@ var BITS = {
5068
6890
  description: "The flashcard set to use for the app flashcards",
5069
6891
  format: TagFormat.plainText,
5070
6892
  maxCount: Count.infinity
6893
+ },
6894
+ {
6895
+ // Override inherited @example from group_standardTags: string root
6896
+ // example. Bare/synthesized emits `example: "true"` (a text literal
6897
+ // that field-coerces to a paragraph "true" in bitmark+ contexts);
6898
+ // valued [@example:text] emits the user's text via `$`.
6899
+ key: ConfigKey.property_example,
6900
+ exportJsonKey: [
6901
+ { "@keyonly": { isExample: true, example: "true" } },
6902
+ { "@absent": { isExample: true, example: "true" } },
6903
+ { isExample: true, example: "$" }
6904
+ ],
6905
+ description: "The example text for the bit",
6906
+ format: TagFormat.plainText,
6907
+ nullable: true
5071
6908
  }
5072
6909
  ],
5073
6910
  rootExampleType: ExampleType.string
@@ -5149,6 +6986,7 @@ var BITS = {
5149
6986
  description: "Article bit, used for articles / paragraphs",
5150
6987
  tags: [
5151
6988
  {
6989
+ exportJsonKey: { title: "$" },
5152
6990
  key: ConfigKey.tag_title,
5153
6991
  description: "The title of the article"
5154
6992
  }
@@ -5256,7 +7094,8 @@ var BITS = {
5256
7094
  key: ConfigKey.tag_title,
5257
7095
  description: "Title and subtitle of the catalog item",
5258
7096
  maxCount: 2,
5259
- jsonKey: "title|multi(count=2, key=subtitle)"
7097
+ jsonKey: "title|multi(count=2, key=subtitle)",
7098
+ exportJsonKey: [{ "@level=1": { title: "$" } }, { "@level=2": { subtitle: "$" } }]
5260
7099
  },
5261
7100
  {
5262
7101
  key: ConfigKey.property_coverImage,
@@ -5266,6 +7105,7 @@ var BITS = {
5266
7105
  {
5267
7106
  key: ConfigKey.resource_coverImage,
5268
7107
  jsonKey: "coverImage|resource(type=image, key=image)",
7108
+ exportJsonKey: { coverImage: { type: "image", image: { src: "$" } } },
5269
7109
  description: "Cover image of the catalog item",
5270
7110
  chain: [
5271
7111
  {
@@ -5393,6 +7233,18 @@ var BITS = {
5393
7233
  key: ConfigKey.group_previewVideos,
5394
7234
  description: "Array of preview videos for the catalog item",
5395
7235
  maxCount: Count.infinity
7236
+ },
7237
+ {
7238
+ // Override inherited @example from group_standardTags: string root example.
7239
+ key: ConfigKey.property_example,
7240
+ exportJsonKey: [
7241
+ { "@keyonly": { isExample: true, example: "true" } },
7242
+ { "@absent": { isExample: true, example: "true" } },
7243
+ { isExample: true, example: "$" }
7244
+ ],
7245
+ description: "The example text for the bit",
7246
+ format: TagFormat.plainText,
7247
+ nullable: true
5396
7248
  }
5397
7249
  ],
5398
7250
  rootExampleType: ExampleType.string
@@ -5406,7 +7258,8 @@ var BITS = {
5406
7258
  key: ConfigKey.tag_title,
5407
7259
  description: "Title and subtitle of the catalog item",
5408
7260
  maxCount: 2,
5409
- jsonKey: "title|multi(count=2, key=subtitle)"
7261
+ jsonKey: "title|multi(count=2, key=subtitle)",
7262
+ exportJsonKey: [{ "@level=1": { title: "$" } }, { "@level=2": { subtitle: "$" } }]
5410
7263
  },
5411
7264
  {
5412
7265
  key: ConfigKey.property_coverImage,
@@ -5416,6 +7269,7 @@ var BITS = {
5416
7269
  {
5417
7270
  key: ConfigKey.resource_coverImage,
5418
7271
  jsonKey: "coverImage|resource(type=image, key=image)",
7272
+ exportJsonKey: { coverImage: { type: "image", image: { src: "$" } } },
5419
7273
  description: "Cover image of the catalog item",
5420
7274
  chain: [
5421
7275
  {
@@ -5543,6 +7397,18 @@ var BITS = {
5543
7397
  key: ConfigKey.group_previewVideos,
5544
7398
  description: "Array of preview videos for the catalog item",
5545
7399
  maxCount: Count.infinity
7400
+ },
7401
+ {
7402
+ // Override inherited @example from group_standardTags: string root example.
7403
+ key: ConfigKey.property_example,
7404
+ exportJsonKey: [
7405
+ { "@keyonly": { isExample: true, example: "true" } },
7406
+ { "@absent": { isExample: true, example: "true" } },
7407
+ { isExample: true, example: "$" }
7408
+ ],
7409
+ description: "The example text for the bit",
7410
+ format: TagFormat.plainText,
7411
+ nullable: true
5546
7412
  }
5547
7413
  ],
5548
7414
  rootExampleType: ExampleType.string
@@ -5556,7 +7422,8 @@ var BITS = {
5556
7422
  key: ConfigKey.tag_title,
5557
7423
  description: "Title and subtitle of the catalog item",
5558
7424
  maxCount: 2,
5559
- jsonKey: "title|multi(count=2, key=subtitle)"
7425
+ jsonKey: "title|multi(count=2, key=subtitle)",
7426
+ exportJsonKey: [{ "@level=1": { title: "$" } }, { "@level=2": { subtitle: "$" } }]
5560
7427
  },
5561
7428
  {
5562
7429
  key: ConfigKey.property_coverImage,
@@ -5682,6 +7549,18 @@ var BITS = {
5682
7549
  key: ConfigKey.group_previewVideos,
5683
7550
  description: "Array of preview videos for the catalog item",
5684
7551
  maxCount: Count.infinity
7552
+ },
7553
+ {
7554
+ // Override inherited @example from group_standardTags: string root example.
7555
+ key: ConfigKey.property_example,
7556
+ exportJsonKey: [
7557
+ { "@keyonly": { isExample: true, example: "true" } },
7558
+ { "@absent": { isExample: true, example: "true" } },
7559
+ { isExample: true, example: "$" }
7560
+ ],
7561
+ description: "The example text for the bit",
7562
+ format: TagFormat.plainText,
7563
+ nullable: true
5685
7564
  }
5686
7565
  ],
5687
7566
  rootExampleType: ExampleType.string
@@ -5883,11 +7762,13 @@ var BITS = {
5883
7762
  description: "Bit alias, used to create an alias for a bit",
5884
7763
  tags: [
5885
7764
  {
7765
+ exportJsonKey: { reference: "$" },
5886
7766
  key: ConfigKey.tag_reference,
5887
7767
  format: TagFormat.plainText,
5888
7768
  description: "The reference to the bit that this alias points to"
5889
7769
  },
5890
7770
  {
7771
+ exportJsonKey: { anchor: "$" },
5891
7772
  key: ConfigKey.tag_anchor,
5892
7773
  format: TagFormat.plainText,
5893
7774
  description: "The anchor for the bit alias, used for linking"
@@ -6089,7 +7970,8 @@ var BITS = {
6089
7970
  description: "The title of the book cover",
6090
7971
  maxCount: 2,
6091
7972
  // title & subtitle
6092
- jsonKey: "title|multi(count=2, key=subtitle)"
7973
+ jsonKey: "title|multi(count=2, key=subtitle)",
7974
+ exportJsonKey: [{ "@level=1": { title: "$" } }, { "@level=2": { subtitle: "$" } }]
6093
7975
  },
6094
7976
  {
6095
7977
  key: ConfigKey.property_coverColor,
@@ -6233,6 +8115,7 @@ var BITS = {
6233
8115
  description: "Chapter bit, used to define chapters in books or articles",
6234
8116
  tags: [
6235
8117
  {
8118
+ exportJsonKey: { anchor: "$" },
6236
8119
  key: ConfigKey.tag_anchor,
6237
8120
  format: TagFormat.plainText,
6238
8121
  description: "The anchor for the chapter, used for linking"
@@ -6240,7 +8123,8 @@ var BITS = {
6240
8123
  {
6241
8124
  key: ConfigKey.tag_title,
6242
8125
  description: "The title of the chapter",
6243
- jsonKey: "title|setMulti(level)"
8126
+ jsonKey: "title|setMulti(level)",
8127
+ exportJsonKey: { title: "$", level: "$level" }
6244
8128
  },
6245
8129
  {
6246
8130
  key: ConfigKey.property_toc,
@@ -6271,8 +8155,31 @@ var BITS = {
6271
8155
  description: "Tags for gaps in cloze and multiple choice text bits"
6272
8156
  },
6273
8157
  {
6274
- key: ConfigKey.group_trueFalse,
8158
+ key: ConfigKey.group_trueFalseInlineSelect,
6275
8159
  description: "Tags for true/false questions in cloze and multiple choice text bits"
8160
+ },
8161
+ {
8162
+ // Bit-level @isCaseSensitive — empty exportJsonKey (no bit-level emission);
8163
+ // defaultValue 'true' phantom-fires onto the ancestor stack so the gap chain's
8164
+ // `@absent: $ancestor` rule resolves to true. Mirrors BPG Builder.ts:1643-1652
8165
+ // `pushDownTree(... 'isCaseSensitive', data.isCaseSensitive ?? true)` for cloze bits.
8166
+ key: ConfigKey.property_isCaseSensitive,
8167
+ exportJsonKey: {},
8168
+ description: "If the cloze answers are case sensitive",
8169
+ format: TagFormat.boolean,
8170
+ defaultValue: "true"
8171
+ },
8172
+ {
8173
+ // Override inherited @example: in `isTopLevelExample` allow-list.
8174
+ key: ConfigKey.property_example,
8175
+ exportJsonKey: [
8176
+ { "@keyonly": { isExample: true } },
8177
+ { "@absent": { isExample: true } },
8178
+ { isExample: true, example: "$" }
8179
+ ],
8180
+ description: "The example(s) for the bit",
8181
+ format: TagFormat.bitmarkText,
8182
+ nullable: true
6276
8183
  }
6277
8184
  ]
6278
8185
  },
@@ -6293,6 +8200,7 @@ var BITS = {
6293
8200
  },
6294
8201
  {
6295
8202
  key: ConfigKey.property_isCaseSensitive,
8203
+ exportJsonKey: {},
6296
8204
  description: "If the cloze answers are case sensitive",
6297
8205
  format: TagFormat.boolean,
6298
8206
  defaultValue: "true"
@@ -6310,18 +8218,56 @@ var BITS = {
6310
8218
  {
6311
8219
  key: ConfigKey.group_gap,
6312
8220
  description: "Tags for gaps in cloze bits"
8221
+ },
8222
+ {
8223
+ // Override inherited @example: cloze is in OLD parser's
8224
+ // `isTopLevelExample` allow-list, so [@example] at bit-header
8225
+ // emits `isExample: true`.
8226
+ key: ConfigKey.property_example,
8227
+ exportJsonKey: [
8228
+ { "@keyonly": { isExample: true } },
8229
+ { "@absent": { isExample: true } },
8230
+ { isExample: true, example: "$" }
8231
+ ],
8232
+ description: "The example(s) for the bit",
8233
+ format: TagFormat.bitmarkText,
8234
+ nullable: true
6313
8235
  }
6314
8236
  ]
6315
8237
  },
6316
8238
  [BitType2.clozeInstructionGrouped]: {
6317
8239
  since: "1.3.0",
6318
8240
  baseBitType: BitType2.cloze,
6319
- description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions"
8241
+ description: "Cloze instruction grouped bit, used for cloze questions with grouped instructions",
8242
+ tags: [
8243
+ {
8244
+ key: ConfigKey.property_quizCountItems,
8245
+ description: "The number of items in the cloze quiz (instruction-grouped default: true)",
8246
+ format: TagFormat.boolean,
8247
+ defaultValue: "true"
8248
+ }
8249
+ // Note: quizStrikethroughSolutions defaults to false for instruction-grouped per OLD
8250
+ // JsonGenerator.ts:1689-1690, which equals the Boolean natural default → no override needed.
8251
+ ]
6320
8252
  },
6321
8253
  [BitType2.clozeSolutionGrouped]: {
6322
8254
  since: "1.3.0",
6323
8255
  baseBitType: BitType2.cloze,
6324
- description: "Cloze solution grouped bit, used for cloze questions with grouped solutions"
8256
+ description: "Cloze solution grouped bit, used for cloze questions with grouped solutions",
8257
+ tags: [
8258
+ {
8259
+ key: ConfigKey.property_quizCountItems,
8260
+ description: "The number of items in the cloze quiz (solution-grouped default: true)",
8261
+ format: TagFormat.boolean,
8262
+ defaultValue: "true"
8263
+ },
8264
+ {
8265
+ key: ConfigKey.property_quizStrikethroughSolutions,
8266
+ description: "If the cloze solutions should be strikethrough (solution-grouped default: true)",
8267
+ format: TagFormat.boolean,
8268
+ defaultValue: "true"
8269
+ }
8270
+ ]
6325
8271
  },
6326
8272
  [BitType2.clozeSeveral]: {
6327
8273
  since: "3.5.0",
@@ -6357,6 +8303,17 @@ var BITS = {
6357
8303
  {
6358
8304
  key: ConfigKey.group_quizCommon,
6359
8305
  description: "Common tags for quiz bits"
8306
+ },
8307
+ {
8308
+ // PLAN-084 (R12): bit-level @isCaseSensitive — empty exportJsonKey
8309
+ // (no bit-level emission); defaultValue 'true' phantom-fires onto
8310
+ // the ancestor stack so each listItem's gap chain `@absent: $ancestor`
8311
+ // rule resolves to true. Mirrors the cloze bit (bits.ts:1326-1336).
8312
+ key: ConfigKey.property_isCaseSensitive,
8313
+ exportJsonKey: {},
8314
+ description: "If the cloze answers are case sensitive",
8315
+ format: TagFormat.boolean,
8316
+ defaultValue: "true"
6360
8317
  }
6361
8318
  ],
6362
8319
  cardSet: CardSetConfigKey.clozeList
@@ -6390,6 +8347,11 @@ var BITS = {
6390
8347
  description: "Formula bit, used for mathematical formulas in articles or documents",
6391
8348
  textFormatDefault: TextFormat2.latex
6392
8349
  },
8350
+ [BitType2.formulaImage]: {
8351
+ since: "5.23.0",
8352
+ baseBitType: BitType2.formula,
8353
+ description: "Formula image bit, used for mathematical formulas rendered as images"
8354
+ },
6393
8355
  [BitType2.smartStandardFormula]: {
6394
8356
  since: "3.11.0",
6395
8357
  baseBitType: BitType2.formula,
@@ -6518,6 +8480,7 @@ var BITS = {
6518
8480
  tags: [
6519
8481
  {
6520
8482
  key: ConfigKey.group_person,
8483
+ exportJsonKey: { partner: { name: "$" } },
6521
8484
  description: "Tags for the person in the conversation"
6522
8485
  }
6523
8486
  ]
@@ -6742,6 +8705,7 @@ var BITS = {
6742
8705
  {
6743
8706
  key: ConfigKey.property_servings,
6744
8707
  jsonKey: "servings.servings",
8708
+ exportJsonKey: { servings: { servings: "$" } },
6745
8709
  description: "The number of servings for the ingredients",
6746
8710
  format: TagFormat.number,
6747
8711
  chain: [
@@ -6767,6 +8731,7 @@ var BITS = {
6767
8731
  format: TagFormat.boolean
6768
8732
  },
6769
8733
  {
8734
+ exportJsonKey: { hint: "$" },
6770
8735
  key: ConfigKey.tag_hint,
6771
8736
  description: "Hint for the ingredient, used to provide additional information",
6772
8737
  format: TagFormat.plainText
@@ -6944,6 +8909,18 @@ var BITS = {
6944
8909
  description: "Reference for the essay, used to link to external resources",
6945
8910
  format: TagFormat.plainText,
6946
8911
  maxCount: Count.infinity
8912
+ },
8913
+ {
8914
+ // Override inherited @example from group_standardTags: string root example.
8915
+ key: ConfigKey.property_example,
8916
+ exportJsonKey: [
8917
+ { "@keyonly": { isExample: true, example: "true" } },
8918
+ { "@absent": { isExample: true, example: "true" } },
8919
+ { isExample: true, example: "$" }
8920
+ ],
8921
+ description: "The example text for the essay bit",
8922
+ format: TagFormat.plainText,
8923
+ nullable: true
6947
8924
  }
6948
8925
  ],
6949
8926
  rootExampleType: ExampleType.string
@@ -6964,8 +8941,21 @@ var BITS = {
6964
8941
  description: "Example bit, used to provide examples in articles or books",
6965
8942
  tags: [
6966
8943
  {
8944
+ exportJsonKey: { title: "$" },
6967
8945
  key: ConfigKey.tag_title,
6968
8946
  description: "The title of the example"
8947
+ },
8948
+ {
8949
+ // Override inherited @example from group_standardTags: string root example.
8950
+ key: ConfigKey.property_example,
8951
+ exportJsonKey: [
8952
+ { "@keyonly": { isExample: true, example: "true" } },
8953
+ { "@absent": { isExample: true, example: "true" } },
8954
+ { isExample: true, example: "$" }
8955
+ ],
8956
+ description: "The example text for the example bit",
8957
+ format: TagFormat.plainText,
8958
+ nullable: true
6969
8959
  }
6970
8960
  ],
6971
8961
  rootExampleType: ExampleType.string
@@ -7051,7 +9041,19 @@ var BITS = {
7051
9041
  [BitType2.articleAi]: {
7052
9042
  since: "1.3.0",
7053
9043
  baseBitType: BitType2.article,
7054
- description: "Article AI bit, used to create AI-generated articles"
9044
+ description: "Article AI bit, used to create AI-generated articles",
9045
+ tags: [
9046
+ {
9047
+ // Shadows the inherited @aiGenerated from the standard group: this
9048
+ // bit type forces aiGenerated:true regardless of source value or
9049
+ // absence. Literal `true` in the export pattern ignores `$`.
9050
+ key: ConfigKey.property_aiGenerated,
9051
+ description: "AI-generated flag, forced true for article-ai bits",
9052
+ format: TagFormat.boolean,
9053
+ defaultValue: "true",
9054
+ exportJsonKey: { aiGenerated: true }
9055
+ }
9056
+ ]
7055
9057
  },
7056
9058
  [BitType2.articleAttachment]: {
7057
9059
  since: "1.3.0",
@@ -7376,7 +9378,19 @@ var BITS = {
7376
9378
  [BitType2.noteAi]: {
7377
9379
  since: "1.3.0",
7378
9380
  baseBitType: BitType2.note,
7379
- description: "Note AI bit, used to create AI-generated notes in articles or books"
9381
+ description: "Note AI bit, used to create AI-generated notes in articles or books",
9382
+ tags: [
9383
+ {
9384
+ // Shadows the inherited @aiGenerated from the standard group: this
9385
+ // bit type forces aiGenerated:true regardless of source value or
9386
+ // absence. Literal `true` in the export pattern ignores `$`.
9387
+ key: ConfigKey.property_aiGenerated,
9388
+ description: "AI-generated flag, forced true for article-ai bits",
9389
+ format: TagFormat.boolean,
9390
+ defaultValue: "true",
9391
+ exportJsonKey: { aiGenerated: true }
9392
+ }
9393
+ ]
7380
9394
  },
7381
9395
  [BitType2.notebookArticle]: {
7382
9396
  since: "1.3.0",
@@ -7501,7 +9515,19 @@ var BITS = {
7501
9515
  [BitType2.summaryAi]: {
7502
9516
  since: "1.3.0",
7503
9517
  baseBitType: BitType2.summary,
7504
- description: "AI-generated summary bit"
9518
+ description: "AI-generated summary bit",
9519
+ tags: [
9520
+ {
9521
+ // Shadows the inherited @aiGenerated from the standard group: this
9522
+ // bit type forces aiGenerated:true regardless of source value or
9523
+ // absence. Literal `true` in the export pattern ignores `$`.
9524
+ key: ConfigKey.property_aiGenerated,
9525
+ description: "AI-generated flag, forced true for article-ai bits",
9526
+ format: TagFormat.boolean,
9527
+ defaultValue: "true",
9528
+ exportJsonKey: { aiGenerated: true }
9529
+ }
9530
+ ]
7505
9531
  },
7506
9532
  [BitType2.videoTranscript]: {
7507
9533
  since: "1.3.0",
@@ -7775,7 +9801,7 @@ var BITS = {
7775
9801
  description: "Product ID for the module product, used to link to a specific product",
7776
9802
  format: TagFormat.plainText,
7777
9803
  minCount: 1,
7778
- maxCount: Count.infinity
9804
+ maxCount: 1
7779
9805
  }
7780
9806
  ]
7781
9807
  },
@@ -7828,9 +9854,13 @@ var BITS = {
7828
9854
  description: "Resource bit tags for logo grave images, used to define additional properties"
7829
9855
  },
7830
9856
  {
7831
- // Image resource
7832
- key: ConfigKey.group_resourceImage,
9857
+ // Image resource — fully-expanded shape so each `&image` emission
9858
+ // produces a complete `{type, image: {src}}` object inside the
9859
+ // `images[]` array. Chain attrs like `[@zoomDisabled]` fold under
9860
+ // `images[].image` via the `[]` last-element-merge semantic.
9861
+ key: ConfigKey.group_resourceImageNoZoom,
7833
9862
  description: "Resource image tags for logo grave images, used to attach images",
9863
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
7834
9864
  minCount: 1,
7835
9865
  maxCount: Count.infinity
7836
9866
  }
@@ -7994,15 +10024,27 @@ var BITS = {
7994
10024
  format: TagFormat.plainText,
7995
10025
  chain: [
7996
10026
  {
10027
+ exportJsonKey: { reference: "$" },
7997
10028
  key: ConfigKey.tag_reference,
7998
10029
  format: TagFormat.plainText,
7999
10030
  description: "Reference tag for the book, used to link to the book in the system",
8000
- maxCount: 2
10031
+ maxCount: 1,
10032
+ chain: [
10033
+ {
10034
+ key: ConfigKey.tag_reference,
10035
+ jsonKey: "referenceEnd",
10036
+ exportJsonKey: { referenceEnd: "$" },
10037
+ format: TagFormat.plainText,
10038
+ description: "End reference tag for the book, used to specify a range",
10039
+ maxCount: 1
10040
+ }
10041
+ ]
8001
10042
  }
8002
10043
  ]
8003
10044
  },
8004
10045
  {
8005
10046
  /* Allow incorrectly chained reference tag */
10047
+ exportJsonKey: { reference: "$" },
8006
10048
  key: ConfigKey.tag_reference,
8007
10049
  format: TagFormat.plainText,
8008
10050
  description: "Reference tag for the book, used to link to the book in the system"
@@ -8049,18 +10091,41 @@ var BITS = {
8049
10091
  format: TagFormat.plainText,
8050
10092
  chain: [
8051
10093
  {
10094
+ exportJsonKey: { reference: "$" },
8052
10095
  key: ConfigKey.tag_reference,
8053
10096
  format: TagFormat.plainText,
8054
10097
  description: "Reference tag for the book, used to link to the book in the system",
8055
- maxCount: 2
10098
+ maxCount: 1,
10099
+ chain: [
10100
+ {
10101
+ key: ConfigKey.tag_reference,
10102
+ jsonKey: "referenceEnd",
10103
+ exportJsonKey: { referenceEnd: "$" },
10104
+ format: TagFormat.plainText,
10105
+ description: "End reference tag for the book, used to specify a range",
10106
+ maxCount: 1
10107
+ }
10108
+ ]
8056
10109
  }
8057
10110
  ]
8058
10111
  },
8059
10112
  {
8060
10113
  /* Allow incorrectly chained reference tag */
10114
+ exportJsonKey: { reference: "$" },
8061
10115
  key: ConfigKey.tag_reference,
8062
10116
  format: TagFormat.plainText,
8063
- description: "Reference tag for the book, used to link to the book in the system"
10117
+ description: "Reference tag for the book, used to link to the book in the system",
10118
+ maxCount: 1,
10119
+ chain: [
10120
+ {
10121
+ key: ConfigKey.tag_reference,
10122
+ jsonKey: "referenceEnd",
10123
+ exportJsonKey: { referenceEnd: "$" },
10124
+ format: TagFormat.plainText,
10125
+ description: "End reference tag for the book, used to specify a range",
10126
+ maxCount: 1
10127
+ }
10128
+ ]
8064
10129
  },
8065
10130
  {
8066
10131
  key: ConfigKey.property_buttonCaption,
@@ -8085,14 +10150,29 @@ var BITS = {
8085
10150
  tags: [
8086
10151
  {
8087
10152
  key: ConfigKey.property_book,
10153
+ // Starter-array shape: each `[@book:value]` chain appends a fresh
10154
+ // `{ book, reference }` entry. The chained `►` (tag_reference) lands
10155
+ // inside the most-recently-appended entry as `reference: $`.
10156
+ exportJsonKey: { book: [{ book: "$" }] },
8088
10157
  description: "Book reference for the page, used to link to a specific book",
8089
10158
  maxCount: Count.infinity,
8090
10159
  chain: [
8091
10160
  {
10161
+ exportJsonKey: { reference: "$" },
8092
10162
  key: ConfigKey.tag_reference,
8093
10163
  format: TagFormat.plainText,
8094
10164
  description: "Reference tag for the book, used to link to the book in the system",
8095
- maxCount: 2
10165
+ maxCount: 1,
10166
+ chain: [
10167
+ {
10168
+ key: ConfigKey.tag_reference,
10169
+ jsonKey: "referenceEnd",
10170
+ exportJsonKey: { referenceEnd: "$" },
10171
+ format: TagFormat.plainText,
10172
+ description: "End reference tag for the book, used to specify a range",
10173
+ maxCount: 1
10174
+ }
10175
+ ]
8096
10176
  }
8097
10177
  ]
8098
10178
  },
@@ -8141,7 +10221,9 @@ var BITS = {
8141
10221
  format: TagFormat.plainText
8142
10222
  }
8143
10223
  ],
8144
- cardSet: CardSetConfigKey.exampleBitList
10224
+ // page-footer emits cards under `sections` (vs `listItems` for the
10225
+ // sibling exampleList / assignmentList bits) — see JsonGenerator.ts.
10226
+ cardSet: CardSetConfigKey.pageFooterSections
8145
10227
  },
8146
10228
  [BitType2.legend]: {
8147
10229
  since: "3.12.0",
@@ -8183,6 +10265,20 @@ var BITS = {
8183
10265
  since: "1.34.0",
8184
10266
  baseBitType: BitType2._standard,
8185
10267
  description: "Definition list bit, used to create lists of definitions in articles or books",
10268
+ tags: [
10269
+ {
10270
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10271
+ key: ConfigKey.property_example,
10272
+ exportJsonKey: [
10273
+ { "@keyonly": { isExample: true } },
10274
+ { "@absent": { isExample: true } },
10275
+ { isExample: true, example: "$" }
10276
+ ],
10277
+ description: "The example(s) for the bit",
10278
+ format: TagFormat.bitmarkText,
10279
+ nullable: true
10280
+ }
10281
+ ],
8186
10282
  cardSet: CardSetConfigKey.definitionList
8187
10283
  },
8188
10284
  [BitType2.metaSearchDefaultTerms]: {
@@ -8205,6 +10301,18 @@ var BITS = {
8205
10301
  {
8206
10302
  key: ConfigKey.group_quizCommon,
8207
10303
  description: "Common quiz tags for flashcards"
10304
+ },
10305
+ {
10306
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10307
+ key: ConfigKey.property_example,
10308
+ exportJsonKey: [
10309
+ { "@keyonly": { isExample: true } },
10310
+ { "@absent": { isExample: true } },
10311
+ { isExample: true, example: "$" }
10312
+ ],
10313
+ description: "The example(s) for the bit",
10314
+ format: TagFormat.bitmarkText,
10315
+ nullable: true
8208
10316
  }
8209
10317
  ],
8210
10318
  cardSet: CardSetConfigKey.flashcard
@@ -8212,7 +10320,11 @@ var BITS = {
8212
10320
  [BitType2.flashcard1]: {
8213
10321
  since: "1.3.0",
8214
10322
  baseBitType: BitType2.flashcard,
8215
- description: "Flashcard 1 bit"
10323
+ description: "Flashcard 1 bit",
10324
+ // PLAN-085: use the flashcard1 cardset (sections.default.maxCount=1)
10325
+ // instead of inheriting `flashcard`'s unbounded cardset, since this
10326
+ // bit type structurally allows only a single card.
10327
+ cardSet: CardSetConfigKey.flashcard1
8216
10328
  },
8217
10329
  [BitType2.qAndACard]: {
8218
10330
  since: "3.25.0",
@@ -8247,8 +10359,24 @@ var BITS = {
8247
10359
  description: "Common quiz tags for highlighted text"
8248
10360
  },
8249
10361
  {
8250
- key: ConfigKey.group_trueFalse,
8251
- description: "True/False quiz tags for highlighted text"
10362
+ // Dedicated highlight inline-select chain: tag_true / tag_false
10363
+ // emit `{type:"highlight", attrs:{texts:[{text,isCorrect?}]}}`
10364
+ // inline body nodes via @body-inline. Bit shares no tag IDs with
10365
+ // multiple-choice / multiple-choice-1 (which use group_trueFalse).
10366
+ key: ConfigKey.group_trueFalseInlineHighlight,
10367
+ description: "True/False quiz tags for highlighted text (inline body nodes)"
10368
+ },
10369
+ {
10370
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10371
+ key: ConfigKey.property_example,
10372
+ exportJsonKey: [
10373
+ { "@keyonly": { isExample: true } },
10374
+ { "@absent": { isExample: true } },
10375
+ { isExample: true, example: "$" }
10376
+ ],
10377
+ description: "The example(s) for the bit",
10378
+ format: TagFormat.bitmarkText,
10379
+ nullable: true
8252
10380
  }
8253
10381
  ]
8254
10382
  },
@@ -8258,14 +10386,8 @@ var BITS = {
8258
10386
  description: "Image bit, used to create images in articles or books",
8259
10387
  tags: [
8260
10388
  {
8261
- key: ConfigKey.resource_backgroundWallpaper,
8262
- description: "Background wallpaper for the image, used to set a background for the image",
8263
- chain: [
8264
- {
8265
- key: ConfigKey.group_resourceImageCommon,
8266
- description: "Common resource image tags for images"
8267
- }
8268
- ]
10389
+ key: ConfigKey.group_backgroundWallpaper,
10390
+ description: "Background wallpaper tags for images, used to define background properties for images"
8269
10391
  },
8270
10392
  {
8271
10393
  key: ConfigKey.group_resourceBitTags,
@@ -8368,8 +10490,15 @@ var BITS = {
8368
10490
  },
8369
10491
  [BitType2.imageSeparator]: {
8370
10492
  since: "1.4.15",
8371
- baseBitType: BitType2.image,
8372
- description: "Image separator bit, used to create separators in articles or books"
10493
+ baseBitType: BitType2._standard,
10494
+ description: "Image separator bit, used to create separators in articles or books",
10495
+ tags: [
10496
+ {
10497
+ key: ConfigKey.group_imageNoZoom,
10498
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
10499
+ }
10500
+ ],
10501
+ resourceAttachmentAllowed: false
8373
10502
  },
8374
10503
  [BitType2.imageSeparatorAlt]: {
8375
10504
  since: "1.16.0",
@@ -8408,15 +10537,20 @@ var BITS = {
8408
10537
  },
8409
10538
  [BitType2.pageBanner]: {
8410
10539
  since: "1.4.3",
8411
- baseBitType: BitType2.image,
10540
+ baseBitType: BitType2._standard,
8412
10541
  description: "Page banner bit, used to create banners for pages in articles or books",
8413
10542
  tags: [
8414
10543
  {
8415
10544
  key: ConfigKey.property_slug,
8416
10545
  description: "Slug for the page banner, used to identify the banner in the system",
8417
10546
  format: TagFormat.plainText
10547
+ },
10548
+ {
10549
+ key: ConfigKey.group_imageNoZoom,
10550
+ description: "Image no zoom tags for images, used to define whether images should have zoom functionality"
8418
10551
  }
8419
- ]
10552
+ ],
10553
+ resourceAttachmentAllowed: false
8420
10554
  },
8421
10555
  [BitType2.pageHero]: {
8422
10556
  since: "1.11.0",
@@ -8431,7 +10565,7 @@ var BITS = {
8431
10565
  },
8432
10566
  [BitType2.tableImage]: {
8433
10567
  since: "1.5.15",
8434
- baseBitType: BitType2.table,
10568
+ baseBitType: BitType2.tableExtended,
8435
10569
  description: "Table image bit, used to create images in tables in articles or books",
8436
10570
  tags: [
8437
10571
  {
@@ -8440,14 +10574,8 @@ var BITS = {
8440
10574
  format: TagFormat.bitmarkText
8441
10575
  },
8442
10576
  {
8443
- key: ConfigKey.resource_backgroundWallpaper,
8444
- description: "Background wallpaper for the image, used to set a background for the image",
8445
- chain: [
8446
- {
8447
- key: ConfigKey.group_resourceImageCommon,
8448
- description: "Common resource image tags for images"
8449
- }
8450
- ]
10577
+ key: ConfigKey.group_backgroundWallpaper,
10578
+ description: "Background wallpaper tags for images, used to define background properties for images"
8451
10579
  },
8452
10580
  {
8453
10581
  key: ConfigKey.group_resourceBitTags,
@@ -8590,9 +10718,10 @@ var BITS = {
8590
10718
  description: "Resource bit tags for logo grave images, used to define additional properties"
8591
10719
  },
8592
10720
  {
8593
- // Image resource
8594
- key: ConfigKey.group_resourceImage,
10721
+ // Image resource — fully-expanded shape (see BitType.extractorImage).
10722
+ key: ConfigKey.group_resourceImageNoZoom,
8595
10723
  description: "Resource image tags for logo grave images, used to attach images",
10724
+ exportJsonKey: { logos: [{ type: "image", image: { src: "$" } }] },
8596
10725
  minCount: 1,
8597
10726
  maxCount: Count.infinity
8598
10727
  }
@@ -8602,7 +10731,17 @@ var BITS = {
8602
10731
  [BitType2.prototypeImages]: {
8603
10732
  since: "1.6.1",
8604
10733
  baseBitType: BitType2.imagesLogoGrave,
8605
- description: "Prototype images bit, used to create prototype images in articles or books"
10734
+ description: "Prototype images bit, used to create prototype images in articles or books",
10735
+ tags: [
10736
+ {
10737
+ // Override inherited `logos` wrapper with `images` for this bit type.
10738
+ key: ConfigKey.group_resourceImageNoZoom,
10739
+ description: "Resource image tags for prototype images",
10740
+ exportJsonKey: { images: [{ type: "image", image: { src: "$" } }] },
10741
+ minCount: 1,
10742
+ maxCount: Count.infinity
10743
+ }
10744
+ ]
8606
10745
  },
8607
10746
  [BitType2.internalLink]: {
8608
10747
  since: "1.3.0",
@@ -8610,6 +10749,7 @@ var BITS = {
8610
10749
  description: "Internal link bit, used to create links to other bits in articles or books",
8611
10750
  tags: [
8612
10751
  {
10752
+ exportJsonKey: { reference: "$" },
8613
10753
  key: ConfigKey.tag_reference,
8614
10754
  format: TagFormat.plainText,
8615
10755
  description: "Reference tag for the internal link, used to link to the target bit"
@@ -8628,8 +10768,21 @@ var BITS = {
8628
10768
  },
8629
10769
  {
8630
10770
  key: ConfigKey.property_reasonableNumOfChars,
10771
+ exportJsonKey: {},
8631
10772
  description: "Reasonable number of characters for the interview, used to limit input size",
8632
10773
  format: TagFormat.number
10774
+ },
10775
+ {
10776
+ // Override inherited @example: in `isTopLevelExample` allow-list.
10777
+ key: ConfigKey.property_example,
10778
+ exportJsonKey: [
10779
+ { "@keyonly": { isExample: true } },
10780
+ { "@absent": { isExample: true } },
10781
+ { isExample: true, example: "$" }
10782
+ ],
10783
+ description: "The example(s) for the bit",
10784
+ format: TagFormat.bitmarkText,
10785
+ nullable: true
8633
10786
  }
8634
10787
  ],
8635
10788
  cardSet: CardSetConfigKey.questions
@@ -8988,6 +11141,18 @@ var BITS = {
8988
11141
  {
8989
11142
  key: ConfigKey.group_mark,
8990
11143
  description: "Mark tags, used to define the content of marks"
11144
+ },
11145
+ {
11146
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11147
+ key: ConfigKey.property_example,
11148
+ exportJsonKey: [
11149
+ { "@keyonly": { isExample: true } },
11150
+ { "@absent": { isExample: true } },
11151
+ { isExample: true, example: "$" }
11152
+ ],
11153
+ description: "The example(s) for the bit",
11154
+ format: TagFormat.bitmarkText,
11155
+ nullable: true
8991
11156
  }
8992
11157
  ]
8993
11158
  },
@@ -9003,9 +11168,27 @@ var BITS = {
9003
11168
  },
9004
11169
  {
9005
11170
  key: ConfigKey.property_isCaseSensitive,
11171
+ exportJsonKey: {},
9006
11172
  description: "Case sensitivity for matching pairs, used to define if matches are case-sensitive",
9007
11173
  format: TagFormat.boolean,
9008
11174
  defaultValue: "true"
11175
+ },
11176
+ {
11177
+ // Override inherited @example: in `isTopLevelExample` allow-list
11178
+ // (covers match, matchAll, matchReverse, matchAllReverse,
11179
+ // matchSolutionGrouped, matchMatrix, matchAudio, matchPicture).
11180
+ // Bit-level fires emit only `isExample`; the @example value (when
11181
+ // provided) is recorded in the ancestor stack and consumed by
11182
+ // per-pair/per-cell variant overrides — not surfaced at bit scope.
11183
+ key: ConfigKey.property_example,
11184
+ exportJsonKey: [
11185
+ { "@keyonly": { isExample: true } },
11186
+ { "@absent": { isExample: true } },
11187
+ { isExample: true }
11188
+ ],
11189
+ description: "The example(s) for the bit",
11190
+ format: TagFormat.bitmarkText,
11191
+ nullable: true
9009
11192
  }
9010
11193
  ],
9011
11194
  cardSet: CardSetConfigKey.matchPairs
@@ -9055,6 +11238,7 @@ var BITS = {
9055
11238
  tags: [
9056
11239
  {
9057
11240
  key: ConfigKey.property_reasonableNumOfChars,
11241
+ exportJsonKey: {},
9058
11242
  description: "Reasonable number of characters for feedback, used to limit input size",
9059
11243
  format: TagFormat.number
9060
11244
  }
@@ -9089,6 +11273,18 @@ var BITS = {
9089
11273
  {
9090
11274
  key: ConfigKey.group_trueFalse,
9091
11275
  description: "True/False tags for multiple choice 1 questions"
11276
+ },
11277
+ {
11278
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11279
+ key: ConfigKey.property_example,
11280
+ exportJsonKey: [
11281
+ { "@keyonly": { isExample: true } },
11282
+ { "@absent": { isExample: true } },
11283
+ { isExample: true, example: "$" }
11284
+ ],
11285
+ description: "The example(s) for the bit",
11286
+ format: TagFormat.bitmarkText,
11287
+ nullable: true
9092
11288
  }
9093
11289
  ]
9094
11290
  },
@@ -9110,8 +11306,20 @@ var BITS = {
9110
11306
  {
9111
11307
  key: ConfigKey.group_trueFalse,
9112
11308
  description: "True/False tags for multiple choice questions"
9113
- }
11309
+ },
9114
11310
  // This is actually for multiple-choice-1, but we support it here as well (as many bits are wrong)
11311
+ {
11312
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11313
+ key: ConfigKey.property_example,
11314
+ exportJsonKey: [
11315
+ { "@keyonly": { isExample: true } },
11316
+ { "@absent": { isExample: true } },
11317
+ { isExample: true, example: "$" }
11318
+ ],
11319
+ description: "The example(s) for the bit",
11320
+ format: TagFormat.bitmarkText,
11321
+ nullable: true
11322
+ }
9115
11323
  ],
9116
11324
  cardSet: CardSetConfigKey.quiz
9117
11325
  },
@@ -9131,8 +11339,20 @@ var BITS = {
9131
11339
  description: "Common quiz tags for multiple choice text questions"
9132
11340
  },
9133
11341
  {
9134
- key: ConfigKey.group_trueFalse,
11342
+ key: ConfigKey.group_trueFalseInlineSelect,
9135
11343
  description: "True/False tags for multiple choice text questions"
11344
+ },
11345
+ {
11346
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11347
+ key: ConfigKey.property_example,
11348
+ exportJsonKey: [
11349
+ { "@keyonly": { isExample: true } },
11350
+ { "@absent": { isExample: true } },
11351
+ { isExample: true, example: "$" }
11352
+ ],
11353
+ description: "The example(s) for the bit",
11354
+ format: TagFormat.bitmarkText,
11355
+ nullable: true
9136
11356
  }
9137
11357
  ]
9138
11358
  },
@@ -9157,8 +11377,20 @@ var BITS = {
9157
11377
  description: "Common quiz tags for multiple response 1 questions"
9158
11378
  },
9159
11379
  {
9160
- key: ConfigKey.group_trueFalse,
11380
+ key: ConfigKey.group_trueFalseResponses,
9161
11381
  description: "True/False tags for multiple response 1 questions"
11382
+ },
11383
+ {
11384
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11385
+ key: ConfigKey.property_example,
11386
+ exportJsonKey: [
11387
+ { "@keyonly": { isExample: true } },
11388
+ { "@absent": { isExample: true } },
11389
+ { isExample: true, example: "$" }
11390
+ ],
11391
+ description: "The example(s) for the bit",
11392
+ format: TagFormat.bitmarkText,
11393
+ nullable: true
9162
11394
  }
9163
11395
  ]
9164
11396
  },
@@ -9178,12 +11410,24 @@ var BITS = {
9178
11410
  description: "Common quiz tags for multiple response questions"
9179
11411
  },
9180
11412
  {
9181
- key: ConfigKey.group_trueFalse,
11413
+ key: ConfigKey.group_trueFalseResponses,
9182
11414
  description: "True/False tags for multiple response questions"
9183
- }
11415
+ },
9184
11416
  // This is actually for multiple-response-1, but we support it here as well (as many bits are wrong)
11417
+ {
11418
+ // Override inherited @example: in `isTopLevelExample` allow-list.
11419
+ key: ConfigKey.property_example,
11420
+ exportJsonKey: [
11421
+ { "@keyonly": { isExample: true } },
11422
+ { "@absent": { isExample: true } },
11423
+ { isExample: true, example: "$" }
11424
+ ],
11425
+ description: "The example(s) for the bit",
11426
+ format: TagFormat.bitmarkText,
11427
+ nullable: true
11428
+ }
9185
11429
  ],
9186
- cardSet: CardSetConfigKey.quiz
11430
+ cardSet: CardSetConfigKey.quizResponses
9187
11431
  },
9188
11432
  [BitType2.coachSelfReflectionMultipleResponse]: {
9189
11433
  since: "1.3.0",
@@ -9196,6 +11440,7 @@ var BITS = {
9196
11440
  description: "Page bit, used to create pages in articles or books",
9197
11441
  tags: [
9198
11442
  {
11443
+ exportJsonKey: { title: "$" },
9199
11444
  key: ConfigKey.tag_title,
9200
11445
  description: "Title of the page, used to display the page title"
9201
11446
  },
@@ -9432,6 +11677,8 @@ var BITS = {
9432
11677
  },
9433
11678
  {
9434
11679
  key: ConfigKey.resource_platformIcon,
11680
+ jsonKey: "platformIcon|resource(type=image, key=image)",
11681
+ exportJsonKey: { platformIcon: { type: "image", image: { src: "$" } } },
9435
11682
  description: "The platform icon",
9436
11683
  chain: [
9437
11684
  {
@@ -9467,6 +11714,8 @@ var BITS = {
9467
11714
  tags: [
9468
11715
  {
9469
11716
  key: ConfigKey.resource_platformLogo,
11717
+ jsonKey: "platformLogo|resource(type=image, key=image)",
11718
+ exportJsonKey: { platformLogo: { type: "image", image: { src: "$" } } },
9470
11719
  description: "The platform logo",
9471
11720
  chain: [
9472
11721
  {
@@ -9555,6 +11804,7 @@ var BITS = {
9555
11804
  {
9556
11805
  key: ConfigKey.resource_platformBackgroundImage,
9557
11806
  jsonKey: "platformBackgroundImage|resource(type=image, key=image)",
11807
+ exportJsonKey: { platformBackgroundImage: { type: "image", image: { src: "$" } } },
9558
11808
  description: "The platform section chat background image",
9559
11809
  chain: [
9560
11810
  {
@@ -9648,6 +11898,7 @@ var BITS = {
9648
11898
  description: "Review note bit, used to create review notes in articles or books",
9649
11899
  tags: [
9650
11900
  {
11901
+ exportJsonKey: { title: "$" },
9651
11902
  key: ConfigKey.tag_title,
9652
11903
  description: "Title of the review note, used to display the note title"
9653
11904
  },
@@ -9666,6 +11917,18 @@ var BITS = {
9666
11917
  key: ConfigKey.property_resolvedBy,
9667
11918
  description: "Resolved by for the review note, used to define who resolved the note",
9668
11919
  format: TagFormat.plainText
11920
+ },
11921
+ {
11922
+ // Override inherited @example from group_standardTags: string root example.
11923
+ key: ConfigKey.property_example,
11924
+ exportJsonKey: [
11925
+ { "@keyonly": { isExample: true, example: "true" } },
11926
+ { "@absent": { isExample: true, example: "true" } },
11927
+ { isExample: true, example: "$" }
11928
+ ],
11929
+ description: "The example text for the review note bit",
11930
+ format: TagFormat.plainText,
11931
+ nullable: true
9669
11932
  }
9670
11933
  ],
9671
11934
  rootExampleType: ExampleType.string
@@ -9718,11 +11981,13 @@ var BITS = {
9718
11981
  tags: [
9719
11982
  // Not sure if these are actually valid here, but include them as they are in the test bit.
9720
11983
  {
11984
+ exportJsonKey: { anchor: "$" },
9721
11985
  key: ConfigKey.tag_anchor,
9722
11986
  format: TagFormat.plainText,
9723
11987
  description: "Anchor for the sample solution, used to link to the solution"
9724
11988
  },
9725
11989
  {
11990
+ exportJsonKey: { reference: "$" },
9726
11991
  key: ConfigKey.tag_reference,
9727
11992
  format: TagFormat.plainText,
9728
11993
  description: "Reference for the sample solution, used to link to the source of the solution"
@@ -9738,6 +12003,20 @@ var BITS = {
9738
12003
  {
9739
12004
  key: ConfigKey.group_quizCommon,
9740
12005
  description: "Common quiz tags for sequences"
12006
+ },
12007
+ {
12008
+ // Override inherited @example from group_standardTags: this bit has
12009
+ // a boolean root example (rootExampleType: boolean), so bit-level
12010
+ // [@example] writes `example: true` in addition to `isExample: true`.
12011
+ key: ConfigKey.property_example,
12012
+ exportJsonKey: [
12013
+ { "@keyonly": { isExample: true, example: true } },
12014
+ { "@absent": { isExample: true, example: true } },
12015
+ { isExample: true, example: "$" }
12016
+ ],
12017
+ description: "The example flag for the bit (boolean)",
12018
+ format: TagFormat.boolean,
12019
+ nullable: true
9741
12020
  }
9742
12021
  ],
9743
12022
  cardSet: CardSetConfigKey.elements,
@@ -9845,6 +12124,7 @@ var BITS = {
9845
12124
  {
9846
12125
  key: ConfigKey.property_ratingLevelStart,
9847
12126
  jsonKey: "ratingLevelStart.level",
12127
+ exportJsonKey: { ratingLevelStart: { level: "$" } },
9848
12128
  description: "Start level for the rating survey, used to define the lowest rating",
9849
12129
  format: TagFormat.number,
9850
12130
  chain: [
@@ -9858,6 +12138,7 @@ var BITS = {
9858
12138
  {
9859
12139
  key: ConfigKey.property_ratingLevelEnd,
9860
12140
  jsonKey: "ratingLevelEnd.level",
12141
+ exportJsonKey: { ratingLevelEnd: { level: "$" } },
9861
12142
  description: "End level for the rating survey, used to define the highest rating",
9862
12143
  format: TagFormat.number,
9863
12144
  chain: [
@@ -10009,14 +12290,8 @@ var BITS = {
10009
12290
  format: TagFormat.bitmarkText
10010
12291
  },
10011
12292
  {
10012
- key: ConfigKey.resource_backgroundWallpaper,
10013
- description: "Background wallpaper for the image, used to set a background for the image",
10014
- chain: [
10015
- {
10016
- key: ConfigKey.group_resourceImageCommon,
10017
- description: "Common resource image tags for images"
10018
- }
10019
- ]
12293
+ key: ConfigKey.group_backgroundWallpaper,
12294
+ description: "Background wallpaper tags for images, used to define background properties for images"
10020
12295
  },
10021
12296
  {
10022
12297
  key: ConfigKey.group_resourceBitTags,
@@ -10393,12 +12668,48 @@ var BITS = {
10393
12668
  format: TagFormat.plainText
10394
12669
  },
10395
12670
  {
12671
+ // PLAN-067 Edit #1: true-false-1's `+` / `-` emit a flat
12672
+ // `{ statement, isCorrect }` at bit root (single-statement bit),
12673
+ // not a `choices[]` array. Format is bitmarkText so the statement
12674
+ // body renders as a ProseMirror tree, matching reference fixtures.
12675
+ exportJsonKey: { statement: "$", isCorrect: true },
10396
12676
  key: ConfigKey.tag_true,
10397
- description: "Tag for the true option in the true/false question"
12677
+ description: "Tag for the true option in the true/false question",
12678
+ format: TagFormat.bitmarkText
10398
12679
  },
10399
12680
  {
12681
+ exportJsonKey: { statement: "$", isCorrect: false },
10400
12682
  key: ConfigKey.tag_false,
10401
- description: "Tag for the false option in the true/false question"
12683
+ description: "Tag for the false option in the true/false question",
12684
+ format: TagFormat.bitmarkText
12685
+ },
12686
+ {
12687
+ // Override inherited @example from group_standardTags: boolean root
12688
+ // example. Note: the legacy reference parser ties the bare-form
12689
+ // example value to `statement.isCorrect`, but the statement is not
12690
+ // yet emitted when this tag fires at bit-header. The pattern below
12691
+ // emits a literal `true` for bare/synthesized and `$` for valued —
12692
+ // sufficient for `[+...]` (correct) statements which dominate the
12693
+ // fixtures; `[-...]` (incorrect) bits with bare `[@example]` would
12694
+ // need either a runtime extension or an explicit `[@example:false]`.
12695
+ //
12696
+ // Format is `boolean` so `[@example:false]` coerces to JSON `false`
12697
+ // (a natural default that gets stripped in optimised mode, matching
12698
+ // BPG's `rootExampleType: boolean`). PlainText would emit the
12699
+ // STRING `"false"` which doesn't strip.
12700
+ key: ConfigKey.property_example,
12701
+ exportJsonKey: [
12702
+ { "@keyonly": { isExample: true, example: true } },
12703
+ { "@absent": { isExample: true, example: true } },
12704
+ // `[@example:false]`: emit `isExample: true` only — `example: false`
12705
+ // is the natural default for boolean, stripped in optimised mode.
12706
+ // Mirrors BPG `removeUnwantedProperties` `ignoreAllFalse: true`.
12707
+ { "@value=false": { isExample: true } },
12708
+ { isExample: true, example: "$" }
12709
+ ],
12710
+ description: "The example flag for the bit (boolean)",
12711
+ format: TagFormat.boolean,
12712
+ nullable: true
10402
12713
  }
10403
12714
  ],
10404
12715
  rootExampleType: ExampleType.boolean
@@ -10422,6 +12733,18 @@ var BITS = {
10422
12733
  key: ConfigKey.property_labelFalse,
10423
12734
  description: "Label for the false option in the true/false question",
10424
12735
  format: TagFormat.plainText
12736
+ },
12737
+ {
12738
+ // Override inherited @example: in `isTopLevelExample` allow-list.
12739
+ key: ConfigKey.property_example,
12740
+ exportJsonKey: [
12741
+ { "@keyonly": { isExample: true } },
12742
+ { "@absent": { isExample: true } },
12743
+ { isExample: true, example: "$" }
12744
+ ],
12745
+ description: "The example(s) for the bit",
12746
+ format: TagFormat.bitmarkText,
12747
+ nullable: true
10425
12748
  }
10426
12749
  ],
10427
12750
  cardSet: CardSetConfigKey.statements
@@ -10611,6 +12934,11 @@ var BITS = {
10611
12934
  baseBitType: BitType2.vendorStripePricingTable,
10612
12935
  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",
10613
12936
  tags: [
12937
+ {
12938
+ key: ConfigKey.property_stripePricingTableDescriptiveName,
12939
+ description: "Descriptive name for the Stripe pricing table",
12940
+ format: TagFormat.plainText
12941
+ },
10614
12942
  {
10615
12943
  key: ConfigKey.property_printOnRequestCustomerId,
10616
12944
  description: "Print-on-request customer ID, used to identify the customer for the print-on-request flow",
@@ -10981,6 +13309,8 @@ var Config = class {
10981
13309
  maxCount: 1,
10982
13310
  chain: tag.chain,
10983
13311
  jsonKey: tag.jsonKey,
13312
+ exportJsonKey: tag.exportJsonKey,
13313
+ hasExportJsonKey: tag.hasExportJsonKey,
10984
13314
  deprecated: tag.deprecated
10985
13315
  });
10986
13316
  finalResourceTags[k] = newTag;
@@ -11085,7 +13415,7 @@ var instance2 = new Config();
11085
13415
  // src/generated/package_info.ts
11086
13416
  var PACKAGE_INFO = {
11087
13417
  "name": "@gmb/bitmark-parser-generator",
11088
- "version": "5.22.0",
13418
+ "version": "5.24.0",
11089
13419
  "author": "Get More Brain Ltd",
11090
13420
  "license": "ISC",
11091
13421
  "description": "A bitmark parser and generator using Peggy.js"
@@ -11679,6 +14009,8 @@ var NodeType = {
11679
14009
  gmbExternalShopItems: "gmbExternalShopItems",
11680
14010
  groupTag: "groupTag",
11681
14011
  groupTagValue: "groupTagValue",
14012
+ gUri: "gUri",
14013
+ gUriValue: "gUriValue",
11682
14014
  handInAcceptFileType: "handInAcceptFileType",
11683
14015
  handInAcceptFileTypeValue: "handInAcceptFileTypeValue",
11684
14016
  handInInstruction: "handInInstruction",
@@ -12021,6 +14353,8 @@ var NodeType = {
12021
14353
  solution: "solution",
12022
14354
  solutions: "solutions",
12023
14355
  solutionsValue: "solutionsValue",
14356
+ sourceBB: "sourceBB",
14357
+ sourceBBValue: "sourceBBValue",
12024
14358
  sourceDocument: "sourceDocument",
12025
14359
  sourceDocumentValue: "sourceDocumentValue",
12026
14360
  sourceRL: "sourceRL",
@@ -12046,6 +14380,8 @@ var NodeType = {
12046
14380
  stillImageFilmLink: "stillImageFilmLink",
12047
14381
  stripePricingTableId: "stripePricingTableId",
12048
14382
  stripePricingTableIdValue: "stripePricingTableIdValue",
14383
+ stripePricingTableDescriptiveName: "stripePricingTableDescriptiveName",
14384
+ stripePricingTableDescriptiveNameValue: "stripePricingTableDescriptiveNameValue",
12049
14385
  stripePublishableKey: "stripePublishableKey",
12050
14386
  stripePublishableKeyValue: "stripePublishableKeyValue",
12051
14387
  subject: "subject",
@@ -24901,6 +27237,7 @@ var Builder = class extends BaseBuilder {
24901
27237
  isCommented: data.isCommented,
24902
27238
  // Properties
24903
27239
  id: this.toAstProperty(bitType, ConfigKey.property_id, data.id, options),
27240
+ gUri: this.toAstProperty(bitType, ConfigKey.property_gUri, data.gUri, options),
24904
27241
  internalComment: this.toAstProperty(
24905
27242
  bitType,
24906
27243
  ConfigKey.property_internalComment,
@@ -25112,6 +27449,7 @@ var Builder = class extends BaseBuilder {
25112
27449
  data.extractorInternal,
25113
27450
  options
25114
27451
  ),
27452
+ sourceBB: this.normaliseSourceBB(data.sourceBB),
25115
27453
  levelCEFRp: this.toAstProperty(
25116
27454
  bitType,
25117
27455
  ConfigKey.property_levelCEFRp,
@@ -25469,6 +27807,12 @@ var Builder = class extends BaseBuilder {
25469
27807
  data.stripePricingTableId,
25470
27808
  options
25471
27809
  ),
27810
+ stripePricingTableDescriptiveName: this.toAstProperty(
27811
+ bitType,
27812
+ ConfigKey.property_stripePricingTableDescriptiveName,
27813
+ data.stripePricingTableDescriptiveName,
27814
+ options
27815
+ ),
25472
27816
  stripePublishableKey: this.toAstProperty(
25473
27817
  bitType,
25474
27818
  ConfigKey.property_stripePublishableKey,
@@ -26132,6 +28476,19 @@ var Builder = class extends BaseBuilder {
26132
28476
  nodes = Object.values(combinedNodes);
26133
28477
  return nodes.length > 0 ? nodes : void 0;
26134
28478
  }
28479
+ /**
28480
+ * Normalise a sourceBB input into the AST shape (always number[][]).
28481
+ * Accepts a single 4-number tuple or an array of tuples; returns undefined when empty.
28482
+ */
28483
+ normaliseSourceBB(data) {
28484
+ if (data == null) return void 0;
28485
+ if (!Array.isArray(data) || data.length === 0) return void 0;
28486
+ const isNested = data.some(Array.isArray);
28487
+ const tuples = (isNested ? data : [data]).filter(
28488
+ (t) => Array.isArray(t) && t.length === 4 && t.every((n) => typeof n === "number")
28489
+ );
28490
+ return tuples.length > 0 ? tuples : void 0;
28491
+ }
26135
28492
  /**
26136
28493
  * Build groupTag node
26137
28494
  *
@@ -28451,6 +30808,23 @@ var BitmarkGenerator = class extends AstWalkerGenerator {
28451
30808
  format: TagFormat.plainText
28452
30809
  });
28453
30810
  }
30811
+ // bitmarkAst -> bits -> bitsValue -> sourceBB
30812
+ enter_sourceBB(node, route) {
30813
+ const parent = this.getParentNode(route);
30814
+ if (parent?.key !== NodeType.bitsValue) return false;
30815
+ const tuples = node.value;
30816
+ if (!Array.isArray(tuples) || tuples.length === 0) return false;
30817
+ this.writeProperty(
30818
+ "sourceBB",
30819
+ tuples.map((t) => t.join(", ")),
30820
+ route,
30821
+ {
30822
+ format: TagFormat.plainText,
30823
+ array: true
30824
+ }
30825
+ );
30826
+ return false;
30827
+ }
28454
30828
  // bitmarkAst -> bits -> bitsValue -> questions -> questionsValue -> additionalSolutions -> additionalSolutionsValue
28455
30829
  leaf_additionalSolutionsValue(node, route) {
28456
30830
  const parent = this.getParentNode(route, 2);
@@ -30574,6 +32948,16 @@ var JsonGenerator = class extends AstWalkerGenerator {
30574
32948
  enter_ratingLevelEnd(node, route) {
30575
32949
  return this.standardHandler(node, route, NodeType.bitsValue, { array: false });
30576
32950
  }
32951
+ // bitmarkAst -> bits -> bitsValue -> sourceBB
32952
+ enter_sourceBB(node, route) {
32953
+ const tuples = node.value;
32954
+ const parent = this.getParentNode(route);
32955
+ if (parent?.key !== NodeType.bitsValue) return false;
32956
+ if (tuples && tuples.length > 0) {
32957
+ this.bitJson.sourceBB = tuples.length === 1 ? tuples[0] : tuples;
32958
+ }
32959
+ return false;
32960
+ }
30577
32961
  // bitmarkAst -> bits -> bitsValue -> productId
30578
32962
  enter_productId(node, route) {
30579
32963
  const productIds = node.value;
@@ -33656,6 +36040,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
33656
36040
  const cardSetType = bitConfig.cardSet?.configKey;
33657
36041
  switch (cardSetType) {
33658
36042
  case CardSetConfigKey.flashcard:
36043
+ case CardSetConfigKey.flashcard1:
33659
36044
  case CardSetConfigKey.definitionList:
33660
36045
  result = parseFlashcardOrDefinitionList(context, bitType, processedCardSet);
33661
36046
  break;
@@ -33669,6 +36054,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
33669
36054
  result = parseFeedback(context, bitType, processedCardSet);
33670
36055
  break;
33671
36056
  case CardSetConfigKey.quiz:
36057
+ case CardSetConfigKey.quizResponses:
33672
36058
  result = parseQuiz(context, bitType, processedCardSet, choicesV1, responsesV1);
33673
36059
  break;
33674
36060
  case CardSetConfigKey.questions:
@@ -33695,6 +36081,7 @@ function buildCards(context, bitType, textFormat, parsedCardSet, statementV1, st
33695
36081
  break;
33696
36082
  case CardSetConfigKey.clozeList:
33697
36083
  case CardSetConfigKey.exampleBitList:
36084
+ case CardSetConfigKey.pageFooterSections:
33698
36085
  case CardSetConfigKey.bookReferenceList:
33699
36086
  result = parseCardBits(context, bitType, textFormat, processedCardSet);
33700
36087
  break;
@@ -35252,6 +37639,17 @@ function propertyContentProcessor(context, contentDepth, tagsConfig, content, ta
35252
37639
  format: TextFormat2.bitmarkText,
35253
37640
  location: TextLocation2.tag
35254
37641
  });
37642
+ case TagFormat.coordinates:
37643
+ const coordRaw = instance3.unbreakscape(v2, {
37644
+ format: TextFormat2.plainText,
37645
+ location: TextLocation2.tag
37646
+ });
37647
+ if (coordRaw == null) return void 0;
37648
+ const coordParts = coordRaw.split(",").map((p) => instance6.asNumber(p.trim()));
37649
+ if (coordParts.length !== 4 || coordParts.some((n) => n == null)) {
37650
+ return void 0;
37651
+ }
37652
+ return coordParts;
35255
37653
  }
35256
37654
  }
35257
37655
  return instance3.unbreakscape(v2, {
@@ -35288,6 +37686,8 @@ function propertyContentProcessor(context, contentDepth, tagsConfig, content, ta
35288
37686
  minCount: 1,
35289
37687
  chain: void 0,
35290
37688
  jsonKey: void 0,
37689
+ exportJsonKey: void 0,
37690
+ hasExportJsonKey: false,
35291
37691
  format: TagFormat.bitmarkText,
35292
37692
  values: void 0,
35293
37693
  defaultValue: void 0,
@@ -35298,19 +37698,18 @@ function propertyContentProcessor(context, contentDepth, tagsConfig, content, ta
35298
37698
  }
35299
37699
 
35300
37700
  // src/parser/bitmark/peg/contentProcessors/ReferenceTagContentProcessor.ts
35301
- function referenceTagContentProcessor(_context, _contentDepth, _tagsConfig, content, target, isReferenceEnd) {
35302
- const { value } = content;
37701
+ function referenceTagContentProcessor(context, _contentDepth, tagsConfig, content, target) {
37702
+ const { value, chain } = content;
35303
37703
  const trimmedStringValue = stringUtils.trimmedString(value);
35304
- if (isReferenceEnd) {
35305
- target.referenceEnd = instance3.unbreakscape(trimmedStringValue, {
35306
- format: TextFormat2.bitmarkText,
35307
- location: TextLocation2.tag
35308
- });
35309
- } else {
35310
- target.reference = instance3.unbreakscape(trimmedStringValue, {
35311
- format: TextFormat2.bitmarkText,
35312
- location: TextLocation2.tag
35313
- });
37704
+ target.reference = instance3.unbreakscape(trimmedStringValue, {
37705
+ format: TextFormat2.bitmarkText,
37706
+ location: TextLocation2.tag
37707
+ });
37708
+ if (Array.isArray(chain) && chain.length > 0) {
37709
+ const sub = context.bitContentProcessor(ContentDepth.Chain, tagsConfig, chain);
37710
+ if (sub.reference != null) {
37711
+ target.referenceEnd = sub.reference;
37712
+ }
35314
37713
  }
35315
37714
  }
35316
37715
 
@@ -35996,7 +38395,6 @@ var BitmarkPegParserProcessor = class {
35996
38395
  result.markConfig = [];
35997
38396
  result.extraProperties = {};
35998
38397
  result.internalComments = [];
35999
- let seenReference = false;
36000
38398
  let inFooter = false;
36001
38399
  const bodyParts = [];
36002
38400
  let bodyTextPart = instance3.EMPTY_STRING;
@@ -36032,15 +38430,7 @@ var BitmarkPegParserProcessor = class {
36032
38430
  defaultTagContentProcessor(this.context, contentDepth, tagsConfig, content, result);
36033
38431
  break;
36034
38432
  case TypeKey.Reference:
36035
- referenceTagContentProcessor(
36036
- this.context,
36037
- contentDepth,
36038
- tagsConfig,
36039
- content,
36040
- result,
36041
- seenReference
36042
- );
36043
- seenReference = true;
38433
+ referenceTagContentProcessor(this.context, contentDepth, tagsConfig, content, result);
36044
38434
  break;
36045
38435
  case TypeKey.Title:
36046
38436
  titleTagContentProcessor(this.context, contentDepth, tagsConfig, content, result);
@@ -40420,6 +42810,117 @@ var JsonStringGenerator = class {
40420
42810
  }
40421
42811
  };
40422
42812
 
42813
+ // src/utils/BoundingBox.ts
42814
+ function toBbox(arr) {
42815
+ if (!Array.isArray(arr) || arr.length !== 4) return null;
42816
+ if (!arr.every(Number.isFinite)) return null;
42817
+ return [arr[0], arr[1], arr[2], arr[3]];
42818
+ }
42819
+ function parseSourceBB(sourceBB) {
42820
+ if (sourceBB == null) return [];
42821
+ if (!Array.isArray(sourceBB) || sourceBB.length === 0) return [];
42822
+ const isNested = sourceBB.some(Array.isArray);
42823
+ const candidates = isNested ? sourceBB : [sourceBB];
42824
+ const out = [];
42825
+ for (const candidate of candidates) {
42826
+ const box = toBbox(candidate);
42827
+ if (box) out.push(box);
42828
+ }
42829
+ return out;
42830
+ }
42831
+ function boundingBoxIntersects(a, b) {
42832
+ return a[0] < b[2] && a[2] > b[0] && a[1] < b[3] && a[3] > b[1];
42833
+ }
42834
+ function boundingBoxUnion(boxes) {
42835
+ if (boxes.length === 0) return null;
42836
+ let [x0, y0, x1, y1] = boxes[0];
42837
+ for (let i = 1; i < boxes.length; i++) {
42838
+ const b = boxes[i];
42839
+ if (b[0] < x0) x0 = b[0];
42840
+ if (b[1] < y0) y0 = b[1];
42841
+ if (b[2] > x1) x1 = b[2];
42842
+ if (b[3] > y1) y1 = b[3];
42843
+ }
42844
+ return [x0, y0, x1, y1];
42845
+ }
42846
+ var BoundingBoxIndex = class _BoundingBoxIndex {
42847
+ constructor(entries) {
42848
+ __publicField(this, "_entries");
42849
+ __publicField(this, "bitmarkParser", new BitmarkParserGenerator());
42850
+ this._entries = entries;
42851
+ }
42852
+ /**
42853
+ * Build an index from already-parsed bits. Bits without a parseable
42854
+ * `sourceBB` are dropped. Order of the input is preserved.
42855
+ *
42856
+ * Pass `options.sourceRL` to scope the index to a single source page —
42857
+ * bits whose `sourceRL` doesn't match (or is absent) are excluded.
42858
+ */
42859
+ static fromBits(wrappers, options) {
42860
+ const filterSourceRL = options?.sourceRL;
42861
+ const entries = [];
42862
+ for (const wrapper of wrappers) {
42863
+ if (!wrapper?.bit) continue;
42864
+ if (filterSourceRL !== void 0 && wrapper.bit.sourceRL !== filterSourceRL) {
42865
+ continue;
42866
+ }
42867
+ const boxes = parseSourceBB(wrapper.bit.sourceBB);
42868
+ if (boxes.length === 0) continue;
42869
+ entries.push({ bit: wrapper.bit, wrapper, boxes });
42870
+ }
42871
+ return new _BoundingBoxIndex(entries);
42872
+ }
42873
+ /** All bits with at least one valid `sourceBB`, in input order. */
42874
+ get entries() {
42875
+ return this._entries;
42876
+ }
42877
+ /**
42878
+ * Bits whose ANY `sourceBB` box intersects `query` (strict overlap —
42879
+ * touching-only edges do NOT count as an intersection).
42880
+ */
42881
+ intersecting(query) {
42882
+ const matches = [];
42883
+ for (const entry of this._entries) {
42884
+ if (entry.boxes.some((box) => boundingBoxIntersects(query, box))) {
42885
+ matches.push(entry);
42886
+ }
42887
+ }
42888
+ return matches;
42889
+ }
42890
+ /**
42891
+ * Flat list of every individual `sourceBB` box that intersects `query`.
42892
+ * For a multi-box bit, only the boxes that themselves intersected are
42893
+ * returned.
42894
+ */
42895
+ intersectingBoxes(query) {
42896
+ const matches = [];
42897
+ for (const entry of this._entries) {
42898
+ for (const box of entry.boxes) {
42899
+ if (boundingBoxIntersects(query, box)) matches.push(box);
42900
+ }
42901
+ }
42902
+ return matches;
42903
+ }
42904
+ /**
42905
+ * Smallest axis-aligned box that fully encloses every individual `sourceBB`
42906
+ * box that intersected the query. For a multi-box bit, only the boxes that
42907
+ * themselves intersected contribute to the union — non-intersecting boxes
42908
+ * from the same bit are ignored. Returns `null` if no box matches.
42909
+ */
42910
+ enclosingBoundingBox(query) {
42911
+ return boundingBoxUnion(this.intersectingBoxes(query));
42912
+ }
42913
+ /** Render selected bits back to bitmark text. Empty input returns `''`. */
42914
+ toBitmark(entries) {
42915
+ if (entries.length === 0) return "";
42916
+ const wrappers = entries.map((e) => e.wrapper);
42917
+ return this.bitmarkParser.convert(wrappers, {
42918
+ outputFormat: Output.bitmark,
42919
+ bitmarkOptions: { prettifyJson: true }
42920
+ });
42921
+ }
42922
+ };
42923
+
40423
42924
  // src/index.ts
40424
42925
  init();
40425
42926
  export {
@@ -40432,6 +42933,7 @@ export {
40432
42933
  BitmarkStringGenerator,
40433
42934
  BitmarkVersion,
40434
42935
  BodyTextFormat,
42936
+ BoundingBoxIndex,
40435
42937
  Builder,
40436
42938
  CardSetVersion,
40437
42939
  InfoFormat,