@longsightgroup/qti3-core 0.10.2 → 0.10.4

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 (188) hide show
  1. package/README.md +21 -2
  2. package/dist/attempt-state.d.ts.map +1 -1
  3. package/dist/attempt-state.js +20 -27
  4. package/dist/attempt-state.js.map +1 -1
  5. package/dist/content-text.d.ts +4 -0
  6. package/dist/content-text.d.ts.map +1 -1
  7. package/dist/content-text.js +137 -12
  8. package/dist/content-text.js.map +1 -1
  9. package/dist/index.d.ts +6 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/media-definition.d.ts +55 -0
  14. package/dist/media-definition.d.ts.map +1 -0
  15. package/dist/media-definition.js +97 -0
  16. package/dist/media-definition.js.map +1 -0
  17. package/dist/package-xml.d.ts +1 -0
  18. package/dist/package-xml.d.ts.map +1 -1
  19. package/dist/package-xml.js +1 -0
  20. package/dist/package-xml.js.map +1 -1
  21. package/dist/parser-custom-interactions.js +1 -0
  22. package/dist/parser-custom-interactions.js.map +1 -1
  23. package/dist/parser-declarations.d.ts +4 -4
  24. package/dist/parser-declarations.d.ts.map +1 -1
  25. package/dist/parser-declarations.js +87 -46
  26. package/dist/parser-declarations.js.map +1 -1
  27. package/dist/parser-item-metadata.d.ts.map +1 -1
  28. package/dist/parser-item-metadata.js +5 -3
  29. package/dist/parser-item-metadata.js.map +1 -1
  30. package/dist/parser-processing.js +10 -9
  31. package/dist/parser-processing.js.map +1 -1
  32. package/dist/parser-values.d.ts +6 -0
  33. package/dist/parser-values.d.ts.map +1 -1
  34. package/dist/parser-values.js +35 -2
  35. package/dist/parser-values.js.map +1 -1
  36. package/dist/parser.d.ts.map +1 -1
  37. package/dist/parser.js +81 -47
  38. package/dist/parser.js.map +1 -1
  39. package/dist/processing-evaluator-collection.d.ts.map +1 -1
  40. package/dist/processing-evaluator-collection.js +8 -2
  41. package/dist/processing-evaluator-collection.js.map +1 -1
  42. package/dist/processing-evaluator-comparison.d.ts.map +1 -1
  43. package/dist/processing-evaluator-comparison.js +22 -6
  44. package/dist/processing-evaluator-comparison.js.map +1 -1
  45. package/dist/processing-evaluator-numeric.d.ts.map +1 -1
  46. package/dist/processing-evaluator-numeric.js +8 -1
  47. package/dist/processing-evaluator-numeric.js.map +1 -1
  48. package/dist/processing-evaluator-variable.d.ts.map +1 -1
  49. package/dist/processing-evaluator-variable.js +1 -1
  50. package/dist/processing-evaluator-variable.js.map +1 -1
  51. package/dist/processing-expression-children.d.ts.map +1 -1
  52. package/dist/processing-expression-children.js +2 -1
  53. package/dist/processing-expression-children.js.map +1 -1
  54. package/dist/processing-limits.d.ts +3 -0
  55. package/dist/processing-limits.d.ts.map +1 -0
  56. package/dist/processing-limits.js +3 -0
  57. package/dist/processing-limits.js.map +1 -0
  58. package/dist/processing-mapping.d.ts.map +1 -1
  59. package/dist/processing-mapping.js +28 -9
  60. package/dist/processing-mapping.js.map +1 -1
  61. package/dist/processing-operators.d.ts +2 -0
  62. package/dist/processing-operators.d.ts.map +1 -1
  63. package/dist/processing-operators.js +34 -5
  64. package/dist/processing-operators.js.map +1 -1
  65. package/dist/processing-values.d.ts +4 -4
  66. package/dist/processing-values.d.ts.map +1 -1
  67. package/dist/processing-values.js +28 -14
  68. package/dist/processing-values.js.map +1 -1
  69. package/dist/processing-variables.d.ts +3 -1
  70. package/dist/processing-variables.d.ts.map +1 -1
  71. package/dist/processing-variables.js +38 -2
  72. package/dist/processing-variables.js.map +1 -1
  73. package/dist/qti-namespaces.d.ts +7 -0
  74. package/dist/qti-namespaces.d.ts.map +1 -0
  75. package/dist/qti-namespaces.js +7 -0
  76. package/dist/qti-namespaces.js.map +1 -0
  77. package/dist/qti-package-items.d.ts.map +1 -1
  78. package/dist/qti-package-items.js +23 -13
  79. package/dist/qti-package-items.js.map +1 -1
  80. package/dist/qti-package-manifest.d.ts.map +1 -1
  81. package/dist/qti-package-manifest.js +4 -3
  82. package/dist/qti-package-manifest.js.map +1 -1
  83. package/dist/qti-package-metadata.d.ts.map +1 -1
  84. package/dist/qti-package-metadata.js +3 -2
  85. package/dist/qti-package-metadata.js.map +1 -1
  86. package/dist/qti-package-xml.d.ts +4 -2
  87. package/dist/qti-package-xml.d.ts.map +1 -1
  88. package/dist/qti-package-xml.js +10 -5
  89. package/dist/qti-package-xml.js.map +1 -1
  90. package/dist/qti-package-zip.d.ts +17 -1
  91. package/dist/qti-package-zip.d.ts.map +1 -1
  92. package/dist/qti-package-zip.js +60 -5
  93. package/dist/qti-package-zip.js.map +1 -1
  94. package/dist/qti-package.d.ts +3 -3
  95. package/dist/qti-package.d.ts.map +1 -1
  96. package/dist/qti-package.js +10 -4
  97. package/dist/qti-package.js.map +1 -1
  98. package/dist/response-validation-policy.d.ts.map +1 -1
  99. package/dist/response-validation-policy.js +5 -6
  100. package/dist/response-validation-policy.js.map +1 -1
  101. package/dist/response-validation.d.ts +12 -2
  102. package/dist/response-validation.d.ts.map +1 -1
  103. package/dist/response-validation.js +122 -19
  104. package/dist/response-validation.js.map +1 -1
  105. package/dist/serializer-processing-expressions.js +1 -1
  106. package/dist/serializer-processing-expressions.js.map +1 -1
  107. package/dist/session.js +1 -1
  108. package/dist/session.js.map +1 -1
  109. package/dist/slider-definition.d.ts +59 -0
  110. package/dist/slider-definition.d.ts.map +1 -0
  111. package/dist/slider-definition.js +198 -0
  112. package/dist/slider-definition.js.map +1 -0
  113. package/dist/support-evidence.d.ts +6 -2
  114. package/dist/support-evidence.d.ts.map +1 -1
  115. package/dist/support-evidence.js +15 -11
  116. package/dist/support-evidence.js.map +1 -1
  117. package/dist/support.d.ts.map +1 -1
  118. package/dist/support.js +96 -100
  119. package/dist/support.js.map +1 -1
  120. package/dist/trusted-item-session.d.ts.map +1 -1
  121. package/dist/trusted-item-session.js +5 -3
  122. package/dist/trusted-item-session.js.map +1 -1
  123. package/dist/types.d.ts +5 -0
  124. package/dist/types.d.ts.map +1 -1
  125. package/dist/validation-declaration-entries.d.ts +13 -0
  126. package/dist/validation-declaration-entries.d.ts.map +1 -0
  127. package/dist/validation-declaration-entries.js +136 -0
  128. package/dist/validation-declaration-entries.js.map +1 -0
  129. package/dist/validation-interactions.d.ts.map +1 -1
  130. package/dist/validation-interactions.js +14 -46
  131. package/dist/validation-interactions.js.map +1 -1
  132. package/dist/validation-primitives.d.ts +5 -1
  133. package/dist/validation-primitives.d.ts.map +1 -1
  134. package/dist/validation-primitives.js +41 -3
  135. package/dist/validation-primitives.js.map +1 -1
  136. package/dist/validation-processing.d.ts.map +1 -1
  137. package/dist/validation-processing.js +125 -1
  138. package/dist/validation-processing.js.map +1 -1
  139. package/dist/validation.d.ts.map +1 -1
  140. package/dist/validation.js +8 -134
  141. package/dist/validation.js.map +1 -1
  142. package/dist/xml.d.ts.map +1 -1
  143. package/dist/xml.js +31 -2
  144. package/dist/xml.js.map +1 -1
  145. package/package.json +1 -1
  146. package/src/attempt-state.ts +22 -26
  147. package/src/content-text.ts +174 -14
  148. package/src/index.ts +28 -0
  149. package/src/media-definition.ts +174 -0
  150. package/src/package-xml.ts +2 -0
  151. package/src/parser-custom-interactions.ts +1 -0
  152. package/src/parser-declarations.ts +134 -40
  153. package/src/parser-item-metadata.ts +5 -3
  154. package/src/parser-processing.ts +10 -9
  155. package/src/parser-values.ts +35 -2
  156. package/src/parser.ts +137 -103
  157. package/src/processing-evaluator-collection.ts +7 -2
  158. package/src/processing-evaluator-comparison.ts +30 -5
  159. package/src/processing-evaluator-numeric.ts +5 -1
  160. package/src/processing-evaluator-variable.ts +1 -0
  161. package/src/processing-expression-children.ts +2 -1
  162. package/src/processing-limits.ts +2 -0
  163. package/src/processing-mapping.ts +32 -8
  164. package/src/processing-operators.ts +38 -5
  165. package/src/processing-values.ts +59 -14
  166. package/src/processing-variables.ts +47 -2
  167. package/src/qti-namespaces.ts +8 -0
  168. package/src/qti-package-items.ts +56 -49
  169. package/src/qti-package-manifest.ts +13 -4
  170. package/src/qti-package-metadata.ts +3 -2
  171. package/src/qti-package-xml.ts +16 -3
  172. package/src/qti-package-zip.ts +128 -3
  173. package/src/qti-package.ts +12 -3
  174. package/src/response-validation-policy.ts +5 -5
  175. package/src/response-validation.ts +185 -20
  176. package/src/serializer-processing-expressions.ts +1 -1
  177. package/src/session.ts +1 -0
  178. package/src/slider-definition.ts +363 -0
  179. package/src/support-evidence.ts +15 -17
  180. package/src/support.ts +106 -102
  181. package/src/trusted-item-session.ts +5 -3
  182. package/src/types.ts +10 -1
  183. package/src/validation-declaration-entries.ts +162 -0
  184. package/src/validation-interactions.ts +14 -64
  185. package/src/validation-primitives.ts +44 -4
  186. package/src/validation-processing.ts +145 -1
  187. package/src/validation.ts +11 -151
  188. package/src/xml.ts +40 -4
