@longsightgroup/qti3-core 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +41 -10
  2. package/dist/content-text.d.ts +14 -0
  3. package/dist/content-text.d.ts.map +1 -0
  4. package/dist/content-text.js +58 -0
  5. package/dist/content-text.js.map +1 -0
  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 +32 -1086
  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 +50 -0
  58. package/dist/shared-vocabulary-authoring.d.ts.map +1 -0
  59. package/dist/shared-vocabulary-authoring.js +302 -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 +96 -56
  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 +159 -10
  83. package/dist/support.js.map +1 -1
  84. package/dist/types.d.ts +38 -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 -249
  92. package/dist/xml.js.map +1 -1
  93. package/package.json +1 -4
  94. package/src/content-text.ts +80 -0
  95. package/src/index.ts +67 -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 +59 -1230
  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 +409 -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 +207 -0
  114. package/src/shared-vocabulary-support.ts +127 -101
  115. package/src/shared-vocabulary.ts +18 -0
  116. package/src/support.ts +174 -14
  117. package/src/types.ts +48 -7
  118. package/src/validation.ts +33 -341
  119. package/src/xml.ts +378 -255
package/src/validation.ts CHANGED
@@ -21,11 +21,7 @@ import type {
21
21
  QtiValue,
22
22
  QtiValidationResult,
23
23
  } from "./types.js";
24
- import {
25
- validateSharedVocabularyExtendedText,
26
- validateSharedVocabularyInputWidth,
27
- validateSharedVocabularyMediaPlayerControls,
28
- } from "./shared-vocabulary-validation.js";
24
+ import { validateInteractionSharedVocabulary } from "./shared-vocabulary-interaction-validation.js";
29
25
  import { isValidQtiPatternMask } from "./pattern-mask.js";
30
26
  import { validateQtiDataSsmlMetadata } from "./tts.js";
31
27
  import { qtiValueToStringList } from "./value-format.js";
@@ -45,6 +41,7 @@ export function validateAssessmentItem(document: QtiDocument): QtiValidationResu
45
41
  validateInteractions(item, diagnostics);
46
42
  validateModalFeedback(item, diagnostics);
47
43
  validateCatalogInfo(item, diagnostics);
44
+ validateCompanionMaterials(item, diagnostics);
48
45
  validateStylesheets(item, diagnostics);
49
46
  diagnostics.push(...validateQtiDataSsmlMetadata(item));
50
47
  validateResponseProcessingTemplate(item, diagnostics);
@@ -716,6 +713,37 @@ function validateOutcomeLookupTables(item: QtiAssessmentItem, diagnostics: QtiDi
716
713
  }
717
714
  }
718
715
 
