@longsightgroup/qti3-core 0.7.3 → 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 +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 +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 +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 +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 +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 +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 +144 -0
  117. package/src/types.ts +42 -7
  118. package/src/validation.ts +33 -341
  119. package/src/xml.ts +376 -276
@@ -0,0 +1,174 @@
1
+ import {
2
+ coerceValue,
3
+ normalizeValueForCardinality,
4
+ parseCardinality,
5
+ parseCoords,
6
+ parseShape,
7
+ } from "./parser-values.js";
8
+ import type {
9
+ QtiBaseType,
10
+ QtiLookupTable,
11
+ QtiOutcomeDeclaration,
12
+ QtiResponseDeclaration,
13
+ QtiScalarValue,
14
+ QtiTemplateDeclaration,
15
+ QtiValue,
16
+ } from "./types.js";
17
+ import { childElements, textContent, type XmlNode } from "./xml.js";
18
+
19
+ export function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration {
20
+ const cardinality = parseCardinality(node.attributes.cardinality);
21
+ const baseType = node.attributes["base-type"] as QtiResponseDeclaration["baseType"];
22
+ return {
23
+ kind: "response",
24
+ identifier: node.attributes.identifier ?? "",
25
+ cardinality,
26
+ baseType,
27
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
28
+ correctResponse: normalizeValueForCardinality(
29
+ parseVariableValue(childElements(node, "qti-correct-response")[0], baseType),
30
+ cardinality,
31
+ ),
32
+ mapping: parseMapping(childElements(node, "qti-mapping")[0]),
33
+ areaMapping: parseAreaMapping(childElements(node, "qti-area-mapping")[0]),
34
+ attributes: node.attributes,
35
+ source: node.source,
36
+ };
37
+ }
38
+
39
+ export function parseOutcomeDeclaration(node: XmlNode): QtiOutcomeDeclaration {
40
+ const baseType = node.attributes["base-type"] as QtiOutcomeDeclaration["baseType"];
41
+ return {
42
+ kind: "outcome",
43
+ identifier: node.attributes.identifier ?? "",
44
+ cardinality: parseCardinality(node.attributes.cardinality),
45
+ baseType,
46
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
47
+ lookupTable: parseLookupTable(node, baseType),
48
+ attributes: node.attributes,
49
+ source: node.source,
50
+ };
51
+ }
52
+
53
+ export function parseTemplateDeclaration(node: XmlNode): QtiTemplateDeclaration {
54
+ const baseType = node.attributes["base-type"] as QtiTemplateDeclaration["baseType"];
55
+ return {
56
+ kind: "template",
57
+ identifier: node.attributes.identifier ?? "",
58
+ cardinality: parseCardinality(node.attributes.cardinality),
59
+ baseType,
60
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
61
+ attributes: node.attributes,
62
+ source: node.source,
63
+ };
64
+ }
65
+
66
+ function parseVariableValue(
67
+ node: XmlNode | undefined,
68
+ baseType: QtiBaseType | undefined,
69
+ ): QtiValue {
70
+ if (!node) return null;
71
+ const valueNodes = childElements(node, "qti-value");
72
+ const entries = valueNodes.map((valueNode) => ({
73
+ fieldIdentifier: valueNode.attributes["field-identifier"],
74
+ value: coerceValue(textContent(valueNode), valueNode.attributes["base-type"] ?? baseType),
75
+ }));
76
+ const recordEntries = entries.filter(
77
+ (entry): entry is { fieldIdentifier: string; value: QtiScalarValue } =>
78
+ Boolean(entry.fieldIdentifier),
79
+ );
80
+ if (recordEntries.length > 0) {
81
+ return Object.fromEntries(recordEntries.map((entry) => [entry.fieldIdentifier, entry.value]));
82
+ }
83
+ if (entries.length === 0) {
84
+ const text = textContent(node);
85
+ return text.length > 0 ? coerceValue(text, baseType) : null;
86
+ }
87
+ if (entries.length === 1) return entries[0]?.value ?? null;
88
+ return entries.map((entry) => entry.value);
89
+ }
90
+
91
+ function parseMapping(node: XmlNode | undefined): QtiResponseDeclaration["mapping"] | undefined {
92
+ if (!node) return undefined;
93
+ return {
94
+ defaultValue: Number(node.attributes["default-value"] ?? 0),
95
+ attributes: node.attributes,
96
+ source: node.source,
97
+ entries: childElements(node, "qti-map-entry").map((entry) => ({
98
+ mapKey: entry.attributes["map-key"],
99
+ mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
100
+ attributes: entry.attributes,
101
+ source: entry.source,
102
+ })),
103
+ };
104
+ }
105
+
106
+ function parseAreaMapping(
107
+ node: XmlNode | undefined,
108
+ ): QtiResponseDeclaration["areaMapping"] | undefined {
109
+ if (!node) return undefined;
110
+ return {
111
+ defaultValue: Number(node.attributes["default-value"] ?? 0),
112
+ attributes: node.attributes,
113
+ source: node.source,
114
+ entries: childElements(node, "qti-area-map-entry").map((entry) => ({
115
+ shape: parseShape(entry.attributes.shape),
116
+ coords: parseCoords(entry.attributes.coords),
117
+ mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
118
+ attributes: entry.attributes,
119
+ source: entry.source,
120
+ })),
121
+ };
122
+ }
123
+
124
+ function parseLookupTable(
125
+ node: XmlNode,
126
+ baseType: QtiOutcomeDeclaration["baseType"],
127
+ ): QtiLookupTable | undefined {
128
+ const matchTable = childElements(node, "qti-match-table")[0];
129
+ if (matchTable) return parseMatchTable(matchTable, baseType);
130
+ const interpolationTable = childElements(node, "qti-interpolation-table")[0];
131
+ if (interpolationTable) return parseInterpolationTable(interpolationTable, baseType);
132
+ return undefined;
133
+ }
134
+
135
+ function parseMatchTable(
136
+ node: XmlNode,
137
+ baseType: QtiOutcomeDeclaration["baseType"],
138
+ ): QtiLookupTable {
139
+ return {
140
+ type: "match",
141
+ defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
142
+ attributes: node.attributes,
143
+ source: node.source,
144
+ entries: childElements(node, "qti-match-table-entry").map((entry) => ({
145
+ sourceValue: Number(entry.attributes["source-value"]),
146
+ targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
147
+ attributes: entry.attributes,
148
+ source: entry.source,
149
+ })),
150
+ };
151
+ }
152
+
153
+ function parseInterpolationTable(
154
+ node: XmlNode,
155
+ baseType: QtiOutcomeDeclaration["baseType"],
156
+ ): QtiLookupTable {
157
+ return {
158
+ type: "interpolation",
159
+ defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
160
+ attributes: node.attributes,
161
+ source: node.source,
162
+ entries: childElements(node, "qti-interpolation-table-entry").map((entry) => ({
163
+ sourceValue: Number(entry.attributes["source-value"]),
164
+ targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
165
+ includeBoundary: entry.attributes["include-boundary"] !== "false",
166
+ attributes: entry.attributes,
167
+ source: entry.source,
168
+ })),
169
+ };
170
+ }
171
+
172
+ function parseLookupValue(value: string | undefined, baseType: string | undefined): QtiValue {
173
+ return value === undefined ? null : coerceValue(value, baseType);
174
+ }
@@ -0,0 +1,194 @@
1
+ import type {
2
+ QtiCatalogCard,
3
+ QtiCatalogCardEntry,
4
+ QtiCatalogFileHref,
5
+ QtiCatalogHtmlContent,
6
+ QtiCatalogInfo,
7
+ QtiCatalogReference,
8
+ QtiCompanionMaterialsInfo,
9
+ QtiCompanionMaterialsUnparsedChild,
10
+ QtiContentNode,
11
+ QtiDiagnostic,
12
+ QtiModalFeedback,
13
+ QtiPhysicalMaterial,
14
+ QtiStylesheet,
15
+ } from "./types.js";
16
+ import { childElements, descendants, textContent, type XmlNode } from "./xml.js";
17
+
18
+ export function firstChildElement(
19
+ parent: XmlNode,
20
+ childName: string,
21
+ diagnostics: QtiDiagnostic[],
22
+ duplicateCode: string,
23
+ ): XmlNode | undefined {
24
+ const matches = childElements(parent, childName);
25
+ if (matches.length > 1) {
26
+ diagnostics.push({
27
+ code: duplicateCode,
28
+ severity: "error",
29
+ message: `${parent.localName} allows at most one ${childName} child.`,
30
+ path: matches[1]?.source?.path ?? parent.source?.path,
31
+ source: matches[1]?.source ?? parent.source,
32
+ });
33
+ }
34
+ return matches[0];
35
+ }
36
+
37
+ export function parseStylesheet(node: XmlNode): QtiStylesheet {
38
+ return {
39
+ href: node.attributes.href ?? "",
40
+ type: node.attributes.type,
41
+ media: node.attributes.media,
42
+ title: node.attributes.title,
43
+ attributes: node.attributes,
44
+ source: node.source,
45
+ };
46
+ }
47
+
48
+ export function parseCatalogReferences(node: XmlNode): QtiCatalogReference[] {
49
+ const references = [
50
+ ...(node.attributes["data-catalog-idref"] ? [node] : []),
51
+ ...descendants(node, (child) => Boolean(child.attributes["data-catalog-idref"])),
52
+ ];
53
+ return references.map((reference) => ({
54
+ idref: reference.attributes["data-catalog-idref"] ?? "",
55
+ source: reference.source,
56
+ }));
57
+ }
58
+
59
+ export function parseModalFeedback(node: XmlNode): QtiModalFeedback {
60
+ const showHide = node.attributes["show-hide"] === "hide" ? "hide" : "show";
61
+ return {
62
+ identifier: node.attributes.identifier ?? "",
63
+ outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
64
+ showHide,
65
+ text: textContent(node),
66
+ source: node.source,
67
+ };
68
+ }
69
+
70
+ export function parseCatalogInfo(node: XmlNode | undefined): QtiCatalogInfo | undefined {
71
+ if (!node) return undefined;
72
+ return {
73
+ catalogs: childElements(node, "qti-catalog").map((catalog) => ({
74
+ id: catalog.attributes.id ?? "",
75
+ attributes: catalog.attributes,
76
+ cards: childElements(catalog, "qti-card").map(parseCatalogCard),
77
+ source: catalog.source,
78
+ })),
79
+ source: node.source,
80
+ };
81
+ }
82
+
83
+ function parseCatalogCard(node: XmlNode): QtiCatalogCard {
84
+ return {
85
+ support: node.attributes.support ?? "",
86
+ htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
87
+ fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
88
+ entries: childElements(node, "qti-card-entry").map(parseCatalogCardEntry),
89
+ attributes: node.attributes,
90
+ source: node.source,
91
+ };
92
+ }
93
+
94
+ function parseCatalogCardEntry(node: XmlNode): QtiCatalogCardEntry {
95
+ return {
96
+ language: node.attributes["xml:lang"] ?? node.attributes.lang,
97
+ default: node.attributes.default === "true",
98
+ htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
99
+ fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
100
+ attributes: node.attributes,
101
+ source: node.source,
102
+ };
103
+ }
104
+
105
+ function parseCatalogHtmlContent(node: XmlNode | undefined): QtiCatalogHtmlContent | undefined {
106
+ if (!node) return undefined;
107
+ return {
108
+ text: textContent(node),
109
+ children: parseCatalogHtmlChildren(node),
110
+ attributes: node.attributes,
111
+ source: node.source,
112
+ };
113
+ }
114
+
115
+ function parseCatalogHtmlChildren(node: XmlNode): QtiContentNode[] {
116
+ const content: QtiContentNode[] = [];
117
+ for (const entry of node.content) {
118
+ if (typeof entry === "string") {
119
+ if (entry.length > 0) content.push({ kind: "text", text: entry, source: node.source });
120
+ continue;
121
+ }
122
+ content.push({
123
+ kind: "element",
124
+ qtiName: entry.localName,
125
+ attributes: entry.attributes,
126
+ children: parseCatalogHtmlChildren(entry),
127
+ source: entry.source,
128
+ });
129
+ }
130
+ return content;
131
+ }
132
+
133
+ function parseCatalogFileHref(node: XmlNode): QtiCatalogFileHref {
134
+ return {
135
+ href: textContent(node).trim(),
136
+ mimeType: node.attributes["mime-type"],
137
+ attributes: node.attributes,
138
+ source: node.source,
139
+ };
140
+ }
141
+
142
+ export function parseCompanionMaterialsInfo(
143
+ node: XmlNode | undefined,
144
+ diagnostics: QtiDiagnostic[],
145
+ ): QtiCompanionMaterialsInfo | undefined {
146
+ if (!node) return undefined;
147
+
148
+ const physicalMaterials: QtiPhysicalMaterial[] = [];
149
+ const unparsedChildren: QtiCompanionMaterialsUnparsedChild[] = [];
150
+
151
+ for (const child of childElements(node)) {
152
+ if (child.localName === "qti-physical-material") {
153
+ const material = parsePhysicalMaterial(child, diagnostics);
154
+ if (material) physicalMaterials.push(material);
155
+ continue;
156
+ }
157
+
158
+ unparsedChildren.push({ qtiName: child.localName, source: child.source });
159
+ diagnostics.push({
160
+ code: "companionMaterials.child.unsupported",
161
+ severity: "warning",
162
+ message: `${child.localName} is not currently modeled in companion materials parsing.`,
163
+ path: child.source?.path,
164
+ source: child.source,
165
+ });
166
+ }
167
+
168
+ return {
169
+ physicalMaterials,
170
+ unparsedChildren,
171
+ source: node.source,
172
+ };
173
+ }
174
+
175
+ function parsePhysicalMaterial(
176
+ node: XmlNode,
177
+ diagnostics: QtiDiagnostic[],
178
+ ): QtiPhysicalMaterial | undefined {
179
+ const text = textContent(node).trim();
180
+ if (text.length === 0) {
181
+ diagnostics.push({
182
+ code: "companionMaterials.physicalMaterial.empty",
183
+ severity: "warning",
184
+ message: "qti-physical-material requires non-empty text content.",
185
+ path: node.source?.path,
186
+ source: node.source,
187
+ });
188
+ return undefined;
189
+ }
190
+ return {
191
+ text,
192
+ source: node.source,
193
+ };
194
+ }