@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.
- package/README.md +41 -10
- package/dist/content-text.d.ts +14 -0
- package/dist/content-text.d.ts.map +1 -0
- package/dist/content-text.js +58 -0
- package/dist/content-text.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/package-xml.d.ts +12 -0
- package/dist/package-xml.d.ts.map +1 -0
- package/dist/package-xml.js +17 -0
- package/dist/package-xml.js.map +1 -0
- package/dist/parser-custom-interactions.d.ts +8 -0
- package/dist/parser-custom-interactions.d.ts.map +1 -0
- package/dist/parser-custom-interactions.js +178 -0
- package/dist/parser-custom-interactions.js.map +1 -0
- package/dist/parser-declarations.d.ts +6 -0
- package/dist/parser-declarations.d.ts.map +1 -0
- package/dist/parser-declarations.js +136 -0
- package/dist/parser-declarations.js.map +1 -0
- package/dist/parser-item-metadata.d.ts +9 -0
- package/dist/parser-item-metadata.d.ts.map +1 -0
- package/dist/parser-item-metadata.js +158 -0
- package/dist/parser-item-metadata.js.map +1 -0
- package/dist/parser-processing.d.ts +5 -0
- package/dist/parser-processing.d.ts.map +1 -0
- package/dist/parser-processing.js +642 -0
- package/dist/parser-processing.js.map +1 -0
- package/dist/parser-values.d.ts +8 -0
- package/dist/parser-values.d.ts.map +1 -0
- package/dist/parser-values.js +49 -0
- package/dist/parser-values.js.map +1 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +32 -1086
- package/dist/parser.js.map +1 -1
- package/dist/processing-expression-tags.d.ts +56 -0
- package/dist/processing-expression-tags.d.ts.map +1 -0
- package/dist/processing-expression-tags.js +52 -0
- package/dist/processing-expression-tags.js.map +1 -0
- package/dist/processing-rules.d.ts +3 -0
- package/dist/processing-rules.d.ts.map +1 -0
- package/dist/processing-rules.js +10 -0
- package/dist/processing-rules.js.map +1 -0
- package/dist/serializer-processing-expressions.d.ts +8 -0
- package/dist/serializer-processing-expressions.d.ts.map +1 -0
- package/dist/serializer-processing-expressions.js +198 -0
- package/dist/serializer-processing-expressions.js.map +1 -0
- package/dist/serializer-processing-xml.d.ts +15 -0
- package/dist/serializer-processing-xml.d.ts.map +1 -0
- package/dist/serializer-processing-xml.js +46 -0
- package/dist/serializer-processing-xml.js.map +1 -0
- package/dist/serializer-processing.d.ts +8 -0
- package/dist/serializer-processing.d.ts.map +1 -0
- package/dist/serializer-processing.js +106 -0
- package/dist/serializer-processing.js.map +1 -0
- package/dist/shared-vocabulary-authoring.d.ts +50 -0
- package/dist/shared-vocabulary-authoring.d.ts.map +1 -0
- package/dist/shared-vocabulary-authoring.js +302 -0
- package/dist/shared-vocabulary-authoring.js.map +1 -0
- package/dist/shared-vocabulary-interaction-sets.d.ts +6 -0
- package/dist/shared-vocabulary-interaction-sets.d.ts.map +1 -0
- package/dist/shared-vocabulary-interaction-sets.js +23 -0
- package/dist/shared-vocabulary-interaction-sets.js.map +1 -0
- package/dist/shared-vocabulary-interaction-validation.d.ts +3 -0
- package/dist/shared-vocabulary-interaction-validation.d.ts.map +1 -0
- package/dist/shared-vocabulary-interaction-validation.js +99 -0
- package/dist/shared-vocabulary-interaction-validation.js.map +1 -0
- package/dist/shared-vocabulary-registry-validation.d.ts +5 -0
- package/dist/shared-vocabulary-registry-validation.d.ts.map +1 -0
- package/dist/shared-vocabulary-registry-validation.js +147 -0
- package/dist/shared-vocabulary-registry-validation.js.map +1 -0
- package/dist/shared-vocabulary-support.d.ts.map +1 -1
- package/dist/shared-vocabulary-support.js +96 -56
- package/dist/shared-vocabulary-support.js.map +1 -1
- package/dist/shared-vocabulary.d.ts +6 -0
- package/dist/shared-vocabulary.d.ts.map +1 -1
- package/dist/shared-vocabulary.js +12 -0
- package/dist/shared-vocabulary.js.map +1 -1
- package/dist/support.d.ts +4 -1
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +159 -10
- package/dist/support.js.map +1 -1
- package/dist/types.d.ts +38 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +29 -250
- package/dist/validation.js.map +1 -1
- package/dist/xml.d.ts +10 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +321 -249
- package/dist/xml.js.map +1 -1
- package/package.json +1 -4
- package/src/content-text.ts +80 -0
- package/src/index.ts +67 -0
- package/src/interaction-test-fixtures.ts +44 -0
- package/src/package-xml.ts +30 -0
- package/src/parser-custom-interactions.ts +309 -0
- package/src/parser-declarations.ts +174 -0
- package/src/parser-item-metadata.ts +194 -0
- package/src/parser-processing.ts +701 -0
- package/src/parser-values.ts +61 -0
- package/src/parser.ts +59 -1230
- package/src/processing-expression-tags.ts +65 -0
- package/src/processing-rules.ts +11 -0
- package/src/serializer-processing-expressions.ts +456 -0
- package/src/serializer-processing-xml.ts +67 -0
- package/src/serializer-processing.fixtures.ts +469 -0
- package/src/serializer-processing.ts +196 -0
- package/src/shared-vocabulary-authoring.ts +409 -0
- package/src/shared-vocabulary-interaction-sets.ts +28 -0
- package/src/shared-vocabulary-interaction-validation.ts +143 -0
- package/src/shared-vocabulary-registry-validation.ts +207 -0
- package/src/shared-vocabulary-support.ts +127 -101
- package/src/shared-vocabulary.ts +18 -0
- package/src/support.ts +174 -14
- package/src/types.ts +48 -7
- package/src/validation.ts +33 -341
- package/src/xml.ts +378 -255
package/src/parser.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { appendContentTextNode, flatTextFromContent } from "./content-text.js";
|
|
2
|
+
import {
|
|
3
|
+
parseOutcomeDeclaration,
|
|
4
|
+
parseResponseDeclaration,
|
|
5
|
+
parseTemplateDeclaration,
|
|
6
|
+
} from "./parser-declarations.js";
|
|
7
|
+
import {
|
|
8
|
+
firstChildElement,
|
|
9
|
+
parseCatalogInfo,
|
|
10
|
+
parseCatalogReferences,
|
|
11
|
+
parseCompanionMaterialsInfo,
|
|
12
|
+
parseModalFeedback,
|
|
13
|
+
parseStylesheet,
|
|
14
|
+
} from "./parser-item-metadata.js";
|
|
15
|
+
import { parseXmlBoolean } from "./parser-values.js";
|
|
16
|
+
import { parseInteractionCustomPayload } from "./parser-custom-interactions.js";
|
|
17
|
+
import { parseResponseProcessing, parseTemplateProcessing } from "./parser-processing.js";
|
|
18
|
+
import {
|
|
19
|
+
interactionNameToType,
|
|
20
|
+
interactionRegistryDiagnostics,
|
|
21
|
+
interactionRegistryStatus,
|
|
22
|
+
processingSupport,
|
|
23
|
+
} from "./support.js";
|
|
2
24
|
import type {
|
|
3
25
|
QtiAssessmentItem,
|
|
4
|
-
QtiBaseType,
|
|
5
|
-
QtiCatalogCard,
|
|
6
|
-
QtiCatalogCardEntry,
|
|
7
|
-
QtiCatalogFileHref,
|
|
8
|
-
QtiCatalogHtmlContent,
|
|
9
|
-
QtiCatalogInfo,
|
|
10
|
-
QtiCatalogReference,
|
|
11
|
-
QtiCardinality,
|
|
12
26
|
QtiChoice,
|
|
13
27
|
QtiChoiceRole,
|
|
14
28
|
QtiContentNode,
|
|
@@ -16,30 +30,10 @@ import type {
|
|
|
16
30
|
QtiDocument,
|
|
17
31
|
QtiInteraction,
|
|
18
32
|
QtiInteractionType,
|
|
19
|
-
QtiLookupOutcomeValue,
|
|
20
|
-
QtiLookupTable,
|
|
21
33
|
QtiMediaSource,
|
|
22
|
-
QtiModalFeedback,
|
|
23
34
|
QtiObjectAsset,
|
|
24
|
-
QtiOutcomeDeclaration,
|
|
25
35
|
QtiParseResult,
|
|
26
|
-
QtiProcessingExpression,
|
|
27
|
-
QtiPortableCustomDefinition,
|
|
28
|
-
QtiPortableCustomInteractionModule,
|
|
29
|
-
QtiPortableCustomInteractionModules,
|
|
30
|
-
QtiPortableCustomVariableBinding,
|
|
31
|
-
QtiRecordValue,
|
|
32
|
-
QtiResponseCondition,
|
|
33
36
|
QtiResponseDeclaration,
|
|
34
|
-
QtiResponseProcessing,
|
|
35
|
-
QtiResponseRule,
|
|
36
|
-
QtiSetOutcomeValue,
|
|
37
|
-
QtiScalarValue,
|
|
38
|
-
QtiStylesheet,
|
|
39
|
-
QtiTemplateDeclaration,
|
|
40
|
-
QtiTemplateProcessing,
|
|
41
|
-
QtiTemplateRule,
|
|
42
|
-
QtiValue,
|
|
43
37
|
} from "./types.js";
|
|
44
38
|
import { validateAssessmentItem } from "./validation.js";
|
|
45
39
|
import { childElements, descendants, parseXmlTree, textContent, type XmlNode } from "./xml.js";
|
|
@@ -147,7 +141,13 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
|
|
|
147
141
|
);
|
|
148
142
|
}
|
|
149
143
|
const modalFeedback = childElements(node, "qti-modal-feedback").map(parseModalFeedback);
|
|
150
|
-
const catalogInfo = parseCatalogInfo(
|
|
144
|
+
const catalogInfo = parseCatalogInfo(
|
|
145
|
+
firstChildElement(node, "qti-catalog-info", diagnostics, "item.child.duplicate"),
|
|
146
|
+
);
|
|
147
|
+
const companionMaterials = parseCompanionMaterialsInfo(
|
|
148
|
+
firstChildElement(node, "qti-companion-materials-info", diagnostics, "item.child.duplicate"),
|
|
149
|
+
diagnostics,
|
|
150
|
+
);
|
|
151
151
|
const catalogReferences = itemBody ? parseCatalogReferences(itemBody) : [];
|
|
152
152
|
const stylesheets = childElements(node, "qti-stylesheet").map(parseStylesheet);
|
|
153
153
|
const prompt = itemBody ? childElements(itemBody, "qti-prompt")[0] : undefined;
|
|
@@ -169,6 +169,7 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
|
|
|
169
169
|
interactions,
|
|
170
170
|
modalFeedback,
|
|
171
171
|
catalogInfo,
|
|
172
|
+
companionMaterials,
|
|
172
173
|
catalogReferences,
|
|
173
174
|
stylesheets,
|
|
174
175
|
body,
|
|
@@ -213,43 +214,10 @@ function diagnoseProcessingElements(
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
function parseStylesheet(node: XmlNode): QtiStylesheet {
|
|
217
|
-
return {
|
|
218
|
-
href: node.attributes.href ?? "",
|
|
219
|
-
type: node.attributes.type,
|
|
220
|
-
media: node.attributes.media,
|
|
221
|
-
title: node.attributes.title,
|
|
222
|
-
attributes: node.attributes,
|
|
223
|
-
source: node.source,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function parseCatalogReferences(node: XmlNode): QtiCatalogReference[] {
|
|
228
|
-
const references = [
|
|
229
|
-
...(node.attributes["data-catalog-idref"] ? [node] : []),
|
|
230
|
-
...descendants(node, (child) => Boolean(child.attributes["data-catalog-idref"])),
|
|
231
|
-
];
|
|
232
|
-
return references.map((reference) => ({
|
|
233
|
-
idref: reference.attributes["data-catalog-idref"] ?? "",
|
|
234
|
-
source: reference.source,
|
|
235
|
-
}));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
217
|
function isInteractionElement(node: XmlNode): boolean {
|
|
239
218
|
return interactionNameToType.has(node.localName) || /^qti-.+-interaction$/.test(node.localName);
|
|
240
219
|
}
|
|
241
220
|
|
|
242
|
-
function parseModalFeedback(node: XmlNode): QtiModalFeedback {
|
|
243
|
-
const showHide = node.attributes["show-hide"] === "hide" ? "hide" : "show";
|
|
244
|
-
return {
|
|
245
|
-
identifier: node.attributes.identifier ?? "",
|
|
246
|
-
outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
|
|
247
|
-
showHide,
|
|
248
|
-
text: textContent(node),
|
|
249
|
-
source: node.source,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
221
|
function parseContentChildren(
|
|
254
222
|
node: XmlNode,
|
|
255
223
|
diagnostics: QtiDiagnostic[],
|
|
@@ -259,7 +227,7 @@ function parseContentChildren(
|
|
|
259
227
|
const content: QtiContentNode[] = [];
|
|
260
228
|
for (const entry of node.content) {
|
|
261
229
|
if (typeof entry === "string") {
|
|
262
|
-
|
|
230
|
+
appendContentTextNode(content, entry, node.source);
|
|
263
231
|
continue;
|
|
264
232
|
}
|
|
265
233
|
const parsed = parseContentNode(entry, diagnostics, responseDeclarationMap, interactions);
|
|
@@ -318,125 +286,6 @@ function parseContentNode(
|
|
|
318
286
|
};
|
|
319
287
|
}
|
|
320
288
|
|
|
321
|
-
function parseCatalogInfo(node: XmlNode | undefined): QtiCatalogInfo | undefined {
|
|
322
|
-
if (!node) return undefined;
|
|
323
|
-
return {
|
|
324
|
-
catalogs: childElements(node, "qti-catalog").map((catalog) => ({
|
|
325
|
-
id: catalog.attributes.id ?? "",
|
|
326
|
-
attributes: catalog.attributes,
|
|
327
|
-
cards: childElements(catalog, "qti-card").map(parseCatalogCard),
|
|
328
|
-
source: catalog.source,
|
|
329
|
-
})),
|
|
330
|
-
source: node.source,
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function parseCatalogCard(node: XmlNode): QtiCatalogCard {
|
|
335
|
-
return {
|
|
336
|
-
support: node.attributes.support ?? "",
|
|
337
|
-
htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
|
|
338
|
-
fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
|
|
339
|
-
entries: childElements(node, "qti-card-entry").map(parseCatalogCardEntry),
|
|
340
|
-
attributes: node.attributes,
|
|
341
|
-
source: node.source,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function parseCatalogCardEntry(node: XmlNode): QtiCatalogCardEntry {
|
|
346
|
-
return {
|
|
347
|
-
language: node.attributes["xml:lang"] ?? node.attributes.lang,
|
|
348
|
-
default: node.attributes.default === "true",
|
|
349
|
-
htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
|
|
350
|
-
fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
|
|
351
|
-
attributes: node.attributes,
|
|
352
|
-
source: node.source,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function parseCatalogHtmlContent(node: XmlNode | undefined): QtiCatalogHtmlContent | undefined {
|
|
357
|
-
if (!node) return undefined;
|
|
358
|
-
return {
|
|
359
|
-
text: textContent(node),
|
|
360
|
-
children: parseCatalogHtmlChildren(node),
|
|
361
|
-
attributes: node.attributes,
|
|
362
|
-
source: node.source,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function parseCatalogHtmlChildren(node: XmlNode): QtiContentNode[] {
|
|
367
|
-
const content: QtiContentNode[] = [];
|
|
368
|
-
for (const entry of node.content) {
|
|
369
|
-
if (typeof entry === "string") {
|
|
370
|
-
if (entry.length > 0) content.push({ kind: "text", text: entry, source: node.source });
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
content.push({
|
|
374
|
-
kind: "element",
|
|
375
|
-
qtiName: entry.localName,
|
|
376
|
-
attributes: entry.attributes,
|
|
377
|
-
children: parseCatalogHtmlChildren(entry),
|
|
378
|
-
source: entry.source,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
return content;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function parseCatalogFileHref(node: XmlNode): QtiCatalogFileHref {
|
|
385
|
-
return {
|
|
386
|
-
href: textContent(node).trim(),
|
|
387
|
-
mimeType: node.attributes["mime-type"],
|
|
388
|
-
attributes: node.attributes,
|
|
389
|
-
source: node.source,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration {
|
|
394
|
-
const cardinality = parseCardinality(node.attributes.cardinality);
|
|
395
|
-
const baseType = node.attributes["base-type"] as QtiResponseDeclaration["baseType"];
|
|
396
|
-
return {
|
|
397
|
-
kind: "response",
|
|
398
|
-
identifier: node.attributes.identifier ?? "",
|
|
399
|
-
cardinality,
|
|
400
|
-
baseType,
|
|
401
|
-
defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
|
|
402
|
-
correctResponse: normalizeValueForCardinality(
|
|
403
|
-
parseVariableValue(childElements(node, "qti-correct-response")[0], baseType),
|
|
404
|
-
cardinality,
|
|
405
|
-
),
|
|
406
|
-
mapping: parseMapping(childElements(node, "qti-mapping")[0]),
|
|
407
|
-
areaMapping: parseAreaMapping(childElements(node, "qti-area-mapping")[0]),
|
|
408
|
-
attributes: node.attributes,
|
|
409
|
-
source: node.source,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function parseOutcomeDeclaration(node: XmlNode): QtiOutcomeDeclaration {
|
|
414
|
-
const baseType = node.attributes["base-type"] as QtiOutcomeDeclaration["baseType"];
|
|
415
|
-
return {
|
|
416
|
-
kind: "outcome",
|
|
417
|
-
identifier: node.attributes.identifier ?? "",
|
|
418
|
-
cardinality: parseCardinality(node.attributes.cardinality),
|
|
419
|
-
baseType,
|
|
420
|
-
defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
|
|
421
|
-
lookupTable: parseLookupTable(node, baseType),
|
|
422
|
-
attributes: node.attributes,
|
|
423
|
-
source: node.source,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function parseTemplateDeclaration(node: XmlNode): QtiTemplateDeclaration {
|
|
428
|
-
const baseType = node.attributes["base-type"] as QtiTemplateDeclaration["baseType"];
|
|
429
|
-
return {
|
|
430
|
-
kind: "template",
|
|
431
|
-
identifier: node.attributes.identifier ?? "",
|
|
432
|
-
cardinality: parseCardinality(node.attributes.cardinality),
|
|
433
|
-
baseType,
|
|
434
|
-
defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
|
|
435
|
-
attributes: node.attributes,
|
|
436
|
-
source: node.source,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
289
|
function parseInteraction(
|
|
441
290
|
node: XmlNode,
|
|
442
291
|
diagnostics: QtiDiagnostic[],
|
|
@@ -448,25 +297,8 @@ function parseInteraction(
|
|
|
448
297
|
? responseDeclarationMap.get(responseIdentifier)
|
|
449
298
|
: undefined;
|
|
450
299
|
const prompt = childElements(node, "qti-prompt")[0];
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
code: "interaction.unsupported",
|
|
454
|
-
severity: "warning",
|
|
455
|
-
message: `${node.localName} is not currently in the support registry.`,
|
|
456
|
-
path: node.source.path,
|
|
457
|
-
source: node.source,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
const support = getInteractionSupport(node.localName);
|
|
461
|
-
if (support?.support === "deprecated") {
|
|
462
|
-
diagnostics.push({
|
|
463
|
-
code: "interaction.deprecated",
|
|
464
|
-
severity: "warning",
|
|
465
|
-
message: `${node.localName} is deprecated. ${support.notes ?? ""}`.trim(),
|
|
466
|
-
path: node.source.path,
|
|
467
|
-
source: node.source,
|
|
468
|
-
});
|
|
469
|
-
}
|
|
300
|
+
const registryStatus = interactionRegistryStatus(node.localName);
|
|
301
|
+
diagnostics.push(...interactionRegistryDiagnostics(node.localName, node.source));
|
|
470
302
|
|
|
471
303
|
const objectNode =
|
|
472
304
|
interactionType === "positionObject"
|
|
@@ -480,13 +312,23 @@ function parseInteraction(
|
|
|
480
312
|
(child) => child.localName === "object" || child.localName === "img",
|
|
481
313
|
)[0];
|
|
482
314
|
|
|
315
|
+
const promptContent = prompt
|
|
316
|
+
? parseContentChildren(prompt, diagnostics, responseDeclarationMap, [])
|
|
317
|
+
: undefined;
|
|
318
|
+
|
|
483
319
|
return {
|
|
484
320
|
type: interactionType ?? "custom",
|
|
321
|
+
registryStatus,
|
|
485
322
|
qtiName: node.localName,
|
|
486
323
|
responseIdentifier,
|
|
487
324
|
responseCardinality: responseDeclaration?.cardinality,
|
|
488
325
|
responseBaseType: responseDeclaration?.baseType,
|
|
489
|
-
prompt:
|
|
326
|
+
prompt: promptContent
|
|
327
|
+
? flatTextFromContent(promptContent, { excludeAnnotations: true }) || undefined
|
|
328
|
+
: prompt
|
|
329
|
+
? textContent(prompt)
|
|
330
|
+
: undefined,
|
|
331
|
+
promptContent: promptContent && promptContent.length > 0 ? promptContent : undefined,
|
|
490
332
|
promptAttributes: prompt?.attributes,
|
|
491
333
|
promptSource: prompt?.source,
|
|
492
334
|
contextText: inlineInteractionContext(node, interactionType),
|
|
@@ -495,11 +337,8 @@ function parseInteraction(
|
|
|
495
337
|
interactionType === "positionObject"
|
|
496
338
|
? parseObjectAsset(positionObjectStageObject(node))
|
|
497
339
|
: undefined,
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
? parsePortableCustomDefinition(node, diagnostics)
|
|
501
|
-
: undefined,
|
|
502
|
-
choices: parseChoices(node),
|
|
340
|
+
...parseInteractionCustomPayload(node, interactionType, diagnostics, isInteractionElement),
|
|
341
|
+
choices: parseChoices(node, diagnostics, responseDeclarationMap),
|
|
503
342
|
hottextSegments: interactionType === "hottext" ? parseHottextSegments(node) : undefined,
|
|
504
343
|
gapMatchSegments:
|
|
505
344
|
interactionType === "gapMatch" || interactionType === "graphicGapMatch"
|
|
@@ -515,172 +354,6 @@ function parseInteraction(
|
|
|
515
354
|
};
|
|
516
355
|
}
|
|
517
356
|
|
|
518
|
-
function parsePortableCustomDefinition(
|
|
519
|
-
node: XmlNode,
|
|
520
|
-
diagnostics: QtiDiagnostic[],
|
|
521
|
-
): QtiPortableCustomDefinition {
|
|
522
|
-
const markup = firstChildElement(node, "qti-interaction-markup", diagnostics);
|
|
523
|
-
const modules = firstChildElement(node, "qti-interaction-modules", diagnostics);
|
|
524
|
-
return {
|
|
525
|
-
responseIdentifier: node.attributes["response-identifier"],
|
|
526
|
-
customInteractionTypeIdentifier: node.attributes["custom-interaction-type-identifier"],
|
|
527
|
-
module: node.attributes.module,
|
|
528
|
-
interactionModules: modules ? parsePortableCustomInteractionModules(modules) : undefined,
|
|
529
|
-
interactionMarkup: markup ? parsePortableCustomMarkupChildren(markup, diagnostics) : [],
|
|
530
|
-
interactionMarkupRaw: markup ? serializeXmlContent(markup) : undefined,
|
|
531
|
-
templateVariables: childElements(node, "qti-template-variable").map((variable) =>
|
|
532
|
-
parsePortableCustomVariableBinding(variable, "template"),
|
|
533
|
-
),
|
|
534
|
-
contextVariables: childElements(node, "qti-context-variable").map((variable) =>
|
|
535
|
-
parsePortableCustomVariableBinding(variable, "context"),
|
|
536
|
-
),
|
|
537
|
-
stylesheets: childElements(node, "qti-stylesheet").map(parseStylesheet),
|
|
538
|
-
catalogInfo: parseCatalogInfo(childElements(node, "qti-catalog-info")[0]),
|
|
539
|
-
dataAttributes: Object.fromEntries(
|
|
540
|
-
Object.entries(node.attributes).filter(([name]) => name.startsWith("data-")),
|
|
541
|
-
),
|
|
542
|
-
attributes: node.attributes,
|
|
543
|
-
source: node.source,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function firstChildElement(
|
|
548
|
-
node: XmlNode,
|
|
549
|
-
name: string,
|
|
550
|
-
diagnostics: QtiDiagnostic[],
|
|
551
|
-
): XmlNode | undefined {
|
|
552
|
-
const matches = childElements(node, name);
|
|
553
|
-
if (matches.length > 1) {
|
|
554
|
-
diagnostics.push({
|
|
555
|
-
code: "interaction.portableCustom.child.duplicate",
|
|
556
|
-
severity: "error",
|
|
557
|
-
message: `${node.localName} allows at most one ${name} child.`,
|
|
558
|
-
path: matches[1]?.source?.path ?? node.source?.path,
|
|
559
|
-
source: matches[1]?.source ?? node.source,
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
return matches[0];
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function parsePortableCustomMarkupChildren(
|
|
566
|
-
node: XmlNode,
|
|
567
|
-
diagnostics: QtiDiagnostic[],
|
|
568
|
-
): QtiContentNode[] {
|
|
569
|
-
const content: QtiContentNode[] = [];
|
|
570
|
-
for (const entry of node.content) {
|
|
571
|
-
if (typeof entry === "string") {
|
|
572
|
-
if (entry.length > 0) content.push({ kind: "text", text: entry, source: node.source });
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
content.push(parsePortableCustomMarkupNode(entry, diagnostics));
|
|
576
|
-
}
|
|
577
|
-
return content;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function parsePortableCustomMarkupNode(
|
|
581
|
-
node: XmlNode,
|
|
582
|
-
diagnostics: QtiDiagnostic[],
|
|
583
|
-
): QtiContentNode {
|
|
584
|
-
if (isInteractionElement(node)) {
|
|
585
|
-
diagnostics.push({
|
|
586
|
-
code: "interaction.portableCustom.markupInteraction",
|
|
587
|
-
severity: "error",
|
|
588
|
-
message: `qti-interaction-markup must not contain nested QTI interaction ${node.localName}.`,
|
|
589
|
-
path: node.source?.path,
|
|
590
|
-
source: node.source,
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (node.localName === "qti-printed-variable") {
|
|
595
|
-
return {
|
|
596
|
-
kind: "printedVariable",
|
|
597
|
-
identifier: node.attributes.identifier ?? "",
|
|
598
|
-
format: node.attributes.format,
|
|
599
|
-
attributes: node.attributes,
|
|
600
|
-
source: node.source,
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (node.localName === "qti-feedback-block" || node.localName === "qti-feedback-inline") {
|
|
605
|
-
return {
|
|
606
|
-
kind: "feedback",
|
|
607
|
-
feedbackType: node.localName === "qti-feedback-block" ? "block" : "inline",
|
|
608
|
-
identifier: node.attributes.identifier ?? "",
|
|
609
|
-
outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
|
|
610
|
-
showHide: node.attributes["show-hide"] === "hide" ? "hide" : "show",
|
|
611
|
-
attributes: node.attributes,
|
|
612
|
-
children: parsePortableCustomMarkupChildren(node, diagnostics),
|
|
613
|
-
source: node.source,
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return {
|
|
618
|
-
kind: "element",
|
|
619
|
-
qtiName: node.localName,
|
|
620
|
-
attributes: node.attributes,
|
|
621
|
-
children: parsePortableCustomMarkupChildren(node, diagnostics),
|
|
622
|
-
source: node.source,
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function parsePortableCustomInteractionModules(node: XmlNode): QtiPortableCustomInteractionModules {
|
|
627
|
-
return {
|
|
628
|
-
primaryConfiguration: node.attributes["primary-configuration"],
|
|
629
|
-
secondaryConfiguration:
|
|
630
|
-
node.attributes["secondary-configuration"] ?? node.attributes["fallback-configuration"],
|
|
631
|
-
modules: childElements(node, "qti-interaction-module").map(
|
|
632
|
-
parsePortableCustomInteractionModule,
|
|
633
|
-
),
|
|
634
|
-
attributes: node.attributes,
|
|
635
|
-
source: node.source,
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function parsePortableCustomInteractionModule(node: XmlNode): QtiPortableCustomInteractionModule {
|
|
640
|
-
return {
|
|
641
|
-
id: node.attributes.id,
|
|
642
|
-
primaryPath: node.attributes["primary-path"],
|
|
643
|
-
fallbackPath: node.attributes["fallback-path"],
|
|
644
|
-
attributes: node.attributes,
|
|
645
|
-
source: node.source,
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function parsePortableCustomVariableBinding(
|
|
650
|
-
node: XmlNode,
|
|
651
|
-
kind: QtiPortableCustomVariableBinding["kind"],
|
|
652
|
-
): QtiPortableCustomVariableBinding {
|
|
653
|
-
return {
|
|
654
|
-
kind,
|
|
655
|
-
identifier: node.attributes.identifier ?? node.attributes["template-identifier"],
|
|
656
|
-
variableIdentifier: node.attributes["variable-identifier"],
|
|
657
|
-
attributes: node.attributes,
|
|
658
|
-
source: node.source,
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function serializeXmlContent(node: XmlNode): string {
|
|
663
|
-
return node.content
|
|
664
|
-
.map((entry) => (typeof entry === "string" ? escapeXmlText(entry) : serializeXmlNode(entry)))
|
|
665
|
-
.join("");
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function serializeXmlNode(node: XmlNode): string {
|
|
669
|
-
const attributes = Object.entries(node.attributes)
|
|
670
|
-
.map(([name, value]) => ` ${name}="${escapeXmlAttribute(value)}"`)
|
|
671
|
-
.join("");
|
|
672
|
-
if (node.content.length === 0) return `<${node.name}${attributes}/>`;
|
|
673
|
-
return `<${node.name}${attributes}>${serializeXmlContent(node)}</${node.name}>`;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function escapeXmlText(value: string): string {
|
|
677
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function escapeXmlAttribute(value: string): string {
|
|
681
|
-
return escapeXmlText(value).replaceAll('"', """);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
357
|
function positionObjectInteractionObject(node: XmlNode): XmlNode | undefined {
|
|
685
358
|
return childElements(node).find(
|
|
686
359
|
(child) => child.localName === "object" || child.localName === "img",
|
|
@@ -965,7 +638,11 @@ function assetTypeFromData(data: string | undefined): string | undefined {
|
|
|
965
638
|
return undefined;
|
|
966
639
|
}
|
|
967
640
|
|
|
968
|
-
function parseChoices(
|
|
641
|
+
function parseChoices(
|
|
642
|
+
node: XmlNode,
|
|
643
|
+
diagnostics: QtiDiagnostic[],
|
|
644
|
+
responseDeclarationMap: Map<string, QtiResponseDeclaration>,
|
|
645
|
+
): QtiChoice[] {
|
|
969
646
|
const choiceNames = new Set([
|
|
970
647
|
"qti-simple-choice",
|
|
971
648
|
"qti-simple-associable-choice",
|
|
@@ -981,14 +658,20 @@ function parseChoices(node: XmlNode): QtiChoice[] {
|
|
|
981
658
|
return descendants(node, (child) => choiceNames.has(child.localName)).map((choice, index) => {
|
|
982
659
|
const identifier = choice.attributes.identifier ?? "";
|
|
983
660
|
const asset = parseChoiceAsset(choice);
|
|
661
|
+
const content = parseContentChildren(choice, diagnostics, responseDeclarationMap, []);
|
|
662
|
+
const flatChoiceText =
|
|
663
|
+
content.length > 0
|
|
664
|
+
? flatTextFromContent(content, { excludeAnnotations: true })
|
|
665
|
+
: textContent(choice);
|
|
984
666
|
return {
|
|
985
667
|
identifier,
|
|
986
668
|
text:
|
|
987
|
-
|
|
669
|
+
flatChoiceText ||
|
|
988
670
|
choice.attributes["object-label"] ||
|
|
989
671
|
asset?.text ||
|
|
990
672
|
identifier ||
|
|
991
673
|
`Choice ${index + 1}`,
|
|
674
|
+
content: content.length > 0 ? content : undefined,
|
|
992
675
|
asset,
|
|
993
676
|
role: choiceRole(choice),
|
|
994
677
|
qtiName: choice.localName,
|
|
@@ -1039,857 +722,3 @@ function matchSetIndex(node: XmlNode): number {
|
|
|
1039
722
|
node.parent?.children.filter((sibling) => sibling.localName === "qti-simple-match-set") ?? [];
|
|
1040
723
|
return siblings.indexOf(node);
|
|
1041
724
|
}
|
|
1042
|
-
|
|
1043
|
-
function parseVariableValue(
|
|
1044
|
-
node: XmlNode | undefined,
|
|
1045
|
-
baseType: QtiBaseType | undefined,
|
|
1046
|
-
): QtiValue {
|
|
1047
|
-
if (!node) return null;
|
|
1048
|
-
const valueNodes = childElements(node, "qti-value");
|
|
1049
|
-
const entries = valueNodes.map((valueNode) => ({
|
|
1050
|
-
fieldIdentifier: valueNode.attributes["field-identifier"],
|
|
1051
|
-
value: coerceValue(textContent(valueNode), valueNode.attributes["base-type"] ?? baseType),
|
|
1052
|
-
}));
|
|
1053
|
-
const recordEntries = entries.filter(
|
|
1054
|
-
(entry): entry is { fieldIdentifier: string; value: QtiScalarValue } =>
|
|
1055
|
-
Boolean(entry.fieldIdentifier),
|
|
1056
|
-
);
|
|
1057
|
-
if (recordEntries.length > 0) {
|
|
1058
|
-
return Object.fromEntries(recordEntries.map((entry) => [entry.fieldIdentifier, entry.value]));
|
|
1059
|
-
}
|
|
1060
|
-
if (entries.length === 0) {
|
|
1061
|
-
const text = textContent(node);
|
|
1062
|
-
return text.length > 0 ? coerceValue(text, baseType) : null;
|
|
1063
|
-
}
|
|
1064
|
-
if (entries.length === 1) return entries[0]?.value ?? null;
|
|
1065
|
-
return entries.map((entry) => entry.value);
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
function parseMapping(node: XmlNode | undefined): QtiResponseDeclaration["mapping"] | undefined {
|
|
1069
|
-
if (!node) return undefined;
|
|
1070
|
-
return {
|
|
1071
|
-
defaultValue: Number(node.attributes["default-value"] ?? 0),
|
|
1072
|
-
attributes: node.attributes,
|
|
1073
|
-
source: node.source,
|
|
1074
|
-
entries: childElements(node, "qti-map-entry").map((entry) => ({
|
|
1075
|
-
mapKey: entry.attributes["map-key"],
|
|
1076
|
-
mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
|
|
1077
|
-
attributes: entry.attributes,
|
|
1078
|
-
source: entry.source,
|
|
1079
|
-
})),
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function parseAreaMapping(
|
|
1084
|
-
node: XmlNode | undefined,
|
|
1085
|
-
): QtiResponseDeclaration["areaMapping"] | undefined {
|
|
1086
|
-
if (!node) return undefined;
|
|
1087
|
-
return {
|
|
1088
|
-
defaultValue: Number(node.attributes["default-value"] ?? 0),
|
|
1089
|
-
attributes: node.attributes,
|
|
1090
|
-
source: node.source,
|
|
1091
|
-
entries: childElements(node, "qti-area-map-entry").map((entry) => ({
|
|
1092
|
-
shape: parseShape(entry.attributes.shape),
|
|
1093
|
-
coords: parseCoords(entry.attributes.coords),
|
|
1094
|
-
mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
|
|
1095
|
-
attributes: entry.attributes,
|
|
1096
|
-
source: entry.source,
|
|
1097
|
-
})),
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
function parseLookupTable(
|
|
1102
|
-
node: XmlNode,
|
|
1103
|
-
baseType: QtiOutcomeDeclaration["baseType"],
|
|
1104
|
-
): QtiLookupTable | undefined {
|
|
1105
|
-
const matchTable = childElements(node, "qti-match-table")[0];
|
|
1106
|
-
if (matchTable) return parseMatchTable(matchTable, baseType);
|
|
1107
|
-
const interpolationTable = childElements(node, "qti-interpolation-table")[0];
|
|
1108
|
-
if (interpolationTable) return parseInterpolationTable(interpolationTable, baseType);
|
|
1109
|
-
return undefined;
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
function parseMatchTable(
|
|
1113
|
-
node: XmlNode,
|
|
1114
|
-
baseType: QtiOutcomeDeclaration["baseType"],
|
|
1115
|
-
): QtiLookupTable {
|
|
1116
|
-
return {
|
|
1117
|
-
type: "match",
|
|
1118
|
-
defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
|
|
1119
|
-
attributes: node.attributes,
|
|
1120
|
-
source: node.source,
|
|
1121
|
-
entries: childElements(node, "qti-match-table-entry").map((entry) => ({
|
|
1122
|
-
sourceValue: Number(entry.attributes["source-value"]),
|
|
1123
|
-
targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
|
|
1124
|
-
attributes: entry.attributes,
|
|
1125
|
-
source: entry.source,
|
|
1126
|
-
})),
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
function parseInterpolationTable(
|
|
1131
|
-
node: XmlNode,
|
|
1132
|
-
baseType: QtiOutcomeDeclaration["baseType"],
|
|
1133
|
-
): QtiLookupTable {
|
|
1134
|
-
return {
|
|
1135
|
-
type: "interpolation",
|
|
1136
|
-
defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
|
|
1137
|
-
attributes: node.attributes,
|
|
1138
|
-
source: node.source,
|
|
1139
|
-
entries: childElements(node, "qti-interpolation-table-entry").map((entry) => ({
|
|
1140
|
-
sourceValue: Number(entry.attributes["source-value"]),
|
|
1141
|
-
targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
|
|
1142
|
-
includeBoundary: entry.attributes["include-boundary"] !== "false",
|
|
1143
|
-
attributes: entry.attributes,
|
|
1144
|
-
source: entry.source,
|
|
1145
|
-
})),
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
function parseLookupValue(value: string | undefined, baseType: string | undefined): QtiValue {
|
|
1150
|
-
return value === undefined ? null : coerceValue(value, baseType);
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
function parseShape(
|
|
1154
|
-
shape: string | undefined,
|
|
1155
|
-
): NonNullable<QtiResponseDeclaration["areaMapping"]>["entries"][number]["shape"] {
|
|
1156
|
-
if (shape === "circle" || shape === "rect" || shape === "poly") return shape;
|
|
1157
|
-
return "default";
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
function parseCoords(value: string | undefined): number[] {
|
|
1161
|
-
return (value ?? "")
|
|
1162
|
-
.split(",")
|
|
1163
|
-
.map((part) => Number(part.trim()))
|
|
1164
|
-
.filter((part) => Number.isFinite(part));
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
function parseResponseProcessing(node: XmlNode | undefined): QtiResponseProcessing | undefined {
|
|
1168
|
-
if (!node) return undefined;
|
|
1169
|
-
return {
|
|
1170
|
-
template: node.attributes.template,
|
|
1171
|
-
rules: parseResponseRules(node),
|
|
1172
|
-
conditions: responseConditionsFromChildren(node),
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
function parseTemplateProcessing(node: XmlNode | undefined): QtiTemplateProcessing | undefined {
|
|
1177
|
-
if (!node) return undefined;
|
|
1178
|
-
return {
|
|
1179
|
-
rules: parseTemplateRules(node),
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function parseTemplateRules(node: XmlNode): QtiTemplateRule[] {
|
|
1184
|
-
return childElements(node)
|
|
1185
|
-
.map(parseTemplateRule)
|
|
1186
|
-
.filter((rule): rule is QtiTemplateRule => rule !== undefined);
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
function parseTemplateRule(node: XmlNode): QtiTemplateRule | undefined {
|
|
1190
|
-
if (node.localName === "qti-set-template-value") {
|
|
1191
|
-
return {
|
|
1192
|
-
type: "setTemplateValue",
|
|
1193
|
-
identifier: node.attributes.identifier ?? "",
|
|
1194
|
-
expression: parseFirstExpression(node) ?? {
|
|
1195
|
-
type: "baseValue",
|
|
1196
|
-
value: null,
|
|
1197
|
-
source: node.source,
|
|
1198
|
-
},
|
|
1199
|
-
source: node.source,
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
if (node.localName === "qti-set-default-value") {
|
|
1204
|
-
return {
|
|
1205
|
-
type: "setDefaultValue",
|
|
1206
|
-
identifier: node.attributes.identifier ?? "",
|
|
1207
|
-
expression: parseFirstExpression(node) ?? {
|
|
1208
|
-
type: "baseValue",
|
|
1209
|
-
value: null,
|
|
1210
|
-
source: node.source,
|
|
1211
|
-
},
|
|
1212
|
-
source: node.source,
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
if (node.localName === "qti-set-correct-response") {
|
|
1217
|
-
return {
|
|
1218
|
-
type: "setCorrectResponse",
|
|
1219
|
-
identifier: node.attributes.identifier ?? "",
|
|
1220
|
-
expression: parseFirstExpression(node) ?? {
|
|
1221
|
-
type: "baseValue",
|
|
1222
|
-
value: null,
|
|
1223
|
-
source: node.source,
|
|
1224
|
-
},
|
|
1225
|
-
source: node.source,
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
if (node.localName === "qti-template-condition") {
|
|
1230
|
-
const templateIf = childElements(node, "qti-template-if")[0];
|
|
1231
|
-
const templateElse = childElements(node, "qti-template-else")[0];
|
|
1232
|
-
return {
|
|
1233
|
-
type: "templateCondition",
|
|
1234
|
-
ifExpression: templateIf ? parseFirstExpression(templateIf) : undefined,
|
|
1235
|
-
thenRules: templateIf ? parseTemplateRules(templateIf) : [],
|
|
1236
|
-
elseIfs: childElements(node, "qti-template-else-if").map((branch) => ({
|
|
1237
|
-
expression: parseFirstExpression(branch),
|
|
1238
|
-
rules: parseTemplateRules(branch),
|
|
1239
|
-
})),
|
|
1240
|
-
elseRules: templateElse ? parseTemplateRules(templateElse) : [],
|
|
1241
|
-
source: node.source,
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if (node.localName === "qti-exit-template") {
|
|
1246
|
-
return {
|
|
1247
|
-
type: "exitTemplate",
|
|
1248
|
-
source: node.source,
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
if (node.localName === "qti-template-constraint") {
|
|
1253
|
-
return {
|
|
1254
|
-
type: "templateConstraint",
|
|
1255
|
-
expression: parseFirstExpression(node) ?? {
|
|
1256
|
-
type: "baseValue",
|
|
1257
|
-
value: null,
|
|
1258
|
-
source: node.source,
|
|
1259
|
-
},
|
|
1260
|
-
source: node.source,
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
return undefined;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
function responseConditionsFromChildren(node: XmlNode): QtiResponseCondition[] {
|
|
1268
|
-
return childElements(node).flatMap((child) => {
|
|
1269
|
-
if (child.localName === "qti-response-condition") return [parseResponseCondition(child)];
|
|
1270
|
-
if (child.localName === "qti-response-processing-fragment") {
|
|
1271
|
-
return responseConditionsFromChildren(child);
|
|
1272
|
-
}
|
|
1273
|
-
return [];
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
function parseResponseCondition(node: XmlNode): QtiResponseCondition {
|
|
1278
|
-
const responseIf = childElements(node, "qti-response-if")[0];
|
|
1279
|
-
const responseElse = childElements(node, "qti-response-else")[0];
|
|
1280
|
-
return {
|
|
1281
|
-
ifExpression: responseIf ? parseFirstExpression(responseIf) : undefined,
|
|
1282
|
-
thenRules: responseIf ? parseResponseRules(responseIf) : [],
|
|
1283
|
-
elseIfs: childElements(node, "qti-response-else-if").map((branch) => ({
|
|
1284
|
-
expression: parseFirstExpression(branch),
|
|
1285
|
-
rules: parseResponseRules(branch),
|
|
1286
|
-
})),
|
|
1287
|
-
elseRules: responseElse ? parseResponseRules(responseElse) : [],
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
function parseResponseRules(node: XmlNode): QtiResponseRule[] {
|
|
1292
|
-
return childElements(node)
|
|
1293
|
-
.map(parseResponseRule)
|
|
1294
|
-
.filter((rule): rule is QtiResponseRule => rule !== undefined);
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
function parseResponseRule(node: XmlNode): QtiResponseRule | undefined {
|
|
1298
|
-
if (node.localName === "qti-response-condition") {
|
|
1299
|
-
return {
|
|
1300
|
-
type: "responseCondition",
|
|
1301
|
-
condition: parseResponseCondition(node),
|
|
1302
|
-
source: node.source,
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
if (node.localName === "qti-set-outcome-value") return parseSetOutcomeValue(node);
|
|
1306
|
-
if (node.localName === "qti-lookup-outcome-value") return parseLookupOutcomeValue(node);
|
|
1307
|
-
if (node.localName === "qti-exit-response") {
|
|
1308
|
-
return { type: "exitResponse", source: node.source };
|
|
1309
|
-
}
|
|
1310
|
-
if (node.localName === "qti-response-processing-fragment") {
|
|
1311
|
-
return {
|
|
1312
|
-
type: "responseProcessingFragment",
|
|
1313
|
-
rules: parseResponseRules(node),
|
|
1314
|
-
source: node.source,
|
|
1315
|
-
};
|
|
1316
|
-
}
|
|
1317
|
-
return undefined;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function parseLookupOutcomeValue(node: XmlNode): QtiLookupOutcomeValue {
|
|
1321
|
-
return {
|
|
1322
|
-
type: "lookupOutcomeValue",
|
|
1323
|
-
identifier: node.attributes.identifier ?? "",
|
|
1324
|
-
expression: parseFirstExpression(node) ?? {
|
|
1325
|
-
type: "baseValue",
|
|
1326
|
-
value: null,
|
|
1327
|
-
source: node.source,
|
|
1328
|
-
},
|
|
1329
|
-
source: node.source,
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
function parseSetOutcomeValue(setNode: XmlNode): QtiSetOutcomeValue {
|
|
1334
|
-
return {
|
|
1335
|
-
type: "setOutcomeValue",
|
|
1336
|
-
identifier: setNode.attributes.identifier ?? "",
|
|
1337
|
-
expression: parseFirstExpression(setNode) ?? {
|
|
1338
|
-
type: "baseValue",
|
|
1339
|
-
value: null,
|
|
1340
|
-
source: setNode.source,
|
|
1341
|
-
},
|
|
1342
|
-
source: setNode.source,
|
|
1343
|
-
};
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
function parseFirstExpression(node: XmlNode): QtiProcessingExpression | undefined {
|
|
1347
|
-
for (const child of node.children) {
|
|
1348
|
-
const expression = parseExpression(child);
|
|
1349
|
-
if (expression) return expression;
|
|
1350
|
-
}
|
|
1351
|
-
return undefined;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
|
|
1355
|
-
if (node.localName === "qti-base-value") {
|
|
1356
|
-
const rawValue = textContent(node);
|
|
1357
|
-
return {
|
|
1358
|
-
type: "baseValue",
|
|
1359
|
-
value: coerceValue(rawValue, node.attributes["base-type"]),
|
|
1360
|
-
rawValue,
|
|
1361
|
-
baseType: node.attributes["base-type"],
|
|
1362
|
-
source: node.source,
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
if (node.localName === "qti-null") {
|
|
1367
|
-
return { type: "null", source: node.source };
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
if (node.localName === "qti-is-null") {
|
|
1371
|
-
const variable = childElements(node, "qti-variable")[0];
|
|
1372
|
-
return {
|
|
1373
|
-
type: "isNull",
|
|
1374
|
-
identifier: variable?.attributes.identifier ?? "",
|
|
1375
|
-
source: node.source,
|
|
1376
|
-
};
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
if (node.localName === "qti-map-response") {
|
|
1380
|
-
return {
|
|
1381
|
-
type: "mapResponse",
|
|
1382
|
-
identifier: node.attributes.identifier ?? "",
|
|
1383
|
-
source: node.source,
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
if (node.localName === "qti-map-response-point") {
|
|
1388
|
-
return {
|
|
1389
|
-
type: "mapResponsePoint",
|
|
1390
|
-
identifier: node.attributes.identifier ?? "",
|
|
1391
|
-
source: node.source,
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
if (node.localName === "qti-correct") {
|
|
1396
|
-
return {
|
|
1397
|
-
type: "correct",
|
|
1398
|
-
identifier: node.attributes.identifier ?? "",
|
|
1399
|
-
source: node.source,
|
|
1400
|
-
};
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
if (node.localName === "qti-default") {
|
|
1404
|
-
return {
|
|
1405
|
-
type: "default",
|
|
1406
|
-
identifier: node.attributes.identifier ?? "",
|
|
1407
|
-
source: node.source,
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
if (node.localName === "qti-variable") {
|
|
1412
|
-
return { type: "variable", identifier: node.attributes.identifier ?? "", source: node.source };
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
if (node.localName === "qti-random-integer") {
|
|
1416
|
-
return {
|
|
1417
|
-
type: "randomInteger",
|
|
1418
|
-
min: Number(node.attributes.min ?? 0),
|
|
1419
|
-
max: Number(node.attributes.max ?? 0),
|
|
1420
|
-
step: Number(node.attributes.step ?? 1),
|
|
1421
|
-
attributes: node.attributes,
|
|
1422
|
-
source: node.source,
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
if (node.localName === "qti-random-float") {
|
|
1427
|
-
return {
|
|
1428
|
-
type: "randomFloat",
|
|
1429
|
-
min: Number(node.attributes.min ?? 0),
|
|
1430
|
-
max: Number(node.attributes.max ?? 0),
|
|
1431
|
-
attributes: node.attributes,
|
|
1432
|
-
source: node.source,
|
|
1433
|
-
};
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
if (node.localName === "qti-random") {
|
|
1437
|
-
const multiple = childElements(node, "qti-multiple")[0];
|
|
1438
|
-
return {
|
|
1439
|
-
type: "random",
|
|
1440
|
-
values: childElements(multiple ?? node)
|
|
1441
|
-
.map(parseExpression)
|
|
1442
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1443
|
-
source: node.source,
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
if (node.localName === "qti-multiple") {
|
|
1448
|
-
return {
|
|
1449
|
-
type: "multiple",
|
|
1450
|
-
expressions: childElements(node)
|
|
1451
|
-
.map(parseExpression)
|
|
1452
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1453
|
-
source: node.source,
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
if (node.localName === "qti-ordered") {
|
|
1458
|
-
return {
|
|
1459
|
-
type: "ordered",
|
|
1460
|
-
expressions: childElements(node)
|
|
1461
|
-
.map(parseExpression)
|
|
1462
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1463
|
-
source: node.source,
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
if (node.localName === "qti-index") {
|
|
1468
|
-
const expression = parseFirstExpression(node);
|
|
1469
|
-
if (expression) {
|
|
1470
|
-
return { type: "index", expression, n: node.attributes.n ?? "", source: node.source };
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
if (node.localName === "qti-container-size") {
|
|
1475
|
-
const expression = parseFirstExpression(node);
|
|
1476
|
-
if (expression) return { type: "containerSize", expression, source: node.source };
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
if (node.localName === "qti-sum") {
|
|
1480
|
-
return {
|
|
1481
|
-
type: "sum",
|
|
1482
|
-
expressions: childElements(node)
|
|
1483
|
-
.map(parseExpression)
|
|
1484
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1485
|
-
source: node.source,
|
|
1486
|
-
};
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
if (node.localName === "qti-product") {
|
|
1490
|
-
return {
|
|
1491
|
-
type: "product",
|
|
1492
|
-
expressions: childElements(node)
|
|
1493
|
-
.map(parseExpression)
|
|
1494
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1495
|
-
source: node.source,
|
|
1496
|
-
};
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
if (node.localName === "qti-min") {
|
|
1500
|
-
return {
|
|
1501
|
-
type: "min",
|
|
1502
|
-
expressions: childElements(node)
|
|
1503
|
-
.map(parseExpression)
|
|
1504
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1505
|
-
source: node.source,
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
if (node.localName === "qti-max") {
|
|
1510
|
-
return {
|
|
1511
|
-
type: "max",
|
|
1512
|
-
expressions: childElements(node)
|
|
1513
|
-
.map(parseExpression)
|
|
1514
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1515
|
-
source: node.source,
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
if (node.localName === "qti-subtract") {
|
|
1520
|
-
const expressions = childElements(node)
|
|
1521
|
-
.map(parseExpression)
|
|
1522
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1523
|
-
const [left, right] = expressions;
|
|
1524
|
-
if (left && right) return { type: "subtract", left, right, source: node.source };
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
if (node.localName === "qti-divide") {
|
|
1528
|
-
const expressions = childElements(node)
|
|
1529
|
-
.map(parseExpression)
|
|
1530
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1531
|
-
const [left, right] = expressions;
|
|
1532
|
-
if (left && right) return { type: "divide", left, right, source: node.source };
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
if (node.localName === "qti-power") {
|
|
1536
|
-
const expressions = childElements(node)
|
|
1537
|
-
.map(parseExpression)
|
|
1538
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1539
|
-
const [left, right] = expressions;
|
|
1540
|
-
if (left && right) return { type: "power", left, right, source: node.source };
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
if (node.localName === "qti-integer-divide") {
|
|
1544
|
-
const expressions = childElements(node)
|
|
1545
|
-
.map(parseExpression)
|
|
1546
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1547
|
-
const [left, right] = expressions;
|
|
1548
|
-
if (left && right) return { type: "integerDivide", left, right, source: node.source };
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
if (node.localName === "qti-integer-modulus") {
|
|
1552
|
-
const expressions = childElements(node)
|
|
1553
|
-
.map(parseExpression)
|
|
1554
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1555
|
-
const [left, right] = expressions;
|
|
1556
|
-
if (left && right) return { type: "integerModulus", left, right, source: node.source };
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
if (node.localName === "qti-round") {
|
|
1560
|
-
const expression = parseFirstExpression(node);
|
|
1561
|
-
if (expression) return { type: "round", expression, source: node.source };
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
if (node.localName === "qti-round-to") {
|
|
1565
|
-
const expression = parseFirstExpression(node);
|
|
1566
|
-
const roundingMode = node.attributes["rounding-mode"];
|
|
1567
|
-
const figures = Number(node.attributes.figures ?? 0);
|
|
1568
|
-
if (
|
|
1569
|
-
expression &&
|
|
1570
|
-
(roundingMode === "decimalPlaces" || roundingMode === "significantFigures") &&
|
|
1571
|
-
Number.isInteger(figures) &&
|
|
1572
|
-
(roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0)
|
|
1573
|
-
) {
|
|
1574
|
-
return { type: "roundTo", expression, roundingMode, figures, source: node.source };
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
if (node.localName === "qti-truncate") {
|
|
1579
|
-
const expression = parseFirstExpression(node);
|
|
1580
|
-
if (expression) return { type: "truncate", expression, source: node.source };
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
if (node.localName === "qti-integer-to-float") {
|
|
1584
|
-
const expression = parseFirstExpression(node);
|
|
1585
|
-
if (expression) return { type: "integerToFloat", expression, source: node.source };
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
if (node.localName === "qti-and") {
|
|
1589
|
-
return {
|
|
1590
|
-
type: "and",
|
|
1591
|
-
expressions: childElements(node)
|
|
1592
|
-
.map(parseExpression)
|
|
1593
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1594
|
-
source: node.source,
|
|
1595
|
-
};
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
if (node.localName === "qti-any-n") {
|
|
1599
|
-
return {
|
|
1600
|
-
type: "anyN",
|
|
1601
|
-
min: node.attributes.min ?? "",
|
|
1602
|
-
max: node.attributes.max ?? "",
|
|
1603
|
-
expressions: childElements(node)
|
|
1604
|
-
.map(parseExpression)
|
|
1605
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1606
|
-
source: node.source,
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
if (node.localName === "qti-or") {
|
|
1611
|
-
return {
|
|
1612
|
-
type: "or",
|
|
1613
|
-
expressions: childElements(node)
|
|
1614
|
-
.map(parseExpression)
|
|
1615
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1616
|
-
source: node.source,
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
if (node.localName === "qti-not") {
|
|
1621
|
-
const expression = parseFirstExpression(node);
|
|
1622
|
-
if (expression) return { type: "not", expression, source: node.source };
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
if (node.localName === "qti-equal") {
|
|
1626
|
-
const [left, right] = childElements(node)
|
|
1627
|
-
.map(parseExpression)
|
|
1628
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1629
|
-
if (left && right) return { type: "equal", left, right, source: node.source };
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
if (node.localName === "qti-equal-rounded") {
|
|
1633
|
-
const [left, right] = childElements(node)
|
|
1634
|
-
.map(parseExpression)
|
|
1635
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1636
|
-
const roundingMode = node.attributes["rounding-mode"] ?? "";
|
|
1637
|
-
const figures = Number(node.attributes.figures ?? 0);
|
|
1638
|
-
if (left && right) {
|
|
1639
|
-
return { type: "equalRounded", left, right, roundingMode, figures, source: node.source };
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
const numericCompareOperator = numericCompareOperatorFor(node.localName);
|
|
1644
|
-
if (numericCompareOperator) {
|
|
1645
|
-
const [left, right] = childElements(node)
|
|
1646
|
-
.map(parseExpression)
|
|
1647
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1648
|
-
if (left && right) {
|
|
1649
|
-
return {
|
|
1650
|
-
type: "numericCompare",
|
|
1651
|
-
operator: numericCompareOperator,
|
|
1652
|
-
left,
|
|
1653
|
-
right,
|
|
1654
|
-
source: node.source,
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
const durationCompareOperator = durationCompareOperatorFor(node.localName);
|
|
1660
|
-
if (durationCompareOperator) {
|
|
1661
|
-
const [left, right] = childElements(node)
|
|
1662
|
-
.map(parseExpression)
|
|
1663
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1664
|
-
if (left && right) {
|
|
1665
|
-
return {
|
|
1666
|
-
type: "durationCompare",
|
|
1667
|
-
operator: durationCompareOperator,
|
|
1668
|
-
left,
|
|
1669
|
-
right,
|
|
1670
|
-
source: node.source,
|
|
1671
|
-
};
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
if (node.localName === "qti-string-match") {
|
|
1676
|
-
const [left, right] = childElements(node)
|
|
1677
|
-
.map(parseExpression)
|
|
1678
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1679
|
-
if (left && right) {
|
|
1680
|
-
return {
|
|
1681
|
-
type: "stringMatch",
|
|
1682
|
-
left,
|
|
1683
|
-
right,
|
|
1684
|
-
caseSensitive: node.attributes["case-sensitive"] !== "false",
|
|
1685
|
-
substring: node.attributes.substring === "true",
|
|
1686
|
-
source: node.source,
|
|
1687
|
-
};
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
if (node.localName === "qti-substring") {
|
|
1692
|
-
const [left, right] = childElements(node)
|
|
1693
|
-
.map(parseExpression)
|
|
1694
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1695
|
-
if (left && right) {
|
|
1696
|
-
return {
|
|
1697
|
-
type: "substring",
|
|
1698
|
-
left,
|
|
1699
|
-
right,
|
|
1700
|
-
caseSensitive: node.attributes["case-sensitive"] !== "false",
|
|
1701
|
-
source: node.source,
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
if (node.localName === "qti-pattern-match") {
|
|
1707
|
-
const expression = parseFirstExpression(node);
|
|
1708
|
-
if (expression) {
|
|
1709
|
-
return {
|
|
1710
|
-
type: "patternMatch",
|
|
1711
|
-
expression,
|
|
1712
|
-
pattern: node.attributes.pattern ?? "",
|
|
1713
|
-
source: node.source,
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
if (node.localName === "qti-field-value") {
|
|
1719
|
-
const expression = parseFirstExpression(node);
|
|
1720
|
-
if (expression) {
|
|
1721
|
-
return {
|
|
1722
|
-
type: "fieldValue",
|
|
1723
|
-
fieldIdentifier: node.attributes["field-identifier"] ?? "",
|
|
1724
|
-
expression,
|
|
1725
|
-
source: node.source,
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
if (node.localName === "qti-member") {
|
|
1731
|
-
const [value, collection] = childElements(node)
|
|
1732
|
-
.map(parseExpression)
|
|
1733
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1734
|
-
if (value && collection) return { type: "member", value, collection, source: node.source };
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
if (node.localName === "qti-delete") {
|
|
1738
|
-
const [value, collection] = childElements(node)
|
|
1739
|
-
.map(parseExpression)
|
|
1740
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1741
|
-
if (value && collection) return { type: "delete", value, collection, source: node.source };
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
if (node.localName === "qti-contains") {
|
|
1745
|
-
const [collection, values] = childElements(node)
|
|
1746
|
-
.map(parseExpression)
|
|
1747
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1748
|
-
if (collection && values) return { type: "contains", collection, values, source: node.source };
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
if (node.localName === "qti-gcd" || node.localName === "qti-lcm") {
|
|
1752
|
-
return {
|
|
1753
|
-
type: node.localName === "qti-gcd" ? "gcd" : "lcm",
|
|
1754
|
-
expressions: childElements(node)
|
|
1755
|
-
.map(parseExpression)
|
|
1756
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1757
|
-
source: node.source,
|
|
1758
|
-
};
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
if (node.localName === "qti-inside") {
|
|
1762
|
-
const expression = parseFirstExpression(node);
|
|
1763
|
-
if (expression) {
|
|
1764
|
-
return {
|
|
1765
|
-
type: "inside",
|
|
1766
|
-
expression,
|
|
1767
|
-
shape: parseShape(node.attributes.shape),
|
|
1768
|
-
coords: parseCoords(node.attributes.coords),
|
|
1769
|
-
attributes: node.attributes,
|
|
1770
|
-
source: node.source,
|
|
1771
|
-
};
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
if (node.localName === "qti-math-constant") {
|
|
1776
|
-
return { type: "mathConstant", name: node.attributes.name ?? "", source: node.source };
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
if (node.localName === "qti-math-operator") {
|
|
1780
|
-
return {
|
|
1781
|
-
type: "mathOperator",
|
|
1782
|
-
name: node.attributes.name ?? "",
|
|
1783
|
-
expressions: childElements(node)
|
|
1784
|
-
.map(parseExpression)
|
|
1785
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1786
|
-
source: node.source,
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
if (node.localName === "qti-repeat") {
|
|
1791
|
-
return {
|
|
1792
|
-
type: "repeat",
|
|
1793
|
-
numberRepeats: node.attributes["number-repeats"] ?? "",
|
|
1794
|
-
expressions: childElements(node)
|
|
1795
|
-
.map(parseExpression)
|
|
1796
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1797
|
-
source: node.source,
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
if (node.localName === "qti-stats-operator") {
|
|
1802
|
-
const expression = parseFirstExpression(node);
|
|
1803
|
-
if (expression) {
|
|
1804
|
-
return {
|
|
1805
|
-
type: "statsOperator",
|
|
1806
|
-
name: node.attributes.name ?? "",
|
|
1807
|
-
expression,
|
|
1808
|
-
source: node.source,
|
|
1809
|
-
};
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
if (node.localName === "qti-custom-operator") {
|
|
1814
|
-
return {
|
|
1815
|
-
type: "customOperator",
|
|
1816
|
-
definition: node.attributes.definition,
|
|
1817
|
-
className: node.attributes.class,
|
|
1818
|
-
attributes: node.attributes,
|
|
1819
|
-
expressions: childElements(node)
|
|
1820
|
-
.map(parseExpression)
|
|
1821
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined),
|
|
1822
|
-
source: node.source,
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
if (node.localName === "qti-match") {
|
|
1827
|
-
const variable = childElements(node, "qti-variable")[0];
|
|
1828
|
-
const correct = childElements(node, "qti-correct")[0];
|
|
1829
|
-
if (variable && correct) {
|
|
1830
|
-
return {
|
|
1831
|
-
type: "matchCorrect",
|
|
1832
|
-
identifier: variable?.attributes.identifier ?? "",
|
|
1833
|
-
correctIdentifier: correct?.attributes.identifier ?? "",
|
|
1834
|
-
source: node.source,
|
|
1835
|
-
};
|
|
1836
|
-
}
|
|
1837
|
-
const [left, right] = childElements(node)
|
|
1838
|
-
.map(parseExpression)
|
|
1839
|
-
.filter((expression): expression is QtiProcessingExpression => expression !== undefined);
|
|
1840
|
-
if (left && right) return { type: "match", left, right, source: node.source };
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
return undefined;
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
function numericCompareOperatorFor(localName: string): "lt" | "lte" | "gt" | "gte" | undefined {
|
|
1847
|
-
if (localName === "qti-lt") return "lt";
|
|
1848
|
-
if (localName === "qti-lte") return "lte";
|
|
1849
|
-
if (localName === "qti-gt") return "gt";
|
|
1850
|
-
if (localName === "qti-gte") return "gte";
|
|
1851
|
-
return undefined;
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
function durationCompareOperatorFor(localName: string): "lt" | "gte" | undefined {
|
|
1855
|
-
if (localName === "qti-duration-lt") return "lt";
|
|
1856
|
-
if (localName === "qti-duration-gte") return "gte";
|
|
1857
|
-
return undefined;
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
function coerceValue(value: string, baseType: string | undefined): QtiScalarValue {
|
|
1861
|
-
if (baseType === "integer") return Number.parseInt(value, 10);
|
|
1862
|
-
if (baseType === "float") return Number.parseFloat(value);
|
|
1863
|
-
if (baseType === "boolean") {
|
|
1864
|
-
if (value === "true") return true;
|
|
1865
|
-
if (value === "false") return false;
|
|
1866
|
-
}
|
|
1867
|
-
return value;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
function parseCardinality(value: string | undefined): QtiCardinality {
|
|
1871
|
-
if (value === "multiple" || value === "ordered" || value === "record") return value;
|
|
1872
|
-
return "single";
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
function parseXmlBoolean(value: string | undefined): boolean | undefined {
|
|
1876
|
-
if (value === "true" || value === "1") return true;
|
|
1877
|
-
if (value === "false" || value === "0") return false;
|
|
1878
|
-
return undefined;
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
function normalizeValueForCardinality(value: QtiValue, cardinality: QtiCardinality): QtiValue {
|
|
1882
|
-
if (
|
|
1883
|
-
(cardinality === "multiple" || cardinality === "ordered") &&
|
|
1884
|
-
value !== null &&
|
|
1885
|
-
!Array.isArray(value) &&
|
|
1886
|
-
!isRecordValue(value)
|
|
1887
|
-
) {
|
|
1888
|
-
return [value];
|
|
1889
|
-
}
|
|
1890
|
-
return value;
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
function isRecordValue(value: QtiValue): value is QtiRecordValue {
|
|
1894
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1895
|
-
}
|