@longsightgroup/qti3-core 0.7.3 → 0.8.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.
Files changed (119) hide show
  1. package/README.md +41 -10
  2. package/dist/content-text.d.ts +7 -1
  3. package/dist/content-text.d.ts.map +1 -1
  4. package/dist/content-text.js +14 -0
  5. package/dist/content-text.js.map +1 -1
  6. package/dist/index.d.ts +7 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +6 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/package-xml.d.ts +12 -0
  11. package/dist/package-xml.d.ts.map +1 -0
  12. package/dist/package-xml.js +17 -0
  13. package/dist/package-xml.js.map +1 -0
  14. package/dist/parser-custom-interactions.d.ts +8 -0
  15. package/dist/parser-custom-interactions.d.ts.map +1 -0
  16. package/dist/parser-custom-interactions.js +178 -0
  17. package/dist/parser-custom-interactions.js.map +1 -0
  18. package/dist/parser-declarations.d.ts +6 -0
  19. package/dist/parser-declarations.d.ts.map +1 -0
  20. package/dist/parser-declarations.js +136 -0
  21. package/dist/parser-declarations.js.map +1 -0
  22. package/dist/parser-item-metadata.d.ts +9 -0
  23. package/dist/parser-item-metadata.d.ts.map +1 -0
  24. package/dist/parser-item-metadata.js +158 -0
  25. package/dist/parser-item-metadata.js.map +1 -0
  26. package/dist/parser-processing.d.ts +5 -0
  27. package/dist/parser-processing.d.ts.map +1 -0
  28. package/dist/parser-processing.js +642 -0
  29. package/dist/parser-processing.js.map +1 -0
  30. package/dist/parser-values.d.ts +8 -0
  31. package/dist/parser-values.d.ts.map +1 -0
  32. package/dist/parser-values.js +49 -0
  33. package/dist/parser-values.js.map +1 -0
  34. package/dist/parser.d.ts.map +1 -1
  35. package/dist/parser.js +15 -1083
  36. package/dist/parser.js.map +1 -1
  37. package/dist/processing-expression-tags.d.ts +56 -0
  38. package/dist/processing-expression-tags.d.ts.map +1 -0
  39. package/dist/processing-expression-tags.js +52 -0
  40. package/dist/processing-expression-tags.js.map +1 -0
  41. package/dist/processing-rules.d.ts +3 -0
  42. package/dist/processing-rules.d.ts.map +1 -0
  43. package/dist/processing-rules.js +10 -0
  44. package/dist/processing-rules.js.map +1 -0
  45. package/dist/serializer-processing-expressions.d.ts +8 -0
  46. package/dist/serializer-processing-expressions.d.ts.map +1 -0
  47. package/dist/serializer-processing-expressions.js +198 -0
  48. package/dist/serializer-processing-expressions.js.map +1 -0
  49. package/dist/serializer-processing-xml.d.ts +15 -0
  50. package/dist/serializer-processing-xml.d.ts.map +1 -0
  51. package/dist/serializer-processing-xml.js +46 -0
  52. package/dist/serializer-processing-xml.js.map +1 -0
  53. package/dist/serializer-processing.d.ts +8 -0
  54. package/dist/serializer-processing.d.ts.map +1 -0
  55. package/dist/serializer-processing.js +106 -0
  56. package/dist/serializer-processing.js.map +1 -0
  57. package/dist/shared-vocabulary-authoring.d.ts +52 -0
  58. package/dist/shared-vocabulary-authoring.d.ts.map +1 -0
  59. package/dist/shared-vocabulary-authoring.js +327 -0
  60. package/dist/shared-vocabulary-authoring.js.map +1 -0
  61. package/dist/shared-vocabulary-interaction-sets.d.ts +6 -0
  62. package/dist/shared-vocabulary-interaction-sets.d.ts.map +1 -0
  63. package/dist/shared-vocabulary-interaction-sets.js +23 -0
  64. package/dist/shared-vocabulary-interaction-sets.js.map +1 -0
  65. package/dist/shared-vocabulary-interaction-validation.d.ts +3 -0
  66. package/dist/shared-vocabulary-interaction-validation.d.ts.map +1 -0
  67. package/dist/shared-vocabulary-interaction-validation.js +99 -0
  68. package/dist/shared-vocabulary-interaction-validation.js.map +1 -0
  69. package/dist/shared-vocabulary-registry-validation.d.ts +5 -0
  70. package/dist/shared-vocabulary-registry-validation.d.ts.map +1 -0
  71. package/dist/shared-vocabulary-registry-validation.js +147 -0
  72. package/dist/shared-vocabulary-registry-validation.js.map +1 -0
  73. package/dist/shared-vocabulary-support.d.ts.map +1 -1
  74. package/dist/shared-vocabulary-support.js +160 -67
  75. package/dist/shared-vocabulary-support.js.map +1 -1
  76. package/dist/shared-vocabulary.d.ts +6 -0
  77. package/dist/shared-vocabulary.d.ts.map +1 -1
  78. package/dist/shared-vocabulary.js +12 -0
  79. package/dist/shared-vocabulary.js.map +1 -1
  80. package/dist/support.d.ts +4 -1
  81. package/dist/support.d.ts.map +1 -1
  82. package/dist/support.js +130 -0
  83. package/dist/support.js.map +1 -1
  84. package/dist/types.d.ts +32 -7
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/validation.d.ts.map +1 -1
  87. package/dist/validation.js +29 -250
  88. package/dist/validation.js.map +1 -1
  89. package/dist/xml.d.ts +10 -0
  90. package/dist/xml.d.ts.map +1 -1
  91. package/dist/xml.js +321 -272
  92. package/dist/xml.js.map +1 -1
  93. package/package.json +1 -4
  94. package/src/content-text.ts +20 -1
  95. package/src/index.ts +68 -0
  96. package/src/interaction-test-fixtures.ts +44 -0
  97. package/src/package-xml.ts +30 -0
  98. package/src/parser-custom-interactions.ts +309 -0
  99. package/src/parser-declarations.ts +174 -0
  100. package/src/parser-item-metadata.ts +194 -0
  101. package/src/parser-processing.ts +701 -0
  102. package/src/parser-values.ts +61 -0
  103. package/src/parser.ts +36 -1227
  104. package/src/processing-expression-tags.ts +65 -0
  105. package/src/processing-rules.ts +11 -0
  106. package/src/serializer-processing-expressions.ts +456 -0
  107. package/src/serializer-processing-xml.ts +67 -0
  108. package/src/serializer-processing.fixtures.ts +469 -0
  109. package/src/serializer-processing.ts +196 -0
  110. package/src/shared-vocabulary-authoring.ts +438 -0
  111. package/src/shared-vocabulary-interaction-sets.ts +28 -0
  112. package/src/shared-vocabulary-interaction-validation.ts +143 -0
  113. package/src/shared-vocabulary-registry-validation.ts +210 -0
  114. package/src/shared-vocabulary-support.ts +200 -127
  115. package/src/shared-vocabulary.ts +18 -0
  116. package/src/support.ts +144 -0
  117. package/src/types.ts +42 -7
  118. package/src/validation.ts +33 -341
  119. package/src/xml.ts +376 -276
