@playcademy/vite-plugin 1.2.1-beta.11 → 1.2.1-beta.13

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.
Files changed (2) hide show
  1. package/dist/index.js +297 -63
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -24315,7 +24315,7 @@ import path2 from "node:path";
24315
24315
  // package.json
24316
24316
  var package_default = {
24317
24317
  name: "@playcademy/vite-plugin",
24318
- version: "1.2.1-beta.11",
24318
+ version: "1.2.1-beta.13",
24319
24319
  type: "module",
24320
24320
  exports: {
24321
24321
  ".": {
@@ -25921,7 +25921,7 @@ var package_default2;
25921
25921
  var init_package = __esm(() => {
25922
25922
  package_default2 = {
25923
25923
  name: "@playcademy/sandbox",
25924
- version: "0.7.1-beta.11",
25924
+ version: "0.7.1-beta.13",
25925
25925
  description: "Local development server for Playcademy game development",
25926
25926
  type: "module",
25927
25927
  exports: {
@@ -34173,6 +34173,23 @@ var init_v3 = __esm(() => {
34173
34173
  var init_esm = __esm(() => {
34174
34174
  init_v3();
34175
34175
  });
34176
+ function qtiDirectedPairValidationIssue(values, contract) {
34177
+ const sourceCounts = new Map;
34178
+ const targetCounts = new Map;
34179
+ for (const value of values) {
34180
+ const [source, target, extra] = value.split(" ");
34181
+ if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
34182
+ return { kind: "invalid-pair", value };
34183
+ }
34184
+ if (!withinQtiCapacity(contract.sources, sourceCounts, source)) {
34185
+ return { kind: "source-capacity", identifier: source };
34186
+ }
34187
+ if (!withinQtiCapacity(contract.targets, targetCounts, target)) {
34188
+ return { kind: "target-capacity", identifier: target };
34189
+ }
34190
+ }
34191
+ return null;
34192
+ }
34176
34193
  function isRecord2(value) {
34177
34194
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
34178
34195
  }
@@ -34475,19 +34492,15 @@ function assessmentResponseValidationMessage(assessment, responses) {
34475
34492
  return null;
34476
34493
  }
34477
34494
  function directedPairsValidationMessage(itemIdentifier, values, contract) {
34478
- const sourceCounts = new Map;
34479
- const targetCounts = new Map;
34480
- for (const value of values) {
34481
- const [source, target, extra] = value.split(" ");
34482
- if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
34495
+ const issue = qtiDirectedPairValidationIssue(values, contract);
34496
+ if (issue) {
34497
+ if (issue.kind === "invalid-pair") {
34483
34498
  return `Response for ${itemIdentifier} contains an invalid association pair.`;
34484
34499
  }
34485
- if (!withinQtiCapacity(contract.sources, sourceCounts, source)) {
34500
+ if (issue.kind === "source-capacity") {
34486
34501
  return `Response for ${itemIdentifier} uses a token beyond its allowed placements.`;
34487
34502
  }
34488
- if (!withinQtiCapacity(contract.targets, targetCounts, target)) {
34489
- return `Response for ${itemIdentifier} fills a target beyond its capacity.`;
34490
- }
34503
+ return `Response for ${itemIdentifier} fills a target beyond its capacity.`;
34491
34504
  }
34492
34505
  return null;
34493
34506
  }
@@ -34567,8 +34580,14 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
34567
34580
  allowed = new Set(interaction.choices.map((candidate) => candidate.identifier));
34568
34581
  } else if (interaction.type === "match") {
34569
34582
  const message = directedPairsValidationMessage(itemIdentifier, values, {
34570
- sources: new Map(interaction.sourceChoices.map((candidate) => [candidate.identifier, 0])),
34571
- targets: new Map(interaction.targetChoices.map((candidate) => [candidate.identifier, 0]))
34583
+ sources: new Map(interaction.sourceChoices.map((candidate) => [
34584
+ candidate.identifier,
34585
+ candidate.matchMax
34586
+ ])),
34587
+ targets: new Map(interaction.targetChoices.map((candidate) => [
34588
+ candidate.identifier,
34589
+ candidate.matchMax
34590
+ ]))
34572
34591
  });
34573
34592
  if (message) {
34574
34593
  return message;
@@ -35354,6 +35373,7 @@ var init_assessment_runtime = __esm(() => {
35354
35373
  init_fxp();
35355
35374
  init_timeback3();
35356
35375
  init_timeback3();
35376
+ init_timeback3();
35357
35377
  init_src();
35358
35378
  init_timeback3();
35359
35379
  init_src();
@@ -112256,18 +112276,18 @@ function serializeSanitizedMathMl(node) {
112256
112276
  const children = node.children.map(serializeSanitizedMathMl).join("");
112257
112277
  return `<${node.localName}${attributes2}>${children}</${node.localName}>`;
112258
112278
  }
112259
- function tableContentNode(element, blankForInteraction) {
112279
+ function tableContentNode(element, blankForInteraction, gapForElement) {
112260
112280
  const rows = xmlDescendants(element, (candidate) => candidate.localName === "tr").map((row) => {
112261
112281
  const cells = row.children.filter((cell) => cell.kind === "element" && (cell.localName === "td" || cell.localName === "th"));
112262
112282
  return {
112263
112283
  header: cells.length > 0 && cells.every((cell) => cell.localName === "th"),
112264
- cells: cells.map((cell) => contentNodesFrom(cell.children, blankForInteraction))
112284
+ cells: cells.map((cell) => contentNodesFrom(cell.children, blankForInteraction, gapForElement))
112265
112285
  };
112266
112286
  });
112267
112287
  return { kind: "table", rows };
112268
112288
  }
112269
- function listContentNode(element, blankForInteraction) {
112270
- const items = element.children.filter((child) => child.kind === "element" && child.localName === "li").map((item) => contentNodesFrom(item.children, blankForInteraction));
112289
+ function listContentNode(element, blankForInteraction, gapForElement) {
112290
+ const items = element.children.filter((child) => child.kind === "element" && child.localName === "li").map((item) => contentNodesFrom(item.children, blankForInteraction, gapForElement));
112271
112291
  return { kind: "list", ordered: element.localName === "ol", items };
112272
112292
  }
112273
112293
  function contentText(raw) {
@@ -112292,15 +112312,18 @@ function trimContentBoundaries(nodes) {
112292
112312
  }
112293
112313
  return kept.filter((node) => node.kind !== "text" || node.text !== "");
112294
112314
  }
112295
- function collectContentNodes(nodes, blankForInteraction) {
112315
+ function collectContentNodes(nodes, blankForInteraction, gapForElement) {
112296
112316
  const result = [];
112297
112317
  for (const node of nodes) {
112318
+ const gapIdentifier = node.kind === "element" ? gapForElement(node) : null;
112298
112319
  if (node.kind === "text") {
112299
112320
  const text3 = contentText(node.value);
112300
112321
  if (text3) {
112301
112322
  result.push({ kind: "text", text: text3 });
112302
112323
  }
112303
- } else if (CONTENT_SKIPPED_TAGS2.has(node.localName)) {} else if (isInteractionElement(node)) {
112324
+ } else if (CONTENT_SKIPPED_TAGS2.has(node.localName)) {} else if (gapIdentifier !== null) {
112325
+ result.push({ kind: "gap", identifier: gapIdentifier });
112326
+ } else if (isInteractionElement(node)) {
112304
112327
  const responseIdentifier = blankForInteraction(node);
112305
112328
  if (responseIdentifier) {
112306
112329
  result.push({ kind: "blank", responseIdentifier });
@@ -112325,19 +112348,19 @@ function collectContentNodes(nodes, blankForInteraction) {
112325
112348
  });
112326
112349
  }
112327
112350
  } else if (node.localName === "table") {
112328
- result.push(tableContentNode(node, blankForInteraction));
112351
+ result.push(tableContentNode(node, blankForInteraction, gapForElement));
112329
112352
  } else if (node.localName === "ul" || node.localName === "ol") {
112330
- const list = listContentNode(node, blankForInteraction);
112353
+ const list = listContentNode(node, blankForInteraction, gapForElement);
112331
112354
  if (list.kind === "list" && list.items.length > 0) {
112332
112355
  result.push(list);
112333
112356
  }
112334
112357
  } else if (node.localName === "p") {
112335
- const children = contentNodesFrom(node.children, blankForInteraction);
112358
+ const children = contentNodesFrom(node.children, blankForInteraction, gapForElement);
112336
112359
  if (children.length > 0) {
112337
112360
  result.push({ kind: "paragraph", children });
112338
112361
  }
112339
112362
  } else if (EMPHASIS_TAGS2.has(node.localName) || STRONG_TAGS2.has(node.localName)) {
112340
- const children = collectContentNodes(node.children, blankForInteraction);
112363
+ const children = collectContentNodes(node.children, blankForInteraction, gapForElement);
112341
112364
  const hasContent = children.some((child) => child.kind !== "text" || Boolean(child.text.trim()));
112342
112365
  if (hasContent) {
112343
112366
  result.push({
@@ -112346,17 +112369,17 @@ function collectContentNodes(nodes, blankForInteraction) {
112346
112369
  });
112347
112370
  }
112348
112371
  } else {
112349
- result.push(...collectContentNodes(node.children, blankForInteraction));
112372
+ result.push(...collectContentNodes(node.children, blankForInteraction, gapForElement));
112350
112373
  }
112351
112374
  }
112352
112375
  return result;
112353
112376
  }
112354
- function contentNodesFrom(nodes, blankForInteraction) {
112355
- return trimContentBoundaries(collectContentNodes(nodes, blankForInteraction));
112377
+ function contentNodesFrom(nodes, blankForInteraction, gapForElement = () => null) {
112378
+ return trimContentBoundaries(collectContentNodes(nodes, blankForInteraction, gapForElement));
112356
112379
  }
112357
112380
  function hasRichContent(nodes) {
112358
112381
  return nodes.some((node) => {
112359
- if (node.kind === "math" || node.kind === "image" || node.kind === "table" || node.kind === "list" || node.kind === "blank") {
112382
+ if (node.kind === "math" || node.kind === "image" || node.kind === "table" || node.kind === "list" || node.kind === "blank" || node.kind === "gap") {
112360
112383
  return true;
112361
112384
  }
112362
112385
  if (node.kind === "paragraph" || node.kind === "emphasis" || node.kind === "strong") {
@@ -112377,7 +112400,14 @@ function parseGapMatchText(interaction) {
112377
112400
  }
112378
112401
  return GAP_MATCH_TOKEN_TAGS2.has(element.localName) ? "" : undefined;
112379
112402
  });
112380
- return { textWithGaps, gaps };
112403
+ const passageNodes = interaction.children.filter((node) => node.kind === "text" || !GAP_MATCH_TOKEN_TAGS2.has(node.localName));
112404
+ const gapContent = contentNodesFrom(passageNodes, () => null, (element) => {
112405
+ if (element.localName !== "qti-gap") {
112406
+ return null;
112407
+ }
112408
+ return parseXmlAttributes(element.attributes).identifier ?? null;
112409
+ });
112410
+ return { textWithGaps, gaps, gapContent };
112381
112411
  }
112382
112412
  function isInlineInteractionElement(element) {
112383
112413
  const type = INTERACTION_TYPES2[element.localName];
@@ -112404,6 +112434,7 @@ function parseInteraction(source, interaction, context2) {
112404
112434
  let gapImages = [];
112405
112435
  let gaps = [];
112406
112436
  let textWithGaps;
112437
+ let gapContent;
112407
112438
  function ownStageGraphic() {
112408
112439
  return firstGraphic(interaction, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2);
112409
112440
  }
@@ -112425,7 +112456,7 @@ function parseInteraction(source, interaction, context2) {
112425
112456
  graphic = ownStageGraphic();
112426
112457
  } else if (type === "gap-match") {
112427
112458
  gapTexts = parseGapTexts(interaction);
112428
- ({ textWithGaps, gaps } = parseGapMatchText(interaction));
112459
+ ({ textWithGaps, gaps, gapContent } = parseGapMatchText(interaction));
112429
112460
  } else if (type === "graphic-gap-match") {
112430
112461
  graphic = ownStageGraphic();
112431
112462
  gapImages = parseGapImages(interaction);
@@ -112453,6 +112484,7 @@ function parseInteraction(source, interaction, context2) {
112453
112484
  gapImages,
112454
112485
  gaps,
112455
112486
  ...textWithGaps === undefined ? {} : { textWithGaps },
112487
+ ...gapContent === undefined ? {} : { gapContent },
112456
112488
  rawXml: xmlElementSource(source, interaction)
112457
112489
  };
112458
112490
  }
@@ -112543,6 +112575,23 @@ function parseQtiItemXml(rawXml) {
112543
112575
  rawXml
112544
112576
  };
112545
112577
  }
112578
+ function qtiDirectedPairValidationIssue2(values, contract) {
112579
+ const sourceCounts = new Map;
112580
+ const targetCounts = new Map;
112581
+ for (const value of values) {
112582
+ const [source, target, extra] = value.split(" ");
112583
+ if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
112584
+ return { kind: "invalid-pair", value };
112585
+ }
112586
+ if (!withinQtiCapacity(contract.sources, sourceCounts, source)) {
112587
+ return { kind: "source-capacity", identifier: source };
112588
+ }
112589
+ if (!withinQtiCapacity(contract.targets, targetCounts, target)) {
112590
+ return { kind: "target-capacity", identifier: target };
112591
+ }
112592
+ }
112593
+ return null;
112594
+ }
112546
112595
  function usableQtiRegionGeometry(shape, coords) {
112547
112596
  const arity2 = REGION_SHAPE_COORDS[shape];
112548
112597
  if (!arity2) {
@@ -113007,6 +113056,17 @@ function interactionBody(item, node, type, declaration, attributes2) {
113007
113056
  if (!attributes2.has("max-associations")) {
113008
113057
  attributes2.set("max-associations", String(correctValueStrings(declaration).length || sources.length));
113009
113058
  }
113059
+ const capacityIssue = qtiDirectedPairValidationIssue2(correctValueStrings(declaration), {
113060
+ sources: new Map(sources.map((choice) => [choice.identifier, 1])),
113061
+ targets: new Map(targets.map((choice) => [choice.identifier, 1]))
113062
+ });
113063
+ if (capacityIssue) {
113064
+ if (capacityIssue.kind === "invalid-pair") {
113065
+ compileError(`Match pair “${capacityIssue.value}” must name one declared source and target choice`);
113066
+ }
113067
+ const role = capacityIssue.kind === "source-capacity" ? "Source" : "Target";
113068
+ compileError(`${role} “${capacityIssue.identifier}” takes part in more associations than its match-max allows`);
113069
+ }
113010
113070
  const sets = [sources, targets].map((choices) => `<qti-simple-match-set>${choices.map((choice) => choiceXml(item, "qti-simple-associable-choice", choice, {
113011
113071
  "match-max": "1"
113012
113072
  })).join("")}</qti-simple-match-set>`).join("");
@@ -113292,6 +113352,14 @@ function qtiAttributeNumber(attributes2, name3, fallback) {
113292
113352
  const value = Number(raw);
113293
113353
  return Number.isFinite(value) ? value : fallback;
113294
113354
  }
113355
+ function qtiAttributeCapacity(attributes2, name3, fallback) {
113356
+ const raw = attributes2[name3]?.trim();
113357
+ if (!raw) {
113358
+ return fallback;
113359
+ }
113360
+ const value = Number(raw);
113361
+ return Number.isInteger(value) && value >= 0 ? value : null;
113362
+ }
113295
113363
  function qtiAttributeMaximum(attributes2, name3, fallback, unlimited) {
113296
113364
  const maximum = qtiAttributeNumber(attributes2, name3, fallback);
113297
113365
  return maximum === 0 ? unlimited : maximum;
@@ -113300,6 +113368,27 @@ function qtiAttributeBoolean(attributes2, name3) {
113300
113368
  const value = attributes2[name3];
113301
113369
  return value === "true" || value === "1";
113302
113370
  }
113371
+ function gapContentIdentifiers(nodes) {
113372
+ return nodes.flatMap((node) => {
113373
+ if (node.kind === "gap") {
113374
+ return [node.identifier];
113375
+ }
113376
+ if (node.kind === "paragraph" || node.kind === "emphasis" || node.kind === "strong") {
113377
+ return gapContentIdentifiers(node.children);
113378
+ }
113379
+ if (node.kind === "list") {
113380
+ return node.items.flatMap(gapContentIdentifiers);
113381
+ }
113382
+ if (node.kind === "table") {
113383
+ return node.rows.flatMap((row) => row.cells.flatMap(gapContentIdentifiers));
113384
+ }
113385
+ return [];
113386
+ });
113387
+ }
113388
+ function qtiGapContentMatchesGaps(nodes, gaps) {
113389
+ const identifiers = gapContentIdentifiers(nodes);
113390
+ return identifiers.length === gaps.length && identifiers.every((identifier, index2) => identifier === gaps[index2]);
113391
+ }
113303
113392
  function normalizedInteractionType(value) {
113304
113393
  if (typeof value !== "string") {
113305
113394
  return;
@@ -113310,6 +113399,9 @@ function normalizedInteractionType(value) {
113310
113399
  function usableChoices(choices) {
113311
113400
  return choices !== undefined && choices.length > 0 && choices.every((choice) => Boolean(choice.identifier));
113312
113401
  }
113402
+ function usableAssociableChoices(choices) {
113403
+ return usableChoices(choices) && choices.every((choice) => qtiAttributeCapacity(choice.attributes, "match-max", 0) !== null);
113404
+ }
113313
113405
  function transportableIdentifiers(identifiers) {
113314
113406
  return identifiers.every((identifier) => Boolean(identifier) && !/\s/.test(identifier));
113315
113407
  }
@@ -113382,7 +113474,7 @@ function authorableQtiInteractionProjection(interaction) {
113382
113474
  }
113383
113475
  case "match": {
113384
113476
  const [sourceChoices, targetChoices] = interaction.choiceSets;
113385
- if (interaction.choiceSets.length !== 2 || !usableChoices(sourceChoices) || !usableChoices(targetChoices) || !transportableIdentifiers([...sourceChoices, ...targetChoices].map((choice) => choice.identifier))) {
113477
+ if (interaction.choiceSets.length !== 2 || !usableAssociableChoices(sourceChoices) || !usableAssociableChoices(targetChoices) || !transportableIdentifiers([...sourceChoices, ...targetChoices].map((choice) => choice.identifier))) {
113386
113478
  return null;
113387
113479
  }
113388
113480
  return {
@@ -113451,8 +113543,8 @@ function authorableQtiInteractionProjection(interaction) {
113451
113543
  };
113452
113544
  }
113453
113545
  case "gap-match": {
113454
- const { gapTexts, gaps, textWithGaps } = interaction;
113455
- if (gapTexts.length === 0 || gaps.length === 0 || textWithGaps === undefined || !gapTexts.every((gapText) => Boolean(gapText.identifier && gapText.content)) || !transportableIdentifiers([...gapTexts.map((gapText) => gapText.identifier), ...gaps])) {
113546
+ const { gapTexts, gaps, textWithGaps, gapContent } = interaction;
113547
+ if (gapTexts.length === 0 || gaps.length === 0 || textWithGaps === undefined || !gapContent || !qtiGapContentMatchesGaps(gapContent, gaps) || !gapTexts.every((gapText) => Boolean(gapText.identifier && gapText.content)) || !transportableIdentifiers([...gapTexts.map((gapText) => gapText.identifier), ...gaps])) {
113456
113548
  return null;
113457
113549
  }
113458
113550
  return {
@@ -113461,6 +113553,7 @@ function authorableQtiInteractionProjection(interaction) {
113461
113553
  gapTexts,
113462
113554
  gaps,
113463
113555
  textWithGaps,
113556
+ gapContent,
113464
113557
  shuffle: qtiAttributeBoolean(attributes2, "shuffle")
113465
113558
  };
113466
113559
  }
@@ -113484,7 +113577,7 @@ function contributesScore(declaration) {
113484
113577
  function responsePairs(left, right) {
113485
113578
  return left.flatMap((first) => right.map((second) => `${first} ${second}`));
113486
113579
  }
113487
- function mappingResponseContext(item, declaration) {
113580
+ function interactionResponseSpace(item, declaration) {
113488
113581
  const interaction = item.interactions.find((candidate) => candidate.responseIdentifier === declaration.identifier);
113489
113582
  if (!interaction) {
113490
113583
  return {};
@@ -113576,21 +113669,49 @@ function processesResponseMapping(item, declaration) {
113576
113669
  }
113577
113670
  return item.responseProcessing.elements.some((element) => element.tagName === "qti-map-response" && (element.attributes.identifier ?? "RESPONSE") === declaration.identifier);
113578
113671
  }
113579
- function responseMappingMaximum(item, declaration) {
113672
+ function responseMappingMaximum(item, declaration, space) {
113580
113673
  if (!declaration.mapping || !processesResponseMapping(item, declaration)) {
113581
113674
  return null;
113582
113675
  }
113583
113676
  return qtiResponseMappingMaximum({
113584
113677
  cardinality: declaration.cardinality,
113585
113678
  mapping: declaration.mapping,
113586
- ...mappingResponseContext(item, declaration)
113679
+ ...space ?? interactionResponseSpace(item, declaration)
113587
113680
  });
113588
113681
  }
113589
113682
  function qtiItemScoringValidationMessage(item) {
113590
113683
  for (const declaration of item.responseDeclarations) {
113684
+ const space = interactionResponseSpace(item, declaration);
113685
+ const { responseValues: responseValues2, maximumResponses } = space;
113686
+ if (responseValues2 && declaration.correctValues.some((value) => !responseValues2.includes(value))) {
113687
+ return `Response ${declaration.identifier} declares a correct value its interaction cannot produce.`;
113688
+ }
113689
+ const interaction = item.interactions.find((candidate) => candidate.responseIdentifier === declaration.identifier);
113690
+ if (interaction?.type === "match") {
113691
+ const [sources = [], targets = []] = interaction.choiceSets;
113692
+ const issue6 = qtiDirectedPairValidationIssue2(declaration.correctValues, {
113693
+ sources: new Map(sources.map((choice) => [
113694
+ choice.identifier,
113695
+ qtiAttributeNumber(choice.attributes, "match-max", 0)
113696
+ ])),
113697
+ targets: new Map(targets.map((choice) => [
113698
+ choice.identifier,
113699
+ qtiAttributeNumber(choice.attributes, "match-max", 0)
113700
+ ]))
113701
+ });
113702
+ if (issue6?.kind === "source-capacity") {
113703
+ return `Response ${declaration.identifier} declares correct pairs that exceed source ${issue6.identifier}'s match-max.`;
113704
+ }
113705
+ if (issue6?.kind === "target-capacity") {
113706
+ return `Response ${declaration.identifier} declares correct pairs that exceed target ${issue6.identifier}'s match-max.`;
113707
+ }
113708
+ }
113709
+ if (declaration.cardinality !== "single" && maximumResponses !== undefined && maximumResponses !== 0 && declaration.correctValues.length > maximumResponses) {
113710
+ return `Response ${declaration.identifier} declares more correct values than its interaction accepts.`;
113711
+ }
113591
113712
  if (declaration.mapping) {
113592
113713
  if (processesResponseMapping(item, declaration)) {
113593
- if (responseMappingMaximum(item, declaration) === null) {
113714
+ if (responseMappingMaximum(item, declaration, space) === null) {
113594
113715
  return `Response ${declaration.identifier} has a mapping with no reachable positive score.`;
113595
113716
  }
113596
113717
  } else if (declaration.correctValues.length === 0) {
@@ -113960,6 +114081,7 @@ var init_qti = __esm(() => {
113960
114081
  init_fxp();
113961
114082
  init_timeback3();
113962
114083
  init_timeback3();
114084
+ init_timeback3();
113963
114085
  EVENT_HANDLER_ATTRIBUTE = /^on/i;
113964
114086
  SCRIPT_SCHEMES2 = {
113965
114087
  javascript: ["java", "script:"].join(""),
@@ -116786,6 +116908,16 @@ function playableChoice(choice) {
116786
116908
  ...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
116787
116909
  };
116788
116910
  }
116911
+ function playableAssociableChoice(choice) {
116912
+ const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
116913
+ if (matchMax === null) {
116914
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
116915
+ }
116916
+ return {
116917
+ ...playableChoice(choice),
116918
+ matchMax
116919
+ };
116920
+ }
116789
116921
  function requireChoices2(choices) {
116790
116922
  if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
116791
116923
  throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
@@ -116871,8 +117003,8 @@ function parseInteraction2(questionIdentifier, interaction) {
116871
117003
  if (sets.length !== 2) {
116872
117004
  throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
116873
117005
  }
116874
- const sourceChoices = requireChoices2(sets[0].map(playableChoice));
116875
- const targetChoices = requireChoices2(sets[1].map(playableChoice));
117006
+ const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
117007
+ const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
116876
117008
  return {
116877
117009
  type,
116878
117010
  responseIdentifier,
@@ -116897,7 +117029,7 @@ function parseInteraction2(questionIdentifier, interaction) {
116897
117029
  };
116898
117030
  }
116899
117031
  if (type === "gap-match") {
116900
- if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined) {
117032
+ if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
116901
117033
  throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
116902
117034
  }
116903
117035
  return {
@@ -116905,6 +117037,7 @@ function parseInteraction2(questionIdentifier, interaction) {
116905
117037
  responseIdentifier,
116906
117038
  ...prompt,
116907
117039
  content: interaction.textWithGaps,
117040
+ contentNodes: interaction.gapContent,
116908
117041
  gapTexts: interaction.gapTexts.map((gapText) => ({
116909
117042
  identifier: gapText.identifier,
116910
117043
  content: gapText.content,
@@ -119009,8 +119142,7 @@ function assertPlayableQtiQuestion(question) {
119009
119142
  }
119010
119143
  const item = parsedQtiItemOrNull(question.rawXml);
119011
119144
  const playable = item && playableQtiInteractions(item);
119012
- const scoringIssue = item && qtiItemScoringValidationMessage(item);
119013
- if (!playable || scoringIssue) {
119145
+ if (!playable || qtiItemScoringValidationMessage(item)) {
119014
119146
  throw new ValidationError(`Question ${question.identifier} uses an interaction Playcademy cannot play or score.`);
119015
119147
  }
119016
119148
  }
@@ -119263,31 +119395,38 @@ ${input.itemBody}
119263
119395
  </qti-response-processing>
119264
119396
  </qti-assessment-item>`;
119265
119397
  }
119266
- function parseInlineChoices(value) {
119267
- if (!Array.isArray(value) || value.length < 2) {
119268
- throw new ValidationError("Inline-choice questions require at least two choices");
119398
+ function parseStructuredChoices(value, options) {
119399
+ const { label, minimum, validate: validate2 } = options;
119400
+ if (!Array.isArray(value) || value.length < minimum) {
119401
+ throw new ValidationError(`${label} questions require at least ${minimum === 1 ? "one choice" : `${minimum} choices`}`);
119269
119402
  }
119270
119403
  const choices = value.map((choice) => {
119271
119404
  const record3 = recordValue(choice);
119272
119405
  const identifier = record3?.identifier;
119273
119406
  const content = record3?.content;
119274
- if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim()) {
119275
- throw new ValidationError("Inline-choice options are invalid");
119407
+ if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim() || validate2 !== undefined && record3 !== undefined && !validate2(record3)) {
119408
+ throw new ValidationError(`${label} options are invalid`);
119276
119409
  }
119277
119410
  return { identifier: identifier.trim(), content: content.trim() };
119278
119411
  });
119279
119412
  if (new Set(choices.map((choice) => choice.identifier)).size !== choices.length) {
119280
- throw new ValidationError("Inline-choice option identifiers must be unique");
119413
+ throw new ValidationError(`${label} option identifiers must be unique`);
119281
119414
  }
119282
119415
  return choices;
119283
119416
  }
119284
- function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers) {
119417
+ function declaredCorrectResponse(input, responseIdentifier) {
119285
119418
  const declarations = Array.isArray(input.responseDeclarations) ? input.responseDeclarations : [];
119286
119419
  const declaration = declarations.map(recordValue).find((candidate) => candidate?.identifier === responseIdentifier);
119287
- const correctResponse = recordValue(declaration?.correctResponse);
119288
- const rawValues = correctResponse?.value;
119289
- const values = Array.isArray(rawValues) ? rawValues.filter((value) => typeof value === "string") : [];
119290
- if (declaration?.cardinality !== "single" || declaration.baseType !== "identifier") {
119420
+ const rawValues = recordValue(declaration?.correctResponse)?.value;
119421
+ return {
119422
+ cardinality: declaration?.cardinality,
119423
+ baseType: declaration?.baseType,
119424
+ values: Array.isArray(rawValues) ? rawValues.filter((value) => typeof value === "string") : []
119425
+ };
119426
+ }
119427
+ function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers) {
119428
+ const { cardinality, baseType, values } = declaredCorrectResponse(input, responseIdentifier);
119429
+ if (cardinality !== "single" || baseType !== "identifier") {
119291
119430
  throw new ValidationError("Inline-choice questions require a single-identifier response declaration");
119292
119431
  }
119293
119432
  if (values.length !== 1 || !choiceIdentifiers.has(values[0])) {
@@ -119295,27 +119434,41 @@ function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifi
119295
119434
  }
119296
119435
  return values[0];
119297
119436
  }
119298
- function buildInlineChoiceQuestionXml(input, identifier, title) {
119437
+ function structuredQuestionEnvelope(input, type, label, title) {
119299
119438
  const interaction = recordValue(input.interaction);
119300
- const interactionType = normalizedQtiInteractionType(interaction?.type ?? input.type);
119301
- if (interactionType !== "inline-choice") {
119439
+ if (normalizedQtiInteractionType(interaction?.type ?? input.type) !== type) {
119302
119440
  return null;
119303
119441
  }
119304
- const responseIdentifier = interaction?.responseIdentifier;
119305
119442
  const structure = recordValue(interaction?.questionStructure);
119443
+ const responseIdentifier = interaction?.responseIdentifier;
119306
119444
  const prompt = structure?.prompt;
119307
- if (!structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
119308
- throw new ValidationError("Inline-choice question data is invalid");
119445
+ if (!interaction || !structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
119446
+ throw new ValidationError(`${label} question data is invalid`);
119309
119447
  }
119310
119448
  if (!title || typeof prompt !== "string" || !prompt.trim()) {
119311
- throw new ValidationError("Inline-choice questions require a title and prompt");
119449
+ throw new ValidationError(`${label} questions require a title and prompt`);
119450
+ }
119451
+ return {
119452
+ interaction,
119453
+ structure,
119454
+ responseIdentifier: responseIdentifier.trim(),
119455
+ prompt: prompt.trim()
119456
+ };
119457
+ }
119458
+ function buildInlineChoiceQuestionXml(input, identifier, title) {
119459
+ const envelope = structuredQuestionEnvelope(input, "inline-choice", "Inline-choice", title);
119460
+ if (!envelope) {
119461
+ return null;
119312
119462
  }
119313
- const normalizedPrompt = prompt.trim();
119314
- const promptParts = normalizedPrompt.split(INLINE_CHOICE_BLANK);
119463
+ const { structure, responseIdentifier, prompt } = envelope;
119464
+ const promptParts = prompt.split(INLINE_CHOICE_BLANK);
119315
119465
  if (promptParts.length !== 2) {
119316
119466
  throw new ValidationError("Inline-choice questions require exactly one blank");
119317
119467
  }
119318
- const choices = parseInlineChoices(structure.inlineChoices);
119468
+ const choices = parseStructuredChoices(structure.inlineChoices, {
119469
+ label: "Inline-choice",
119470
+ minimum: 2
119471
+ });
119319
119472
  const choiceIdentifiers = new Set(choices.map((choice) => choice.identifier));
119320
119473
  const correctIdentifier = inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers);
119321
119474
  const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
@@ -119333,6 +119486,86 @@ ${choicesXml}
119333
119486
  itemBody
119334
119487
  });
119335
119488
  }
119489
+ function parseStructuredMatchChoices(value, label) {
119490
+ return parseStructuredChoices(value, {
119491
+ label: `Match ${label}`,
119492
+ minimum: 1,
119493
+ validate: (choice) => choice.matchMax === 1
119494
+ });
119495
+ }
119496
+ function structuredMatchCorrectPairs(input, responseIdentifier, sources, targets) {
119497
+ const { cardinality, baseType, values } = declaredCorrectResponse(input, responseIdentifier);
119498
+ if (cardinality !== "multiple" || baseType !== "directedPair") {
119499
+ throw new ValidationError("Match questions require a multiple directed-pair response declaration");
119500
+ }
119501
+ const sourceIdentifiers = new Set(sources.map((choice) => choice.identifier));
119502
+ const targetIdentifiers = new Set(targets.map((choice) => choice.identifier));
119503
+ const valid = values.length > 0 && new Set(values).size === values.length && values.every((value) => {
119504
+ const [source, target, extra] = value.split(" ");
119505
+ return extra === undefined && Boolean(source && target) && sourceIdentifiers.has(source) && targetIdentifiers.has(target);
119506
+ });
119507
+ if (!valid) {
119508
+ throw new ValidationError("Every Match correct response must name one declared source and target choice");
119509
+ }
119510
+ return values;
119511
+ }
119512
+ function buildMatchQuestionXml(input, identifier, title) {
119513
+ const envelope = structuredQuestionEnvelope(input, "match", "Match", title);
119514
+ if (!envelope) {
119515
+ return null;
119516
+ }
119517
+ const { interaction, structure, responseIdentifier, prompt } = envelope;
119518
+ const sources = parseStructuredMatchChoices(structure.sourceChoices, "source");
119519
+ const targets = parseStructuredMatchChoices(structure.targetChoices, "target");
119520
+ const sourceIdentifiers = new Set(sources.map((choice) => choice.identifier));
119521
+ if (targets.some((choice) => sourceIdentifiers.has(choice.identifier))) {
119522
+ throw new ValidationError("Match source and target choice identifiers must be unique");
119523
+ }
119524
+ const correctValues = structuredMatchCorrectPairs(input, responseIdentifier, sources, targets);
119525
+ const maxAssociations = interaction.maxAssociations;
119526
+ if (typeof maxAssociations !== "number" || !Number.isInteger(maxAssociations) || maxAssociations < 0 || maxAssociations !== 0 && maxAssociations < correctValues.length) {
119527
+ throw new ValidationError("Match max associations must fit every correct pair");
119528
+ }
119529
+ const item = {
119530
+ key: identifier,
119531
+ title,
119532
+ body: [
119533
+ {
119534
+ kind: "interaction",
119535
+ type: "match",
119536
+ responseIdentifier,
119537
+ attributes: {
119538
+ shuffle: interaction.shuffle === true,
119539
+ "max-associations": maxAssociations
119540
+ },
119541
+ content: [
119542
+ {
119543
+ kind: "element",
119544
+ name: "qti-prompt",
119545
+ children: [{ kind: "text", text: prompt }]
119546
+ }
119547
+ ],
119548
+ choiceSets: [sources, targets].map((choices) => choices.map((choice) => ({
119549
+ identifier: choice.identifier,
119550
+ content: [{ kind: "text", text: choice.content }]
119551
+ })))
119552
+ }
119553
+ ],
119554
+ responseDeclarations: [
119555
+ {
119556
+ identifier: responseIdentifier,
119557
+ cardinality: "multiple",
119558
+ baseType: "directedPair",
119559
+ correctValues
119560
+ }
119561
+ ]
119562
+ };
119563
+ try {
119564
+ return compileQtiAuthoringItemXml(item, { identifier, title });
119565
+ } catch (error88) {
119566
+ throw new ValidationError(`This Match question cannot be compiled to QTI: ${errorMessage2(error88)}`);
119567
+ }
119568
+ }
119336
119569
  function parseHottextSegments2(value) {
119337
119570
  if (!Array.isArray(value) || value.length === 0) {
119338
119571
  throw new ValidationError("Hottext questions require passage segments");
@@ -119637,7 +119870,8 @@ var init_timeback_qti_authoring_util = __esm(() => {
119637
119870
  buildNumericQuestionXml,
119638
119871
  buildAuthoringDocumentQuestionXml,
119639
119872
  buildHottextQuestionXml,
119640
- buildInlineChoiceQuestionXml
119873
+ buildInlineChoiceQuestionXml,
119874
+ buildMatchQuestionXml
119641
119875
  ];
119642
119876
  });
119643
119877
  function newPlaycademyTestIdentifier() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "1.2.1-beta.11",
3
+ "version": "1.2.1-beta.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -19,14 +19,14 @@
19
19
  "dependencies": {
20
20
  "archiver": "^7.0.1",
21
21
  "picocolors": "^1.1.1",
22
- "playcademy": "0.28.1-beta.11"
22
+ "playcademy": "0.28.1-beta.13"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@electric-sql/pglite": "^0.3.16",
26
26
  "@inquirer/prompts": "^7.8.6",
27
27
  "@playcademy/constants": "0.0.1",
28
- "@playcademy/sandbox": "0.7.1-beta.11",
29
- "@playcademy/sdk": "0.16.1-beta.11",
28
+ "@playcademy/sandbox": "0.7.1-beta.13",
29
+ "@playcademy/sdk": "0.16.1-beta.13",
30
30
  "@playcademy/types": "0.0.1",
31
31
  "@playcademy/utils": "0.0.1",
32
32
  "@types/archiver": "^6.0.3",