@longsightgroup/qti3-core 0.6.0 → 0.7.1
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/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.js +13 -1
- package/dist/parser.js.map +1 -1
- package/dist/shared-vocabulary-coverage-policy.d.ts +10 -0
- package/dist/shared-vocabulary-coverage-policy.d.ts.map +1 -0
- package/dist/shared-vocabulary-coverage-policy.js +61 -0
- package/dist/shared-vocabulary-coverage-policy.js.map +1 -0
- package/dist/shared-vocabulary-generated-families.d.ts +21 -0
- package/dist/shared-vocabulary-generated-families.d.ts.map +1 -0
- package/dist/shared-vocabulary-generated-families.js +194 -0
- package/dist/shared-vocabulary-generated-families.js.map +1 -0
- package/dist/shared-vocabulary-levels.d.ts +4 -0
- package/dist/shared-vocabulary-levels.d.ts.map +1 -0
- package/dist/shared-vocabulary-levels.js +4 -0
- package/dist/shared-vocabulary-levels.js.map +1 -0
- package/dist/shared-vocabulary-support.d.ts +3 -0
- package/dist/shared-vocabulary-support.d.ts.map +1 -0
- package/dist/shared-vocabulary-support.js +208 -0
- package/dist/shared-vocabulary-support.js.map +1 -0
- package/dist/shared-vocabulary-validation.d.ts +40 -0
- package/dist/shared-vocabulary-validation.d.ts.map +1 -0
- package/dist/shared-vocabulary-validation.js +108 -0
- package/dist/shared-vocabulary-validation.js.map +1 -0
- package/dist/shared-vocabulary.d.ts +35 -0
- package/dist/shared-vocabulary.d.ts.map +1 -0
- package/dist/shared-vocabulary.js +128 -0
- package/dist/shared-vocabulary.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +364 -0
- package/dist/validation.js.map +1 -1
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +100 -0
- package/dist/xml.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +56 -0
- package/src/parser.ts +17 -1
- package/src/shared-vocabulary-coverage-policy.ts +79 -0
- package/src/shared-vocabulary-generated-families.ts +252 -0
- package/src/shared-vocabulary-levels.ts +7 -0
- package/src/shared-vocabulary-support.ts +340 -0
- package/src/shared-vocabulary-validation.ts +182 -0
- package/src/shared-vocabulary.ts +177 -0
- package/src/types.ts +11 -0
- package/src/validation.ts +466 -0
- package/src/xml.ts +107 -0
package/dist/validation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateSharedVocabularyExtendedText, validateSharedVocabularyInputWidth, validateSharedVocabularyMediaPlayerControls, } from "./shared-vocabulary-validation.js";
|
|
1
2
|
import { validateQtiDataSsmlMetadata } from "./tts.js";
|
|
2
3
|
import { qtiValueToStringList } from "./value-format.js";
|
|
3
4
|
const BUILT_IN_COMPLETION_STATUS = "completionStatus";
|
|
@@ -7,6 +8,7 @@ export function validateAssessmentItem(document) {
|
|
|
7
8
|
requireIdentifier("qti-assessment-item", item.identifier, diagnostics, item.source);
|
|
8
9
|
validateAssessmentItemRoot(item, diagnostics);
|
|
9
10
|
validateItemBody(item, diagnostics);
|
|
11
|
+
validateItemBodySharedVocabulary(item, diagnostics);
|
|
10
12
|
validateDeclarationIdentifiers(item, diagnostics);
|
|
11
13
|
validateOutcomeLookupTables(item, diagnostics);
|
|
12
14
|
validateInteractions(item, diagnostics);
|
|
@@ -152,6 +154,64 @@ function validateItemBody(item, diagnostics) {
|
|
|
152
154
|
source: item.source,
|
|
153
155
|
});
|
|
154
156
|
}
|
|
157
|
+
function validateItemBodySharedVocabulary(item, diagnostics) {
|
|
158
|
+
validateContentSharedVocabulary(item.body, diagnostics);
|
|
159
|
+
}
|
|
160
|
+
function validateContentSharedVocabulary(nodes, diagnostics) {
|
|
161
|
+
for (const node of nodes) {
|
|
162
|
+
if (node.kind !== "element")
|
|
163
|
+
continue;
|
|
164
|
+
validateLayoutVocabularyClasses(node, diagnostics);
|
|
165
|
+
if (sharedClassNames(node.attributes).includes("qti-layout-row")) {
|
|
166
|
+
validateLayoutRow(node, diagnostics);
|
|
167
|
+
}
|
|
168
|
+
validateContentSharedVocabulary(node.children, diagnostics);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function validateLayoutVocabularyClasses(node, diagnostics) {
|
|
172
|
+
const classNames = sharedClassNames(node.attributes);
|
|
173
|
+
for (const className of classNames) {
|
|
174
|
+
if (isLayoutColumnClassName(className) && layoutColumnValue(className) === undefined) {
|
|
175
|
+
diagnostics.push({
|
|
176
|
+
code: "item.sharedVocabulary.layoutColumnInvalid",
|
|
177
|
+
severity: "warning",
|
|
178
|
+
message: `Shared vocabulary class ${className} is not supported; expected qti-layout-col1 through qti-layout-col12, or dashed qti-layout-col-1 through qti-layout-col-12.`,
|
|
179
|
+
path: node.source?.path,
|
|
180
|
+
source: node.source,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (isLayoutOffsetClassName(className) && layoutOffsetValue(className) === undefined) {
|
|
184
|
+
diagnostics.push({
|
|
185
|
+
code: "item.sharedVocabulary.layoutOffsetInvalid",
|
|
186
|
+
severity: "warning",
|
|
187
|
+
message: `Shared vocabulary class ${className} is not supported; expected qti-layout-offset1 through qti-layout-offset11, or dashed qti-layout-offset-1 through qti-layout-offset-11.`,
|
|
188
|
+
path: node.source?.path,
|
|
189
|
+
source: node.source,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function validateLayoutRow(node, diagnostics) {
|
|
195
|
+
let totalColumns = 0;
|
|
196
|
+
for (const child of node.children) {
|
|
197
|
+
if (child.kind !== "element")
|
|
198
|
+
continue;
|
|
199
|
+
const classNames = sharedClassNames(child.attributes);
|
|
200
|
+
const column = firstLayoutColumnValue(classNames);
|
|
201
|
+
if (column === undefined)
|
|
202
|
+
continue;
|
|
203
|
+
totalColumns += (firstLayoutOffsetValue(classNames) ?? 0) + column;
|
|
204
|
+
}
|
|
205
|
+
if (totalColumns <= 12)
|
|
206
|
+
return;
|
|
207
|
+
diagnostics.push({
|
|
208
|
+
code: "item.sharedVocabulary.layoutRowOverflow",
|
|
209
|
+
severity: "warning",
|
|
210
|
+
message: `qti-layout-row column groupings plus offsets total ${totalColumns}; the QTI shared vocabulary grid allows at most twelve columns per row.`,
|
|
211
|
+
path: node.source?.path,
|
|
212
|
+
source: node.source,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
155
215
|
function requireIdentifier(elementName, identifier, diagnostics, source) {
|
|
156
216
|
if (!identifier || identifier.trim().length === 0) {
|
|
157
217
|
diagnostics.push({
|
|
@@ -1275,6 +1335,7 @@ function validateInteractions(item, diagnostics) {
|
|
|
1275
1335
|
for (const interaction of item.interactions) {
|
|
1276
1336
|
validateInteractionResponseReference(interaction, responseIdentifiers, diagnostics);
|
|
1277
1337
|
validateInteractionResponseShape(interaction, diagnostics);
|
|
1338
|
+
validateInteractionSharedVocabulary(interaction, diagnostics);
|
|
1278
1339
|
validateInteractionChoices(interaction, diagnostics);
|
|
1279
1340
|
validateInteractionChildren(interaction, diagnostics);
|
|
1280
1341
|
validateInteractionRequiredAttributes(interaction, diagnostics);
|
|
@@ -1289,6 +1350,297 @@ function validateInteractions(item, diagnostics) {
|
|
|
1289
1350
|
: undefined, diagnostics);
|
|
1290
1351
|
}
|
|
1291
1352
|
}
|
|
1353
|
+
function validateInteractionSharedVocabulary(interaction, diagnostics) {
|
|
1354
|
+
if (interaction.type !== "choice" &&
|
|
1355
|
+
interaction.type !== "match" &&
|
|
1356
|
+
interaction.type !== "gapMatch" &&
|
|
1357
|
+
interaction.type !== "graphicGapMatch" &&
|
|
1358
|
+
interaction.type !== "inlineChoice" &&
|
|
1359
|
+
interaction.type !== "textEntry" &&
|
|
1360
|
+
interaction.type !== "extendedText" &&
|
|
1361
|
+
interaction.type !== "media" &&
|
|
1362
|
+
interaction.type !== "order") {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const classNames = sharedClassNames(interaction.attributes);
|
|
1366
|
+
const classNameSet = new Set(classNames);
|
|
1367
|
+
if (interaction.type === "media") {
|
|
1368
|
+
validateMediaInteractionSharedVocabulary(interaction, diagnostics);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (interaction.type === "inlineChoice" || interaction.type === "textEntry") {
|
|
1372
|
+
validateInteractionInputWidthSharedVocabulary(interaction, classNames, diagnostics);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (interaction.type === "extendedText") {
|
|
1376
|
+
diagnostics.push(...validateSharedVocabularyExtendedText({
|
|
1377
|
+
classNames,
|
|
1378
|
+
subjectQtiName: interaction.qtiName,
|
|
1379
|
+
path: interaction.source?.path,
|
|
1380
|
+
source: interaction.source,
|
|
1381
|
+
}));
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (interaction.type === "choice" || interaction.type === "order") {
|
|
1385
|
+
validateSharedVocabularyLabelClasses(interaction, classNames, diagnostics);
|
|
1386
|
+
}
|
|
1387
|
+
if (interaction.type === "match" ||
|
|
1388
|
+
interaction.type === "gapMatch" ||
|
|
1389
|
+
interaction.type === "graphicGapMatch") {
|
|
1390
|
+
if (interaction.type === "match") {
|
|
1391
|
+
validateMatchInteractionSharedVocabulary(interaction, classNames, diagnostics);
|
|
1392
|
+
}
|
|
1393
|
+
validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics);
|
|
1394
|
+
validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics);
|
|
1395
|
+
if (interaction.type === "gapMatch") {
|
|
1396
|
+
validateGapInputWidthSharedVocabulary(interaction, diagnostics);
|
|
1397
|
+
}
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
if (interaction.type === "order") {
|
|
1401
|
+
validateOrderInteractionSharedVocabulary(interaction, classNames, classNameSet, diagnostics);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics);
|
|
1405
|
+
const validStackingClasses = new Set();
|
|
1406
|
+
const invalidStackingClasses = new Set();
|
|
1407
|
+
for (const className of classNames) {
|
|
1408
|
+
const stacking = /^qti-choices-stacking-(\d+)$/.exec(className)?.[1];
|
|
1409
|
+
if (stacking === undefined)
|
|
1410
|
+
continue;
|
|
1411
|
+
const count = Number(stacking);
|
|
1412
|
+
if (count >= 1 && count <= 5)
|
|
1413
|
+
validStackingClasses.add(className);
|
|
1414
|
+
else
|
|
1415
|
+
invalidStackingClasses.add(className);
|
|
1416
|
+
}
|
|
1417
|
+
if (validStackingClasses.size > 1) {
|
|
1418
|
+
diagnostics.push({
|
|
1419
|
+
code: "interaction.sharedVocabulary.stackingConflict",
|
|
1420
|
+
severity: "warning",
|
|
1421
|
+
message: `qti-choice-interaction should not include multiple qti-choices-stacking-* classes: ${[...validStackingClasses].join(", ")}. The first valid stacking class in class attribute order takes precedence at runtime.`,
|
|
1422
|
+
path: interaction.source?.path,
|
|
1423
|
+
source: interaction.source,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
for (const className of invalidStackingClasses) {
|
|
1427
|
+
diagnostics.push({
|
|
1428
|
+
code: "interaction.sharedVocabulary.stackingInvalid",
|
|
1429
|
+
severity: "warning",
|
|
1430
|
+
message: `qti-choice-interaction shared vocabulary class ${className} is not supported; expected qti-choices-stacking-1 through qti-choices-stacking-5.`,
|
|
1431
|
+
path: interaction.source?.path,
|
|
1432
|
+
source: interaction.source,
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function validateSharedVocabularyLabelClasses(interaction, classNames, diagnostics) {
|
|
1437
|
+
const labelClasses = classNames.filter((className) => [
|
|
1438
|
+
"qti-labels-decimal",
|
|
1439
|
+
"qti-labels-cjk-ideographic",
|
|
1440
|
+
"qti-labels-lower-alpha",
|
|
1441
|
+
"qti-labels-upper-alpha",
|
|
1442
|
+
].includes(className));
|
|
1443
|
+
if (new Set(labelClasses).size > 1) {
|
|
1444
|
+
diagnostics.push({
|
|
1445
|
+
code: "interaction.sharedVocabulary.labelsConflict",
|
|
1446
|
+
severity: "warning",
|
|
1447
|
+
message: `${interaction.qtiName} should not include multiple qti-labels-* style classes: ${[...new Set(labelClasses)].join(", ")}. qti-labels-decimal takes precedence over qti-labels-cjk-ideographic, then qti-labels-lower-alpha, then qti-labels-upper-alpha at runtime.`,
|
|
1448
|
+
path: interaction.source?.path,
|
|
1449
|
+
source: interaction.source,
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
const suffixClasses = classNames.filter((className) => [
|
|
1453
|
+
"qti-labels-suffix-none",
|
|
1454
|
+
"qti-labels-suffix-period",
|
|
1455
|
+
"qti-labels-suffix-parenthesis",
|
|
1456
|
+
].includes(className));
|
|
1457
|
+
if (new Set(suffixClasses).size <= 1)
|
|
1458
|
+
return;
|
|
1459
|
+
diagnostics.push({
|
|
1460
|
+
code: "interaction.sharedVocabulary.labelSuffixConflict",
|
|
1461
|
+
severity: "warning",
|
|
1462
|
+
message: `${interaction.qtiName} should not include multiple qti-labels-suffix-* classes: ${[...new Set(suffixClasses)].join(", ")}. qti-labels-suffix-none takes precedence over qti-labels-suffix-period, then qti-labels-suffix-parenthesis at runtime.`,
|
|
1463
|
+
path: interaction.source?.path,
|
|
1464
|
+
source: interaction.source,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
function validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics) {
|
|
1468
|
+
if (!classNameSet.has("qti-orientation-horizontal") ||
|
|
1469
|
+
!classNameSet.has("qti-orientation-vertical")) {
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
diagnostics.push({
|
|
1473
|
+
code: "interaction.sharedVocabulary.orientationConflict",
|
|
1474
|
+
severity: "warning",
|
|
1475
|
+
message: `${interaction.qtiName} should not include both qti-orientation-horizontal and qti-orientation-vertical; qti-orientation-horizontal takes precedence at runtime.`,
|
|
1476
|
+
path: interaction.source?.path,
|
|
1477
|
+
source: interaction.source,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
function validateOrderInteractionSharedVocabulary(interaction, classNames, classNameSet, diagnostics) {
|
|
1481
|
+
validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics);
|
|
1482
|
+
validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics);
|
|
1483
|
+
validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics);
|
|
1484
|
+
}
|
|
1485
|
+
const SHARED_VOCABULARY_CHOICES_POSITION_CLASSES = [
|
|
1486
|
+
"qti-choices-top",
|
|
1487
|
+
"qti-choices-bottom",
|
|
1488
|
+
"qti-choices-left",
|
|
1489
|
+
"qti-choices-right",
|
|
1490
|
+
];
|
|
1491
|
+
function validateMatchInteractionSharedVocabulary(interaction, classNames, diagnostics) {
|
|
1492
|
+
const hasTabular = classNames.includes("qti-match-tabular");
|
|
1493
|
+
const hasHeaderHidden = classNames.includes("qti-header-hidden");
|
|
1494
|
+
const firstColumnHeader = interaction.attributes["data-first-column-header"];
|
|
1495
|
+
if (!hasTabular && (hasHeaderHidden || firstColumnHeader !== undefined)) {
|
|
1496
|
+
diagnostics.push({
|
|
1497
|
+
code: "interaction.sharedVocabulary.matchTabularContext",
|
|
1498
|
+
severity: "warning",
|
|
1499
|
+
message: "qti-match-interaction shared vocabulary class qti-header-hidden and data-first-column-header are only relevant when qti-match-tabular is specified; they are ignored at runtime.",
|
|
1500
|
+
path: interaction.source?.path,
|
|
1501
|
+
source: interaction.source,
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
if (hasTabular && hasHeaderHidden && firstColumnHeader !== undefined) {
|
|
1505
|
+
diagnostics.push({
|
|
1506
|
+
code: "interaction.sharedVocabulary.matchTabularHeaderHidden",
|
|
1507
|
+
severity: "warning",
|
|
1508
|
+
message: "qti-match-interaction data-first-column-header is ignored when qti-header-hidden suppresses the tabular header row.",
|
|
1509
|
+
path: interaction.source?.path,
|
|
1510
|
+
source: interaction.source,
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
if (!hasTabular)
|
|
1514
|
+
return;
|
|
1515
|
+
const choicesPositionClasses = classNames.filter((className) => SHARED_VOCABULARY_CHOICES_POSITION_CLASSES.includes(className));
|
|
1516
|
+
if (choicesPositionClasses.length > 0 ||
|
|
1517
|
+
interaction.attributes["data-choices-container-width"] !== undefined) {
|
|
1518
|
+
diagnostics.push({
|
|
1519
|
+
code: "interaction.sharedVocabulary.matchTabularChoicesConflict",
|
|
1520
|
+
severity: "warning",
|
|
1521
|
+
message: "qti-match-interaction qti-match-tabular uses a matrix layout; qti-choices-* position classes and data-choices-container-width are ignored at runtime.",
|
|
1522
|
+
path: interaction.source?.path,
|
|
1523
|
+
source: interaction.source,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
if (!hasHeaderHidden && (firstColumnHeader === undefined || firstColumnHeader === "")) {
|
|
1527
|
+
diagnostics.push({
|
|
1528
|
+
code: "interaction.sharedVocabulary.matchTabularFirstColumnHeader",
|
|
1529
|
+
severity: "warning",
|
|
1530
|
+
message: "qti-match-interaction with qti-match-tabular should specify data-first-column-header for the top-left table header when the tabular header row is shown.",
|
|
1531
|
+
path: interaction.source?.path,
|
|
1532
|
+
source: interaction.source,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics) {
|
|
1537
|
+
const choicesPositionClasses = classNames.filter((className) => SHARED_VOCABULARY_CHOICES_POSITION_CLASSES.includes(className));
|
|
1538
|
+
if (new Set(choicesPositionClasses).size > 1) {
|
|
1539
|
+
diagnostics.push({
|
|
1540
|
+
code: "interaction.sharedVocabulary.orderChoicesPositionConflict",
|
|
1541
|
+
severity: "warning",
|
|
1542
|
+
message: `${interaction.qtiName} should not include multiple qti-choices-* position classes: ${[...new Set(choicesPositionClasses)].join(", ")}. The first position class in class attribute order takes precedence at runtime.`,
|
|
1543
|
+
path: interaction.source?.path,
|
|
1544
|
+
source: interaction.source,
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics) {
|
|
1549
|
+
const width = interaction.attributes["data-choices-container-width"];
|
|
1550
|
+
if (width === undefined)
|
|
1551
|
+
return;
|
|
1552
|
+
const parsed = Number(width);
|
|
1553
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1554
|
+
diagnostics.push({
|
|
1555
|
+
code: "interaction.sharedVocabulary.orderChoicesContainerWidth",
|
|
1556
|
+
severity: "warning",
|
|
1557
|
+
message: `${interaction.qtiName} data-choices-container-width must be a positive pixel value; the invalid value is ignored at runtime.`,
|
|
1558
|
+
path: interaction.source?.path,
|
|
1559
|
+
source: interaction.source,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
function validateGapInputWidthSharedVocabulary(interaction, diagnostics) {
|
|
1564
|
+
for (const gap of interaction.choices.filter((choice) => choice.qtiName === "qti-gap")) {
|
|
1565
|
+
diagnostics.push(...validateSharedVocabularyInputWidth({
|
|
1566
|
+
classNames: sharedClassNames(gap.attributes),
|
|
1567
|
+
subjectQtiName: "qti-gap",
|
|
1568
|
+
path: gap.source?.path ?? interaction.source?.path,
|
|
1569
|
+
source: gap.source ?? interaction.source,
|
|
1570
|
+
conflictCode: "interaction.sharedVocabulary.gapInputWidthConflict",
|
|
1571
|
+
invalidCode: "interaction.sharedVocabulary.gapInputWidthInvalid",
|
|
1572
|
+
}));
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function validateInteractionInputWidthSharedVocabulary(interaction, classNames, diagnostics) {
|
|
1576
|
+
diagnostics.push(...validateSharedVocabularyInputWidth({
|
|
1577
|
+
classNames,
|
|
1578
|
+
subjectQtiName: interaction.qtiName,
|
|
1579
|
+
path: interaction.source?.path,
|
|
1580
|
+
source: interaction.source,
|
|
1581
|
+
conflictCode: "interaction.sharedVocabulary.inputWidthConflict",
|
|
1582
|
+
invalidCode: "interaction.sharedVocabulary.inputWidthInvalid",
|
|
1583
|
+
}));
|
|
1584
|
+
}
|
|
1585
|
+
function validateMediaInteractionSharedVocabulary(interaction, diagnostics) {
|
|
1586
|
+
diagnostics.push(...validateSharedVocabularyMediaPlayerControls({
|
|
1587
|
+
value: interaction.attributes["data-qti-media-player-controls"],
|
|
1588
|
+
subjectQtiName: interaction.qtiName,
|
|
1589
|
+
path: interaction.source?.path,
|
|
1590
|
+
source: interaction.source,
|
|
1591
|
+
}));
|
|
1592
|
+
if (!interaction.object)
|
|
1593
|
+
return;
|
|
1594
|
+
diagnostics.push(...validateSharedVocabularyMediaPlayerControls({
|
|
1595
|
+
value: interaction.object.attributes["data-qti-media-player-controls"],
|
|
1596
|
+
subjectQtiName: `${interaction.qtiName} media object`,
|
|
1597
|
+
path: interaction.object.source?.path ?? interaction.source?.path,
|
|
1598
|
+
source: interaction.object.source ?? interaction.source,
|
|
1599
|
+
}));
|
|
1600
|
+
}
|
|
1601
|
+
function sharedClassNames(attributes) {
|
|
1602
|
+
return (attributes.class ?? "").split(/\s+/).filter(Boolean);
|
|
1603
|
+
}
|
|
1604
|
+
function isLayoutColumnClassName(className) {
|
|
1605
|
+
return /^qti-layout-col-?\w+$/.test(className);
|
|
1606
|
+
}
|
|
1607
|
+
function layoutColumnValue(className) {
|
|
1608
|
+
const rawValue = /^qti-layout-col-?(\d+)$/.exec(className)?.[1];
|
|
1609
|
+
if (rawValue === undefined)
|
|
1610
|
+
return undefined;
|
|
1611
|
+
const value = Number(rawValue);
|
|
1612
|
+
if (value < 1 || value > 12)
|
|
1613
|
+
return undefined;
|
|
1614
|
+
return value;
|
|
1615
|
+
}
|
|
1616
|
+
function firstLayoutColumnValue(classNames) {
|
|
1617
|
+
for (const className of classNames) {
|
|
1618
|
+
const value = layoutColumnValue(className);
|
|
1619
|
+
if (value !== undefined)
|
|
1620
|
+
return value;
|
|
1621
|
+
}
|
|
1622
|
+
return undefined;
|
|
1623
|
+
}
|
|
1624
|
+
function isLayoutOffsetClassName(className) {
|
|
1625
|
+
return /^qti-layout-offset-?\w+$/.test(className);
|
|
1626
|
+
}
|
|
1627
|
+
function layoutOffsetValue(className) {
|
|
1628
|
+
const rawValue = /^qti-layout-offset-?(\d+)$/.exec(className)?.[1];
|
|
1629
|
+
if (rawValue === undefined)
|
|
1630
|
+
return undefined;
|
|
1631
|
+
const value = Number(rawValue);
|
|
1632
|
+
if (value < 1 || value > 11)
|
|
1633
|
+
return undefined;
|
|
1634
|
+
return value;
|
|
1635
|
+
}
|
|
1636
|
+
function firstLayoutOffsetValue(classNames) {
|
|
1637
|
+
for (const className of classNames) {
|
|
1638
|
+
const value = layoutOffsetValue(className);
|
|
1639
|
+
if (value !== undefined)
|
|
1640
|
+
return value;
|
|
1641
|
+
}
|
|
1642
|
+
return undefined;
|
|
1643
|
+
}
|
|
1292
1644
|
function validateInteractionResponseReference(interaction, responseIdentifiers, diagnostics) {
|
|
1293
1645
|
if (!interaction.responseIdentifier) {
|
|
1294
1646
|
if (interaction.type !== "endAttempt") {
|
|
@@ -1662,8 +2014,20 @@ function validateChoiceLimitAttributes(choice, diagnostics) {
|
|
|
1662
2014
|
validateChoiceNonNegativeIntegerAttribute(choice, "match-max", diagnostics);
|
|
1663
2015
|
validateChoiceNonNegativeIntegerAttribute(choice, "match-min", diagnostics);
|
|
1664
2016
|
validateChoiceMinMaxPair(choice, "match-min", "match-max", diagnostics);
|
|
2017
|
+
validateGapImageAsset(choice, diagnostics);
|
|
1665
2018
|
validateHotspotGeometry(choice, diagnostics);
|
|
1666
2019
|
}
|
|
2020
|
+
function validateGapImageAsset(choice, diagnostics) {
|
|
2021
|
+
if (choice.qtiName !== "qti-gap-img" || choice.asset?.data)
|
|
2022
|
+
return;
|
|
2023
|
+
diagnostics.push({
|
|
2024
|
+
code: "choice.gapImg.media.required",
|
|
2025
|
+
severity: "error",
|
|
2026
|
+
message: `qti-gap-img ${choice.identifier} requires an img, object, or picture child with a usable src or data attribute.`,
|
|
2027
|
+
path: choice.source?.path,
|
|
2028
|
+
source: choice.source,
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
1667
2031
|
function requiresMatchMax(choice) {
|
|
1668
2032
|
return (choice.qtiName === "qti-simple-associable-choice" ||
|
|
1669
2033
|
choice.qtiName === "qti-associable-hotspot" ||
|