@@ -1,4 +1,4 @@
1
- import { validateSharedVocabularyExtendedText, validateSharedVocabularyInputWidth, validateSharedVocabularyMediaPlayerControls, } from "./shared-vocabulary-validation.js";
1
+ import { validateInteractionSharedVocabulary } from "./shared-vocabulary-interaction-validation.js";
2
2
  import { isValidQtiPatternMask } from "./pattern-mask.js";
3
3
  import { validateQtiDataSsmlMetadata } from "./tts.js";
4
4
  import { qtiValueToStringList } from "./value-format.js";
@@ -15,6 +15,7 @@ export function validateAssessmentItem(document) {
15
15
  validateInteractions(item, diagnostics);
16
16
  validateModalFeedback(item, diagnostics);
17
17
  validateCatalogInfo(item, diagnostics);
18
+ validateCompanionMaterials(item, diagnostics);
18
19
  validateStylesheets(item, diagnostics);
19
20
  diagnostics.push(...validateQtiDataSsmlMetadata(item));
20
21
  validateResponseProcessingTemplate(item, diagnostics);
@@ -595,6 +596,33 @@ function validateOutcomeLookupTables(item, diagnostics) {
595
596
  }
596
597
  }
597
598
  }
599
+ function validateCompanionMaterials(item, diagnostics) {
600
+ const companionMaterials = item.companionMaterials;
601
+ if (!companionMaterials)
602
+ return;
603
+ for (const child of companionMaterials.unparsedChildren) {
604
+ if (child.qtiName === "qti-physical-material") {
605
+ diagnostics.push({
606
+ code: "companionMaterials.model.inconsistent",
607
+ severity: "error",
608
+ message: "Empty qti-physical-material must be represented as a parse warning, not an unparsed child.",
609
+ path: child.source?.path,
610
+ source: child.source,
611
+ });
612
+ }
613
+ }
614
+ for (const material of companionMaterials.physicalMaterials) {
615
+ if (material.text.trim().length === 0) {
616
+ diagnostics.push({
617
+ code: "companionMaterials.physicalMaterial.empty.model",
618
+ severity: "error",
619
+ message: "qti-physical-material requires non-empty text content in the parsed companion materials model.",
620
+ path: material.source?.path,
621
+ source: material.source,
622
+ });
623
+ }
624
+ }
625
+ }
598
626
  function validateStylesheets(item, diagnostics) {
599
627
  for (const stylesheet of item.stylesheets) {
600
628
  if (stylesheet.href.trim().length > 0)
@@ -1352,255 +1380,6 @@ function validateInteractions(item, diagnostics) {
1352
1380
  : undefined, diagnostics);
1353
1381
  }
1354
1382
  }