@@ -4,11 +4,13 @@ import {
4
4
  parseBaseType,
5
5
  parseCardinality,
6
6
  parseCoords,
7
+ parseFiniteNumber,
7
8
  parseShape,
8
9
  parseXmlBoolean,
9
10
  } from "./parser-values.js";
10
11
  import type {
11
12
  QtiBaseType,
13
+ QtiDiagnostic,
12
14
  QtiLookupTable,
13
15
  QtiOutcomeDeclaration,
14
16
  QtiResponseDeclaration,
@@ -16,9 +18,17 @@ import type {
16
18
  QtiTemplateDeclaration,
17
19
  QtiValue,
18
20
  } from "./types.js";
21
+ import {
22
+ validateAreaMapEntryAttributes,
23
+ validateLookupTableEntryAttributes,
24
+ validateMapEntryAttributes,
25
+ } from "./validation-declaration-entries.js";
19
26
  import { childElements, textContent, type XmlNode } from "./xml.js";
20
27
 
21
- export function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration {
28
+ export function parseResponseDeclaration(
29
+ node: XmlNode,
30
+ diagnostics: QtiDiagnostic[],
31
+ ): QtiResponseDeclaration {
22
32
  const cardinality = parseCardinality(node.attributes.cardinality);
23
33
  const baseType = parseBaseType(node.attributes["base-type"]);
24
34
  return {
@@ -26,40 +36,66 @@ export function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration
26
36
  identifier: node.attributes.identifier ?? "",
27
37
  cardinality,
28
38
  baseType,
29
- defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
39
+ defaultValue: parseVariableValue(
40
+ childElements(node, "qti-default-value")[0],
41
+ baseType,
42
+ diagnostics,
43
+ ),
30
44
  correctResponse: normalizeValueForCardinality(
31
- parseVariableValue(childElements(node, "qti-correct-response")[0], baseType),
45
+ parseVariableValue(childElements(node, "qti-correct-response")[0], baseType, diagnostics),
32
46
  cardinality,
33
47
  ),
34
- mapping: parseMapping(childElements(node, "qti-mapping")[0]),
35
- areaMapping: parseAreaMapping(childElements(node, "qti-area-mapping")[0]),
48
+ mapping: parseMapping(
49
+ childElements(node, "qti-mapping")[0],
50
+ node.attributes.identifier ?? "",
51
+ diagnostics,
52
+ ),
53
+ areaMapping: parseAreaMapping(
54
+ childElements(node, "qti-area-mapping")[0],
55
+ node.attributes.identifier ?? "",
56
+ diagnostics,
57
+ ),
36
58
  attributes: node.attributes,
37
59
  source: node.source,
38
60
  };
39
61
  }
40
62
 
41
- export function parseOutcomeDeclaration(node: XmlNode): QtiOutcomeDeclaration {
63
+ export function parseOutcomeDeclaration(
64
+ node: XmlNode,
65
+ diagnostics: QtiDiagnostic[],
66
+ ): QtiOutcomeDeclaration {
42
67
  const baseType = parseBaseType(node.attributes["base-type"]);
43
68
  return {
44
69
  kind: "outcome",
45
70
  identifier: node.attributes.identifier ?? "",
46
71
  cardinality: parseCardinality(node.attributes.cardinality),
47
72
  baseType,
48
- defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
49
- lookupTable: parseLookupTable(node, baseType),
73
+ defaultValue: parseVariableValue(
74
+ childElements(node, "qti-default-value")[0],
75
+ baseType,
76
+ diagnostics,
77
+ ),
78
+ lookupTable: parseLookupTable(node, baseType, diagnostics),
50
79
  attributes: node.attributes,
51
80
  source: node.source,
52
81
  };
53
82
  }
54
83
 
55
- export function parseTemplateDeclaration(node: XmlNode): QtiTemplateDeclaration {
84
+ export function parseTemplateDeclaration(
85
+ node: XmlNode,
86
+ diagnostics: QtiDiagnostic[],
87
+ ): QtiTemplateDeclaration {
56
88
  const baseType = parseBaseType(node.attributes["base-type"]);
57
89
  return {
58
90
  kind: "template",
59
91
  identifier: node.attributes.identifier ?? "",
60
92
  cardinality: parseCardinality(node.attributes.cardinality),
61
93
  baseType,
62
- defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
94
+ defaultValue: parseVariableValue(
95
+ childElements(node, "qti-default-value")[0],
96
+ baseType,
97
+ diagnostics,
98
+ ),
63
99
  attributes: node.attributes,
64
100
  source: node.source,
65
101
  };
@@ -68,6 +104,7 @@ export function parseTemplateDeclaration(node: XmlNode): QtiTemplateDeclaration
68
104
  function parseVariableValue(
69
105
  node: XmlNode | undefined,
70
106
  baseType: QtiBaseType | undefined,
107
+ diagnostics: QtiDiagnostic[],
71
108
  ): QtiValue {
72
109
  if (!node) return null;
73
110
  const valueNodes = childElements(node, "qti-value");
@@ -79,6 +116,16 @@ function parseVariableValue(
79
116
  (entry): entry is { fieldIdentifier: string; value: QtiScalarValue } =>
80
117
  Boolean(entry.fieldIdentifier),
81
118
  );
119
+ const fieldedEntryCount = entries.filter((entry) => entry.fieldIdentifier !== undefined).length;
120
+ if (fieldedEntryCount > 0 && fieldedEntryCount < entries.length) {
121
+ diagnostics.push({
122
+ code: "declaration.value.fieldIdentifier.mixed",
123
+ severity: "error",
124
+ message: `${node.localName} must not mix qti-value children with and without field-identifier.`,
125
+ path: node.source.path,
126
+ source: node.source,
127
+ });
128
+ }
82
129
  if (recordEntries.length > 0) {
83
130
  return Object.fromEntries(recordEntries.map((entry) => [entry.fieldIdentifier, entry.value]));
84
131
  }
@@ -90,87 +137,134 @@ function parseVariableValue(
90
137
  return entries.map((entry) => entry.value);
91
138
  }
92
139
 
93
- function parseMapping(node: XmlNode | undefined): QtiResponseDeclaration["mapping"] | undefined {
140
+ function parseMapping(
141
+ node: XmlNode | undefined,
142
+ declarationIdentifier: string,
143
+ diagnostics: QtiDiagnostic[],
144
+ ): QtiResponseDeclaration["mapping"] | undefined {
94
145
  if (!node) return undefined;
95
- return {
96
- defaultValue: Number(node.attributes["default-value"] ?? 0),
97
- attributes: node.attributes,
98
- source: node.source,
99
- entries: childElements(node, "qti-map-entry").map((entry) => ({
146
+ const defaultValue = parseFiniteNumber(node.attributes["default-value"]);
147
+ const entries: NonNullable<QtiResponseDeclaration["mapping"]>["entries"] = [];
148
+ for (const entry of childElements(node, "qti-map-entry")) {
149
+ const mappedValue = parseFiniteNumber(entry.attributes["mapped-value"]);
150
+ if (mappedValue === undefined) {
151
+ validateMapEntryAttributes(declarationIdentifier, entry, diagnostics);
152
+ continue;
153
+ }
154
+ entries.push({
100
155
  mapKey: entry.attributes["map-key"],
101
- mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
156
+ mappedValue,
102
157
  attributes: entry.attributes,
103
158
  source: entry.source,
104
- })),
159
+ });
160
+ }
161
+ return {
162
+ defaultValue: defaultValue ?? 0,
163
+ attributes: node.attributes,
164
+ source: node.source,
165
+ entries,
105
166
  };
106
167
  }
107
168
 
108
169
  function parseAreaMapping(
109
170
  node: XmlNode | undefined,
171
+ declarationIdentifier: string,
172
+ diagnostics: QtiDiagnostic[],
110
173
  ): QtiResponseDeclaration["areaMapping"] | undefined {
111
174
  if (!node) return undefined;
112
- return {
113
- defaultValue: Number(node.attributes["default-value"] ?? 0),
114
- attributes: node.attributes,
115
- source: node.source,
116
- entries: childElements(node, "qti-area-map-entry").map((entry) => ({
175
+ const defaultValue = parseFiniteNumber(node.attributes["default-value"]);
176
+ const entries: NonNullable<QtiResponseDeclaration["areaMapping"]>["entries"] = [];
177
+ for (const entry of childElements(node, "qti-area-map-entry")) {
178
+ const mappedValue = parseFiniteNumber(entry.attributes["mapped-value"]);
179
+ if (mappedValue === undefined) {
180
+ validateAreaMapEntryAttributes(declarationIdentifier, entry, diagnostics);
181
+ continue;
182
+ }
183
+ entries.push({
117
184
  shape: parseShape(entry.attributes.shape),
118
185
  coords: parseCoords(entry.attributes.coords),
119
- mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
186
+ mappedValue,
120
187
  attributes: entry.attributes,
121
188
  source: entry.source,
122
- })),
189
+ });
190
+ }
191
+ return {
192
+ defaultValue: defaultValue ?? 0,
193
+ attributes: node.attributes,
194
+ source: node.source,
195
+ entries,
123
196
  };
124
197
  }
125
198
 
126
199
  function parseLookupTable(
127
200
  node: XmlNode,
128
201
  baseType: QtiOutcomeDeclaration["baseType"],
202
+ diagnostics: QtiDiagnostic[],
129
203
  ): QtiLookupTable | undefined {
130
204
  const matchTable = childElements(node, "qti-match-table")[0];
131
- if (matchTable) return parseMatchTable(matchTable, baseType);
205
+ if (matchTable) return parseMatchTable(matchTable, baseType, diagnostics);
132
206
  const interpolationTable = childElements(node, "qti-interpolation-table")[0];
133
- if (interpolationTable) return parseInterpolationTable(interpolationTable, baseType);
207
+ if (interpolationTable) return parseInterpolationTable(interpolationTable, baseType, diagnostics);
134
208
  return undefined;
135
209
  }
136
210
 
137
211
  function parseMatchTable(
138
212
  node: XmlNode,
139
213
  baseType: QtiOutcomeDeclaration["baseType"],
214
+ diagnostics: QtiDiagnostic[],
140
215
  ): QtiLookupTable {
141
216
  return {
142
217
  type: "match",
143
218
  defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
144
219
  attributes: node.attributes,
145
220
  source: node.source,
146
- entries: childElements(node, "qti-match-table-entry").map((entry) => ({
147
- sourceValue: Number(entry.attributes["source-value"]),
148
- targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
149
- attributes: entry.attributes,
150
- source: entry.source,
151
- })),
221
+ entries: parseLookupEntries(node, "match", baseType, diagnostics),
152
222
  };
153
223
  }
154
224
 
155
225
  function parseInterpolationTable(
156
226
  node: XmlNode,
157
227
  baseType: QtiOutcomeDeclaration["baseType"],
228
+ diagnostics: QtiDiagnostic[],
158
229
  ): QtiLookupTable {
159
230
  return {
160
231
  type: "interpolation",
161
232
  defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
162
233
  attributes: node.attributes,
163
234
  source: node.source,
164
- entries: childElements(node, "qti-interpolation-table-entry").map((entry) => ({
165
- sourceValue: Number(entry.attributes["source-value"]),
166
- targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
167
- includeBoundary: parseXmlBoolean(entry.attributes["include-boundary"]) ?? true,
168
- attributes: entry.attributes,
169
- source: entry.source,
170
- })),
235
+ entries: parseLookupEntries(node, "interpolation", baseType, diagnostics),
171
236
  };
172
237
  }
173
238
 
174
239
  function parseLookupValue(value: string | undefined, baseType: string | undefined): QtiValue {
175
240
  return value === undefined ? null : coerceValue(value, baseType);
176
241
  }
242
+
243
+ function parseLookupEntries(
244
+ node: XmlNode,
245
+ tableType: QtiLookupTable["type"],
246
+ baseType: QtiOutcomeDeclaration["baseType"],
247
+ diagnostics: QtiDiagnostic[],
248
+ ): QtiLookupTable["entries"] {
249
+ const entryName =
250
+ tableType === "match" ? "qti-match-table-entry" : "qti-interpolation-table-entry";
251
+ const entries: QtiLookupTable["entries"] = [];
252
+ for (const entry of childElements(node, entryName)) {
253
+ const sourceValue = parseFiniteNumber(entry.attributes["source-value"]);
254
+ if (sourceValue === undefined) {
255
+ validateLookupTableEntryAttributes(tableType, entry, diagnostics);
256
+ continue;
257
+ }
258
+ entries.push({
259
+ sourceValue,
260
+ targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
261
+ includeBoundary:
262
+ tableType === "interpolation"
263
+ ? (parseXmlBoolean(entry.attributes["include-boundary"]) ?? true)
264
+ : undefined,
265
+ attributes: entry.attributes,
266
+ source: entry.source,
267
+ });
268
+ }
269
+ return entries;
270
+ }
@@ -19,6 +19,7 @@ import {
19
19
  type ParsedCompanionMaterialChildQtiName,
20
20
  pushCompanionMaterialParseWarning,
21
21
  } from "./companion-materials.js";
22
+ import { visibleTextContent } from "./content-text.js";
22
23
  import { parseXmlBoolean } from "./parser-values.js";
23
24
  import { childElements, descendants, textContent, type XmlNode } from "./xml.js";
24
25
 
@@ -75,7 +76,7 @@ export function parseModalFeedback(node: XmlNode): QtiModalFeedback {
75
76
  identifier: node.attributes.identifier ?? "",
76
77
  outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
77
78
  showHide,
78
- text: textContent(node),
79
+ text: visibleTextContent(node),
79
80
  source: node.source,
80
81
  };
81
82
  }
@@ -119,7 +120,7 @@ function parseCatalogCardEntry(node: XmlNode): QtiCatalogCardEntry {
119
120
  function parseCatalogHtmlContent(node: XmlNode | undefined): QtiCatalogHtmlContent | undefined {
120
121
  if (!node) return undefined;
121
122
  return {
122
- text: textContent(node),
123
+ text: visibleTextContent(node),
123
124
  children: parseCatalogHtmlChildren(node),
124
125
  attributes: node.attributes,
125
126
  source: node.source,
@@ -136,6 +137,7 @@ function parseCatalogHtmlChildren(node: XmlNode): QtiContentNode[] {
136
137
  content.push({
137
138
  kind: "element",
138
139
  qtiName: entry.localName,
140
+ namespaceUri: entry.uri,
139
141
  attributes: entry.attributes,
140
142
  children: parseCatalogHtmlChildren(entry),
141
143
  source: entry.source,
@@ -202,7 +204,7 @@ function parsePhysicalMaterial(
202
204
  node: XmlNode,
203
205
  diagnostics: QtiDiagnostic[],
204
206
  ): QtiPhysicalMaterial | undefined {
205
- const text = textContent(node).trim();
207
+ const text = visibleTextContent(node);
206
208
  if (text.length === 0) {
207
209
  return pushCompanionMaterialParseWarning(
208
210
  diagnostics,
@@ -646,15 +646,16 @@ function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
646
646
  }
647
647
 
648
648
  if (node.localName === "qti-stats-operator") {
649
- const expression = parseFirstExpression(node);
650
- if (expression) {
651
- return {
652
- type: "statsOperator",
653
- name: node.attributes.name ?? "",
654
- expression,
655
- source: node.source,
656
- };
657
- }
649
+ const expressions = childElements(node)
650
+ .map(parseExpression)
651
+ .filter((expression): expression is QtiProcessingExpression => expression !== undefined);
652
+ return {
653
+ type: "statsOperator",
654
+ name: node.attributes.name ?? "",
655
+ expression: expressions[0] ?? { type: "null", source: node.source },
656
+ expressions,
657
+ source: node.source,
658
+ };
658
659
  }
659
660
 
660
661
  if (node.localName === "qti-custom-operator") {
@@ -20,6 +20,7 @@ const QTI_BASE_TYPES = new Set<string>([
20
20
  "file",
21
21
  "uri",
22
22
  ]);
23
+ const QTI_FINITE_FLOAT_PATTERN = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?$/;
23
24
 
24
25
  export function isQtiBaseType(value: string): value is QtiBaseType {
25
26
  return QTI_BASE_TYPES.has(value);
@@ -43,8 +44,11 @@ export function numericTuple4(values: number[]): [number, number, number, number
43
44
  }
44
45
 
45
46
  export function coerceValue(value: string, baseType: string | undefined): QtiScalarValue {
46
- if (baseType === "integer") return Number.parseInt(value, 10);
47
- if (baseType === "float") return Number.parseFloat(value);
47
+ if (baseType === "integer") return parseInteger(value) ?? value;
48
+ if (baseType === "float") return parseFiniteNumber(value) ?? value;
49
+ if (baseType === "pair" || baseType === "directedPair") {
50
+ return parseQtiPair(value, baseType) ?? value;
51
+ }
48
52
  if (baseType === "boolean") {
49
53
  const parsed = parseXmlBoolean(value);
50
54
  if (parsed !== undefined) return parsed;
@@ -53,6 +57,26 @@ export function coerceValue(value: string, baseType: string | undefined): QtiSca
53
57
  return value;
54
58
  }
55
59
 
60
+ /** Parse and canonicalize a QTI pair while preserving directed-pair endpoint order. */
61
+ export function parseQtiPair(
62
+ value: string | undefined,
63
+ baseType: "pair" | "directedPair",
64
+ ): string | undefined {
65
+ if (value === undefined) return undefined;
66
+ const parts = value.trim().split(/\s+/);
67
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) return undefined;
68
+ return (baseType === "pair" ? parts.toSorted() : parts).join(" ");
69
+ }
70
+
71
+ /** Parse a complete integer lexical value without accepting a numeric prefix. */
72
+ export function parseInteger(value: string | undefined): number | undefined {
73
+ if (value === undefined) return undefined;
74
+ const normalized = value.trim();
75
+ if (!/^-?\d+$/.test(normalized)) return undefined;
76
+ const parsed = Number(normalized);
77
+ return Number.isInteger(parsed) ? parsed : undefined;
78
+ }
79
+
56
80
  export function parseCardinality(value: string | undefined): QtiCardinality {
57
81
  if (value === "multiple" || value === "ordered" || value === "record") return value;
58
82
  return "single";
@@ -66,6 +90,15 @@ export function parseXmlBoolean(value: string | undefined): boolean | undefined
66
90
  return undefined;
67
91
  }
68
92
 
93
+ /** Parse the finite subset of the XML Schema float lexical space used by QTI values. */
94
+ export function parseFiniteNumber(value: string | undefined): number | undefined {
95
+ if (value === undefined) return undefined;
96
+ const normalized = value.trim();
97
+ if (!QTI_FINITE_FLOAT_PATTERN.test(normalized)) return undefined;
98
+ const parsed = Number(normalized);
99
+ return Number.isFinite(parsed) ? parsed : undefined;
100
+ }
101
+
69
102
  export function normalizeValueForCardinality(
70
103
  value: QtiValue,
71
104
  cardinality: QtiCardinality,