@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.
- package/README.md +21 -2
- package/dist/attempt-state.d.ts.map +1 -1
- package/dist/attempt-state.js +20 -27
- package/dist/attempt-state.js.map +1 -1
- package/dist/content-text.d.ts +4 -0
- package/dist/content-text.d.ts.map +1 -1
- package/dist/content-text.js +137 -12
- package/dist/content-text.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/media-definition.d.ts +55 -0
- package/dist/media-definition.d.ts.map +1 -0
- package/dist/media-definition.js +97 -0
- package/dist/media-definition.js.map +1 -0
- package/dist/package-xml.d.ts +1 -0
- package/dist/package-xml.d.ts.map +1 -1
- package/dist/package-xml.js +1 -0
- package/dist/package-xml.js.map +1 -1
- package/dist/parser-custom-interactions.js +1 -0
- package/dist/parser-custom-interactions.js.map +1 -1
- package/dist/parser-declarations.d.ts +4 -4
- package/dist/parser-declarations.d.ts.map +1 -1
- package/dist/parser-declarations.js +87 -46
- package/dist/parser-declarations.js.map +1 -1
- package/dist/parser-item-metadata.d.ts.map +1 -1
- package/dist/parser-item-metadata.js +5 -3
- package/dist/parser-item-metadata.js.map +1 -1
- package/dist/parser-processing.js +10 -9
- package/dist/parser-processing.js.map +1 -1
- package/dist/parser-values.d.ts +6 -0
- package/dist/parser-values.d.ts.map +1 -1
- package/dist/parser-values.js +35 -2
- package/dist/parser-values.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +81 -47
- package/dist/parser.js.map +1 -1
- package/dist/processing-evaluator-collection.d.ts.map +1 -1
- package/dist/processing-evaluator-collection.js +8 -2
- package/dist/processing-evaluator-collection.js.map +1 -1
- package/dist/processing-evaluator-comparison.d.ts.map +1 -1
- package/dist/processing-evaluator-comparison.js +22 -6
- package/dist/processing-evaluator-comparison.js.map +1 -1
- package/dist/processing-evaluator-numeric.d.ts.map +1 -1
- package/dist/processing-evaluator-numeric.js +8 -1
- package/dist/processing-evaluator-numeric.js.map +1 -1
- package/dist/processing-evaluator-variable.d.ts.map +1 -1
- package/dist/processing-evaluator-variable.js +1 -1
- package/dist/processing-evaluator-variable.js.map +1 -1
- package/dist/processing-expression-children.d.ts.map +1 -1
- package/dist/processing-expression-children.js +2 -1
- package/dist/processing-expression-children.js.map +1 -1
- package/dist/processing-limits.d.ts +3 -0
- package/dist/processing-limits.d.ts.map +1 -0
- package/dist/processing-limits.js +3 -0
- package/dist/processing-limits.js.map +1 -0
- package/dist/processing-mapping.d.ts.map +1 -1
- package/dist/processing-mapping.js +28 -9
- package/dist/processing-mapping.js.map +1 -1
- package/dist/processing-operators.d.ts +2 -0
- package/dist/processing-operators.d.ts.map +1 -1
- package/dist/processing-operators.js +34 -5
- package/dist/processing-operators.js.map +1 -1
- package/dist/processing-values.d.ts +4 -4
- package/dist/processing-values.d.ts.map +1 -1
- package/dist/processing-values.js +28 -14
- package/dist/processing-values.js.map +1 -1
- package/dist/processing-variables.d.ts +3 -1
- package/dist/processing-variables.d.ts.map +1 -1
- package/dist/processing-variables.js +38 -2
- package/dist/processing-variables.js.map +1 -1
- package/dist/qti-namespaces.d.ts +7 -0
- package/dist/qti-namespaces.d.ts.map +1 -0
- package/dist/qti-namespaces.js +7 -0
- package/dist/qti-namespaces.js.map +1 -0
- package/dist/qti-package-items.d.ts.map +1 -1
- package/dist/qti-package-items.js +23 -13
- package/dist/qti-package-items.js.map +1 -1
- package/dist/qti-package-manifest.d.ts.map +1 -1
- package/dist/qti-package-manifest.js +4 -3
- package/dist/qti-package-manifest.js.map +1 -1
- package/dist/qti-package-metadata.d.ts.map +1 -1
- package/dist/qti-package-metadata.js +3 -2
- package/dist/qti-package-metadata.js.map +1 -1
- package/dist/qti-package-xml.d.ts +4 -2
- package/dist/qti-package-xml.d.ts.map +1 -1
- package/dist/qti-package-xml.js +10 -5
- package/dist/qti-package-xml.js.map +1 -1
- package/dist/qti-package-zip.d.ts +17 -1
- package/dist/qti-package-zip.d.ts.map +1 -1
- package/dist/qti-package-zip.js +60 -5
- package/dist/qti-package-zip.js.map +1 -1
- package/dist/qti-package.d.ts +3 -3
- package/dist/qti-package.d.ts.map +1 -1
- package/dist/qti-package.js +10 -4
- package/dist/qti-package.js.map +1 -1
- package/dist/response-validation-policy.d.ts.map +1 -1
- package/dist/response-validation-policy.js +5 -6
- package/dist/response-validation-policy.js.map +1 -1
- package/dist/response-validation.d.ts +12 -2
- package/dist/response-validation.d.ts.map +1 -1
- package/dist/response-validation.js +122 -19
- package/dist/response-validation.js.map +1 -1
- package/dist/serializer-processing-expressions.js +1 -1
- package/dist/serializer-processing-expressions.js.map +1 -1
- package/dist/session.js +1 -1
- package/dist/session.js.map +1 -1
- package/dist/slider-definition.d.ts +59 -0
- package/dist/slider-definition.d.ts.map +1 -0
- package/dist/slider-definition.js +198 -0
- package/dist/slider-definition.js.map +1 -0
- package/dist/support-evidence.d.ts +6 -2
- package/dist/support-evidence.d.ts.map +1 -1
- package/dist/support-evidence.js +15 -11
- package/dist/support-evidence.js.map +1 -1
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +96 -100
- package/dist/support.js.map +1 -1
- package/dist/trusted-item-session.d.ts.map +1 -1
- package/dist/trusted-item-session.js +5 -3
- package/dist/trusted-item-session.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation-declaration-entries.d.ts +13 -0
- package/dist/validation-declaration-entries.d.ts.map +1 -0
- package/dist/validation-declaration-entries.js +136 -0
- package/dist/validation-declaration-entries.js.map +1 -0
- package/dist/validation-interactions.d.ts.map +1 -1
- package/dist/validation-interactions.js +14 -46
- package/dist/validation-interactions.js.map +1 -1
- package/dist/validation-primitives.d.ts +5 -1
- package/dist/validation-primitives.d.ts.map +1 -1
- package/dist/validation-primitives.js +41 -3
- package/dist/validation-primitives.js.map +1 -1
- package/dist/validation-processing.d.ts.map +1 -1
- package/dist/validation-processing.js +125 -1
- package/dist/validation-processing.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +8 -134
- package/dist/validation.js.map +1 -1
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +31 -2
- package/dist/xml.js.map +1 -1
- package/package.json +1 -1
- package/src/attempt-state.ts +22 -26
- package/src/content-text.ts +174 -14
- package/src/index.ts +28 -0
- package/src/media-definition.ts +174 -0
- package/src/package-xml.ts +2 -0
- package/src/parser-custom-interactions.ts +1 -0
- package/src/parser-declarations.ts +134 -40
- package/src/parser-item-metadata.ts +5 -3
- package/src/parser-processing.ts +10 -9
- package/src/parser-values.ts +35 -2
- package/src/parser.ts +137 -103
- package/src/processing-evaluator-collection.ts +7 -2
- package/src/processing-evaluator-comparison.ts +30 -5
- package/src/processing-evaluator-numeric.ts +5 -1
- package/src/processing-evaluator-variable.ts +1 -0
- package/src/processing-expression-children.ts +2 -1
- package/src/processing-limits.ts +2 -0
- package/src/processing-mapping.ts +32 -8
- package/src/processing-operators.ts +38 -5
- package/src/processing-values.ts +59 -14
- package/src/processing-variables.ts +47 -2
- package/src/qti-namespaces.ts +8 -0
- package/src/qti-package-items.ts +56 -49
- package/src/qti-package-manifest.ts +13 -4
- package/src/qti-package-metadata.ts +3 -2
- package/src/qti-package-xml.ts +16 -3
- package/src/qti-package-zip.ts +128 -3
- package/src/qti-package.ts +12 -3
- package/src/response-validation-policy.ts +5 -5
- package/src/response-validation.ts +185 -20
- package/src/serializer-processing-expressions.ts +1 -1
- package/src/session.ts +1 -0
- package/src/slider-definition.ts +363 -0
- package/src/support-evidence.ts +15 -17
- package/src/support.ts +106 -102
- package/src/trusted-item-session.ts +5 -3
- package/src/types.ts +10 -1
- package/src/validation-declaration-entries.ts +162 -0
- package/src/validation-interactions.ts +14 -64
- package/src/validation-primitives.ts +44 -4
- package/src/validation-processing.ts +145 -1
- package/src/validation.ts +11 -151
- package/src/xml.ts +40 -4
package/src/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendContentTextNode, flatTextFromContent } from "./content-text.js";
|
|
1
|
+
import { appendContentTextNode, flatTextFromContent, visibleTextContent } from "./content-text.js";
|
|
2
2
|
import {
|
|
3
3
|
parseOutcomeDeclaration,
|
|
4
4
|
parseResponseDeclaration,
|
|
@@ -36,9 +36,9 @@ import type {
|
|
|
36
36
|
QtiResponseDeclaration,
|
|
37
37
|
} from "./types.js";
|
|
38
38
|
import { validateAssessmentItem } from "./validation.js";
|
|
39
|
-
import {
|
|
39
|
+
import { QTI_ASI_NAMESPACE } from "./qti-namespaces.js";
|
|
40
|
+
import { childElements, descendants, parseXmlTree, type XmlNode } from "./xml.js";
|
|
40
41
|
|
|
41
|
-
const qtiAssessmentItemNamespace = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0";
|
|
42
42
|
const supportedProcessingNames = new Set(processingSupport.map((entry) => entry.qtiName));
|
|
43
43
|
const processingContainerNames = new Set(["qti-template-processing", "qti-response-processing"]);
|
|
44
44
|
const responseProcessingForbiddenNames = new Set([
|
|
@@ -78,7 +78,7 @@ export function parseQtiXml(xml: string): QtiParseResult {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const itemNode =
|
|
81
|
-
tree.root.localName === "qti-assessment-item" && tree.root.uri ===
|
|
81
|
+
tree.root.localName === "qti-assessment-item" && tree.root.uri === QTI_ASI_NAMESPACE
|
|
82
82
|
? tree.root
|
|
83
83
|
: undefined;
|
|
84
84
|
if (!itemNode) {
|
|
@@ -87,7 +87,7 @@ export function parseQtiXml(xml: string): QtiParseResult {
|
|
|
87
87
|
severity: "error",
|
|
88
88
|
message:
|
|
89
89
|
tree.root.localName === "qti-assessment-item"
|
|
90
|
-
? `Expected qti-assessment-item in namespace ${
|
|
90
|
+
? `Expected qti-assessment-item in namespace ${QTI_ASI_NAMESPACE}, found ${tree.root.uri ?? "(none)"}.`
|
|
91
91
|
: `Expected qti-assessment-item root, found ${tree.root.localName}.`,
|
|
92
92
|
path: tree.root.source.path,
|
|
93
93
|
source: tree.root.source,
|
|
@@ -95,6 +95,23 @@ export function parseQtiXml(xml: string): QtiParseResult {
|
|
|
95
95
|
return { ok: false, diagnostics };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
const foreignQtiElements = descendants(
|
|
99
|
+
itemNode,
|
|
100
|
+
(node) => node.localName.startsWith("qti-") && node.uri !== QTI_ASI_NAMESPACE,
|
|
101
|
+
);
|
|
102
|
+
if (foreignQtiElements.length > 0) {
|
|
103
|
+
for (const node of foreignQtiElements) {
|
|
104
|
+
diagnostics.push({
|
|
105
|
+
code: "qti.namespace",
|
|
106
|
+
severity: "error",
|
|
107
|
+
message: `Expected ${node.localName} in namespace ${QTI_ASI_NAMESPACE}, found ${node.uri ?? "(none)"}.`,
|
|
108
|
+
path: node.source.path,
|
|
109
|
+
source: node.source,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return { ok: false, diagnostics };
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
const item = parseAssessmentItem(itemNode, diagnostics);
|
|
99
116
|
const document: QtiDocument = { item, diagnostics };
|
|
100
117
|
const validation = validateAssessmentItem(document);
|
|
@@ -109,16 +126,16 @@ export function parseQtiXml(xml: string): QtiParseResult {
|
|
|
109
126
|
function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAssessmentItem {
|
|
110
127
|
const identifier = node.attributes.identifier ?? "";
|
|
111
128
|
const responseDeclarations = childElements(node, "qti-response-declaration").map(
|
|
112
|
-
parseResponseDeclaration,
|
|
129
|
+
(declarationNode) => parseResponseDeclaration(declarationNode, diagnostics),
|
|
113
130
|
);
|
|
114
131
|
const responseDeclarationMap = new Map(
|
|
115
132
|
responseDeclarations.map((declaration) => [declaration.identifier, declaration]),
|
|
116
133
|
);
|
|
117
134
|
const outcomeDeclarations = childElements(node, "qti-outcome-declaration").map(
|
|
118
|
-
parseOutcomeDeclaration,
|
|
135
|
+
(declarationNode) => parseOutcomeDeclaration(declarationNode, diagnostics),
|
|
119
136
|
);
|
|
120
137
|
const templateDeclarations = childElements(node, "qti-template-declaration").map(
|
|
121
|
-
parseTemplateDeclaration,
|
|
138
|
+
(declarationNode) => parseTemplateDeclaration(declarationNode, diagnostics),
|
|
122
139
|
);
|
|
123
140
|
const templateProcessing = parseTemplateProcessing(
|
|
124
141
|
childElements(node, "qti-template-processing")[0],
|
|
@@ -166,7 +183,7 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
|
|
|
166
183
|
adaptive: parseXmlBoolean(node.attributes.adaptive) === true,
|
|
167
184
|
timeDependent: parseXmlBoolean(node.attributes["time-dependent"]),
|
|
168
185
|
attributes: node.attributes,
|
|
169
|
-
prompt: prompt ?
|
|
186
|
+
prompt: prompt ? visibleTextContent(prompt) : undefined,
|
|
170
187
|
itemBodyAttributes: itemBody?.attributes,
|
|
171
188
|
itemBodySource: itemBody?.source,
|
|
172
189
|
responseDeclarations,
|
|
@@ -181,7 +198,10 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
|
|
|
181
198
|
catalogReferences,
|
|
182
199
|
stylesheets,
|
|
183
200
|
body,
|
|
184
|
-
bodyText:
|
|
201
|
+
bodyText: flatTextFromContent(body, {
|
|
202
|
+
excludeAnnotations: true,
|
|
203
|
+
interactionText: (interactionIndex) => interactions[interactionIndex]?.prompt ?? "",
|
|
204
|
+
}),
|
|
185
205
|
source: node.source,
|
|
186
206
|
};
|
|
187
207
|
}
|
|
@@ -223,7 +243,18 @@ function diagnoseProcessingElements(
|
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
function isInteractionElement(node: XmlNode): boolean {
|
|
226
|
-
return
|
|
246
|
+
return (
|
|
247
|
+
node.uri === QTI_ASI_NAMESPACE &&
|
|
248
|
+
(interactionNameToType.has(node.localName) || /^qti-.+-interaction$/.test(node.localName))
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isQtiElement(node: XmlNode, localName?: string): boolean {
|
|
253
|
+
return node.uri === QTI_ASI_NAMESPACE && (!localName || node.localName === localName);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isQtiContentElement(node: XmlNode, localNames: ReadonlySet<string>): boolean {
|
|
257
|
+
return node.uri === QTI_ASI_NAMESPACE && localNames.has(node.localName);
|
|
227
258
|
}
|
|
228
259
|
|
|
229
260
|
function parseContentChildren(
|
|
@@ -262,7 +293,7 @@ function parseContentNode(
|
|
|
262
293
|
};
|
|
263
294
|
}
|
|
264
295
|
|
|
265
|
-
if (node
|
|
296
|
+
if (isQtiElement(node, "qti-printed-variable")) {
|
|
266
297
|
return {
|
|
267
298
|
kind: "printedVariable",
|
|
268
299
|
identifier: node.attributes.identifier ?? "",
|
|
@@ -272,7 +303,7 @@ function parseContentNode(
|
|
|
272
303
|
};
|
|
273
304
|
}
|
|
274
305
|
|
|
275
|
-
if (node
|
|
306
|
+
if (isQtiElement(node, "qti-feedback-block") || isQtiElement(node, "qti-feedback-inline")) {
|
|
276
307
|
return {
|
|
277
308
|
kind: "feedback",
|
|
278
309
|
feedbackType: node.localName === "qti-feedback-block" ? "block" : "inline",
|
|
@@ -288,6 +319,7 @@ function parseContentNode(
|
|
|
288
319
|
return {
|
|
289
320
|
kind: "element",
|
|
290
321
|
qtiName: node.localName,
|
|
322
|
+
namespaceUri: node.uri,
|
|
291
323
|
attributes: node.attributes,
|
|
292
324
|
children: parseContentChildren(node, diagnostics, responseDeclarationMap, interactions),
|
|
293
325
|
source: node.source,
|
|
@@ -304,7 +336,7 @@ function parseInteraction(
|
|
|
304
336
|
const responseDeclaration = responseIdentifier
|
|
305
337
|
? responseDeclarationMap.get(responseIdentifier)
|
|
306
338
|
: undefined;
|
|
307
|
-
const prompt = childElements(node, "qti-prompt")
|
|
339
|
+
const prompt = childElements(node, "qti-prompt").find((child) => isQtiElement(child));
|
|
308
340
|
const registryStatus = interactionRegistryStatus(node.localName);
|
|
309
341
|
diagnostics.push(...interactionRegistryDiagnostics(node.localName, node.source));
|
|
310
342
|
|
|
@@ -315,10 +347,7 @@ function parseInteraction(
|
|
|
315
347
|
? mediaInteractionObject(node)
|
|
316
348
|
: interactionType === "drawing"
|
|
317
349
|
? drawingInteractionObject(node)
|
|
318
|
-
: descendants(
|
|
319
|
-
node,
|
|
320
|
-
(child) => child.localName === "object" || child.localName === "img",
|
|
321
|
-
)[0];
|
|
350
|
+
: descendants(node, (child) => isQtiContentElement(child, objectElementNames))[0];
|
|
322
351
|
|
|
323
352
|
const promptContent = prompt
|
|
324
353
|
? parseContentChildren(prompt, diagnostics, responseDeclarationMap, [])
|
|
@@ -334,7 +363,7 @@ function parseInteraction(
|
|
|
334
363
|
prompt: promptContent
|
|
335
364
|
? flatTextFromContent(promptContent, { excludeAnnotations: true }) || undefined
|
|
336
365
|
: prompt
|
|
337
|
-
?
|
|
366
|
+
? visibleTextContent(prompt)
|
|
338
367
|
: undefined,
|
|
339
368
|
promptContent: promptContent && promptContent.length > 0 ? promptContent : undefined,
|
|
340
369
|
promptAttributes: prompt?.attributes,
|
|
@@ -352,51 +381,42 @@ function parseInteraction(
|
|
|
352
381
|
interactionType === "gapMatch" || interactionType === "graphicGapMatch"
|
|
353
382
|
? parseGapMatchSegments(node)
|
|
354
383
|
: undefined,
|
|
355
|
-
childElements: childElements(node)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
384
|
+
childElements: childElements(node)
|
|
385
|
+
.filter((child) => isQtiElement(child))
|
|
386
|
+
.map((child) => ({
|
|
387
|
+
qtiName: child.localName,
|
|
388
|
+
source: child.source,
|
|
389
|
+
})),
|
|
359
390
|
attributes: node.attributes,
|
|
360
|
-
text:
|
|
391
|
+
text: visibleTextContent(node),
|
|
361
392
|
source: node.source,
|
|
362
393
|
};
|
|
363
394
|
}
|
|
364
395
|
|
|
365
396
|
function positionObjectInteractionObject(node: XmlNode): XmlNode | undefined {
|
|
366
|
-
return childElements(node).find(
|
|
367
|
-
(child) => child.localName === "object" || child.localName === "img",
|
|
368
|
-
);
|
|
397
|
+
return childElements(node).find((child) => isQtiContentElement(child, objectElementNames));
|
|
369
398
|
}
|
|
370
399
|
|
|
371
400
|
function mediaInteractionObject(node: XmlNode): XmlNode | undefined {
|
|
372
|
-
return childElements(node).find(
|
|
373
|
-
(child) =>
|
|
374
|
-
child.localName === "audio" ||
|
|
375
|
-
child.localName === "video" ||
|
|
376
|
-
child.localName === "object" ||
|
|
377
|
-
child.localName === "img",
|
|
378
|
-
);
|
|
401
|
+
return childElements(node).find((child) => isQtiContentElement(child, mediaElementNames));
|
|
379
402
|
}
|
|
380
403
|
|
|
381
404
|
function drawingInteractionObject(node: XmlNode): XmlNode | undefined {
|
|
382
|
-
return childElements(node).find(
|
|
383
|
-
(child) =>
|
|
384
|
-
child.localName === "object" || child.localName === "img" || child.localName === "picture",
|
|
385
|
-
);
|
|
405
|
+
return childElements(node).find((child) => isQtiContentElement(child, drawingElementNames));
|
|
386
406
|
}
|
|
387
407
|
|
|
388
408
|
function positionObjectStageObject(node: XmlNode): XmlNode | undefined {
|
|
389
409
|
const ancestorStage = nearestAncestor(node, "qti-position-object-stage");
|
|
390
|
-
const stage =
|
|
410
|
+
const stage =
|
|
411
|
+
ancestorStage ??
|
|
412
|
+
childElements(node, "qti-position-object-stage").find((child) => isQtiElement(child));
|
|
391
413
|
if (!stage) return undefined;
|
|
392
|
-
return childElements(stage).find(
|
|
393
|
-
(child) => child.localName === "object" || child.localName === "img",
|
|
394
|
-
);
|
|
414
|
+
return childElements(stage).find((child) => isQtiContentElement(child, objectElementNames));
|
|
395
415
|
}
|
|
396
416
|
|
|
397
417
|
function nearestAncestor(node: XmlNode, localName: string): XmlNode | undefined {
|
|
398
418
|
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
399
|
-
if (parent
|
|
419
|
+
if (isQtiElement(parent, localName)) return parent;
|
|
400
420
|
}
|
|
401
421
|
return undefined;
|
|
402
422
|
}
|
|
@@ -411,13 +431,13 @@ function parseHottextSegments(node: XmlNode): QtiInteraction["hottextSegments"]
|
|
|
411
431
|
return;
|
|
412
432
|
}
|
|
413
433
|
|
|
414
|
-
if (entry
|
|
434
|
+
if (isQtiElement(entry, "qti-prompt")) return;
|
|
415
435
|
|
|
416
|
-
if (entry
|
|
436
|
+
if (isQtiElement(entry, "qti-hottext")) {
|
|
417
437
|
segments.push({
|
|
418
438
|
kind: "hottext",
|
|
419
439
|
identifier: entry.attributes.identifier ?? "",
|
|
420
|
-
text:
|
|
440
|
+
text: visibleTextContent(entry),
|
|
421
441
|
attributes: entry.attributes,
|
|
422
442
|
source: entry.source,
|
|
423
443
|
});
|
|
@@ -425,7 +445,7 @@ function parseHottextSegments(node: XmlNode): QtiInteraction["hottextSegments"]
|
|
|
425
445
|
}
|
|
426
446
|
|
|
427
447
|
for (const child of entry.content) visit(child);
|
|
428
|
-
if (entry
|
|
448
|
+
if (isQtiContentElement(entry, blockBoundaryElementNames)) {
|
|
429
449
|
segments.push({ kind: "text", text: " " });
|
|
430
450
|
}
|
|
431
451
|
};
|
|
@@ -444,11 +464,11 @@ function parseGapMatchSegments(node: XmlNode): QtiInteraction["gapMatchSegments"
|
|
|
444
464
|
return;
|
|
445
465
|
}
|
|
446
466
|
|
|
447
|
-
if (entry
|
|
448
|
-
if (entry
|
|
449
|
-
if (entry
|
|
467
|
+
if (isQtiElement(entry, "qti-prompt")) return;
|
|
468
|
+
if (isQtiElement(entry, "qti-gap-text") || isQtiElement(entry, "qti-gap-img")) return;
|
|
469
|
+
if (isQtiContentElement(entry, objectElementNames)) return;
|
|
450
470
|
|
|
451
|
-
if (entry
|
|
471
|
+
if (isQtiElement(entry, "qti-gap")) {
|
|
452
472
|
segments.push({
|
|
453
473
|
kind: "gap",
|
|
454
474
|
identifier: entry.attributes.identifier ?? "",
|
|
@@ -459,7 +479,7 @@ function parseGapMatchSegments(node: XmlNode): QtiInteraction["gapMatchSegments"
|
|
|
459
479
|
}
|
|
460
480
|
|
|
461
481
|
for (const child of entry.content) visit(child);
|
|
462
|
-
if (entry
|
|
482
|
+
if (isQtiContentElement(entry, blockBoundaryElementNames)) {
|
|
463
483
|
segments.push({ kind: "text", text: " " });
|
|
464
484
|
}
|
|
465
485
|
};
|
|
@@ -475,7 +495,7 @@ function inlineInteractionContext(
|
|
|
475
495
|
if (interactionType !== "inlineChoice" && interactionType !== "textEntry") return undefined;
|
|
476
496
|
const parent = node.parent;
|
|
477
497
|
if (!parent) return undefined;
|
|
478
|
-
return normalizeInlineContext(parent.text) ?? normalizeInlineContext(
|
|
498
|
+
return normalizeInlineContext(parent.text) ?? normalizeInlineContext(visibleTextContent(parent));
|
|
479
499
|
}
|
|
480
500
|
|
|
481
501
|
function normalizeInlineContext(value: string): string | undefined {
|
|
@@ -488,7 +508,10 @@ function normalizeInlineContext(value: string): string | undefined {
|
|
|
488
508
|
|
|
489
509
|
function parseObjectAsset(node: XmlNode | undefined): QtiObjectAsset | undefined {
|
|
490
510
|
if (!node) return undefined;
|
|
491
|
-
const pictureImage =
|
|
511
|
+
const pictureImage =
|
|
512
|
+
node.localName === "picture"
|
|
513
|
+
? childElements(node, "img").find((child) => isQtiElement(child))
|
|
514
|
+
: undefined;
|
|
492
515
|
const data = node.attributes.data ?? node.attributes.src ?? pictureImage?.attributes.src;
|
|
493
516
|
const sources = parseMediaSources(node);
|
|
494
517
|
const tracks = parseMediaTracks(node);
|
|
@@ -505,7 +528,7 @@ function parseObjectAsset(node: XmlNode | undefined): QtiObjectAsset | undefined
|
|
|
505
528
|
node.attributes.height ?? pictureImage?.attributes.height ?? inferredSvgDimensions?.height,
|
|
506
529
|
sources,
|
|
507
530
|
tracks,
|
|
508
|
-
text:
|
|
531
|
+
text: visibleTextContent(node) || node.attributes.alt || pictureImage?.attributes.alt || "",
|
|
509
532
|
attributes: node.attributes,
|
|
510
533
|
source: node.source,
|
|
511
534
|
};
|
|
@@ -596,27 +619,31 @@ function formatDimension(value: number): string {
|
|
|
596
619
|
}
|
|
597
620
|
|
|
598
621
|
function parseMediaSources(node: XmlNode): QtiMediaSource[] {
|
|
599
|
-
return childElements(node, "source")
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
src
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
622
|
+
return childElements(node, "source")
|
|
623
|
+
.filter((source) => isQtiElement(source))
|
|
624
|
+
.map((source) => {
|
|
625
|
+
const src = source.attributes.src ?? firstSrcsetCandidate(source.attributes.srcset);
|
|
626
|
+
return {
|
|
627
|
+
src,
|
|
628
|
+
type: source.attributes.type ?? assetTypeFromData(src),
|
|
629
|
+
attributes: source.attributes,
|
|
630
|
+
source: source.source,
|
|
631
|
+
};
|
|
632
|
+
});
|
|
608
633
|
}
|
|
609
634
|
|
|
610
635
|
function parseMediaTracks(node: XmlNode): QtiObjectAsset["tracks"] {
|
|
611
|
-
return childElements(node, "track")
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
636
|
+
return childElements(node, "track")
|
|
637
|
+
.filter((track) => isQtiElement(track))
|
|
638
|
+
.map((track) => ({
|
|
639
|
+
kind: track.attributes.kind,
|
|
640
|
+
src: track.attributes.src,
|
|
641
|
+
srclang: track.attributes.srclang,
|
|
642
|
+
label: track.attributes.label,
|
|
643
|
+
default: track.attributes.default !== undefined,
|
|
644
|
+
attributes: track.attributes,
|
|
645
|
+
source: track.source,
|
|
646
|
+
}));
|
|
620
647
|
}
|
|
621
648
|
|
|
622
649
|
function firstSourceType(sources: QtiMediaSource[]): string | undefined {
|
|
@@ -663,37 +690,38 @@ function parseChoices(
|
|
|
663
690
|
"qti-gap",
|
|
664
691
|
]);
|
|
665
692
|
|
|
666
|
-
return descendants(node, (child) =>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
693
|
+
return descendants(node, (child) => isQtiContentElement(child, choiceNames)).map(
|
|
694
|
+
(choice, index) => {
|
|
695
|
+
const identifier = choice.attributes.identifier ?? "";
|
|
696
|
+
const asset = parseChoiceAsset(choice);
|
|
697
|
+
const content = parseContentChildren(choice, diagnostics, responseDeclarationMap, []);
|
|
698
|
+
const flatChoiceText =
|
|
699
|
+
content.length > 0
|
|
700
|
+
? flatTextFromContent(content, { excludeAnnotations: true })
|
|
701
|
+
: visibleTextContent(choice);
|
|
702
|
+
return {
|
|
703
|
+
identifier,
|
|
704
|
+
text:
|
|
705
|
+
flatChoiceText ||
|
|
706
|
+
choice.attributes["object-label"] ||
|
|
707
|
+
asset?.text ||
|
|
708
|
+
identifier ||
|
|
709
|
+
`Choice ${index + 1}`,
|
|
710
|
+
content: content.length > 0 ? content : undefined,
|
|
711
|
+
asset,
|
|
712
|
+
role: choiceRole(choice),
|
|
713
|
+
qtiName: choice.localName,
|
|
714
|
+
attributes: choice.attributes,
|
|
715
|
+
source: choice.source,
|
|
716
|
+
};
|
|
717
|
+
},
|
|
718
|
+
);
|
|
690
719
|
}
|
|
691
720
|
|
|
692
721
|
function parseChoiceAsset(choice: XmlNode): QtiObjectAsset | undefined {
|
|
693
722
|
if (choice.localName !== "qti-gap-img") return undefined;
|
|
694
|
-
const assetNode = childElements(choice).find(
|
|
695
|
-
(child)
|
|
696
|
-
child.localName === "img" || child.localName === "object" || child.localName === "picture",
|
|
723
|
+
const assetNode = childElements(choice).find((child) =>
|
|
724
|
+
isQtiContentElement(child, drawingElementNames),
|
|
697
725
|
);
|
|
698
726
|
return parseObjectAsset(assetNode);
|
|
699
727
|
}
|
|
@@ -708,7 +736,8 @@ function choiceRole(node: XmlNode): QtiChoiceRole {
|
|
|
708
736
|
return "hotspot";
|
|
709
737
|
}
|
|
710
738
|
if (node.localName === "qti-simple-associable-choice") {
|
|
711
|
-
const matchSet =
|
|
739
|
+
const matchSet =
|
|
740
|
+
node.parent && isQtiElement(node.parent, "qti-simple-match-set") ? node.parent : undefined;
|
|
712
741
|
const interaction = nearestInteraction(node);
|
|
713
742
|
if (matchSet && interaction?.localName === "qti-match-interaction") {
|
|
714
743
|
return matchSetIndex(matchSet) === 0 ? "matchSource" : "matchTarget";
|
|
@@ -720,13 +749,18 @@ function choiceRole(node: XmlNode): QtiChoiceRole {
|
|
|
720
749
|
|
|
721
750
|
function nearestInteraction(node: XmlNode): XmlNode | undefined {
|
|
722
751
|
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
723
|
-
if (interactionNameToType.has(parent.localName)) return parent;
|
|
752
|
+
if (isQtiElement(parent) && interactionNameToType.has(parent.localName)) return parent;
|
|
724
753
|
}
|
|
725
754
|
return undefined;
|
|
726
755
|
}
|
|
727
756
|
|
|
728
757
|
function matchSetIndex(node: XmlNode): number {
|
|
729
758
|
const siblings =
|
|
730
|
-
node.parent?.children.filter((sibling) => sibling
|
|
759
|
+
node.parent?.children.filter((sibling) => isQtiElement(sibling, "qti-simple-match-set")) ?? [];
|
|
731
760
|
return siblings.indexOf(node);
|
|
732
761
|
}
|
|
762
|
+
|
|
763
|
+
const objectElementNames = new Set(["object", "img"]);
|
|
764
|
+
const mediaElementNames = new Set(["audio", "video", ...objectElementNames]);
|
|
765
|
+
const drawingElementNames = new Set(["picture", ...objectElementNames]);
|
|
766
|
+
const blockBoundaryElementNames = new Set(["p", "div"]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { QtiProcessingExpression, QtiScalarValue, QtiValue } from "./types.js";
|
|
2
2
|
import { assertNever } from "./assert-never.js";
|
|
3
3
|
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
4
|
+
import { MAX_QTI_REPEAT_RESULT_ELEMENTS } from "./processing-limits.js";
|
|
4
5
|
import { valueContainer } from "./processing-values.js";
|
|
5
6
|
|
|
6
7
|
type CollectionExpression = Extract<
|
|
@@ -39,11 +40,15 @@ export function evaluateRepeatExpression(
|
|
|
39
40
|
context: EvaluationContext,
|
|
40
41
|
): QtiValue {
|
|
41
42
|
const repeats = context.indexValue(expression.numberRepeats);
|
|
42
|
-
if (repeats === undefined || repeats < 1)
|
|
43
|
+
if (repeats === undefined || repeats < 1 || repeats > MAX_QTI_REPEAT_RESULT_ELEMENTS) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
43
46
|
const container: QtiScalarValue[] = [];
|
|
44
47
|
for (let repeat = 0; repeat < repeats; repeat += 1) {
|
|
45
48
|
for (const item of expression.expressions) {
|
|
46
|
-
|
|
49
|
+
const values = valueContainer(context.evaluate(item));
|
|
50
|
+
if (values.length > MAX_QTI_REPEAT_RESULT_ELEMENTS - container.length) return null;
|
|
51
|
+
for (const value of values) container.push(value);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
return container.length > 0 ? container : null;
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
valuesEqual,
|
|
10
10
|
valueContainer,
|
|
11
11
|
} from "./processing-values.js";
|
|
12
|
-
import { expressionIsOrdered } from "./processing-variables.js";
|
|
12
|
+
import { expressionBaseType, expressionIsOrdered } from "./processing-variables.js";
|
|
13
13
|
import { roundWithMode } from "./processing-operators.js";
|
|
14
14
|
|
|
15
15
|
type ComparisonExpression = Extract<
|
|
@@ -40,14 +40,30 @@ export function evaluateComparisonExpression(
|
|
|
40
40
|
right,
|
|
41
41
|
expressionIsOrdered(expression.left, context.document) ||
|
|
42
42
|
expressionIsOrdered(expression.right, context.document),
|
|
43
|
+
expressionBaseType(expression.left, context.document) ??
|
|
44
|
+
expressionBaseType(expression.right, context.document),
|
|
43
45
|
);
|
|
44
46
|
}
|
|
45
47
|
case "equal": {
|
|
46
48
|
const left = context.evaluate(expression.left);
|
|
47
49
|
const right = context.evaluate(expression.right);
|
|
48
|
-
return left === null || right === null
|
|
50
|
+
return left === null || right === null
|
|
51
|
+
? null
|
|
52
|
+
: valuesEqual(
|
|
53
|
+
left,
|
|
54
|
+
right,
|
|
55
|
+
false,
|
|
56
|
+
expressionBaseType(expression.left, context.document) ??
|
|
57
|
+
expressionBaseType(expression.right, context.document),
|
|
58
|
+
);
|
|
49
59
|
}
|
|
50
60
|
case "equalRounded": {
|
|
61
|
+
const validRounding =
|
|
62
|
+
Number.isInteger(expression.figures) &&
|
|
63
|
+
(expression.roundingMode === "decimalPlaces"
|
|
64
|
+
? expression.figures >= 0
|
|
65
|
+
: expression.roundingMode === "significantFigures" && expression.figures > 0);
|
|
66
|
+
if (!validRounding) return null;
|
|
51
67
|
const left = context.evaluate(expression.left);
|
|
52
68
|
const right = context.evaluate(expression.right);
|
|
53
69
|
if (left === null || right === null) return null;
|
|
@@ -80,7 +96,10 @@ export function evaluateComparisonExpression(
|
|
|
80
96
|
const value = context.evaluate(expression.value);
|
|
81
97
|
const collection = context.evaluate(expression.collection);
|
|
82
98
|
if (value === null || collection === null) return null;
|
|
83
|
-
|
|
99
|
+
const baseType =
|
|
100
|
+
expressionBaseType(expression.value, context.document) ??
|
|
101
|
+
expressionBaseType(expression.collection, context.document);
|
|
102
|
+
return valueContainer(collection).some((item) => valuesEqual(item, value, false, baseType));
|
|
84
103
|
}
|
|
85
104
|
case "delete": {
|
|
86
105
|
const value = context.evaluate(expression.value);
|
|
@@ -88,7 +107,10 @@ export function evaluateComparisonExpression(
|
|
|
88
107
|
if (value === null || collectionValue === null) return null;
|
|
89
108
|
const collection = valueContainer(collectionValue);
|
|
90
109
|
if (collection.length === 0) return null;
|
|
91
|
-
const
|
|
110
|
+
const baseType =
|
|
111
|
+
expressionBaseType(expression.value, context.document) ??
|
|
112
|
+
expressionBaseType(expression.collection, context.document);
|
|
113
|
+
const filtered = collection.filter((item) => !valuesEqual(item, value, false, baseType));
|
|
92
114
|
return filtered.length > 0 ? filtered : null;
|
|
93
115
|
}
|
|
94
116
|
case "contains": {
|
|
@@ -98,7 +120,10 @@ export function evaluateComparisonExpression(
|
|
|
98
120
|
const collection = valueContainer(collectionValue);
|
|
99
121
|
const values = valueContainer(valuesValue);
|
|
100
122
|
if (collection.length === 0 || values.length === 0) return null;
|
|
101
|
-
|
|
123
|
+
const baseType =
|
|
124
|
+
expressionBaseType(expression.collection, context.document) ??
|
|
125
|
+
expressionBaseType(expression.values, context.document);
|
|
126
|
+
return containsValues(collection, values, baseType);
|
|
102
127
|
}
|
|
103
128
|
default:
|
|
104
129
|
return assertNever(expression);
|
|
@@ -47,10 +47,12 @@ export function evaluateNumericExpression(
|
|
|
47
47
|
): QtiValue {
|
|
48
48
|
switch (expression.type) {
|
|
49
49
|
case "sum": {
|
|
50
|
+
if (expression.expressions.length === 0) return null;
|
|
50
51
|
const values = context.numericOperands(expression.expressions);
|
|
51
52
|
return values ? values.reduce((sum, value) => sum + value, 0) : null;
|
|
52
53
|
}
|
|
53
54
|
case "product": {
|
|
55
|
+
if (expression.expressions.length === 0) return null;
|
|
54
56
|
const values = context.numericOperands(expression.expressions);
|
|
55
57
|
return values ? values.reduce((product, value) => product * value, 1) : null;
|
|
56
58
|
}
|
|
@@ -127,7 +129,9 @@ export function evaluateNumericExpression(
|
|
|
127
129
|
return mathOperatorValue(expression.name, values.map(numericValue));
|
|
128
130
|
}
|
|
129
131
|
case "statsOperator": {
|
|
130
|
-
const
|
|
132
|
+
const expressions = expression.expressions ?? [expression.expression];
|
|
133
|
+
if (expressions.length !== 1) return null;
|
|
134
|
+
const value = context.evaluate(expressions[0]!);
|
|
131
135
|
if (value === null) return null;
|
|
132
136
|
return statsOperatorValue(expression.name, valueContainer(value).map(numericValue));
|
|
133
137
|
}
|
|
@@ -43,8 +43,9 @@ export function expressionChildren(expression: QtiProcessingExpression): QtiProc
|
|
|
43
43
|
case "patternMatch":
|
|
44
44
|
case "fieldValue":
|
|
45
45
|
case "inside":
|
|
46
|
-
case "statsOperator":
|
|
47
46
|
return [expression.expression];
|
|
47
|
+
case "statsOperator":
|
|
48
|
+
return expression.expressions ?? [expression.expression];
|
|
48
49
|
case "member":
|
|
49
50
|
case "delete":
|
|
50
51
|
return [expression.value, expression.collection];
|