1355
- function validateInteractionSharedVocabulary(interaction, diagnostics) {
1356
- if (interaction.type !== "choice" &&
1357
- interaction.type !== "match" &&
1358
- interaction.type !== "gapMatch" &&
1359
- interaction.type !== "graphicGapMatch" &&
1360
- interaction.type !== "inlineChoice" &&
1361
- interaction.type !== "textEntry" &&
1362
- interaction.type !== "extendedText" &&
1363
- interaction.type !== "media" &&
1364
- interaction.type !== "order") {
1365
- return;
1366
- }
1367
- const classNames = sharedClassNames(interaction.attributes);
1368
- const classNameSet = new Set(classNames);
1369
- if (interaction.type === "media") {
1370
- validateMediaInteractionSharedVocabulary(interaction, diagnostics);
1371
- return;
1372
- }
1373
- if (interaction.type === "inlineChoice" || interaction.type === "textEntry") {
1374
- validateInteractionInputWidthSharedVocabulary(interaction, classNames, diagnostics);
1375
- return;
1376
- }
1377
- if (interaction.type === "extendedText") {
1378
- diagnostics.push(...validateSharedVocabularyExtendedText({
1379
- classNames,
1380
- subjectQtiName: interaction.qtiName,
1381
- expectedLength: interaction.attributes["expected-length"],
1382
- path: interaction.source?.path,
1383
- source: interaction.source,
1384
- }));
1385
- return;
1386
- }
1387
- if (interaction.type === "choice" || interaction.type === "order") {
1388
- validateSharedVocabularyLabelClasses(interaction, classNames, diagnostics);
1389
- }
1390
- if (interaction.type === "match" ||
1391
- interaction.type === "gapMatch" ||
1392
- interaction.type === "graphicGapMatch") {
1393
- if (interaction.type === "match") {
1394
- validateMatchInteractionSharedVocabulary(interaction, classNames, diagnostics);
1395
- }
1396
- validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics);
1397
- validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics);
1398
- if (interaction.type === "gapMatch") {
1399
- validateGapInputWidthSharedVocabulary(interaction, diagnostics);
1400
- }
1401
- return;
1402
- }
1403
- if (interaction.type === "order") {
1404
- validateOrderInteractionSharedVocabulary(interaction, classNames, classNameSet, diagnostics);
1405
- return;
1406
- }
1407
- validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics);
1408
- const validStackingClasses = new Set();
1409
- const invalidStackingClasses = new Set();
1410
- for (const className of classNames) {
1411
- const stacking = /^qti-choices-stacking-(\d+)$/.exec(className)?.[1];
1412
- if (stacking === undefined)
1413
- continue;
1414
- const count = Number(stacking);
1415
- if (count >= 1 && count <= 5)
1416
- validStackingClasses.add(className);
1417
- else
1418
- invalidStackingClasses.add(className);
1419
- }
1420
- if (validStackingClasses.size > 1) {
1421
- diagnostics.push({
1422
- code: "interaction.sharedVocabulary.stackingConflict",
1423
- severity: "warning",
1424
- 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.`,
1425
- path: interaction.source?.path,
1426
- source: interaction.source,
1427
- });
1428
- }
1429
- for (const className of invalidStackingClasses) {
1430
- diagnostics.push({
1431
- code: "interaction.sharedVocabulary.stackingInvalid",
1432
- severity: "warning",
1433
- message: `qti-choice-interaction shared vocabulary class ${className} is not supported; expected qti-choices-stacking-1 through qti-choices-stacking-5.`,
1434
- path: interaction.source?.path,
1435
- source: interaction.source,
1436
- });
1437
- }
1438
- }
1439
- function validateSharedVocabularyLabelClasses(interaction, classNames, diagnostics) {
1440
- const labelClasses = classNames.filter((className) => [
1441
- "qti-labels-decimal",
1442
- "qti-labels-cjk-ideographic",
1443
- "qti-labels-lower-alpha",
1444
- "qti-labels-upper-alpha",
1445
- ].includes(className));
1446
- if (new Set(labelClasses).size > 1) {
1447
- diagnostics.push({
1448
- code: "interaction.sharedVocabulary.labelsConflict",
1449
- severity: "warning",
1450
- 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.`,
1451
- path: interaction.source?.path,
1452
- source: interaction.source,
1453
- });
1454
- }
1455
- const suffixClasses = classNames.filter((className) => [
1456
- "qti-labels-suffix-none",
1457
- "qti-labels-suffix-period",
1458
- "qti-labels-suffix-parenthesis",
1459
- ].includes(className));
1460
- if (new Set(suffixClasses).size <= 1)
1461
- return;
1462
- diagnostics.push({
1463
- code: "interaction.sharedVocabulary.labelSuffixConflict",
1464
- severity: "warning",
1465
- 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.`,
1466
- path: interaction.source?.path,
1467
- source: interaction.source,
1468
- });
1469
- }
1470
- function validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics) {
1471
- if (!classNameSet.has("qti-orientation-horizontal") ||
1472
- !classNameSet.has("qti-orientation-vertical")) {
1473
- return;
1474
- }
1475
- diagnostics.push({
1476
- code: "interaction.sharedVocabulary.orientationConflict",
1477
- severity: "warning",
1478
- message: `${interaction.qtiName} should not include both qti-orientation-horizontal and qti-orientation-vertical; qti-orientation-horizontal takes precedence at runtime.`,
1479
- path: interaction.source?.path,
1480
- source: interaction.source,
1481
- });
1482
- }
1483
- function validateOrderInteractionSharedVocabulary(interaction, classNames, classNameSet, diagnostics) {
1484
- validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics);
1485
- validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics);
1486
- validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics);
1487
- }
1488
- const SHARED_VOCABULARY_CHOICES_POSITION_CLASSES = [
1489
- "qti-choices-top",
1490
- "qti-choices-bottom",
1491
- "qti-choices-left",
1492
- "qti-choices-right",
1493
- ];
1494
- function validateMatchInteractionSharedVocabulary(interaction, classNames, diagnostics) {
1495
- const hasTabular = classNames.includes("qti-match-tabular");
1496
- const hasHeaderHidden = classNames.includes("qti-header-hidden");
1497
- const firstColumnHeader = interaction.attributes["data-first-column-header"];
1498
- if (!hasTabular && (hasHeaderHidden || firstColumnHeader !== undefined)) {
1499
- diagnostics.push({
1500
- code: "interaction.sharedVocabulary.matchTabularContext",
1501
- severity: "warning",
1502
- 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.",
1503
- path: interaction.source?.path,
1504
- source: interaction.source,
1505
- });
1506
- }
1507
- if (hasTabular && hasHeaderHidden && firstColumnHeader !== undefined) {
1508
- diagnostics.push({
1509
- code: "interaction.sharedVocabulary.matchTabularHeaderHidden",
1510
- severity: "warning",
1511
- message: "qti-match-interaction data-first-column-header is ignored when qti-header-hidden suppresses the tabular header row.",
1512
- path: interaction.source?.path,
1513
- source: interaction.source,
1514
- });
1515
- }
1516
- if (!hasTabular)
1517
- return;
1518
- const choicesPositionClasses = classNames.filter((className) => SHARED_VOCABULARY_CHOICES_POSITION_CLASSES.includes(className));
1519
- if (choicesPositionClasses.length > 0 ||
1520
- interaction.attributes["data-choices-container-width"] !== undefined) {
1521
- diagnostics.push({
1522
- code: "interaction.sharedVocabulary.matchTabularChoicesConflict",
1523
- severity: "warning",
1524
- message: "qti-match-interaction qti-match-tabular uses a matrix layout; qti-choices-* position classes and data-choices-container-width are ignored at runtime.",
1525
- path: interaction.source?.path,
1526
- source: interaction.source,
1527
- });
1528
- }
1529
- if (!hasHeaderHidden && (firstColumnHeader === undefined || firstColumnHeader === "")) {
1530
- diagnostics.push({
1531
- code: "interaction.sharedVocabulary.matchTabularFirstColumnHeader",
1532
- severity: "warning",
1533
- 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.",
1534
- path: interaction.source?.path,
1535
- source: interaction.source,
1536
- });
1537
- }
1538
- }
1539
- function validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics) {
1540
- const choicesPositionClasses = classNames.filter((className) => SHARED_VOCABULARY_CHOICES_POSITION_CLASSES.includes(className));
1541
- if (new Set(choicesPositionClasses).size > 1) {
1542
- diagnostics.push({
1543
- code: "interaction.sharedVocabulary.orderChoicesPositionConflict",
1544
- severity: "warning",
1545
- 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.`,
1546
- path: interaction.source?.path,
1547
- source: interaction.source,
1548
- });
1549
- }
1550
- }
1551
- function validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics) {
1552
- const width = interaction.attributes["data-choices-container-width"];
1553
- if (width === undefined)
1554
- return;
1555
- const parsed = Number(width);
1556
- if (!Number.isFinite(parsed) || parsed <= 0) {
1557
- diagnostics.push({
1558
- code: "interaction.sharedVocabulary.orderChoicesContainerWidth",
1559
- severity: "warning",
1560
- message: `${interaction.qtiName} data-choices-container-width must be a positive pixel value; the invalid value is ignored at runtime.`,
1561
- path: interaction.source?.path,
1562
- source: interaction.source,
1563
- });
1564
- }
1565
- }
1566
- function validateGapInputWidthSharedVocabulary(interaction, diagnostics) {
1567
- for (const gap of interaction.choices.filter((choice) => choice.qtiName === "qti-gap")) {
1568
- diagnostics.push(...validateSharedVocabularyInputWidth({
1569
- classNames: sharedClassNames(gap.attributes),
1570
- subjectQtiName: "qti-gap",
1571
- path: gap.source?.path ?? interaction.source?.path,
1572
- source: gap.source ?? interaction.source,
1573
- conflictCode: "interaction.sharedVocabulary.gapInputWidthConflict",
1574
- invalidCode: "interaction.sharedVocabulary.gapInputWidthInvalid",
1575
- }));
1576
- }
1577
- }
1578
- function validateInteractionInputWidthSharedVocabulary(interaction, classNames, diagnostics) {
1579
- diagnostics.push(...validateSharedVocabularyInputWidth({
1580
- classNames,
1581
- subjectQtiName: interaction.qtiName,
1582
- path: interaction.source?.path,
1583
- source: interaction.source,
1584
- conflictCode: "interaction.sharedVocabulary.inputWidthConflict",
1585
- invalidCode: "interaction.sharedVocabulary.inputWidthInvalid",
1586
- }));
1587
- }
1588
- function validateMediaInteractionSharedVocabulary(interaction, diagnostics) {
1589
- diagnostics.push(...validateSharedVocabularyMediaPlayerControls({
1590
- value: interaction.attributes["data-qti-media-player-controls"],
1591
- subjectQtiName: interaction.qtiName,
1592
- path: interaction.source?.path,
1593
- source: interaction.source,
1594
- }));
1595
- if (!interaction.object)
1596
- return;
1597
- diagnostics.push(...validateSharedVocabularyMediaPlayerControls({
1598
- value: interaction.object.attributes["data-qti-media-player-controls"],
1599
- subjectQtiName: `${interaction.qtiName} media object`,
1600
- path: interaction.object.source?.path ?? interaction.source?.path,
1601
- source: interaction.object.source ?? interaction.source,
1602
- }));
1603
- }
1604
1383
  function sharedClassNames(attributes) {
1605
1384
  return (attributes.class ?? "").split(/\s+/).filter(Boolean);
1606
1385
  }