@playcademy/sandbox 0.7.1-beta.11 → 0.7.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.
- package/dist/cli.js +296 -62
- package/dist/server.js +296 -62
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1123,7 +1123,7 @@ var package_default;
|
|
|
1123
1123
|
var init_package = __esm(() => {
|
|
1124
1124
|
package_default = {
|
|
1125
1125
|
name: "@playcademy/sandbox",
|
|
1126
|
-
version: "0.7.1-beta.
|
|
1126
|
+
version: "0.7.1-beta.13",
|
|
1127
1127
|
description: "Local development server for Playcademy game development",
|
|
1128
1128
|
type: "module",
|
|
1129
1129
|
exports: {
|
|
@@ -9251,6 +9251,23 @@ var init_esm = __esm(() => {
|
|
|
9251
9251
|
});
|
|
9252
9252
|
|
|
9253
9253
|
// ../timeback/dist/engines/assessment-runtime/index.js
|
|
9254
|
+
function qtiDirectedPairValidationIssue(values, contract) {
|
|
9255
|
+
const sourceCounts = new Map;
|
|
9256
|
+
const targetCounts = new Map;
|
|
9257
|
+
for (const value of values) {
|
|
9258
|
+
const [source, target, extra] = value.split(" ");
|
|
9259
|
+
if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
|
|
9260
|
+
return { kind: "invalid-pair", value };
|
|
9261
|
+
}
|
|
9262
|
+
if (!withinQtiCapacity(contract.sources, sourceCounts, source)) {
|
|
9263
|
+
return { kind: "source-capacity", identifier: source };
|
|
9264
|
+
}
|
|
9265
|
+
if (!withinQtiCapacity(contract.targets, targetCounts, target)) {
|
|
9266
|
+
return { kind: "target-capacity", identifier: target };
|
|
9267
|
+
}
|
|
9268
|
+
}
|
|
9269
|
+
return null;
|
|
9270
|
+
}
|
|
9254
9271
|
function isRecord2(value) {
|
|
9255
9272
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9256
9273
|
}
|
|
@@ -9553,19 +9570,15 @@ function assessmentResponseValidationMessage(assessment, responses) {
|
|
|
9553
9570
|
return null;
|
|
9554
9571
|
}
|
|
9555
9572
|
function directedPairsValidationMessage(itemIdentifier, values, contract) {
|
|
9556
|
-
const
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
const [source, target, extra] = value.split(" ");
|
|
9560
|
-
if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
|
|
9573
|
+
const issue = qtiDirectedPairValidationIssue(values, contract);
|
|
9574
|
+
if (issue) {
|
|
9575
|
+
if (issue.kind === "invalid-pair") {
|
|
9561
9576
|
return `Response for ${itemIdentifier} contains an invalid association pair.`;
|
|
9562
9577
|
}
|
|
9563
|
-
if (
|
|
9578
|
+
if (issue.kind === "source-capacity") {
|
|
9564
9579
|
return `Response for ${itemIdentifier} uses a token beyond its allowed placements.`;
|
|
9565
9580
|
}
|
|
9566
|
-
|
|
9567
|
-
return `Response for ${itemIdentifier} fills a target beyond its capacity.`;
|
|
9568
|
-
}
|
|
9581
|
+
return `Response for ${itemIdentifier} fills a target beyond its capacity.`;
|
|
9569
9582
|
}
|
|
9570
9583
|
return null;
|
|
9571
9584
|
}
|
|
@@ -9645,8 +9658,14 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
|
|
|
9645
9658
|
allowed = new Set(interaction.choices.map((candidate) => candidate.identifier));
|
|
9646
9659
|
} else if (interaction.type === "match") {
|
|
9647
9660
|
const message = directedPairsValidationMessage(itemIdentifier, values, {
|
|
9648
|
-
sources: new Map(interaction.sourceChoices.map((candidate) => [
|
|
9649
|
-
|
|
9661
|
+
sources: new Map(interaction.sourceChoices.map((candidate) => [
|
|
9662
|
+
candidate.identifier,
|
|
9663
|
+
candidate.matchMax
|
|
9664
|
+
])),
|
|
9665
|
+
targets: new Map(interaction.targetChoices.map((candidate) => [
|
|
9666
|
+
candidate.identifier,
|
|
9667
|
+
candidate.matchMax
|
|
9668
|
+
]))
|
|
9650
9669
|
});
|
|
9651
9670
|
if (message) {
|
|
9652
9671
|
return message;
|
|
@@ -10372,6 +10391,7 @@ var init_assessment_runtime = __esm(() => {
|
|
|
10372
10391
|
init_fxp();
|
|
10373
10392
|
init_timeback3();
|
|
10374
10393
|
init_timeback3();
|
|
10394
|
+
init_timeback3();
|
|
10375
10395
|
init_src();
|
|
10376
10396
|
init_timeback3();
|
|
10377
10397
|
init_src();
|
|
@@ -86427,18 +86447,18 @@ function serializeSanitizedMathMl(node) {
|
|
|
86427
86447
|
const children = node.children.map(serializeSanitizedMathMl).join("");
|
|
86428
86448
|
return `<${node.localName}${attributes2}>${children}</${node.localName}>`;
|
|
86429
86449
|
}
|
|
86430
|
-
function tableContentNode(element, blankForInteraction) {
|
|
86450
|
+
function tableContentNode(element, blankForInteraction, gapForElement) {
|
|
86431
86451
|
const rows = xmlDescendants(element, (candidate) => candidate.localName === "tr").map((row) => {
|
|
86432
86452
|
const cells = row.children.filter((cell) => cell.kind === "element" && (cell.localName === "td" || cell.localName === "th"));
|
|
86433
86453
|
return {
|
|
86434
86454
|
header: cells.length > 0 && cells.every((cell) => cell.localName === "th"),
|
|
86435
|
-
cells: cells.map((cell) => contentNodesFrom(cell.children, blankForInteraction))
|
|
86455
|
+
cells: cells.map((cell) => contentNodesFrom(cell.children, blankForInteraction, gapForElement))
|
|
86436
86456
|
};
|
|
86437
86457
|
});
|
|
86438
86458
|
return { kind: "table", rows };
|
|
86439
86459
|
}
|
|
86440
|
-
function listContentNode(element, blankForInteraction) {
|
|
86441
|
-
const items = element.children.filter((child) => child.kind === "element" && child.localName === "li").map((item) => contentNodesFrom(item.children, blankForInteraction));
|
|
86460
|
+
function listContentNode(element, blankForInteraction, gapForElement) {
|
|
86461
|
+
const items = element.children.filter((child) => child.kind === "element" && child.localName === "li").map((item) => contentNodesFrom(item.children, blankForInteraction, gapForElement));
|
|
86442
86462
|
return { kind: "list", ordered: element.localName === "ol", items };
|
|
86443
86463
|
}
|
|
86444
86464
|
function contentText(raw) {
|
|
@@ -86463,15 +86483,18 @@ function trimContentBoundaries(nodes) {
|
|
|
86463
86483
|
}
|
|
86464
86484
|
return kept.filter((node) => node.kind !== "text" || node.text !== "");
|
|
86465
86485
|
}
|
|
86466
|
-
function collectContentNodes(nodes, blankForInteraction) {
|
|
86486
|
+
function collectContentNodes(nodes, blankForInteraction, gapForElement) {
|
|
86467
86487
|
const result = [];
|
|
86468
86488
|
for (const node of nodes) {
|
|
86489
|
+
const gapIdentifier = node.kind === "element" ? gapForElement(node) : null;
|
|
86469
86490
|
if (node.kind === "text") {
|
|
86470
86491
|
const text3 = contentText(node.value);
|
|
86471
86492
|
if (text3) {
|
|
86472
86493
|
result.push({ kind: "text", text: text3 });
|
|
86473
86494
|
}
|
|
86474
|
-
} else if (CONTENT_SKIPPED_TAGS2.has(node.localName)) {} else if (
|
|
86495
|
+
} else if (CONTENT_SKIPPED_TAGS2.has(node.localName)) {} else if (gapIdentifier !== null) {
|
|
86496
|
+
result.push({ kind: "gap", identifier: gapIdentifier });
|
|
86497
|
+
} else if (isInteractionElement(node)) {
|
|
86475
86498
|
const responseIdentifier = blankForInteraction(node);
|
|
86476
86499
|
if (responseIdentifier) {
|
|
86477
86500
|
result.push({ kind: "blank", responseIdentifier });
|
|
@@ -86496,19 +86519,19 @@ function collectContentNodes(nodes, blankForInteraction) {
|
|
|
86496
86519
|
});
|
|
86497
86520
|
}
|
|
86498
86521
|
} else if (node.localName === "table") {
|
|
86499
|
-
result.push(tableContentNode(node, blankForInteraction));
|
|
86522
|
+
result.push(tableContentNode(node, blankForInteraction, gapForElement));
|
|
86500
86523
|
} else if (node.localName === "ul" || node.localName === "ol") {
|
|
86501
|
-
const list = listContentNode(node, blankForInteraction);
|
|
86524
|
+
const list = listContentNode(node, blankForInteraction, gapForElement);
|
|
86502
86525
|
if (list.kind === "list" && list.items.length > 0) {
|
|
86503
86526
|
result.push(list);
|
|
86504
86527
|
}
|
|
86505
86528
|
} else if (node.localName === "p") {
|
|
86506
|
-
const children = contentNodesFrom(node.children, blankForInteraction);
|
|
86529
|
+
const children = contentNodesFrom(node.children, blankForInteraction, gapForElement);
|
|
86507
86530
|
if (children.length > 0) {
|
|
86508
86531
|
result.push({ kind: "paragraph", children });
|
|
86509
86532
|
}
|
|
86510
86533
|
} else if (EMPHASIS_TAGS2.has(node.localName) || STRONG_TAGS2.has(node.localName)) {
|
|
86511
|
-
const children = collectContentNodes(node.children, blankForInteraction);
|
|
86534
|
+
const children = collectContentNodes(node.children, blankForInteraction, gapForElement);
|
|
86512
86535
|
const hasContent = children.some((child) => child.kind !== "text" || Boolean(child.text.trim()));
|
|
86513
86536
|
if (hasContent) {
|
|
86514
86537
|
result.push({
|
|
@@ -86517,17 +86540,17 @@ function collectContentNodes(nodes, blankForInteraction) {
|
|
|
86517
86540
|
});
|
|
86518
86541
|
}
|
|
86519
86542
|
} else {
|
|
86520
|
-
result.push(...collectContentNodes(node.children, blankForInteraction));
|
|
86543
|
+
result.push(...collectContentNodes(node.children, blankForInteraction, gapForElement));
|
|
86521
86544
|
}
|
|
86522
86545
|
}
|
|
86523
86546
|
return result;
|
|
86524
86547
|
}
|
|
86525
|
-
function contentNodesFrom(nodes, blankForInteraction) {
|
|
86526
|
-
return trimContentBoundaries(collectContentNodes(nodes, blankForInteraction));
|
|
86548
|
+
function contentNodesFrom(nodes, blankForInteraction, gapForElement = () => null) {
|
|
86549
|
+
return trimContentBoundaries(collectContentNodes(nodes, blankForInteraction, gapForElement));
|
|
86527
86550
|
}
|
|
86528
86551
|
function hasRichContent(nodes) {
|
|
86529
86552
|
return nodes.some((node) => {
|
|
86530
|
-
if (node.kind === "math" || node.kind === "image" || node.kind === "table" || node.kind === "list" || node.kind === "blank") {
|
|
86553
|
+
if (node.kind === "math" || node.kind === "image" || node.kind === "table" || node.kind === "list" || node.kind === "blank" || node.kind === "gap") {
|
|
86531
86554
|
return true;
|
|
86532
86555
|
}
|
|
86533
86556
|
if (node.kind === "paragraph" || node.kind === "emphasis" || node.kind === "strong") {
|
|
@@ -86548,7 +86571,14 @@ function parseGapMatchText(interaction) {
|
|
|
86548
86571
|
}
|
|
86549
86572
|
return GAP_MATCH_TOKEN_TAGS2.has(element.localName) ? "" : undefined;
|
|
86550
86573
|
});
|
|
86551
|
-
|
|
86574
|
+
const passageNodes = interaction.children.filter((node) => node.kind === "text" || !GAP_MATCH_TOKEN_TAGS2.has(node.localName));
|
|
86575
|
+
const gapContent = contentNodesFrom(passageNodes, () => null, (element) => {
|
|
86576
|
+
if (element.localName !== "qti-gap") {
|
|
86577
|
+
return null;
|
|
86578
|
+
}
|
|
86579
|
+
return parseXmlAttributes(element.attributes).identifier ?? null;
|
|
86580
|
+
});
|
|
86581
|
+
return { textWithGaps, gaps, gapContent };
|
|
86552
86582
|
}
|
|
86553
86583
|
function isInlineInteractionElement(element) {
|
|
86554
86584
|
const type = INTERACTION_TYPES2[element.localName];
|
|
@@ -86575,6 +86605,7 @@ function parseInteraction(source, interaction, context2) {
|
|
|
86575
86605
|
let gapImages = [];
|
|
86576
86606
|
let gaps = [];
|
|
86577
86607
|
let textWithGaps;
|
|
86608
|
+
let gapContent;
|
|
86578
86609
|
function ownStageGraphic() {
|
|
86579
86610
|
return firstGraphic(interaction, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2);
|
|
86580
86611
|
}
|
|
@@ -86596,7 +86627,7 @@ function parseInteraction(source, interaction, context2) {
|
|
|
86596
86627
|
graphic = ownStageGraphic();
|
|
86597
86628
|
} else if (type === "gap-match") {
|
|
86598
86629
|
gapTexts = parseGapTexts(interaction);
|
|
86599
|
-
({ textWithGaps, gaps } = parseGapMatchText(interaction));
|
|
86630
|
+
({ textWithGaps, gaps, gapContent } = parseGapMatchText(interaction));
|
|
86600
86631
|
} else if (type === "graphic-gap-match") {
|
|
86601
86632
|
graphic = ownStageGraphic();
|
|
86602
86633
|
gapImages = parseGapImages(interaction);
|
|
@@ -86624,6 +86655,7 @@ function parseInteraction(source, interaction, context2) {
|
|
|
86624
86655
|
gapImages,
|
|
86625
86656
|
gaps,
|
|
86626
86657
|
...textWithGaps === undefined ? {} : { textWithGaps },
|
|
86658
|
+
...gapContent === undefined ? {} : { gapContent },
|
|
86627
86659
|
rawXml: xmlElementSource(source, interaction)
|
|
86628
86660
|
};
|
|
86629
86661
|
}
|
|
@@ -86714,6 +86746,23 @@ function parseQtiItemXml(rawXml) {
|
|
|
86714
86746
|
rawXml
|
|
86715
86747
|
};
|
|
86716
86748
|
}
|
|
86749
|
+
function qtiDirectedPairValidationIssue2(values, contract) {
|
|
86750
|
+
const sourceCounts = new Map;
|
|
86751
|
+
const targetCounts = new Map;
|
|
86752
|
+
for (const value of values) {
|
|
86753
|
+
const [source, target, extra] = value.split(" ");
|
|
86754
|
+
if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
|
|
86755
|
+
return { kind: "invalid-pair", value };
|
|
86756
|
+
}
|
|
86757
|
+
if (!withinQtiCapacity(contract.sources, sourceCounts, source)) {
|
|
86758
|
+
return { kind: "source-capacity", identifier: source };
|
|
86759
|
+
}
|
|
86760
|
+
if (!withinQtiCapacity(contract.targets, targetCounts, target)) {
|
|
86761
|
+
return { kind: "target-capacity", identifier: target };
|
|
86762
|
+
}
|
|
86763
|
+
}
|
|
86764
|
+
return null;
|
|
86765
|
+
}
|
|
86717
86766
|
function usableQtiRegionGeometry(shape, coords) {
|
|
86718
86767
|
const arity2 = REGION_SHAPE_COORDS[shape];
|
|
86719
86768
|
if (!arity2) {
|
|
@@ -87178,6 +87227,17 @@ function interactionBody(item, node, type, declaration, attributes2) {
|
|
|
87178
87227
|
if (!attributes2.has("max-associations")) {
|
|
87179
87228
|
attributes2.set("max-associations", String(correctValueStrings(declaration).length || sources.length));
|
|
87180
87229
|
}
|
|
87230
|
+
const capacityIssue = qtiDirectedPairValidationIssue2(correctValueStrings(declaration), {
|
|
87231
|
+
sources: new Map(sources.map((choice) => [choice.identifier, 1])),
|
|
87232
|
+
targets: new Map(targets.map((choice) => [choice.identifier, 1]))
|
|
87233
|
+
});
|
|
87234
|
+
if (capacityIssue) {
|
|
87235
|
+
if (capacityIssue.kind === "invalid-pair") {
|
|
87236
|
+
compileError(`Match pair “${capacityIssue.value}” must name one declared source and target choice`);
|
|
87237
|
+
}
|
|
87238
|
+
const role = capacityIssue.kind === "source-capacity" ? "Source" : "Target";
|
|
87239
|
+
compileError(`${role} “${capacityIssue.identifier}” takes part in more associations than its match-max allows`);
|
|
87240
|
+
}
|
|
87181
87241
|
const sets = [sources, targets].map((choices) => `<qti-simple-match-set>${choices.map((choice) => choiceXml(item, "qti-simple-associable-choice", choice, {
|
|
87182
87242
|
"match-max": "1"
|
|
87183
87243
|
})).join("")}</qti-simple-match-set>`).join("");
|
|
@@ -87463,6 +87523,14 @@ function qtiAttributeNumber(attributes2, name3, fallback) {
|
|
|
87463
87523
|
const value = Number(raw);
|
|
87464
87524
|
return Number.isFinite(value) ? value : fallback;
|
|
87465
87525
|
}
|
|
87526
|
+
function qtiAttributeCapacity(attributes2, name3, fallback) {
|
|
87527
|
+
const raw = attributes2[name3]?.trim();
|
|
87528
|
+
if (!raw) {
|
|
87529
|
+
return fallback;
|
|
87530
|
+
}
|
|
87531
|
+
const value = Number(raw);
|
|
87532
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
87533
|
+
}
|
|
87466
87534
|
function qtiAttributeMaximum(attributes2, name3, fallback, unlimited) {
|
|
87467
87535
|
const maximum = qtiAttributeNumber(attributes2, name3, fallback);
|
|
87468
87536
|
return maximum === 0 ? unlimited : maximum;
|
|
@@ -87471,6 +87539,27 @@ function qtiAttributeBoolean(attributes2, name3) {
|
|
|
87471
87539
|
const value = attributes2[name3];
|
|
87472
87540
|
return value === "true" || value === "1";
|
|
87473
87541
|
}
|
|
87542
|
+
function gapContentIdentifiers(nodes) {
|
|
87543
|
+
return nodes.flatMap((node) => {
|
|
87544
|
+
if (node.kind === "gap") {
|
|
87545
|
+
return [node.identifier];
|
|
87546
|
+
}
|
|
87547
|
+
if (node.kind === "paragraph" || node.kind === "emphasis" || node.kind === "strong") {
|
|
87548
|
+
return gapContentIdentifiers(node.children);
|
|
87549
|
+
}
|
|
87550
|
+
if (node.kind === "list") {
|
|
87551
|
+
return node.items.flatMap(gapContentIdentifiers);
|
|
87552
|
+
}
|
|
87553
|
+
if (node.kind === "table") {
|
|
87554
|
+
return node.rows.flatMap((row) => row.cells.flatMap(gapContentIdentifiers));
|
|
87555
|
+
}
|
|
87556
|
+
return [];
|
|
87557
|
+
});
|
|
87558
|
+
}
|
|
87559
|
+
function qtiGapContentMatchesGaps(nodes, gaps) {
|
|
87560
|
+
const identifiers = gapContentIdentifiers(nodes);
|
|
87561
|
+
return identifiers.length === gaps.length && identifiers.every((identifier, index2) => identifier === gaps[index2]);
|
|
87562
|
+
}
|
|
87474
87563
|
function normalizedInteractionType(value) {
|
|
87475
87564
|
if (typeof value !== "string") {
|
|
87476
87565
|
return;
|
|
@@ -87481,6 +87570,9 @@ function normalizedInteractionType(value) {
|
|
|
87481
87570
|
function usableChoices(choices) {
|
|
87482
87571
|
return choices !== undefined && choices.length > 0 && choices.every((choice) => Boolean(choice.identifier));
|
|
87483
87572
|
}
|
|
87573
|
+
function usableAssociableChoices(choices) {
|
|
87574
|
+
return usableChoices(choices) && choices.every((choice) => qtiAttributeCapacity(choice.attributes, "match-max", 0) !== null);
|
|
87575
|
+
}
|
|
87484
87576
|
function transportableIdentifiers(identifiers) {
|
|
87485
87577
|
return identifiers.every((identifier) => Boolean(identifier) && !/\s/.test(identifier));
|
|
87486
87578
|
}
|
|
@@ -87553,7 +87645,7 @@ function authorableQtiInteractionProjection(interaction) {
|
|
|
87553
87645
|
}
|
|
87554
87646
|
case "match": {
|
|
87555
87647
|
const [sourceChoices, targetChoices] = interaction.choiceSets;
|
|
87556
|
-
if (interaction.choiceSets.length !== 2 || !
|
|
87648
|
+
if (interaction.choiceSets.length !== 2 || !usableAssociableChoices(sourceChoices) || !usableAssociableChoices(targetChoices) || !transportableIdentifiers([...sourceChoices, ...targetChoices].map((choice) => choice.identifier))) {
|
|
87557
87649
|
return null;
|
|
87558
87650
|
}
|
|
87559
87651
|
return {
|
|
@@ -87622,8 +87714,8 @@ function authorableQtiInteractionProjection(interaction) {
|
|
|
87622
87714
|
};
|
|
87623
87715
|
}
|
|
87624
87716
|
case "gap-match": {
|
|
87625
|
-
const { gapTexts, gaps, textWithGaps } = interaction;
|
|
87626
|
-
if (gapTexts.length === 0 || gaps.length === 0 || textWithGaps === undefined || !gapTexts.every((gapText) => Boolean(gapText.identifier && gapText.content)) || !transportableIdentifiers([...gapTexts.map((gapText) => gapText.identifier), ...gaps])) {
|
|
87717
|
+
const { gapTexts, gaps, textWithGaps, gapContent } = interaction;
|
|
87718
|
+
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])) {
|
|
87627
87719
|
return null;
|
|
87628
87720
|
}
|
|
87629
87721
|
return {
|
|
@@ -87632,6 +87724,7 @@ function authorableQtiInteractionProjection(interaction) {
|
|
|
87632
87724
|
gapTexts,
|
|
87633
87725
|
gaps,
|
|
87634
87726
|
textWithGaps,
|
|
87727
|
+
gapContent,
|
|
87635
87728
|
shuffle: qtiAttributeBoolean(attributes2, "shuffle")
|
|
87636
87729
|
};
|
|
87637
87730
|
}
|
|
@@ -87655,7 +87748,7 @@ function contributesScore(declaration) {
|
|
|
87655
87748
|
function responsePairs(left, right) {
|
|
87656
87749
|
return left.flatMap((first) => right.map((second) => `${first} ${second}`));
|
|
87657
87750
|
}
|
|
87658
|
-
function
|
|
87751
|
+
function interactionResponseSpace(item, declaration) {
|
|
87659
87752
|
const interaction = item.interactions.find((candidate) => candidate.responseIdentifier === declaration.identifier);
|
|
87660
87753
|
if (!interaction) {
|
|
87661
87754
|
return {};
|
|
@@ -87747,21 +87840,49 @@ function processesResponseMapping(item, declaration) {
|
|
|
87747
87840
|
}
|
|
87748
87841
|
return item.responseProcessing.elements.some((element) => element.tagName === "qti-map-response" && (element.attributes.identifier ?? "RESPONSE") === declaration.identifier);
|
|
87749
87842
|
}
|
|
87750
|
-
function responseMappingMaximum(item, declaration) {
|
|
87843
|
+
function responseMappingMaximum(item, declaration, space) {
|
|
87751
87844
|
if (!declaration.mapping || !processesResponseMapping(item, declaration)) {
|
|
87752
87845
|
return null;
|
|
87753
87846
|
}
|
|
87754
87847
|
return qtiResponseMappingMaximum({
|
|
87755
87848
|
cardinality: declaration.cardinality,
|
|
87756
87849
|
mapping: declaration.mapping,
|
|
87757
|
-
...
|
|
87850
|
+
...space ?? interactionResponseSpace(item, declaration)
|
|
87758
87851
|
});
|
|
87759
87852
|
}
|
|
87760
87853
|
function qtiItemScoringValidationMessage(item) {
|
|
87761
87854
|
for (const declaration of item.responseDeclarations) {
|
|
87855
|
+
const space = interactionResponseSpace(item, declaration);
|
|
87856
|
+
const { responseValues: responseValues2, maximumResponses } = space;
|
|
87857
|
+
if (responseValues2 && declaration.correctValues.some((value) => !responseValues2.includes(value))) {
|
|
87858
|
+
return `Response ${declaration.identifier} declares a correct value its interaction cannot produce.`;
|
|
87859
|
+
}
|
|
87860
|
+
const interaction = item.interactions.find((candidate) => candidate.responseIdentifier === declaration.identifier);
|
|
87861
|
+
if (interaction?.type === "match") {
|
|
87862
|
+
const [sources = [], targets = []] = interaction.choiceSets;
|
|
87863
|
+
const issue6 = qtiDirectedPairValidationIssue2(declaration.correctValues, {
|
|
87864
|
+
sources: new Map(sources.map((choice) => [
|
|
87865
|
+
choice.identifier,
|
|
87866
|
+
qtiAttributeNumber(choice.attributes, "match-max", 0)
|
|
87867
|
+
])),
|
|
87868
|
+
targets: new Map(targets.map((choice) => [
|
|
87869
|
+
choice.identifier,
|
|
87870
|
+
qtiAttributeNumber(choice.attributes, "match-max", 0)
|
|
87871
|
+
]))
|
|
87872
|
+
});
|
|
87873
|
+
if (issue6?.kind === "source-capacity") {
|
|
87874
|
+
return `Response ${declaration.identifier} declares correct pairs that exceed source ${issue6.identifier}'s match-max.`;
|
|
87875
|
+
}
|
|
87876
|
+
if (issue6?.kind === "target-capacity") {
|
|
87877
|
+
return `Response ${declaration.identifier} declares correct pairs that exceed target ${issue6.identifier}'s match-max.`;
|
|
87878
|
+
}
|
|
87879
|
+
}
|
|
87880
|
+
if (declaration.cardinality !== "single" && maximumResponses !== undefined && maximumResponses !== 0 && declaration.correctValues.length > maximumResponses) {
|
|
87881
|
+
return `Response ${declaration.identifier} declares more correct values than its interaction accepts.`;
|
|
87882
|
+
}
|
|
87762
87883
|
if (declaration.mapping) {
|
|
87763
87884
|
if (processesResponseMapping(item, declaration)) {
|
|
87764
|
-
if (responseMappingMaximum(item, declaration) === null) {
|
|
87885
|
+
if (responseMappingMaximum(item, declaration, space) === null) {
|
|
87765
87886
|
return `Response ${declaration.identifier} has a mapping with no reachable positive score.`;
|
|
87766
87887
|
}
|
|
87767
87888
|
} else if (declaration.correctValues.length === 0) {
|
|
@@ -88084,6 +88205,7 @@ var init_qti = __esm(() => {
|
|
|
88084
88205
|
init_fxp();
|
|
88085
88206
|
init_timeback3();
|
|
88086
88207
|
init_timeback3();
|
|
88208
|
+
init_timeback3();
|
|
88087
88209
|
EVENT_HANDLER_ATTRIBUTE = /^on/i;
|
|
88088
88210
|
SCRIPT_SCHEMES2 = {
|
|
88089
88211
|
javascript: ["java", "script:"].join(""),
|
|
@@ -90920,6 +91042,16 @@ function playableChoice(choice) {
|
|
|
90920
91042
|
...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
|
|
90921
91043
|
};
|
|
90922
91044
|
}
|
|
91045
|
+
function playableAssociableChoice(choice) {
|
|
91046
|
+
const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
|
|
91047
|
+
if (matchMax === null) {
|
|
91048
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
|
|
91049
|
+
}
|
|
91050
|
+
return {
|
|
91051
|
+
...playableChoice(choice),
|
|
91052
|
+
matchMax
|
|
91053
|
+
};
|
|
91054
|
+
}
|
|
90923
91055
|
function requireChoices2(choices) {
|
|
90924
91056
|
if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
|
|
90925
91057
|
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
|
|
@@ -91005,8 +91137,8 @@ function parseInteraction2(questionIdentifier, interaction) {
|
|
|
91005
91137
|
if (sets.length !== 2) {
|
|
91006
91138
|
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
|
|
91007
91139
|
}
|
|
91008
|
-
const sourceChoices = requireChoices2(sets[0].map(
|
|
91009
|
-
const targetChoices = requireChoices2(sets[1].map(
|
|
91140
|
+
const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
|
|
91141
|
+
const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
|
|
91010
91142
|
return {
|
|
91011
91143
|
type,
|
|
91012
91144
|
responseIdentifier,
|
|
@@ -91031,7 +91163,7 @@ function parseInteraction2(questionIdentifier, interaction) {
|
|
|
91031
91163
|
};
|
|
91032
91164
|
}
|
|
91033
91165
|
if (type === "gap-match") {
|
|
91034
|
-
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined) {
|
|
91166
|
+
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
|
|
91035
91167
|
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
|
|
91036
91168
|
}
|
|
91037
91169
|
return {
|
|
@@ -91039,6 +91171,7 @@ function parseInteraction2(questionIdentifier, interaction) {
|
|
|
91039
91171
|
responseIdentifier,
|
|
91040
91172
|
...prompt,
|
|
91041
91173
|
content: interaction.textWithGaps,
|
|
91174
|
+
contentNodes: interaction.gapContent,
|
|
91042
91175
|
gapTexts: interaction.gapTexts.map((gapText) => ({
|
|
91043
91176
|
identifier: gapText.identifier,
|
|
91044
91177
|
content: gapText.content,
|
|
@@ -93148,8 +93281,7 @@ function assertPlayableQtiQuestion(question) {
|
|
|
93148
93281
|
}
|
|
93149
93282
|
const item = parsedQtiItemOrNull(question.rawXml);
|
|
93150
93283
|
const playable = item && playableQtiInteractions(item);
|
|
93151
|
-
|
|
93152
|
-
if (!playable || scoringIssue) {
|
|
93284
|
+
if (!playable || qtiItemScoringValidationMessage(item)) {
|
|
93153
93285
|
throw new ValidationError(`Question ${question.identifier} uses an interaction Playcademy cannot play or score.`);
|
|
93154
93286
|
}
|
|
93155
93287
|
}
|
|
@@ -93402,31 +93534,38 @@ ${input.itemBody}
|
|
|
93402
93534
|
</qti-response-processing>
|
|
93403
93535
|
</qti-assessment-item>`;
|
|
93404
93536
|
}
|
|
93405
|
-
function
|
|
93406
|
-
|
|
93407
|
-
|
|
93537
|
+
function parseStructuredChoices(value, options) {
|
|
93538
|
+
const { label, minimum, validate: validate2 } = options;
|
|
93539
|
+
if (!Array.isArray(value) || value.length < minimum) {
|
|
93540
|
+
throw new ValidationError(`${label} questions require at least ${minimum === 1 ? "one choice" : `${minimum} choices`}`);
|
|
93408
93541
|
}
|
|
93409
93542
|
const choices = value.map((choice) => {
|
|
93410
93543
|
const record3 = recordValue(choice);
|
|
93411
93544
|
const identifier = record3?.identifier;
|
|
93412
93545
|
const content = record3?.content;
|
|
93413
|
-
if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim()) {
|
|
93414
|
-
throw new ValidationError(
|
|
93546
|
+
if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim() || validate2 !== undefined && record3 !== undefined && !validate2(record3)) {
|
|
93547
|
+
throw new ValidationError(`${label} options are invalid`);
|
|
93415
93548
|
}
|
|
93416
93549
|
return { identifier: identifier.trim(), content: content.trim() };
|
|
93417
93550
|
});
|
|
93418
93551
|
if (new Set(choices.map((choice) => choice.identifier)).size !== choices.length) {
|
|
93419
|
-
throw new ValidationError(
|
|
93552
|
+
throw new ValidationError(`${label} option identifiers must be unique`);
|
|
93420
93553
|
}
|
|
93421
93554
|
return choices;
|
|
93422
93555
|
}
|
|
93423
|
-
function
|
|
93556
|
+
function declaredCorrectResponse(input, responseIdentifier) {
|
|
93424
93557
|
const declarations = Array.isArray(input.responseDeclarations) ? input.responseDeclarations : [];
|
|
93425
93558
|
const declaration = declarations.map(recordValue).find((candidate) => candidate?.identifier === responseIdentifier);
|
|
93426
|
-
const
|
|
93427
|
-
|
|
93428
|
-
|
|
93429
|
-
|
|
93559
|
+
const rawValues = recordValue(declaration?.correctResponse)?.value;
|
|
93560
|
+
return {
|
|
93561
|
+
cardinality: declaration?.cardinality,
|
|
93562
|
+
baseType: declaration?.baseType,
|
|
93563
|
+
values: Array.isArray(rawValues) ? rawValues.filter((value) => typeof value === "string") : []
|
|
93564
|
+
};
|
|
93565
|
+
}
|
|
93566
|
+
function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers) {
|
|
93567
|
+
const { cardinality, baseType, values } = declaredCorrectResponse(input, responseIdentifier);
|
|
93568
|
+
if (cardinality !== "single" || baseType !== "identifier") {
|
|
93430
93569
|
throw new ValidationError("Inline-choice questions require a single-identifier response declaration");
|
|
93431
93570
|
}
|
|
93432
93571
|
if (values.length !== 1 || !choiceIdentifiers.has(values[0])) {
|
|
@@ -93434,27 +93573,41 @@ function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifi
|
|
|
93434
93573
|
}
|
|
93435
93574
|
return values[0];
|
|
93436
93575
|
}
|
|
93437
|
-
function
|
|
93576
|
+
function structuredQuestionEnvelope(input, type, label, title) {
|
|
93438
93577
|
const interaction = recordValue(input.interaction);
|
|
93439
|
-
|
|
93440
|
-
if (interactionType !== "inline-choice") {
|
|
93578
|
+
if (normalizedQtiInteractionType(interaction?.type ?? input.type) !== type) {
|
|
93441
93579
|
return null;
|
|
93442
93580
|
}
|
|
93443
|
-
const responseIdentifier = interaction?.responseIdentifier;
|
|
93444
93581
|
const structure = recordValue(interaction?.questionStructure);
|
|
93582
|
+
const responseIdentifier = interaction?.responseIdentifier;
|
|
93445
93583
|
const prompt = structure?.prompt;
|
|
93446
|
-
if (!structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
|
|
93447
|
-
throw new ValidationError(
|
|
93584
|
+
if (!interaction || !structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
|
|
93585
|
+
throw new ValidationError(`${label} question data is invalid`);
|
|
93448
93586
|
}
|
|
93449
93587
|
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
93450
|
-
throw new ValidationError(
|
|
93588
|
+
throw new ValidationError(`${label} questions require a title and prompt`);
|
|
93589
|
+
}
|
|
93590
|
+
return {
|
|
93591
|
+
interaction,
|
|
93592
|
+
structure,
|
|
93593
|
+
responseIdentifier: responseIdentifier.trim(),
|
|
93594
|
+
prompt: prompt.trim()
|
|
93595
|
+
};
|
|
93596
|
+
}
|
|
93597
|
+
function buildInlineChoiceQuestionXml(input, identifier, title) {
|
|
93598
|
+
const envelope = structuredQuestionEnvelope(input, "inline-choice", "Inline-choice", title);
|
|
93599
|
+
if (!envelope) {
|
|
93600
|
+
return null;
|
|
93451
93601
|
}
|
|
93452
|
-
const
|
|
93453
|
-
const promptParts =
|
|
93602
|
+
const { structure, responseIdentifier, prompt } = envelope;
|
|
93603
|
+
const promptParts = prompt.split(INLINE_CHOICE_BLANK);
|
|
93454
93604
|
if (promptParts.length !== 2) {
|
|
93455
93605
|
throw new ValidationError("Inline-choice questions require exactly one blank");
|
|
93456
93606
|
}
|
|
93457
|
-
const choices =
|
|
93607
|
+
const choices = parseStructuredChoices(structure.inlineChoices, {
|
|
93608
|
+
label: "Inline-choice",
|
|
93609
|
+
minimum: 2
|
|
93610
|
+
});
|
|
93458
93611
|
const choiceIdentifiers = new Set(choices.map((choice) => choice.identifier));
|
|
93459
93612
|
const correctIdentifier = inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers);
|
|
93460
93613
|
const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
|
|
@@ -93472,6 +93625,86 @@ ${choicesXml}
|
|
|
93472
93625
|
itemBody
|
|
93473
93626
|
});
|
|
93474
93627
|
}
|
|
93628
|
+
function parseStructuredMatchChoices(value, label) {
|
|
93629
|
+
return parseStructuredChoices(value, {
|
|
93630
|
+
label: `Match ${label}`,
|
|
93631
|
+
minimum: 1,
|
|
93632
|
+
validate: (choice) => choice.matchMax === 1
|
|
93633
|
+
});
|
|
93634
|
+
}
|
|
93635
|
+
function structuredMatchCorrectPairs(input, responseIdentifier, sources, targets) {
|
|
93636
|
+
const { cardinality, baseType, values } = declaredCorrectResponse(input, responseIdentifier);
|
|
93637
|
+
if (cardinality !== "multiple" || baseType !== "directedPair") {
|
|
93638
|
+
throw new ValidationError("Match questions require a multiple directed-pair response declaration");
|
|
93639
|
+
}
|
|
93640
|
+
const sourceIdentifiers = new Set(sources.map((choice) => choice.identifier));
|
|
93641
|
+
const targetIdentifiers = new Set(targets.map((choice) => choice.identifier));
|
|
93642
|
+
const valid = values.length > 0 && new Set(values).size === values.length && values.every((value) => {
|
|
93643
|
+
const [source, target, extra] = value.split(" ");
|
|
93644
|
+
return extra === undefined && Boolean(source && target) && sourceIdentifiers.has(source) && targetIdentifiers.has(target);
|
|
93645
|
+
});
|
|
93646
|
+
if (!valid) {
|
|
93647
|
+
throw new ValidationError("Every Match correct response must name one declared source and target choice");
|
|
93648
|
+
}
|
|
93649
|
+
return values;
|
|
93650
|
+
}
|
|
93651
|
+
function buildMatchQuestionXml(input, identifier, title) {
|
|
93652
|
+
const envelope = structuredQuestionEnvelope(input, "match", "Match", title);
|
|
93653
|
+
if (!envelope) {
|
|
93654
|
+
return null;
|
|
93655
|
+
}
|
|
93656
|
+
const { interaction, structure, responseIdentifier, prompt } = envelope;
|
|
93657
|
+
const sources = parseStructuredMatchChoices(structure.sourceChoices, "source");
|
|
93658
|
+
const targets = parseStructuredMatchChoices(structure.targetChoices, "target");
|
|
93659
|
+
const sourceIdentifiers = new Set(sources.map((choice) => choice.identifier));
|
|
93660
|
+
if (targets.some((choice) => sourceIdentifiers.has(choice.identifier))) {
|
|
93661
|
+
throw new ValidationError("Match source and target choice identifiers must be unique");
|
|
93662
|
+
}
|
|
93663
|
+
const correctValues = structuredMatchCorrectPairs(input, responseIdentifier, sources, targets);
|
|
93664
|
+
const maxAssociations = interaction.maxAssociations;
|
|
93665
|
+
if (typeof maxAssociations !== "number" || !Number.isInteger(maxAssociations) || maxAssociations < 0 || maxAssociations !== 0 && maxAssociations < correctValues.length) {
|
|
93666
|
+
throw new ValidationError("Match max associations must fit every correct pair");
|
|
93667
|
+
}
|
|
93668
|
+
const item = {
|
|
93669
|
+
key: identifier,
|
|
93670
|
+
title,
|
|
93671
|
+
body: [
|
|
93672
|
+
{
|
|
93673
|
+
kind: "interaction",
|
|
93674
|
+
type: "match",
|
|
93675
|
+
responseIdentifier,
|
|
93676
|
+
attributes: {
|
|
93677
|
+
shuffle: interaction.shuffle === true,
|
|
93678
|
+
"max-associations": maxAssociations
|
|
93679
|
+
},
|
|
93680
|
+
content: [
|
|
93681
|
+
{
|
|
93682
|
+
kind: "element",
|
|
93683
|
+
name: "qti-prompt",
|
|
93684
|
+
children: [{ kind: "text", text: prompt }]
|
|
93685
|
+
}
|
|
93686
|
+
],
|
|
93687
|
+
choiceSets: [sources, targets].map((choices) => choices.map((choice) => ({
|
|
93688
|
+
identifier: choice.identifier,
|
|
93689
|
+
content: [{ kind: "text", text: choice.content }]
|
|
93690
|
+
})))
|
|
93691
|
+
}
|
|
93692
|
+
],
|
|
93693
|
+
responseDeclarations: [
|
|
93694
|
+
{
|
|
93695
|
+
identifier: responseIdentifier,
|
|
93696
|
+
cardinality: "multiple",
|
|
93697
|
+
baseType: "directedPair",
|
|
93698
|
+
correctValues
|
|
93699
|
+
}
|
|
93700
|
+
]
|
|
93701
|
+
};
|
|
93702
|
+
try {
|
|
93703
|
+
return compileQtiAuthoringItemXml(item, { identifier, title });
|
|
93704
|
+
} catch (error88) {
|
|
93705
|
+
throw new ValidationError(`This Match question cannot be compiled to QTI: ${errorMessage(error88)}`);
|
|
93706
|
+
}
|
|
93707
|
+
}
|
|
93475
93708
|
function parseHottextSegments2(value) {
|
|
93476
93709
|
if (!Array.isArray(value) || value.length === 0) {
|
|
93477
93710
|
throw new ValidationError("Hottext questions require passage segments");
|
|
@@ -93773,7 +94006,8 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
93773
94006
|
buildNumericQuestionXml,
|
|
93774
94007
|
buildAuthoringDocumentQuestionXml,
|
|
93775
94008
|
buildHottextQuestionXml,
|
|
93776
|
-
buildInlineChoiceQuestionXml
|
|
94009
|
+
buildInlineChoiceQuestionXml,
|
|
94010
|
+
buildMatchQuestionXml
|
|
93777
94011
|
];
|
|
93778
94012
|
});
|
|
93779
94013
|
|
package/dist/server.js
CHANGED
|
@@ -1122,7 +1122,7 @@ var package_default;
|
|
|
1122
1122
|
var init_package = __esm(() => {
|
|
1123
1123
|
package_default = {
|
|
1124
1124
|
name: "@playcademy/sandbox",
|
|
1125
|
-
version: "0.7.1-beta.
|
|
1125
|
+
version: "0.7.1-beta.13",
|
|
1126
1126
|
description: "Local development server for Playcademy game development",
|
|
1127
1127
|
type: "module",
|
|
1128
1128
|
exports: {
|
|
@@ -9250,6 +9250,23 @@ var init_esm = __esm(() => {
|
|
|
9250
9250
|
});
|
|
9251
9251
|
|
|
9252
9252
|
// ../timeback/dist/engines/assessment-runtime/index.js
|
|
9253
|
+
function qtiDirectedPairValidationIssue(values, contract) {
|
|
9254
|
+
const sourceCounts = new Map;
|
|
9255
|
+
const targetCounts = new Map;
|
|
9256
|
+
for (const value of values) {
|
|
9257
|
+
const [source, target, extra] = value.split(" ");
|
|
9258
|
+
if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
|
|
9259
|
+
return { kind: "invalid-pair", value };
|
|
9260
|
+
}
|
|
9261
|
+
if (!withinQtiCapacity(contract.sources, sourceCounts, source)) {
|
|
9262
|
+
return { kind: "source-capacity", identifier: source };
|
|
9263
|
+
}
|
|
9264
|
+
if (!withinQtiCapacity(contract.targets, targetCounts, target)) {
|
|
9265
|
+
return { kind: "target-capacity", identifier: target };
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
return null;
|
|
9269
|
+
}
|
|
9253
9270
|
function isRecord2(value) {
|
|
9254
9271
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9255
9272
|
}
|
|
@@ -9552,19 +9569,15 @@ function assessmentResponseValidationMessage(assessment, responses) {
|
|
|
9552
9569
|
return null;
|
|
9553
9570
|
}
|
|
9554
9571
|
function directedPairsValidationMessage(itemIdentifier, values, contract) {
|
|
9555
|
-
const
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
const [source, target, extra] = value.split(" ");
|
|
9559
|
-
if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
|
|
9572
|
+
const issue = qtiDirectedPairValidationIssue(values, contract);
|
|
9573
|
+
if (issue) {
|
|
9574
|
+
if (issue.kind === "invalid-pair") {
|
|
9560
9575
|
return `Response for ${itemIdentifier} contains an invalid association pair.`;
|
|
9561
9576
|
}
|
|
9562
|
-
if (
|
|
9577
|
+
if (issue.kind === "source-capacity") {
|
|
9563
9578
|
return `Response for ${itemIdentifier} uses a token beyond its allowed placements.`;
|
|
9564
9579
|
}
|
|
9565
|
-
|
|
9566
|
-
return `Response for ${itemIdentifier} fills a target beyond its capacity.`;
|
|
9567
|
-
}
|
|
9580
|
+
return `Response for ${itemIdentifier} fills a target beyond its capacity.`;
|
|
9568
9581
|
}
|
|
9569
9582
|
return null;
|
|
9570
9583
|
}
|
|
@@ -9644,8 +9657,14 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
|
|
|
9644
9657
|
allowed = new Set(interaction.choices.map((candidate) => candidate.identifier));
|
|
9645
9658
|
} else if (interaction.type === "match") {
|
|
9646
9659
|
const message = directedPairsValidationMessage(itemIdentifier, values, {
|
|
9647
|
-
sources: new Map(interaction.sourceChoices.map((candidate) => [
|
|
9648
|
-
|
|
9660
|
+
sources: new Map(interaction.sourceChoices.map((candidate) => [
|
|
9661
|
+
candidate.identifier,
|
|
9662
|
+
candidate.matchMax
|
|
9663
|
+
])),
|
|
9664
|
+
targets: new Map(interaction.targetChoices.map((candidate) => [
|
|
9665
|
+
candidate.identifier,
|
|
9666
|
+
candidate.matchMax
|
|
9667
|
+
]))
|
|
9649
9668
|
});
|
|
9650
9669
|
if (message) {
|
|
9651
9670
|
return message;
|
|
@@ -10371,6 +10390,7 @@ var init_assessment_runtime = __esm(() => {
|
|
|
10371
10390
|
init_fxp();
|
|
10372
10391
|
init_timeback3();
|
|
10373
10392
|
init_timeback3();
|
|
10393
|
+
init_timeback3();
|
|
10374
10394
|
init_src();
|
|
10375
10395
|
init_timeback3();
|
|
10376
10396
|
init_src();
|
|
@@ -86426,18 +86446,18 @@ function serializeSanitizedMathMl(node) {
|
|
|
86426
86446
|
const children = node.children.map(serializeSanitizedMathMl).join("");
|
|
86427
86447
|
return `<${node.localName}${attributes2}>${children}</${node.localName}>`;
|
|
86428
86448
|
}
|
|
86429
|
-
function tableContentNode(element, blankForInteraction) {
|
|
86449
|
+
function tableContentNode(element, blankForInteraction, gapForElement) {
|
|
86430
86450
|
const rows = xmlDescendants(element, (candidate) => candidate.localName === "tr").map((row) => {
|
|
86431
86451
|
const cells = row.children.filter((cell) => cell.kind === "element" && (cell.localName === "td" || cell.localName === "th"));
|
|
86432
86452
|
return {
|
|
86433
86453
|
header: cells.length > 0 && cells.every((cell) => cell.localName === "th"),
|
|
86434
|
-
cells: cells.map((cell) => contentNodesFrom(cell.children, blankForInteraction))
|
|
86454
|
+
cells: cells.map((cell) => contentNodesFrom(cell.children, blankForInteraction, gapForElement))
|
|
86435
86455
|
};
|
|
86436
86456
|
});
|
|
86437
86457
|
return { kind: "table", rows };
|
|
86438
86458
|
}
|
|
86439
|
-
function listContentNode(element, blankForInteraction) {
|
|
86440
|
-
const items = element.children.filter((child) => child.kind === "element" && child.localName === "li").map((item) => contentNodesFrom(item.children, blankForInteraction));
|
|
86459
|
+
function listContentNode(element, blankForInteraction, gapForElement) {
|
|
86460
|
+
const items = element.children.filter((child) => child.kind === "element" && child.localName === "li").map((item) => contentNodesFrom(item.children, blankForInteraction, gapForElement));
|
|
86441
86461
|
return { kind: "list", ordered: element.localName === "ol", items };
|
|
86442
86462
|
}
|
|
86443
86463
|
function contentText(raw) {
|
|
@@ -86462,15 +86482,18 @@ function trimContentBoundaries(nodes) {
|
|
|
86462
86482
|
}
|
|
86463
86483
|
return kept.filter((node) => node.kind !== "text" || node.text !== "");
|
|
86464
86484
|
}
|
|
86465
|
-
function collectContentNodes(nodes, blankForInteraction) {
|
|
86485
|
+
function collectContentNodes(nodes, blankForInteraction, gapForElement) {
|
|
86466
86486
|
const result = [];
|
|
86467
86487
|
for (const node of nodes) {
|
|
86488
|
+
const gapIdentifier = node.kind === "element" ? gapForElement(node) : null;
|
|
86468
86489
|
if (node.kind === "text") {
|
|
86469
86490
|
const text3 = contentText(node.value);
|
|
86470
86491
|
if (text3) {
|
|
86471
86492
|
result.push({ kind: "text", text: text3 });
|
|
86472
86493
|
}
|
|
86473
|
-
} else if (CONTENT_SKIPPED_TAGS2.has(node.localName)) {} else if (
|
|
86494
|
+
} else if (CONTENT_SKIPPED_TAGS2.has(node.localName)) {} else if (gapIdentifier !== null) {
|
|
86495
|
+
result.push({ kind: "gap", identifier: gapIdentifier });
|
|
86496
|
+
} else if (isInteractionElement(node)) {
|
|
86474
86497
|
const responseIdentifier = blankForInteraction(node);
|
|
86475
86498
|
if (responseIdentifier) {
|
|
86476
86499
|
result.push({ kind: "blank", responseIdentifier });
|
|
@@ -86495,19 +86518,19 @@ function collectContentNodes(nodes, blankForInteraction) {
|
|
|
86495
86518
|
});
|
|
86496
86519
|
}
|
|
86497
86520
|
} else if (node.localName === "table") {
|
|
86498
|
-
result.push(tableContentNode(node, blankForInteraction));
|
|
86521
|
+
result.push(tableContentNode(node, blankForInteraction, gapForElement));
|
|
86499
86522
|
} else if (node.localName === "ul" || node.localName === "ol") {
|
|
86500
|
-
const list = listContentNode(node, blankForInteraction);
|
|
86523
|
+
const list = listContentNode(node, blankForInteraction, gapForElement);
|
|
86501
86524
|
if (list.kind === "list" && list.items.length > 0) {
|
|
86502
86525
|
result.push(list);
|
|
86503
86526
|
}
|
|
86504
86527
|
} else if (node.localName === "p") {
|
|
86505
|
-
const children = contentNodesFrom(node.children, blankForInteraction);
|
|
86528
|
+
const children = contentNodesFrom(node.children, blankForInteraction, gapForElement);
|
|
86506
86529
|
if (children.length > 0) {
|
|
86507
86530
|
result.push({ kind: "paragraph", children });
|
|
86508
86531
|
}
|
|
86509
86532
|
} else if (EMPHASIS_TAGS2.has(node.localName) || STRONG_TAGS2.has(node.localName)) {
|
|
86510
|
-
const children = collectContentNodes(node.children, blankForInteraction);
|
|
86533
|
+
const children = collectContentNodes(node.children, blankForInteraction, gapForElement);
|
|
86511
86534
|
const hasContent = children.some((child) => child.kind !== "text" || Boolean(child.text.trim()));
|
|
86512
86535
|
if (hasContent) {
|
|
86513
86536
|
result.push({
|
|
@@ -86516,17 +86539,17 @@ function collectContentNodes(nodes, blankForInteraction) {
|
|
|
86516
86539
|
});
|
|
86517
86540
|
}
|
|
86518
86541
|
} else {
|
|
86519
|
-
result.push(...collectContentNodes(node.children, blankForInteraction));
|
|
86542
|
+
result.push(...collectContentNodes(node.children, blankForInteraction, gapForElement));
|
|
86520
86543
|
}
|
|
86521
86544
|
}
|
|
86522
86545
|
return result;
|
|
86523
86546
|
}
|
|
86524
|
-
function contentNodesFrom(nodes, blankForInteraction) {
|
|
86525
|
-
return trimContentBoundaries(collectContentNodes(nodes, blankForInteraction));
|
|
86547
|
+
function contentNodesFrom(nodes, blankForInteraction, gapForElement = () => null) {
|
|
86548
|
+
return trimContentBoundaries(collectContentNodes(nodes, blankForInteraction, gapForElement));
|
|
86526
86549
|
}
|
|
86527
86550
|
function hasRichContent(nodes) {
|
|
86528
86551
|
return nodes.some((node) => {
|
|
86529
|
-
if (node.kind === "math" || node.kind === "image" || node.kind === "table" || node.kind === "list" || node.kind === "blank") {
|
|
86552
|
+
if (node.kind === "math" || node.kind === "image" || node.kind === "table" || node.kind === "list" || node.kind === "blank" || node.kind === "gap") {
|
|
86530
86553
|
return true;
|
|
86531
86554
|
}
|
|
86532
86555
|
if (node.kind === "paragraph" || node.kind === "emphasis" || node.kind === "strong") {
|
|
@@ -86547,7 +86570,14 @@ function parseGapMatchText(interaction) {
|
|
|
86547
86570
|
}
|
|
86548
86571
|
return GAP_MATCH_TOKEN_TAGS2.has(element.localName) ? "" : undefined;
|
|
86549
86572
|
});
|
|
86550
|
-
|
|
86573
|
+
const passageNodes = interaction.children.filter((node) => node.kind === "text" || !GAP_MATCH_TOKEN_TAGS2.has(node.localName));
|
|
86574
|
+
const gapContent = contentNodesFrom(passageNodes, () => null, (element) => {
|
|
86575
|
+
if (element.localName !== "qti-gap") {
|
|
86576
|
+
return null;
|
|
86577
|
+
}
|
|
86578
|
+
return parseXmlAttributes(element.attributes).identifier ?? null;
|
|
86579
|
+
});
|
|
86580
|
+
return { textWithGaps, gaps, gapContent };
|
|
86551
86581
|
}
|
|
86552
86582
|
function isInlineInteractionElement(element) {
|
|
86553
86583
|
const type = INTERACTION_TYPES2[element.localName];
|
|
@@ -86574,6 +86604,7 @@ function parseInteraction(source, interaction, context2) {
|
|
|
86574
86604
|
let gapImages = [];
|
|
86575
86605
|
let gaps = [];
|
|
86576
86606
|
let textWithGaps;
|
|
86607
|
+
let gapContent;
|
|
86577
86608
|
function ownStageGraphic() {
|
|
86578
86609
|
return firstGraphic(interaction, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2);
|
|
86579
86610
|
}
|
|
@@ -86595,7 +86626,7 @@ function parseInteraction(source, interaction, context2) {
|
|
|
86595
86626
|
graphic = ownStageGraphic();
|
|
86596
86627
|
} else if (type === "gap-match") {
|
|
86597
86628
|
gapTexts = parseGapTexts(interaction);
|
|
86598
|
-
({ textWithGaps, gaps } = parseGapMatchText(interaction));
|
|
86629
|
+
({ textWithGaps, gaps, gapContent } = parseGapMatchText(interaction));
|
|
86599
86630
|
} else if (type === "graphic-gap-match") {
|
|
86600
86631
|
graphic = ownStageGraphic();
|
|
86601
86632
|
gapImages = parseGapImages(interaction);
|
|
@@ -86623,6 +86654,7 @@ function parseInteraction(source, interaction, context2) {
|
|
|
86623
86654
|
gapImages,
|
|
86624
86655
|
gaps,
|
|
86625
86656
|
...textWithGaps === undefined ? {} : { textWithGaps },
|
|
86657
|
+
...gapContent === undefined ? {} : { gapContent },
|
|
86626
86658
|
rawXml: xmlElementSource(source, interaction)
|
|
86627
86659
|
};
|
|
86628
86660
|
}
|
|
@@ -86713,6 +86745,23 @@ function parseQtiItemXml(rawXml) {
|
|
|
86713
86745
|
rawXml
|
|
86714
86746
|
};
|
|
86715
86747
|
}
|
|
86748
|
+
function qtiDirectedPairValidationIssue2(values, contract) {
|
|
86749
|
+
const sourceCounts = new Map;
|
|
86750
|
+
const targetCounts = new Map;
|
|
86751
|
+
for (const value of values) {
|
|
86752
|
+
const [source, target, extra] = value.split(" ");
|
|
86753
|
+
if (extra !== undefined || !source || !target || !contract.sources.has(source) || !contract.targets.has(target)) {
|
|
86754
|
+
return { kind: "invalid-pair", value };
|
|
86755
|
+
}
|
|
86756
|
+
if (!withinQtiCapacity(contract.sources, sourceCounts, source)) {
|
|
86757
|
+
return { kind: "source-capacity", identifier: source };
|
|
86758
|
+
}
|
|
86759
|
+
if (!withinQtiCapacity(contract.targets, targetCounts, target)) {
|
|
86760
|
+
return { kind: "target-capacity", identifier: target };
|
|
86761
|
+
}
|
|
86762
|
+
}
|
|
86763
|
+
return null;
|
|
86764
|
+
}
|
|
86716
86765
|
function usableQtiRegionGeometry(shape, coords) {
|
|
86717
86766
|
const arity2 = REGION_SHAPE_COORDS[shape];
|
|
86718
86767
|
if (!arity2) {
|
|
@@ -87177,6 +87226,17 @@ function interactionBody(item, node, type, declaration, attributes2) {
|
|
|
87177
87226
|
if (!attributes2.has("max-associations")) {
|
|
87178
87227
|
attributes2.set("max-associations", String(correctValueStrings(declaration).length || sources.length));
|
|
87179
87228
|
}
|
|
87229
|
+
const capacityIssue = qtiDirectedPairValidationIssue2(correctValueStrings(declaration), {
|
|
87230
|
+
sources: new Map(sources.map((choice) => [choice.identifier, 1])),
|
|
87231
|
+
targets: new Map(targets.map((choice) => [choice.identifier, 1]))
|
|
87232
|
+
});
|
|
87233
|
+
if (capacityIssue) {
|
|
87234
|
+
if (capacityIssue.kind === "invalid-pair") {
|
|
87235
|
+
compileError(`Match pair “${capacityIssue.value}” must name one declared source and target choice`);
|
|
87236
|
+
}
|
|
87237
|
+
const role = capacityIssue.kind === "source-capacity" ? "Source" : "Target";
|
|
87238
|
+
compileError(`${role} “${capacityIssue.identifier}” takes part in more associations than its match-max allows`);
|
|
87239
|
+
}
|
|
87180
87240
|
const sets = [sources, targets].map((choices) => `<qti-simple-match-set>${choices.map((choice) => choiceXml(item, "qti-simple-associable-choice", choice, {
|
|
87181
87241
|
"match-max": "1"
|
|
87182
87242
|
})).join("")}</qti-simple-match-set>`).join("");
|
|
@@ -87462,6 +87522,14 @@ function qtiAttributeNumber(attributes2, name3, fallback) {
|
|
|
87462
87522
|
const value = Number(raw);
|
|
87463
87523
|
return Number.isFinite(value) ? value : fallback;
|
|
87464
87524
|
}
|
|
87525
|
+
function qtiAttributeCapacity(attributes2, name3, fallback) {
|
|
87526
|
+
const raw = attributes2[name3]?.trim();
|
|
87527
|
+
if (!raw) {
|
|
87528
|
+
return fallback;
|
|
87529
|
+
}
|
|
87530
|
+
const value = Number(raw);
|
|
87531
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
87532
|
+
}
|
|
87465
87533
|
function qtiAttributeMaximum(attributes2, name3, fallback, unlimited) {
|
|
87466
87534
|
const maximum = qtiAttributeNumber(attributes2, name3, fallback);
|
|
87467
87535
|
return maximum === 0 ? unlimited : maximum;
|
|
@@ -87470,6 +87538,27 @@ function qtiAttributeBoolean(attributes2, name3) {
|
|
|
87470
87538
|
const value = attributes2[name3];
|
|
87471
87539
|
return value === "true" || value === "1";
|
|
87472
87540
|
}
|
|
87541
|
+
function gapContentIdentifiers(nodes) {
|
|
87542
|
+
return nodes.flatMap((node) => {
|
|
87543
|
+
if (node.kind === "gap") {
|
|
87544
|
+
return [node.identifier];
|
|
87545
|
+
}
|
|
87546
|
+
if (node.kind === "paragraph" || node.kind === "emphasis" || node.kind === "strong") {
|
|
87547
|
+
return gapContentIdentifiers(node.children);
|
|
87548
|
+
}
|
|
87549
|
+
if (node.kind === "list") {
|
|
87550
|
+
return node.items.flatMap(gapContentIdentifiers);
|
|
87551
|
+
}
|
|
87552
|
+
if (node.kind === "table") {
|
|
87553
|
+
return node.rows.flatMap((row) => row.cells.flatMap(gapContentIdentifiers));
|
|
87554
|
+
}
|
|
87555
|
+
return [];
|
|
87556
|
+
});
|
|
87557
|
+
}
|
|
87558
|
+
function qtiGapContentMatchesGaps(nodes, gaps) {
|
|
87559
|
+
const identifiers = gapContentIdentifiers(nodes);
|
|
87560
|
+
return identifiers.length === gaps.length && identifiers.every((identifier, index2) => identifier === gaps[index2]);
|
|
87561
|
+
}
|
|
87473
87562
|
function normalizedInteractionType(value) {
|
|
87474
87563
|
if (typeof value !== "string") {
|
|
87475
87564
|
return;
|
|
@@ -87480,6 +87569,9 @@ function normalizedInteractionType(value) {
|
|
|
87480
87569
|
function usableChoices(choices) {
|
|
87481
87570
|
return choices !== undefined && choices.length > 0 && choices.every((choice) => Boolean(choice.identifier));
|
|
87482
87571
|
}
|
|
87572
|
+
function usableAssociableChoices(choices) {
|
|
87573
|
+
return usableChoices(choices) && choices.every((choice) => qtiAttributeCapacity(choice.attributes, "match-max", 0) !== null);
|
|
87574
|
+
}
|
|
87483
87575
|
function transportableIdentifiers(identifiers) {
|
|
87484
87576
|
return identifiers.every((identifier) => Boolean(identifier) && !/\s/.test(identifier));
|
|
87485
87577
|
}
|
|
@@ -87552,7 +87644,7 @@ function authorableQtiInteractionProjection(interaction) {
|
|
|
87552
87644
|
}
|
|
87553
87645
|
case "match": {
|
|
87554
87646
|
const [sourceChoices, targetChoices] = interaction.choiceSets;
|
|
87555
|
-
if (interaction.choiceSets.length !== 2 || !
|
|
87647
|
+
if (interaction.choiceSets.length !== 2 || !usableAssociableChoices(sourceChoices) || !usableAssociableChoices(targetChoices) || !transportableIdentifiers([...sourceChoices, ...targetChoices].map((choice) => choice.identifier))) {
|
|
87556
87648
|
return null;
|
|
87557
87649
|
}
|
|
87558
87650
|
return {
|
|
@@ -87621,8 +87713,8 @@ function authorableQtiInteractionProjection(interaction) {
|
|
|
87621
87713
|
};
|
|
87622
87714
|
}
|
|
87623
87715
|
case "gap-match": {
|
|
87624
|
-
const { gapTexts, gaps, textWithGaps } = interaction;
|
|
87625
|
-
if (gapTexts.length === 0 || gaps.length === 0 || textWithGaps === undefined || !gapTexts.every((gapText) => Boolean(gapText.identifier && gapText.content)) || !transportableIdentifiers([...gapTexts.map((gapText) => gapText.identifier), ...gaps])) {
|
|
87716
|
+
const { gapTexts, gaps, textWithGaps, gapContent } = interaction;
|
|
87717
|
+
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])) {
|
|
87626
87718
|
return null;
|
|
87627
87719
|
}
|
|
87628
87720
|
return {
|
|
@@ -87631,6 +87723,7 @@ function authorableQtiInteractionProjection(interaction) {
|
|
|
87631
87723
|
gapTexts,
|
|
87632
87724
|
gaps,
|
|
87633
87725
|
textWithGaps,
|
|
87726
|
+
gapContent,
|
|
87634
87727
|
shuffle: qtiAttributeBoolean(attributes2, "shuffle")
|
|
87635
87728
|
};
|
|
87636
87729
|
}
|
|
@@ -87654,7 +87747,7 @@ function contributesScore(declaration) {
|
|
|
87654
87747
|
function responsePairs(left, right) {
|
|
87655
87748
|
return left.flatMap((first) => right.map((second) => `${first} ${second}`));
|
|
87656
87749
|
}
|
|
87657
|
-
function
|
|
87750
|
+
function interactionResponseSpace(item, declaration) {
|
|
87658
87751
|
const interaction = item.interactions.find((candidate) => candidate.responseIdentifier === declaration.identifier);
|
|
87659
87752
|
if (!interaction) {
|
|
87660
87753
|
return {};
|
|
@@ -87746,21 +87839,49 @@ function processesResponseMapping(item, declaration) {
|
|
|
87746
87839
|
}
|
|
87747
87840
|
return item.responseProcessing.elements.some((element) => element.tagName === "qti-map-response" && (element.attributes.identifier ?? "RESPONSE") === declaration.identifier);
|
|
87748
87841
|
}
|
|
87749
|
-
function responseMappingMaximum(item, declaration) {
|
|
87842
|
+
function responseMappingMaximum(item, declaration, space) {
|
|
87750
87843
|
if (!declaration.mapping || !processesResponseMapping(item, declaration)) {
|
|
87751
87844
|
return null;
|
|
87752
87845
|
}
|
|
87753
87846
|
return qtiResponseMappingMaximum({
|
|
87754
87847
|
cardinality: declaration.cardinality,
|
|
87755
87848
|
mapping: declaration.mapping,
|
|
87756
|
-
...
|
|
87849
|
+
...space ?? interactionResponseSpace(item, declaration)
|
|
87757
87850
|
});
|
|
87758
87851
|
}
|
|
87759
87852
|
function qtiItemScoringValidationMessage(item) {
|
|
87760
87853
|
for (const declaration of item.responseDeclarations) {
|
|
87854
|
+
const space = interactionResponseSpace(item, declaration);
|
|
87855
|
+
const { responseValues: responseValues2, maximumResponses } = space;
|
|
87856
|
+
if (responseValues2 && declaration.correctValues.some((value) => !responseValues2.includes(value))) {
|
|
87857
|
+
return `Response ${declaration.identifier} declares a correct value its interaction cannot produce.`;
|
|
87858
|
+
}
|
|
87859
|
+
const interaction = item.interactions.find((candidate) => candidate.responseIdentifier === declaration.identifier);
|
|
87860
|
+
if (interaction?.type === "match") {
|
|
87861
|
+
const [sources = [], targets = []] = interaction.choiceSets;
|
|
87862
|
+
const issue6 = qtiDirectedPairValidationIssue2(declaration.correctValues, {
|
|
87863
|
+
sources: new Map(sources.map((choice) => [
|
|
87864
|
+
choice.identifier,
|
|
87865
|
+
qtiAttributeNumber(choice.attributes, "match-max", 0)
|
|
87866
|
+
])),
|
|
87867
|
+
targets: new Map(targets.map((choice) => [
|
|
87868
|
+
choice.identifier,
|
|
87869
|
+
qtiAttributeNumber(choice.attributes, "match-max", 0)
|
|
87870
|
+
]))
|
|
87871
|
+
});
|
|
87872
|
+
if (issue6?.kind === "source-capacity") {
|
|
87873
|
+
return `Response ${declaration.identifier} declares correct pairs that exceed source ${issue6.identifier}'s match-max.`;
|
|
87874
|
+
}
|
|
87875
|
+
if (issue6?.kind === "target-capacity") {
|
|
87876
|
+
return `Response ${declaration.identifier} declares correct pairs that exceed target ${issue6.identifier}'s match-max.`;
|
|
87877
|
+
}
|
|
87878
|
+
}
|
|
87879
|
+
if (declaration.cardinality !== "single" && maximumResponses !== undefined && maximumResponses !== 0 && declaration.correctValues.length > maximumResponses) {
|
|
87880
|
+
return `Response ${declaration.identifier} declares more correct values than its interaction accepts.`;
|
|
87881
|
+
}
|
|
87761
87882
|
if (declaration.mapping) {
|
|
87762
87883
|
if (processesResponseMapping(item, declaration)) {
|
|
87763
|
-
if (responseMappingMaximum(item, declaration) === null) {
|
|
87884
|
+
if (responseMappingMaximum(item, declaration, space) === null) {
|
|
87764
87885
|
return `Response ${declaration.identifier} has a mapping with no reachable positive score.`;
|
|
87765
87886
|
}
|
|
87766
87887
|
} else if (declaration.correctValues.length === 0) {
|
|
@@ -88083,6 +88204,7 @@ var init_qti = __esm(() => {
|
|
|
88083
88204
|
init_fxp();
|
|
88084
88205
|
init_timeback3();
|
|
88085
88206
|
init_timeback3();
|
|
88207
|
+
init_timeback3();
|
|
88086
88208
|
EVENT_HANDLER_ATTRIBUTE = /^on/i;
|
|
88087
88209
|
SCRIPT_SCHEMES2 = {
|
|
88088
88210
|
javascript: ["java", "script:"].join(""),
|
|
@@ -90919,6 +91041,16 @@ function playableChoice(choice) {
|
|
|
90919
91041
|
...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
|
|
90920
91042
|
};
|
|
90921
91043
|
}
|
|
91044
|
+
function playableAssociableChoice(choice) {
|
|
91045
|
+
const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
|
|
91046
|
+
if (matchMax === null) {
|
|
91047
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
|
|
91048
|
+
}
|
|
91049
|
+
return {
|
|
91050
|
+
...playableChoice(choice),
|
|
91051
|
+
matchMax
|
|
91052
|
+
};
|
|
91053
|
+
}
|
|
90922
91054
|
function requireChoices2(choices) {
|
|
90923
91055
|
if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
|
|
90924
91056
|
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
|
|
@@ -91004,8 +91136,8 @@ function parseInteraction2(questionIdentifier, interaction) {
|
|
|
91004
91136
|
if (sets.length !== 2) {
|
|
91005
91137
|
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
|
|
91006
91138
|
}
|
|
91007
|
-
const sourceChoices = requireChoices2(sets[0].map(
|
|
91008
|
-
const targetChoices = requireChoices2(sets[1].map(
|
|
91139
|
+
const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
|
|
91140
|
+
const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
|
|
91009
91141
|
return {
|
|
91010
91142
|
type,
|
|
91011
91143
|
responseIdentifier,
|
|
@@ -91030,7 +91162,7 @@ function parseInteraction2(questionIdentifier, interaction) {
|
|
|
91030
91162
|
};
|
|
91031
91163
|
}
|
|
91032
91164
|
if (type === "gap-match") {
|
|
91033
|
-
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined) {
|
|
91165
|
+
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
|
|
91034
91166
|
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
|
|
91035
91167
|
}
|
|
91036
91168
|
return {
|
|
@@ -91038,6 +91170,7 @@ function parseInteraction2(questionIdentifier, interaction) {
|
|
|
91038
91170
|
responseIdentifier,
|
|
91039
91171
|
...prompt,
|
|
91040
91172
|
content: interaction.textWithGaps,
|
|
91173
|
+
contentNodes: interaction.gapContent,
|
|
91041
91174
|
gapTexts: interaction.gapTexts.map((gapText) => ({
|
|
91042
91175
|
identifier: gapText.identifier,
|
|
91043
91176
|
content: gapText.content,
|
|
@@ -93147,8 +93280,7 @@ function assertPlayableQtiQuestion(question) {
|
|
|
93147
93280
|
}
|
|
93148
93281
|
const item = parsedQtiItemOrNull(question.rawXml);
|
|
93149
93282
|
const playable = item && playableQtiInteractions(item);
|
|
93150
|
-
|
|
93151
|
-
if (!playable || scoringIssue) {
|
|
93283
|
+
if (!playable || qtiItemScoringValidationMessage(item)) {
|
|
93152
93284
|
throw new ValidationError(`Question ${question.identifier} uses an interaction Playcademy cannot play or score.`);
|
|
93153
93285
|
}
|
|
93154
93286
|
}
|
|
@@ -93401,31 +93533,38 @@ ${input.itemBody}
|
|
|
93401
93533
|
</qti-response-processing>
|
|
93402
93534
|
</qti-assessment-item>`;
|
|
93403
93535
|
}
|
|
93404
|
-
function
|
|
93405
|
-
|
|
93406
|
-
|
|
93536
|
+
function parseStructuredChoices(value, options) {
|
|
93537
|
+
const { label, minimum, validate: validate2 } = options;
|
|
93538
|
+
if (!Array.isArray(value) || value.length < minimum) {
|
|
93539
|
+
throw new ValidationError(`${label} questions require at least ${minimum === 1 ? "one choice" : `${minimum} choices`}`);
|
|
93407
93540
|
}
|
|
93408
93541
|
const choices = value.map((choice) => {
|
|
93409
93542
|
const record3 = recordValue(choice);
|
|
93410
93543
|
const identifier = record3?.identifier;
|
|
93411
93544
|
const content = record3?.content;
|
|
93412
|
-
if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim()) {
|
|
93413
|
-
throw new ValidationError(
|
|
93545
|
+
if (typeof identifier !== "string" || !identifier.trim() || typeof content !== "string" || !content.trim() || validate2 !== undefined && record3 !== undefined && !validate2(record3)) {
|
|
93546
|
+
throw new ValidationError(`${label} options are invalid`);
|
|
93414
93547
|
}
|
|
93415
93548
|
return { identifier: identifier.trim(), content: content.trim() };
|
|
93416
93549
|
});
|
|
93417
93550
|
if (new Set(choices.map((choice) => choice.identifier)).size !== choices.length) {
|
|
93418
|
-
throw new ValidationError(
|
|
93551
|
+
throw new ValidationError(`${label} option identifiers must be unique`);
|
|
93419
93552
|
}
|
|
93420
93553
|
return choices;
|
|
93421
93554
|
}
|
|
93422
|
-
function
|
|
93555
|
+
function declaredCorrectResponse(input, responseIdentifier) {
|
|
93423
93556
|
const declarations = Array.isArray(input.responseDeclarations) ? input.responseDeclarations : [];
|
|
93424
93557
|
const declaration = declarations.map(recordValue).find((candidate) => candidate?.identifier === responseIdentifier);
|
|
93425
|
-
const
|
|
93426
|
-
|
|
93427
|
-
|
|
93428
|
-
|
|
93558
|
+
const rawValues = recordValue(declaration?.correctResponse)?.value;
|
|
93559
|
+
return {
|
|
93560
|
+
cardinality: declaration?.cardinality,
|
|
93561
|
+
baseType: declaration?.baseType,
|
|
93562
|
+
values: Array.isArray(rawValues) ? rawValues.filter((value) => typeof value === "string") : []
|
|
93563
|
+
};
|
|
93564
|
+
}
|
|
93565
|
+
function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers) {
|
|
93566
|
+
const { cardinality, baseType, values } = declaredCorrectResponse(input, responseIdentifier);
|
|
93567
|
+
if (cardinality !== "single" || baseType !== "identifier") {
|
|
93429
93568
|
throw new ValidationError("Inline-choice questions require a single-identifier response declaration");
|
|
93430
93569
|
}
|
|
93431
93570
|
if (values.length !== 1 || !choiceIdentifiers.has(values[0])) {
|
|
@@ -93433,27 +93572,41 @@ function inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifi
|
|
|
93433
93572
|
}
|
|
93434
93573
|
return values[0];
|
|
93435
93574
|
}
|
|
93436
|
-
function
|
|
93575
|
+
function structuredQuestionEnvelope(input, type, label, title) {
|
|
93437
93576
|
const interaction = recordValue(input.interaction);
|
|
93438
|
-
|
|
93439
|
-
if (interactionType !== "inline-choice") {
|
|
93577
|
+
if (normalizedQtiInteractionType(interaction?.type ?? input.type) !== type) {
|
|
93440
93578
|
return null;
|
|
93441
93579
|
}
|
|
93442
|
-
const responseIdentifier = interaction?.responseIdentifier;
|
|
93443
93580
|
const structure = recordValue(interaction?.questionStructure);
|
|
93581
|
+
const responseIdentifier = interaction?.responseIdentifier;
|
|
93444
93582
|
const prompt = structure?.prompt;
|
|
93445
|
-
if (!structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
|
|
93446
|
-
throw new ValidationError(
|
|
93583
|
+
if (!interaction || !structure || typeof responseIdentifier !== "string" || !responseIdentifier.trim()) {
|
|
93584
|
+
throw new ValidationError(`${label} question data is invalid`);
|
|
93447
93585
|
}
|
|
93448
93586
|
if (!title || typeof prompt !== "string" || !prompt.trim()) {
|
|
93449
|
-
throw new ValidationError(
|
|
93587
|
+
throw new ValidationError(`${label} questions require a title and prompt`);
|
|
93588
|
+
}
|
|
93589
|
+
return {
|
|
93590
|
+
interaction,
|
|
93591
|
+
structure,
|
|
93592
|
+
responseIdentifier: responseIdentifier.trim(),
|
|
93593
|
+
prompt: prompt.trim()
|
|
93594
|
+
};
|
|
93595
|
+
}
|
|
93596
|
+
function buildInlineChoiceQuestionXml(input, identifier, title) {
|
|
93597
|
+
const envelope = structuredQuestionEnvelope(input, "inline-choice", "Inline-choice", title);
|
|
93598
|
+
if (!envelope) {
|
|
93599
|
+
return null;
|
|
93450
93600
|
}
|
|
93451
|
-
const
|
|
93452
|
-
const promptParts =
|
|
93601
|
+
const { structure, responseIdentifier, prompt } = envelope;
|
|
93602
|
+
const promptParts = prompt.split(INLINE_CHOICE_BLANK);
|
|
93453
93603
|
if (promptParts.length !== 2) {
|
|
93454
93604
|
throw new ValidationError("Inline-choice questions require exactly one blank");
|
|
93455
93605
|
}
|
|
93456
|
-
const choices =
|
|
93606
|
+
const choices = parseStructuredChoices(structure.inlineChoices, {
|
|
93607
|
+
label: "Inline-choice",
|
|
93608
|
+
minimum: 2
|
|
93609
|
+
});
|
|
93457
93610
|
const choiceIdentifiers = new Set(choices.map((choice) => choice.identifier));
|
|
93458
93611
|
const correctIdentifier = inlineChoiceCorrectIdentifier(input, responseIdentifier, choiceIdentifiers);
|
|
93459
93612
|
const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
|
|
@@ -93471,6 +93624,86 @@ ${choicesXml}
|
|
|
93471
93624
|
itemBody
|
|
93472
93625
|
});
|
|
93473
93626
|
}
|
|
93627
|
+
function parseStructuredMatchChoices(value, label) {
|
|
93628
|
+
return parseStructuredChoices(value, {
|
|
93629
|
+
label: `Match ${label}`,
|
|
93630
|
+
minimum: 1,
|
|
93631
|
+
validate: (choice) => choice.matchMax === 1
|
|
93632
|
+
});
|
|
93633
|
+
}
|
|
93634
|
+
function structuredMatchCorrectPairs(input, responseIdentifier, sources, targets) {
|
|
93635
|
+
const { cardinality, baseType, values } = declaredCorrectResponse(input, responseIdentifier);
|
|
93636
|
+
if (cardinality !== "multiple" || baseType !== "directedPair") {
|
|
93637
|
+
throw new ValidationError("Match questions require a multiple directed-pair response declaration");
|
|
93638
|
+
}
|
|
93639
|
+
const sourceIdentifiers = new Set(sources.map((choice) => choice.identifier));
|
|
93640
|
+
const targetIdentifiers = new Set(targets.map((choice) => choice.identifier));
|
|
93641
|
+
const valid = values.length > 0 && new Set(values).size === values.length && values.every((value) => {
|
|
93642
|
+
const [source, target, extra] = value.split(" ");
|
|
93643
|
+
return extra === undefined && Boolean(source && target) && sourceIdentifiers.has(source) && targetIdentifiers.has(target);
|
|
93644
|
+
});
|
|
93645
|
+
if (!valid) {
|
|
93646
|
+
throw new ValidationError("Every Match correct response must name one declared source and target choice");
|
|
93647
|
+
}
|
|
93648
|
+
return values;
|
|
93649
|
+
}
|
|
93650
|
+
function buildMatchQuestionXml(input, identifier, title) {
|
|
93651
|
+
const envelope = structuredQuestionEnvelope(input, "match", "Match", title);
|
|
93652
|
+
if (!envelope) {
|
|
93653
|
+
return null;
|
|
93654
|
+
}
|
|
93655
|
+
const { interaction, structure, responseIdentifier, prompt } = envelope;
|
|
93656
|
+
const sources = parseStructuredMatchChoices(structure.sourceChoices, "source");
|
|
93657
|
+
const targets = parseStructuredMatchChoices(structure.targetChoices, "target");
|
|
93658
|
+
const sourceIdentifiers = new Set(sources.map((choice) => choice.identifier));
|
|
93659
|
+
if (targets.some((choice) => sourceIdentifiers.has(choice.identifier))) {
|
|
93660
|
+
throw new ValidationError("Match source and target choice identifiers must be unique");
|
|
93661
|
+
}
|
|
93662
|
+
const correctValues = structuredMatchCorrectPairs(input, responseIdentifier, sources, targets);
|
|
93663
|
+
const maxAssociations = interaction.maxAssociations;
|
|
93664
|
+
if (typeof maxAssociations !== "number" || !Number.isInteger(maxAssociations) || maxAssociations < 0 || maxAssociations !== 0 && maxAssociations < correctValues.length) {
|
|
93665
|
+
throw new ValidationError("Match max associations must fit every correct pair");
|
|
93666
|
+
}
|
|
93667
|
+
const item = {
|
|
93668
|
+
key: identifier,
|
|
93669
|
+
title,
|
|
93670
|
+
body: [
|
|
93671
|
+
{
|
|
93672
|
+
kind: "interaction",
|
|
93673
|
+
type: "match",
|
|
93674
|
+
responseIdentifier,
|
|
93675
|
+
attributes: {
|
|
93676
|
+
shuffle: interaction.shuffle === true,
|
|
93677
|
+
"max-associations": maxAssociations
|
|
93678
|
+
},
|
|
93679
|
+
content: [
|
|
93680
|
+
{
|
|
93681
|
+
kind: "element",
|
|
93682
|
+
name: "qti-prompt",
|
|
93683
|
+
children: [{ kind: "text", text: prompt }]
|
|
93684
|
+
}
|
|
93685
|
+
],
|
|
93686
|
+
choiceSets: [sources, targets].map((choices) => choices.map((choice) => ({
|
|
93687
|
+
identifier: choice.identifier,
|
|
93688
|
+
content: [{ kind: "text", text: choice.content }]
|
|
93689
|
+
})))
|
|
93690
|
+
}
|
|
93691
|
+
],
|
|
93692
|
+
responseDeclarations: [
|
|
93693
|
+
{
|
|
93694
|
+
identifier: responseIdentifier,
|
|
93695
|
+
cardinality: "multiple",
|
|
93696
|
+
baseType: "directedPair",
|
|
93697
|
+
correctValues
|
|
93698
|
+
}
|
|
93699
|
+
]
|
|
93700
|
+
};
|
|
93701
|
+
try {
|
|
93702
|
+
return compileQtiAuthoringItemXml(item, { identifier, title });
|
|
93703
|
+
} catch (error88) {
|
|
93704
|
+
throw new ValidationError(`This Match question cannot be compiled to QTI: ${errorMessage(error88)}`);
|
|
93705
|
+
}
|
|
93706
|
+
}
|
|
93474
93707
|
function parseHottextSegments2(value) {
|
|
93475
93708
|
if (!Array.isArray(value) || value.length === 0) {
|
|
93476
93709
|
throw new ValidationError("Hottext questions require passage segments");
|
|
@@ -93772,7 +94005,8 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
93772
94005
|
buildNumericQuestionXml,
|
|
93773
94006
|
buildAuthoringDocumentQuestionXml,
|
|
93774
94007
|
buildHottextQuestionXml,
|
|
93775
|
-
buildInlineChoiceQuestionXml
|
|
94008
|
+
buildInlineChoiceQuestionXml,
|
|
94009
|
+
buildMatchQuestionXml
|
|
93776
94010
|
];
|
|
93777
94011
|
});
|
|
93778
94012
|
|