716
+ function validateCompanionMaterials(item: QtiAssessmentItem, diagnostics: QtiDiagnostic[]): void {
717
+ const companionMaterials = item.companionMaterials;
718
+ if (!companionMaterials) return;
719
+
720
+ for (const child of companionMaterials.unparsedChildren) {
721
+ if (child.qtiName === "qti-physical-material") {
722
+ diagnostics.push({
723
+ code: "companionMaterials.model.inconsistent",
724
+ severity: "error",
725
+ message:
726
+ "Empty qti-physical-material must be represented as a parse warning, not an unparsed child.",
727
+ path: child.source?.path,
728
+ source: child.source,
729
+ });
730
+ }
731
+ }
732
+
733
+ for (const material of companionMaterials.physicalMaterials) {
734
+ if (material.text.trim().length === 0) {
735
+ diagnostics.push({
736
+ code: "companionMaterials.physicalMaterial.empty.model",
737
+ severity: "error",
738
+ message:
739
+ "qti-physical-material requires non-empty text content in the parsed companion materials model.",
740
+ path: material.source?.path,
741
+ source: material.source,
742
+ });
743
+ }
744
+ }
745
+ }
746
+
719
747
  function validateStylesheets(item: QtiAssessmentItem, diagnostics: QtiDiagnostic[]): void {
720
748
  for (const stylesheet of item.stylesheets) {
721
749
  if (stylesheet.href.trim().length > 0) continue;
@@ -1692,342 +1720,6 @@ function validateInteractions(item: QtiAssessmentItem, diagnostics: QtiDiagnosti
1692
1720
  }
1693
1721
  }
1694
1722
 
1695
- function validateInteractionSharedVocabulary(
1696
- interaction: QtiInteraction,
1697
- diagnostics: QtiDiagnostic[],
1698
- ): void {
1699
- if (
1700
- interaction.type !== "choice" &&
1701
- interaction.type !== "match" &&
1702
- interaction.type !== "gapMatch" &&
1703
- interaction.type !== "graphicGapMatch" &&
1704
- interaction.type !== "inlineChoice" &&
1705
- interaction.type !== "textEntry" &&
1706
- interaction.type !== "extendedText" &&
1707
- interaction.type !== "media" &&
1708
- interaction.type !== "order"
1709
- ) {
1710
- return;
1711
- }
1712
- const classNames = sharedClassNames(interaction.attributes);
1713
- const classNameSet = new Set(classNames);
1714
- if (interaction.type === "media") {
1715
- validateMediaInteractionSharedVocabulary(interaction, diagnostics);
1716
- return;
1717
- }
1718
-
1719
- if (interaction.type === "inlineChoice" || interaction.type === "textEntry") {
1720
- validateInteractionInputWidthSharedVocabulary(interaction, classNames, diagnostics);
1721
- return;
1722
- }
1723
-
1724
- if (interaction.type === "extendedText") {
1725
- diagnostics.push(
1726
- ...validateSharedVocabularyExtendedText({
1727
- classNames,
1728
- subjectQtiName: interaction.qtiName,
1729
- expectedLength: interaction.attributes["expected-length"],
1730
- path: interaction.source?.path,
1731
- source: interaction.source,
1732
- }),
1733
- );
1734
- return;
1735
- }
1736
-
1737
- if (interaction.type === "choice" || interaction.type === "order") {
1738
- validateSharedVocabularyLabelClasses(interaction, classNames, diagnostics);
1739
- }
1740
-
1741
- if (
1742
- interaction.type === "match" ||
1743
- interaction.type === "gapMatch" ||
1744
- interaction.type === "graphicGapMatch"
1745
- ) {
1746
- if (interaction.type === "match") {
1747
- validateMatchInteractionSharedVocabulary(interaction, classNames, diagnostics);
1748
- }
1749
- validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics);
1750
- validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics);
1751
- if (interaction.type === "gapMatch") {
1752
- validateGapInputWidthSharedVocabulary(interaction, diagnostics);
1753
- }
1754
- return;
1755
- }
1756
-
1757
- if (interaction.type === "order") {
1758
- validateOrderInteractionSharedVocabulary(interaction, classNames, classNameSet, diagnostics);
1759
- return;
1760
- }
1761
-
1762
- validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics);
1763
-
1764
- const validStackingClasses = new Set<string>();
1765
- const invalidStackingClasses = new Set<string>();
1766
- for (const className of classNames) {
1767
- const stacking = /^qti-choices-stacking-(\d+)$/.exec(className)?.[1];
1768
- if (stacking === undefined) continue;
1769
- const count = Number(stacking);
1770
- if (count >= 1 && count <= 5) validStackingClasses.add(className);
1771
- else invalidStackingClasses.add(className);
1772
- }
1773
-
1774
- if (validStackingClasses.size > 1) {
1775
- diagnostics.push({
1776
- code: "interaction.sharedVocabulary.stackingConflict",
1777
- severity: "warning",
1778
- 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.`,
1779
- path: interaction.source?.path,
1780
- source: interaction.source,
1781
- });
1782
- }
1783
-
1784
- for (const className of invalidStackingClasses) {
1785
- diagnostics.push({
1786
- code: "interaction.sharedVocabulary.stackingInvalid",
1787
- severity: "warning",
1788
- message: `qti-choice-interaction shared vocabulary class ${className} is not supported; expected qti-choices-stacking-1 through qti-choices-stacking-5.`,
1789
- path: interaction.source?.path,
1790
- source: interaction.source,
1791
- });
1792
- }
1793
- }
1794
-
1795
- function validateSharedVocabularyLabelClasses(
1796
- interaction: QtiInteraction,
1797
- classNames: string[],
1798
- diagnostics: QtiDiagnostic[],
1799
- ): void {
1800
- const labelClasses = classNames.filter((className) =>
1801
- [
1802
- "qti-labels-decimal",
1803
- "qti-labels-cjk-ideographic",
1804
- "qti-labels-lower-alpha",
1805
- "qti-labels-upper-alpha",
1806
- ].includes(className),
1807
- );
1808
- if (new Set(labelClasses).size > 1) {
1809
- diagnostics.push({
1810
- code: "interaction.sharedVocabulary.labelsConflict",
1811
- severity: "warning",
1812
- 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.`,
1813
- path: interaction.source?.path,
1814
- source: interaction.source,
1815
- });
1816
- }
1817
-
1818
- const suffixClasses = classNames.filter((className) =>
1819
- [
1820
- "qti-labels-suffix-none",
1821
- "qti-labels-suffix-period",
1822
- "qti-labels-suffix-parenthesis",
1823
- ].includes(className),
1824
- );
1825
- if (new Set(suffixClasses).size <= 1) return;
1826
- diagnostics.push({
1827
- code: "interaction.sharedVocabulary.labelSuffixConflict",
1828
- severity: "warning",
1829
- 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.`,
1830
- path: interaction.source?.path,
1831
- source: interaction.source,
1832
- });
1833
- }
1834
-
1835
- function validateOrientationSharedVocabulary(
1836
- interaction: QtiInteraction,
1837
- classNameSet: Set<string>,
1838
- diagnostics: QtiDiagnostic[],
1839
- ): void {
1840
- if (
1841
- !classNameSet.has("qti-orientation-horizontal") ||
1842
- !classNameSet.has("qti-orientation-vertical")
1843
- ) {
1844
- return;
1845
- }
1846
- diagnostics.push({
1847
- code: "interaction.sharedVocabulary.orientationConflict",
1848
- severity: "warning",
1849
- message: `${interaction.qtiName} should not include both qti-orientation-horizontal and qti-orientation-vertical; qti-orientation-horizontal takes precedence at runtime.`,
1850
- path: interaction.source?.path,
1851
- source: interaction.source,
1852
- });
1853
- }
1854
-
1855
- function validateOrderInteractionSharedVocabulary(
1856
- interaction: QtiInteraction,
1857
- classNames: string[],
1858
- classNameSet: Set<string>,
1859
- diagnostics: QtiDiagnostic[],
1860
- ): void {
1861
- validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics);
1862
- validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics);
1863
- validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics);
1864
- }
1865
-
1866
- const SHARED_VOCABULARY_CHOICES_POSITION_CLASSES = [
1867
- "qti-choices-top",
1868
- "qti-choices-bottom",
1869
- "qti-choices-left",
1870
- "qti-choices-right",
1871
- ] as const;
1872
-
1873
- function validateMatchInteractionSharedVocabulary(
1874
- interaction: QtiInteraction,
1875
- classNames: string[],
1876
- diagnostics: QtiDiagnostic[],
1877
- ): void {
1878
- const hasTabular = classNames.includes("qti-match-tabular");
1879
- const hasHeaderHidden = classNames.includes("qti-header-hidden");
1880
- const firstColumnHeader = interaction.attributes["data-first-column-header"];
1881
- if (!hasTabular && (hasHeaderHidden || firstColumnHeader !== undefined)) {
1882
- diagnostics.push({
1883
- code: "interaction.sharedVocabulary.matchTabularContext",
1884
- severity: "warning",
1885
- message:
1886
- "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.",
1887
- path: interaction.source?.path,
1888
- source: interaction.source,
1889
- });
1890
- }
1891
- if (hasTabular && hasHeaderHidden && firstColumnHeader !== undefined) {
1892
- diagnostics.push({
1893
- code: "interaction.sharedVocabulary.matchTabularHeaderHidden",
1894
- severity: "warning",
1895
- message:
1896
- "qti-match-interaction data-first-column-header is ignored when qti-header-hidden suppresses the tabular header row.",
1897
- path: interaction.source?.path,
1898
- source: interaction.source,
1899
- });
1900
- }
1901
- if (!hasTabular) return;
1902
-
1903
- const choicesPositionClasses = classNames.filter((className) =>
1904
- SHARED_VOCABULARY_CHOICES_POSITION_CLASSES.includes(
1905
- className as (typeof SHARED_VOCABULARY_CHOICES_POSITION_CLASSES)[number],
1906
- ),
1907
- );
1908
- if (
1909
- choicesPositionClasses.length > 0 ||
1910
- interaction.attributes["data-choices-container-width"] !== undefined
1911
- ) {
1912
- diagnostics.push({
1913
- code: "interaction.sharedVocabulary.matchTabularChoicesConflict",
1914
- severity: "warning",
1915
- message:
1916
- "qti-match-interaction qti-match-tabular uses a matrix layout; qti-choices-* position classes and data-choices-container-width are ignored at runtime.",
1917
- path: interaction.source?.path,
1918
- source: interaction.source,
1919
- });
1920
- }
1921
- if (!hasHeaderHidden && (firstColumnHeader === undefined || firstColumnHeader === "")) {
1922
- diagnostics.push({
1923
- code: "interaction.sharedVocabulary.matchTabularFirstColumnHeader",
1924
- severity: "warning",
1925
- message:
1926
- "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.",
1927
- path: interaction.source?.path,
1928
- source: interaction.source,
1929
- });
1930
- }
1931
- }
1932
-
1933
- function validateChoicesPositionSharedVocabulary(
1934
- interaction: QtiInteraction,
1935
- classNames: string[],
1936
- diagnostics: QtiDiagnostic[],
1937
- ): void {
1938
- const choicesPositionClasses = classNames.filter((className) =>
1939
- SHARED_VOCABULARY_CHOICES_POSITION_CLASSES.includes(
1940
- className as (typeof SHARED_VOCABULARY_CHOICES_POSITION_CLASSES)[number],
1941
- ),
1942
- );
1943
- if (new Set(choicesPositionClasses).size > 1) {
1944
- diagnostics.push({
1945
- code: "interaction.sharedVocabulary.orderChoicesPositionConflict",
1946
- severity: "warning",
1947
- 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.`,
1948
- path: interaction.source?.path,
1949
- source: interaction.source,
1950
- });
1951
- }
1952
- }
1953
-
1954
- function validateChoicesContainerWidthSharedVocabulary(
1955
- interaction: QtiInteraction,
1956
- diagnostics: QtiDiagnostic[],
1957
- ): void {
1958
- const width = interaction.attributes["data-choices-container-width"];
1959
- if (width === undefined) return;
1960
- const parsed = Number(width);
1961
- if (!Number.isFinite(parsed) || parsed <= 0) {
1962
- diagnostics.push({
1963
- code: "interaction.sharedVocabulary.orderChoicesContainerWidth",
1964
- severity: "warning",
1965
- message: `${interaction.qtiName} data-choices-container-width must be a positive pixel value; the invalid value is ignored at runtime.`,
1966
- path: interaction.source?.path,
1967
- source: interaction.source,
1968
- });
1969
- }
1970
- }
1971
-
1972
- function validateGapInputWidthSharedVocabulary(
1973
- interaction: QtiInteraction,
1974
- diagnostics: QtiDiagnostic[],
1975
- ): void {
1976
- for (const gap of interaction.choices.filter((choice) => choice.qtiName === "qti-gap")) {
1977
- diagnostics.push(
1978
- ...validateSharedVocabularyInputWidth({
1979
- classNames: sharedClassNames(gap.attributes),
1980
- subjectQtiName: "qti-gap",
1981
- path: gap.source?.path ?? interaction.source?.path,
1982
- source: gap.source ?? interaction.source,
1983
- conflictCode: "interaction.sharedVocabulary.gapInputWidthConflict",
1984
- invalidCode: "interaction.sharedVocabulary.gapInputWidthInvalid",
1985
- }),
1986
- );
1987
- }
1988
- }
1989
-
1990
- function validateInteractionInputWidthSharedVocabulary(
1991
- interaction: QtiInteraction,
1992
- classNames: string[],
1993
- diagnostics: QtiDiagnostic[],
1994
- ): void {
1995
- diagnostics.push(
1996
- ...validateSharedVocabularyInputWidth({
1997
- classNames,
1998
- subjectQtiName: interaction.qtiName,
1999
- path: interaction.source?.path,
2000
- source: interaction.source,
2001
- conflictCode: "interaction.sharedVocabulary.inputWidthConflict",
2002
- invalidCode: "interaction.sharedVocabulary.inputWidthInvalid",
2003
- }),
2004
- );
2005
- }
2006
-
2007
- function validateMediaInteractionSharedVocabulary(
2008
- interaction: QtiInteraction,
2009
- diagnostics: QtiDiagnostic[],
2010
- ): void {
2011
- diagnostics.push(
2012
- ...validateSharedVocabularyMediaPlayerControls({
2013
- value: interaction.attributes["data-qti-media-player-controls"],
2014
- subjectQtiName: interaction.qtiName,
2015
- path: interaction.source?.path,
2016
- source: interaction.source,
2017
- }),
2018
- );
2019
-
2020
- if (!interaction.object) return;
2021
- diagnostics.push(
2022
- ...validateSharedVocabularyMediaPlayerControls({
2023
- value: interaction.object.attributes["data-qti-media-player-controls"],
2024
- subjectQtiName: `${interaction.qtiName} media object`,
2025
- path: interaction.object.source?.path ?? interaction.source?.path,
2026
- source: interaction.object.source ?? interaction.source,
2027
- }),
2028
- );
2029
- }
2030
-
2031
1723
  function sharedClassNames(attributes: Record<string, string>): string[] {
2032
1724
  return (attributes.class ?? "").split(/\s+/).filter(Boolean);
2033
1725
  }