@longsightgroup/qti3-core 0.1.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/dist/parser.js ADDED
@@ -0,0 +1,1338 @@
1
+ import { getInteractionSupport, interactionNameToType, processingSupport } from "./support.js";
2
+ import { validateAssessmentItem } from "./validation.js";
3
+ import { childElements, descendants, parseXmlTree, textContent } from "./xml.js";
4
+ const supportedProcessingNames = new Set(processingSupport.map((entry) => entry.qtiName));
5
+ const processingContainerNames = new Set(["qti-template-processing", "qti-response-processing"]);
6
+ const responseProcessingForbiddenNames = new Set([
7
+ "qti-number-correct",
8
+ "qti-number-incorrect",
9
+ "qti-number-presented",
10
+ "qti-number-responded",
11
+ "qti-number-selected",
12
+ "qti-outcome-minimum",
13
+ "qti-outcome-maximum",
14
+ "qti-test-variables",
15
+ ]);
16
+ export function parseQtiXml(xml) {
17
+ const diagnostics = [];
18
+ const tree = parseXmlTree(xml);
19
+ for (const error of tree.errors) {
20
+ diagnostics.push({
21
+ code: "xml.parse",
22
+ severity: "error",
23
+ message: error.message,
24
+ });
25
+ }
26
+ if (!tree.root) {
27
+ diagnostics.push({
28
+ code: "xml.empty",
29
+ severity: "error",
30
+ message: "No XML root element was found.",
31
+ });
32
+ return { ok: false, diagnostics };
33
+ }
34
+ const itemNode = tree.root.localName === "qti-assessment-item" ? tree.root : undefined;
35
+ if (!itemNode) {
36
+ diagnostics.push({
37
+ code: "qti.root",
38
+ severity: "error",
39
+ message: `Expected qti-assessment-item root, found ${tree.root.localName}.`,
40
+ path: tree.root.source.path,
41
+ source: tree.root.source,
42
+ });
43
+ return { ok: false, diagnostics };
44
+ }
45
+ const item = parseAssessmentItem(itemNode, diagnostics);
46
+ const document = { item, diagnostics };
47
+ const validation = validateAssessmentItem(document);
48
+ diagnostics.push(...validation.diagnostics);
49
+ return {
50
+ ok: diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
51
+ document,
52
+ diagnostics,
53
+ };
54
+ }
55
+ function parseAssessmentItem(node, diagnostics) {
56
+ const identifier = node.attributes.identifier ?? "";
57
+ const responseDeclarations = childElements(node, "qti-response-declaration").map(parseResponseDeclaration);
58
+ const responseDeclarationMap = new Map(responseDeclarations.map((declaration) => [declaration.identifier, declaration]));
59
+ const outcomeDeclarations = childElements(node, "qti-outcome-declaration").map(parseOutcomeDeclaration);
60
+ const templateDeclarations = childElements(node, "qti-template-declaration").map(parseTemplateDeclaration);
61
+ const templateProcessing = parseTemplateProcessing(childElements(node, "qti-template-processing")[0]);
62
+ const responseProcessing = parseResponseProcessing(childElements(node, "qti-response-processing")[0]);
63
+ diagnoseProcessingElements(childElements(node, "qti-template-processing")[0], diagnostics);
64
+ diagnoseProcessingElements(childElements(node, "qti-response-processing")[0], diagnostics);
65
+ const itemBody = childElements(node, "qti-item-body")[0];
66
+ const interactions = [];
67
+ const body = itemBody
68
+ ? parseContentChildren(itemBody, diagnostics, responseDeclarationMap, interactions)
69
+ : [];
70
+ if (!itemBody) {
71
+ interactions.push(...descendants(node, isInteractionElement).map((interactionNode) => parseInteraction(interactionNode, diagnostics, responseDeclarationMap)));
72
+ }
73
+ const modalFeedback = childElements(node, "qti-modal-feedback").map(parseModalFeedback);
74
+ const catalogInfo = parseCatalogInfo(childElements(node, "qti-catalog-info")[0]);
75
+ const catalogReferences = itemBody ? parseCatalogReferences(itemBody) : [];
76
+ const stylesheets = childElements(node, "qti-stylesheet").map(parseStylesheet);
77
+ const prompt = itemBody ? childElements(itemBody, "qti-prompt")[0] : undefined;
78
+ return {
79
+ identifier,
80
+ title: node.attributes.title,
81
+ adaptive: node.attributes.adaptive === "true",
82
+ prompt: prompt ? textContent(prompt) : undefined,
83
+ responseDeclarations,
84
+ outcomeDeclarations,
85
+ templateDeclarations,
86
+ templateProcessing,
87
+ responseProcessing,
88
+ interactions,
89
+ modalFeedback,
90
+ catalogInfo,
91
+ catalogReferences,
92
+ stylesheets,
93
+ body,
94
+ bodyText: textContent(node),
95
+ source: node.source,
96
+ };
97
+ }
98
+ function diagnoseProcessingElements(processingNode, diagnostics) {
99
+ if (!processingNode)
100
+ return;
101
+ for (const node of [processingNode, ...descendants(processingNode, () => true)]) {
102
+ if (!node.localName.startsWith("qti-"))
103
+ continue;
104
+ if (processingNode.localName === "qti-response-processing" &&
105
+ responseProcessingForbiddenNames.has(node.localName)) {
106
+ diagnostics.push({
107
+ code: "processing.response.forbidden",
108
+ severity: "error",
109
+ message: `${node.localName} must not be used in qti-response-processing.`,
110
+ path: node.source.path,
111
+ source: node.source,
112
+ });
113
+ continue;
114
+ }
115
+ if (processingContainerNames.has(node.localName) ||
116
+ supportedProcessingNames.has(node.localName)) {
117
+ continue;
118
+ }
119
+ diagnostics.push({
120
+ code: "processing.unsupported",
121
+ severity: "error",
122
+ message: `${node.localName} is not currently supported as a QTI processing element.`,
123
+ path: node.source.path,
124
+ source: node.source,
125
+ });
126
+ }
127
+ }
128
+ function parseStylesheet(node) {
129
+ return {
130
+ href: node.attributes.href ?? "",
131
+ type: node.attributes.type,
132
+ media: node.attributes.media,
133
+ title: node.attributes.title,
134
+ attributes: node.attributes,
135
+ source: node.source,
136
+ };
137
+ }
138
+ function parseCatalogReferences(node) {
139
+ const references = [
140
+ ...(node.attributes["data-catalog-idref"] ? [node] : []),
141
+ ...descendants(node, (child) => Boolean(child.attributes["data-catalog-idref"])),
142
+ ];
143
+ return references.map((reference) => ({
144
+ idref: reference.attributes["data-catalog-idref"] ?? "",
145
+ source: reference.source,
146
+ }));
147
+ }
148
+ function isInteractionElement(node) {
149
+ return interactionNameToType.has(node.localName) || /^qti-.+-interaction$/.test(node.localName);
150
+ }
151
+ function parseModalFeedback(node) {
152
+ const showHide = node.attributes["show-hide"] === "hide" ? "hide" : "show";
153
+ return {
154
+ identifier: node.attributes.identifier ?? "",
155
+ outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
156
+ showHide,
157
+ text: textContent(node),
158
+ source: node.source,
159
+ };
160
+ }
161
+ function parseContentChildren(node, diagnostics, responseDeclarationMap, interactions) {
162
+ const content = [];
163
+ for (const entry of node.content) {
164
+ if (typeof entry === "string") {
165
+ if (entry.length > 0)
166
+ content.push({ kind: "text", text: entry, source: node.source });
167
+ continue;
168
+ }
169
+ const parsed = parseContentNode(entry, diagnostics, responseDeclarationMap, interactions);
170
+ if (parsed)
171
+ content.push(parsed);
172
+ }
173
+ return content;
174
+ }
175
+ function parseContentNode(node, diagnostics, responseDeclarationMap, interactions) {
176
+ if (isInteractionElement(node)) {
177
+ const interaction = parseInteraction(node, diagnostics, responseDeclarationMap);
178
+ const interactionIndex = interactions.push(interaction) - 1;
179
+ return {
180
+ kind: "interaction",
181
+ interactionIndex,
182
+ qtiName: node.localName,
183
+ responseIdentifier: interaction.responseIdentifier,
184
+ source: node.source,
185
+ };
186
+ }
187
+ if (node.localName === "qti-printed-variable") {
188
+ return {
189
+ kind: "printedVariable",
190
+ identifier: node.attributes.identifier ?? "",
191
+ format: node.attributes.format,
192
+ attributes: node.attributes,
193
+ source: node.source,
194
+ };
195
+ }
196
+ if (node.localName === "qti-feedback-block" || node.localName === "qti-feedback-inline") {
197
+ return {
198
+ kind: "feedback",
199
+ feedbackType: node.localName === "qti-feedback-block" ? "block" : "inline",
200
+ identifier: node.attributes.identifier ?? "",
201
+ outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
202
+ showHide: node.attributes["show-hide"] === "hide" ? "hide" : "show",
203
+ attributes: node.attributes,
204
+ children: parseContentChildren(node, diagnostics, responseDeclarationMap, interactions),
205
+ source: node.source,
206
+ };
207
+ }
208
+ return {
209
+ kind: "element",
210
+ qtiName: node.localName,
211
+ attributes: node.attributes,
212
+ children: parseContentChildren(node, diagnostics, responseDeclarationMap, interactions),
213
+ source: node.source,
214
+ };
215
+ }
216
+ function parseCatalogInfo(node) {
217
+ if (!node)
218
+ return undefined;
219
+ return {
220
+ catalogs: childElements(node, "qti-catalog").map((catalog) => ({
221
+ id: catalog.attributes.id ?? "",
222
+ attributes: catalog.attributes,
223
+ cards: childElements(catalog, "qti-card").map(parseCatalogCard),
224
+ source: catalog.source,
225
+ })),
226
+ source: node.source,
227
+ };
228
+ }
229
+ function parseCatalogCard(node) {
230
+ return {
231
+ support: node.attributes.support ?? "",
232
+ htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
233
+ fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
234
+ entries: childElements(node, "qti-card-entry").map(parseCatalogCardEntry),
235
+ attributes: node.attributes,
236
+ source: node.source,
237
+ };
238
+ }
239
+ function parseCatalogCardEntry(node) {
240
+ return {
241
+ language: node.attributes["xml:lang"] ?? node.attributes.lang,
242
+ default: node.attributes.default === "true",
243
+ htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
244
+ fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
245
+ attributes: node.attributes,
246
+ source: node.source,
247
+ };
248
+ }
249
+ function parseCatalogHtmlContent(node) {
250
+ if (!node)
251
+ return undefined;
252
+ return {
253
+ text: textContent(node),
254
+ children: parseCatalogHtmlChildren(node),
255
+ attributes: node.attributes,
256
+ source: node.source,
257
+ };
258
+ }
259
+ function parseCatalogHtmlChildren(node) {
260
+ const content = [];
261
+ for (const entry of node.content) {
262
+ if (typeof entry === "string") {
263
+ if (entry.length > 0)
264
+ content.push({ kind: "text", text: entry, source: node.source });
265
+ continue;
266
+ }
267
+ content.push({
268
+ kind: "element",
269
+ qtiName: entry.localName,
270
+ attributes: entry.attributes,
271
+ children: parseCatalogHtmlChildren(entry),
272
+ source: entry.source,
273
+ });
274
+ }
275
+ return content;
276
+ }
277
+ function parseCatalogFileHref(node) {
278
+ return {
279
+ href: textContent(node).trim(),
280
+ mimeType: node.attributes["mime-type"],
281
+ attributes: node.attributes,
282
+ source: node.source,
283
+ };
284
+ }
285
+ function parseResponseDeclaration(node) {
286
+ const cardinality = parseCardinality(node.attributes.cardinality);
287
+ return {
288
+ kind: "response",
289
+ identifier: node.attributes.identifier ?? "",
290
+ cardinality,
291
+ baseType: node.attributes["base-type"],
292
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0]),
293
+ correctResponse: normalizeValueForCardinality(parseVariableValue(childElements(node, "qti-correct-response")[0]), cardinality),
294
+ mapping: parseMapping(childElements(node, "qti-mapping")[0]),
295
+ areaMapping: parseAreaMapping(childElements(node, "qti-area-mapping")[0]),
296
+ attributes: node.attributes,
297
+ source: node.source,
298
+ };
299
+ }
300
+ function parseOutcomeDeclaration(node) {
301
+ const baseType = node.attributes["base-type"];
302
+ return {
303
+ kind: "outcome",
304
+ identifier: node.attributes.identifier ?? "",
305
+ cardinality: parseCardinality(node.attributes.cardinality),
306
+ baseType,
307
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0]),
308
+ lookupTable: parseLookupTable(node, baseType),
309
+ attributes: node.attributes,
310
+ source: node.source,
311
+ };
312
+ }
313
+ function parseTemplateDeclaration(node) {
314
+ return {
315
+ kind: "template",
316
+ identifier: node.attributes.identifier ?? "",
317
+ cardinality: parseCardinality(node.attributes.cardinality),
318
+ baseType: node.attributes["base-type"],
319
+ defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0]),
320
+ attributes: node.attributes,
321
+ source: node.source,
322
+ };
323
+ }
324
+ function parseInteraction(node, diagnostics, responseDeclarationMap) {
325
+ const interactionType = interactionNameToType.get(node.localName);
326
+ const responseIdentifier = node.attributes["response-identifier"];
327
+ const responseDeclaration = responseIdentifier
328
+ ? responseDeclarationMap.get(responseIdentifier)
329
+ : undefined;
330
+ const prompt = childElements(node, "qti-prompt")[0];
331
+ if (!interactionType) {
332
+ diagnostics.push({
333
+ code: "interaction.unsupported",
334
+ severity: "warning",
335
+ message: `${node.localName} is not currently in the support registry.`,
336
+ path: node.source.path,
337
+ source: node.source,
338
+ });
339
+ }
340
+ const support = getInteractionSupport(node.localName);
341
+ if (support?.support === "deprecated") {
342
+ diagnostics.push({
343
+ code: "interaction.deprecated",
344
+ severity: "warning",
345
+ message: `${node.localName} is deprecated. ${support.notes ?? ""}`.trim(),
346
+ path: node.source.path,
347
+ source: node.source,
348
+ });
349
+ }
350
+ const objectNode = interactionType === "positionObject"
351
+ ? positionObjectInteractionObject(node)
352
+ : descendants(node, (child) => child.localName === "object" || child.localName === "img")[0];
353
+ return {
354
+ type: interactionType ?? "custom",
355
+ qtiName: node.localName,
356
+ responseIdentifier,
357
+ responseCardinality: responseDeclaration?.cardinality,
358
+ responseBaseType: responseDeclaration?.baseType,
359
+ prompt: prompt ? textContent(prompt) : undefined,
360
+ contextText: inlineInteractionContext(node, interactionType),
361
+ object: parseObjectAsset(objectNode),
362
+ positionObjectStage: interactionType === "positionObject"
363
+ ? parseObjectAsset(positionObjectStageObject(node))
364
+ : undefined,
365
+ choices: parseChoices(node),
366
+ hottextSegments: interactionType === "hottext" ? parseHottextSegments(node) : undefined,
367
+ gapMatchSegments: interactionType === "gapMatch" || interactionType === "graphicGapMatch"
368
+ ? parseGapMatchSegments(node)
369
+ : undefined,
370
+ childElements: childElements(node).map((child) => ({
371
+ qtiName: child.localName,
372
+ source: child.source,
373
+ })),
374
+ attributes: node.attributes,
375
+ text: textContent(node),
376
+ source: node.source,
377
+ };
378
+ }
379
+ function positionObjectInteractionObject(node) {
380
+ return childElements(node).find((child) => child.localName === "object" || child.localName === "img");
381
+ }
382
+ function positionObjectStageObject(node) {
383
+ const ancestorStage = nearestAncestor(node, "qti-position-object-stage");
384
+ const stage = ancestorStage ?? childElements(node, "qti-position-object-stage")[0];
385
+ if (!stage)
386
+ return undefined;
387
+ return childElements(stage).find((child) => child.localName === "object" || child.localName === "img");
388
+ }
389
+ function nearestAncestor(node, localName) {
390
+ for (let parent = node.parent; parent; parent = parent.parent) {
391
+ if (parent.localName === localName)
392
+ return parent;
393
+ }
394
+ return undefined;
395
+ }
396
+ function parseHottextSegments(node) {
397
+ const segments = [];
398
+ const visit = (entry) => {
399
+ if (typeof entry === "string") {
400
+ const text = entry.replace(/\s+/g, " ");
401
+ if (text.trim().length > 0)
402
+ segments.push({ kind: "text", text });
403
+ return;
404
+ }
405
+ if (entry.localName === "qti-prompt")
406
+ return;
407
+ if (entry.localName === "qti-hottext") {
408
+ segments.push({
409
+ kind: "hottext",
410
+ identifier: entry.attributes.identifier ?? "",
411
+ text: textContent(entry),
412
+ attributes: entry.attributes,
413
+ source: entry.source,
414
+ });
415
+ return;
416
+ }
417
+ for (const child of entry.content)
418
+ visit(child);
419
+ if (entry.localName === "p" || entry.localName === "div") {
420
+ segments.push({ kind: "text", text: " " });
421
+ }
422
+ };
423
+ for (const entry of node.content)
424
+ visit(entry);
425
+ return segments;
426
+ }
427
+ function parseGapMatchSegments(node) {
428
+ const segments = [];
429
+ const visit = (entry) => {
430
+ if (typeof entry === "string") {
431
+ const text = entry.replace(/\s+/g, " ");
432
+ if (text.trim().length > 0)
433
+ segments.push({ kind: "text", text });
434
+ return;
435
+ }
436
+ if (entry.localName === "qti-prompt")
437
+ return;
438
+ if (entry.localName === "qti-gap-text" || entry.localName === "qti-gap-img")
439
+ return;
440
+ if (entry.localName === "object" || entry.localName === "img")
441
+ return;
442
+ if (entry.localName === "qti-gap") {
443
+ segments.push({
444
+ kind: "gap",
445
+ identifier: entry.attributes.identifier ?? "",
446
+ attributes: entry.attributes,
447
+ source: entry.source,
448
+ });
449
+ return;
450
+ }
451
+ for (const child of entry.content)
452
+ visit(child);
453
+ if (entry.localName === "p" || entry.localName === "div") {
454
+ segments.push({ kind: "text", text: " " });
455
+ }
456
+ };
457
+ for (const entry of node.content)
458
+ visit(entry);
459
+ return segments;
460
+ }
461
+ function inlineInteractionContext(node, interactionType) {
462
+ if (interactionType !== "inlineChoice" && interactionType !== "textEntry")
463
+ return undefined;
464
+ const parent = node.parent;
465
+ if (!parent)
466
+ return undefined;
467
+ return normalizeInlineContext(parent.text) ?? normalizeInlineContext(textContent(parent));
468
+ }
469
+ function normalizeInlineContext(value) {
470
+ const normalized = value
471
+ .replace(/\s+/g, " ")
472
+ .replace(/\s+([.,;:!?])/g, "$1")
473
+ .trim();
474
+ return normalized.length > 0 ? normalized : undefined;
475
+ }
476
+ function parseObjectAsset(node) {
477
+ if (!node)
478
+ return undefined;
479
+ const data = node.attributes.data ?? node.attributes.src;
480
+ return {
481
+ data,
482
+ type: node.attributes.type ?? assetTypeFromData(data),
483
+ width: node.attributes.width,
484
+ height: node.attributes.height,
485
+ text: textContent(node),
486
+ attributes: node.attributes,
487
+ source: node.source,
488
+ };
489
+ }
490
+ function assetTypeFromData(data) {
491
+ if (!data)
492
+ return undefined;
493
+ if (data.startsWith("data:image/svg+xml"))
494
+ return "image/svg+xml";
495
+ if (data.startsWith("data:image/"))
496
+ return "image/*";
497
+ if (/\.(svg|png|jpg|jpeg|gif|webp)(?:[?#].*)?$/i.test(data))
498
+ return "image/*";
499
+ return undefined;
500
+ }
501
+ function parseChoices(node) {
502
+ const choiceNames = new Set([
503
+ "qti-simple-choice",
504
+ "qti-simple-associable-choice",
505
+ "qti-inline-choice",
506
+ "qti-gap-text",
507
+ "qti-gap-img",
508
+ "qti-hottext",
509
+ "qti-hotspot-choice",
510
+ "qti-associable-hotspot",
511
+ "qti-gap",
512
+ ]);
513
+ return descendants(node, (child) => choiceNames.has(child.localName)).map((choice, index) => {
514
+ const identifier = choice.attributes.identifier ?? "";
515
+ return {
516
+ identifier,
517
+ text: textContent(choice) || identifier || `Choice ${index + 1}`,
518
+ role: choiceRole(choice),
519
+ qtiName: choice.localName,
520
+ attributes: choice.attributes,
521
+ source: choice.source,
522
+ };
523
+ });
524
+ }
525
+ function choiceRole(node) {
526
+ if (node.localName === "qti-simple-choice")
527
+ return "simpleChoice";
528
+ if (node.localName === "qti-inline-choice")
529
+ return "inlineChoice";
530
+ if (node.localName === "qti-gap-text" || node.localName === "qti-gap-img")
531
+ return "gapChoice";
532
+ if (node.localName === "qti-gap")
533
+ return "gap";
534
+ if (node.localName === "qti-hottext")
535
+ return "hottext";
536
+ if (node.localName === "qti-hotspot-choice" || node.localName === "qti-associable-hotspot") {
537
+ return "hotspot";
538
+ }
539
+ if (node.localName === "qti-simple-associable-choice") {
540
+ const matchSet = node.parent?.localName === "qti-simple-match-set" ? node.parent : undefined;
541
+ const interaction = nearestInteraction(node);
542
+ if (matchSet && interaction?.localName === "qti-match-interaction") {
543
+ return matchSetIndex(matchSet) === 0 ? "matchSource" : "matchTarget";
544
+ }
545
+ return "associableChoice";
546
+ }
547
+ return "simpleChoice";
548
+ }
549
+ function nearestInteraction(node) {
550
+ for (let parent = node.parent; parent; parent = parent.parent) {
551
+ if (interactionNameToType.has(parent.localName))
552
+ return parent;
553
+ }
554
+ return undefined;
555
+ }
556
+ function matchSetIndex(node) {
557
+ const siblings = node.parent?.children.filter((sibling) => sibling.localName === "qti-simple-match-set") ?? [];
558
+ return siblings.indexOf(node);
559
+ }
560
+ function parseVariableValue(node) {
561
+ if (!node)
562
+ return null;
563
+ const valueNodes = childElements(node, "qti-value");
564
+ const recordEntries = valueNodes
565
+ .map((valueNode) => ({
566
+ fieldIdentifier: valueNode.attributes["field-identifier"],
567
+ value: coerceValue(textContent(valueNode), valueNode.attributes["base-type"]),
568
+ }))
569
+ .filter((entry) => Boolean(entry.fieldIdentifier));
570
+ if (recordEntries.length > 0) {
571
+ return Object.fromEntries(recordEntries.map((entry) => [entry.fieldIdentifier, entry.value]));
572
+ }
573
+ const values = valueNodes.map((valueNode) => textContent(valueNode));
574
+ if (values.length === 0) {
575
+ const text = textContent(node);
576
+ return text.length > 0 ? text : null;
577
+ }
578
+ if (values.length === 1)
579
+ return values[0] ?? null;
580
+ return values;
581
+ }
582
+ function parseMapping(node) {
583
+ if (!node)
584
+ return undefined;
585
+ return {
586
+ defaultValue: Number(node.attributes["default-value"] ?? 0),
587
+ attributes: node.attributes,
588
+ source: node.source,
589
+ entries: childElements(node, "qti-map-entry").map((entry) => ({
590
+ mapKey: entry.attributes["map-key"],
591
+ mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
592
+ attributes: entry.attributes,
593
+ source: entry.source,
594
+ })),
595
+ };
596
+ }
597
+ function parseAreaMapping(node) {
598
+ if (!node)
599
+ return undefined;
600
+ return {
601
+ defaultValue: Number(node.attributes["default-value"] ?? 0),
602
+ attributes: node.attributes,
603
+ source: node.source,
604
+ entries: childElements(node, "qti-area-map-entry").map((entry) => ({
605
+ shape: parseShape(entry.attributes.shape),
606
+ coords: parseCoords(entry.attributes.coords),
607
+ mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
608
+ attributes: entry.attributes,
609
+ source: entry.source,
610
+ })),
611
+ };
612
+ }
613
+ function parseLookupTable(node, baseType) {
614
+ const matchTable = childElements(node, "qti-match-table")[0];
615
+ if (matchTable)
616
+ return parseMatchTable(matchTable, baseType);
617
+ const interpolationTable = childElements(node, "qti-interpolation-table")[0];
618
+ if (interpolationTable)
619
+ return parseInterpolationTable(interpolationTable, baseType);
620
+ return undefined;
621
+ }
622
+ function parseMatchTable(node, baseType) {
623
+ return {
624
+ type: "match",
625
+ defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
626
+ attributes: node.attributes,
627
+ source: node.source,
628
+ entries: childElements(node, "qti-match-table-entry").map((entry) => ({
629
+ sourceValue: Number(entry.attributes["source-value"]),
630
+ targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
631
+ attributes: entry.attributes,
632
+ source: entry.source,
633
+ })),
634
+ };
635
+ }
636
+ function parseInterpolationTable(node, baseType) {
637
+ return {
638
+ type: "interpolation",
639
+ defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
640
+ attributes: node.attributes,
641
+ source: node.source,
642
+ entries: childElements(node, "qti-interpolation-table-entry").map((entry) => ({
643
+ sourceValue: Number(entry.attributes["source-value"]),
644
+ targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
645
+ includeBoundary: entry.attributes["include-boundary"] !== "false",
646
+ attributes: entry.attributes,
647
+ source: entry.source,
648
+ })),
649
+ };
650
+ }
651
+ function parseLookupValue(value, baseType) {
652
+ return value === undefined ? null : coerceValue(value, baseType);
653
+ }
654
+ function parseShape(shape) {
655
+ if (shape === "circle" || shape === "rect" || shape === "poly")
656
+ return shape;
657
+ return "default";
658
+ }
659
+ function parseCoords(value) {
660
+ return (value ?? "")
661
+ .split(",")
662
+ .map((part) => Number(part.trim()))
663
+ .filter((part) => Number.isFinite(part));
664
+ }
665
+ function parseResponseProcessing(node) {
666
+ if (!node)
667
+ return undefined;
668
+ return {
669
+ template: node.attributes.template,
670
+ rules: parseResponseRules(node),
671
+ conditions: responseConditionsFromChildren(node),
672
+ };
673
+ }
674
+ function parseTemplateProcessing(node) {
675
+ if (!node)
676
+ return undefined;
677
+ return {
678
+ rules: parseTemplateRules(node),
679
+ };
680
+ }
681
+ function parseTemplateRules(node) {
682
+ return childElements(node)
683
+ .map(parseTemplateRule)
684
+ .filter((rule) => rule !== undefined);
685
+ }
686
+ function parseTemplateRule(node) {
687
+ if (node.localName === "qti-set-template-value") {
688
+ return {
689
+ type: "setTemplateValue",
690
+ identifier: node.attributes.identifier ?? "",
691
+ expression: parseFirstExpression(node) ?? {
692
+ type: "baseValue",
693
+ value: null,
694
+ source: node.source,
695
+ },
696
+ source: node.source,
697
+ };
698
+ }
699
+ if (node.localName === "qti-set-default-value") {
700
+ return {
701
+ type: "setDefaultValue",
702
+ identifier: node.attributes.identifier ?? "",
703
+ expression: parseFirstExpression(node) ?? {
704
+ type: "baseValue",
705
+ value: null,
706
+ source: node.source,
707
+ },
708
+ source: node.source,
709
+ };
710
+ }
711
+ if (node.localName === "qti-set-correct-response") {
712
+ return {
713
+ type: "setCorrectResponse",
714
+ identifier: node.attributes.identifier ?? "",
715
+ expression: parseFirstExpression(node) ?? {
716
+ type: "baseValue",
717
+ value: null,
718
+ source: node.source,
719
+ },
720
+ source: node.source,
721
+ };
722
+ }
723
+ if (node.localName === "qti-template-condition") {
724
+ const templateIf = childElements(node, "qti-template-if")[0];
725
+ const templateElse = childElements(node, "qti-template-else")[0];
726
+ return {
727
+ type: "templateCondition",
728
+ ifExpression: templateIf ? parseFirstExpression(templateIf) : undefined,
729
+ thenRules: templateIf ? parseTemplateRules(templateIf) : [],
730
+ elseIfs: childElements(node, "qti-template-else-if").map((branch) => ({
731
+ expression: parseFirstExpression(branch),
732
+ rules: parseTemplateRules(branch),
733
+ })),
734
+ elseRules: templateElse ? parseTemplateRules(templateElse) : [],
735
+ source: node.source,
736
+ };
737
+ }
738
+ if (node.localName === "qti-exit-template") {
739
+ return {
740
+ type: "exitTemplate",
741
+ source: node.source,
742
+ };
743
+ }
744
+ if (node.localName === "qti-template-constraint") {
745
+ return {
746
+ type: "templateConstraint",
747
+ expression: parseFirstExpression(node) ?? {
748
+ type: "baseValue",
749
+ value: null,
750
+ source: node.source,
751
+ },
752
+ source: node.source,
753
+ };
754
+ }
755
+ return undefined;
756
+ }
757
+ function responseConditionsFromChildren(node) {
758
+ return childElements(node).flatMap((child) => {
759
+ if (child.localName === "qti-response-condition")
760
+ return [parseResponseCondition(child)];
761
+ if (child.localName === "qti-response-processing-fragment") {
762
+ return responseConditionsFromChildren(child);
763
+ }
764
+ return [];
765
+ });
766
+ }
767
+ function parseResponseCondition(node) {
768
+ const responseIf = childElements(node, "qti-response-if")[0];
769
+ const responseElse = childElements(node, "qti-response-else")[0];
770
+ return {
771
+ ifExpression: responseIf ? parseFirstExpression(responseIf) : undefined,
772
+ thenRules: responseIf ? parseResponseRules(responseIf) : [],
773
+ elseIfs: childElements(node, "qti-response-else-if").map((branch) => ({
774
+ expression: parseFirstExpression(branch),
775
+ rules: parseResponseRules(branch),
776
+ })),
777
+ elseRules: responseElse ? parseResponseRules(responseElse) : [],
778
+ };
779
+ }
780
+ function parseResponseRules(node) {
781
+ return childElements(node)
782
+ .map(parseResponseRule)
783
+ .filter((rule) => rule !== undefined);
784
+ }
785
+ function parseResponseRule(node) {
786
+ if (node.localName === "qti-response-condition") {
787
+ return {
788
+ type: "responseCondition",
789
+ condition: parseResponseCondition(node),
790
+ source: node.source,
791
+ };
792
+ }
793
+ if (node.localName === "qti-set-outcome-value")
794
+ return parseSetOutcomeValue(node);
795
+ if (node.localName === "qti-lookup-outcome-value")
796
+ return parseLookupOutcomeValue(node);
797
+ if (node.localName === "qti-exit-response") {
798
+ return { type: "exitResponse", source: node.source };
799
+ }
800
+ if (node.localName === "qti-response-processing-fragment") {
801
+ return {
802
+ type: "responseProcessingFragment",
803
+ rules: parseResponseRules(node),
804
+ source: node.source,
805
+ };
806
+ }
807
+ return undefined;
808
+ }
809
+ function parseLookupOutcomeValue(node) {
810
+ return {
811
+ type: "lookupOutcomeValue",
812
+ identifier: node.attributes.identifier ?? "",
813
+ expression: parseFirstExpression(node) ?? {
814
+ type: "baseValue",
815
+ value: null,
816
+ source: node.source,
817
+ },
818
+ source: node.source,
819
+ };
820
+ }
821
+ function parseSetOutcomeValue(setNode) {
822
+ return {
823
+ type: "setOutcomeValue",
824
+ identifier: setNode.attributes.identifier ?? "",
825
+ expression: parseFirstExpression(setNode) ?? {
826
+ type: "baseValue",
827
+ value: null,
828
+ source: setNode.source,
829
+ },
830
+ source: setNode.source,
831
+ };
832
+ }
833
+ function parseFirstExpression(node) {
834
+ for (const child of node.children) {
835
+ const expression = parseExpression(child);
836
+ if (expression)
837
+ return expression;
838
+ }
839
+ return undefined;
840
+ }
841
+ function parseExpression(node) {
842
+ if (node.localName === "qti-base-value") {
843
+ const rawValue = textContent(node);
844
+ return {
845
+ type: "baseValue",
846
+ value: coerceValue(rawValue, node.attributes["base-type"]),
847
+ rawValue,
848
+ baseType: node.attributes["base-type"],
849
+ source: node.source,
850
+ };
851
+ }
852
+ if (node.localName === "qti-null") {
853
+ return { type: "null", source: node.source };
854
+ }
855
+ if (node.localName === "qti-is-null") {
856
+ const variable = childElements(node, "qti-variable")[0];
857
+ return {
858
+ type: "isNull",
859
+ identifier: variable?.attributes.identifier ?? "",
860
+ source: node.source,
861
+ };
862
+ }
863
+ if (node.localName === "qti-map-response") {
864
+ return {
865
+ type: "mapResponse",
866
+ identifier: node.attributes.identifier ?? "",
867
+ source: node.source,
868
+ };
869
+ }
870
+ if (node.localName === "qti-map-response-point") {
871
+ return {
872
+ type: "mapResponsePoint",
873
+ identifier: node.attributes.identifier ?? "",
874
+ source: node.source,
875
+ };
876
+ }
877
+ if (node.localName === "qti-correct") {
878
+ return {
879
+ type: "correct",
880
+ identifier: node.attributes.identifier ?? "",
881
+ source: node.source,
882
+ };
883
+ }
884
+ if (node.localName === "qti-default") {
885
+ return {
886
+ type: "default",
887
+ identifier: node.attributes.identifier ?? "",
888
+ source: node.source,
889
+ };
890
+ }
891
+ if (node.localName === "qti-variable") {
892
+ return { type: "variable", identifier: node.attributes.identifier ?? "", source: node.source };
893
+ }
894
+ if (node.localName === "qti-random-integer") {
895
+ return {
896
+ type: "randomInteger",
897
+ min: Number(node.attributes.min ?? 0),
898
+ max: Number(node.attributes.max ?? 0),
899
+ step: Number(node.attributes.step ?? 1),
900
+ attributes: node.attributes,
901
+ source: node.source,
902
+ };
903
+ }
904
+ if (node.localName === "qti-random-float") {
905
+ return {
906
+ type: "randomFloat",
907
+ min: Number(node.attributes.min ?? 0),
908
+ max: Number(node.attributes.max ?? 0),
909
+ attributes: node.attributes,
910
+ source: node.source,
911
+ };
912
+ }
913
+ if (node.localName === "qti-random") {
914
+ const multiple = childElements(node, "qti-multiple")[0];
915
+ return {
916
+ type: "random",
917
+ values: childElements(multiple ?? node)
918
+ .map(parseExpression)
919
+ .filter((expression) => expression !== undefined),
920
+ source: node.source,
921
+ };
922
+ }
923
+ if (node.localName === "qti-multiple") {
924
+ return {
925
+ type: "multiple",
926
+ expressions: childElements(node)
927
+ .map(parseExpression)
928
+ .filter((expression) => expression !== undefined),
929
+ source: node.source,
930
+ };
931
+ }
932
+ if (node.localName === "qti-ordered") {
933
+ return {
934
+ type: "ordered",
935
+ expressions: childElements(node)
936
+ .map(parseExpression)
937
+ .filter((expression) => expression !== undefined),
938
+ source: node.source,
939
+ };
940
+ }
941
+ if (node.localName === "qti-index") {
942
+ const expression = parseFirstExpression(node);
943
+ if (expression) {
944
+ return { type: "index", expression, n: node.attributes.n ?? "", source: node.source };
945
+ }
946
+ }
947
+ if (node.localName === "qti-container-size") {
948
+ const expression = parseFirstExpression(node);
949
+ if (expression)
950
+ return { type: "containerSize", expression, source: node.source };
951
+ }
952
+ if (node.localName === "qti-sum") {
953
+ return {
954
+ type: "sum",
955
+ expressions: childElements(node)
956
+ .map(parseExpression)
957
+ .filter((expression) => expression !== undefined),
958
+ source: node.source,
959
+ };
960
+ }
961
+ if (node.localName === "qti-product") {
962
+ return {
963
+ type: "product",
964
+ expressions: childElements(node)
965
+ .map(parseExpression)
966
+ .filter((expression) => expression !== undefined),
967
+ source: node.source,
968
+ };
969
+ }
970
+ if (node.localName === "qti-min") {
971
+ return {
972
+ type: "min",
973
+ expressions: childElements(node)
974
+ .map(parseExpression)
975
+ .filter((expression) => expression !== undefined),
976
+ source: node.source,
977
+ };
978
+ }
979
+ if (node.localName === "qti-max") {
980
+ return {
981
+ type: "max",
982
+ expressions: childElements(node)
983
+ .map(parseExpression)
984
+ .filter((expression) => expression !== undefined),
985
+ source: node.source,
986
+ };
987
+ }
988
+ if (node.localName === "qti-subtract") {
989
+ const expressions = childElements(node)
990
+ .map(parseExpression)
991
+ .filter((expression) => expression !== undefined);
992
+ const [left, right] = expressions;
993
+ if (left && right)
994
+ return { type: "subtract", left, right, source: node.source };
995
+ }
996
+ if (node.localName === "qti-divide") {
997
+ const expressions = childElements(node)
998
+ .map(parseExpression)
999
+ .filter((expression) => expression !== undefined);
1000
+ const [left, right] = expressions;
1001
+ if (left && right)
1002
+ return { type: "divide", left, right, source: node.source };
1003
+ }
1004
+ if (node.localName === "qti-power") {
1005
+ const expressions = childElements(node)
1006
+ .map(parseExpression)
1007
+ .filter((expression) => expression !== undefined);
1008
+ const [left, right] = expressions;
1009
+ if (left && right)
1010
+ return { type: "power", left, right, source: node.source };
1011
+ }
1012
+ if (node.localName === "qti-integer-divide") {
1013
+ const expressions = childElements(node)
1014
+ .map(parseExpression)
1015
+ .filter((expression) => expression !== undefined);
1016
+ const [left, right] = expressions;
1017
+ if (left && right)
1018
+ return { type: "integerDivide", left, right, source: node.source };
1019
+ }
1020
+ if (node.localName === "qti-integer-modulus") {
1021
+ const expressions = childElements(node)
1022
+ .map(parseExpression)
1023
+ .filter((expression) => expression !== undefined);
1024
+ const [left, right] = expressions;
1025
+ if (left && right)
1026
+ return { type: "integerModulus", left, right, source: node.source };
1027
+ }
1028
+ if (node.localName === "qti-round") {
1029
+ const expression = parseFirstExpression(node);
1030
+ if (expression)
1031
+ return { type: "round", expression, source: node.source };
1032
+ }
1033
+ if (node.localName === "qti-round-to") {
1034
+ const expression = parseFirstExpression(node);
1035
+ const roundingMode = node.attributes["rounding-mode"];
1036
+ const figures = Number(node.attributes.figures ?? 0);
1037
+ if (expression &&
1038
+ (roundingMode === "decimalPlaces" || roundingMode === "significantFigures") &&
1039
+ Number.isInteger(figures) &&
1040
+ (roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0)) {
1041
+ return { type: "roundTo", expression, roundingMode, figures, source: node.source };
1042
+ }
1043
+ }
1044
+ if (node.localName === "qti-truncate") {
1045
+ const expression = parseFirstExpression(node);
1046
+ if (expression)
1047
+ return { type: "truncate", expression, source: node.source };
1048
+ }
1049
+ if (node.localName === "qti-integer-to-float") {
1050
+ const expression = parseFirstExpression(node);
1051
+ if (expression)
1052
+ return { type: "integerToFloat", expression, source: node.source };
1053
+ }
1054
+ if (node.localName === "qti-and") {
1055
+ return {
1056
+ type: "and",
1057
+ expressions: childElements(node)
1058
+ .map(parseExpression)
1059
+ .filter((expression) => expression !== undefined),
1060
+ source: node.source,
1061
+ };
1062
+ }
1063
+ if (node.localName === "qti-any-n") {
1064
+ return {
1065
+ type: "anyN",
1066
+ min: node.attributes.min ?? "",
1067
+ max: node.attributes.max ?? "",
1068
+ expressions: childElements(node)
1069
+ .map(parseExpression)
1070
+ .filter((expression) => expression !== undefined),
1071
+ source: node.source,
1072
+ };
1073
+ }
1074
+ if (node.localName === "qti-or") {
1075
+ return {
1076
+ type: "or",
1077
+ expressions: childElements(node)
1078
+ .map(parseExpression)
1079
+ .filter((expression) => expression !== undefined),
1080
+ source: node.source,
1081
+ };
1082
+ }
1083
+ if (node.localName === "qti-not") {
1084
+ const expression = parseFirstExpression(node);
1085
+ if (expression)
1086
+ return { type: "not", expression, source: node.source };
1087
+ }
1088
+ if (node.localName === "qti-equal") {
1089
+ const [left, right] = childElements(node)
1090
+ .map(parseExpression)
1091
+ .filter((expression) => expression !== undefined);
1092
+ if (left && right)
1093
+ return { type: "equal", left, right, source: node.source };
1094
+ }
1095
+ if (node.localName === "qti-equal-rounded") {
1096
+ const [left, right] = childElements(node)
1097
+ .map(parseExpression)
1098
+ .filter((expression) => expression !== undefined);
1099
+ const roundingMode = node.attributes["rounding-mode"] ?? "";
1100
+ const figures = Number(node.attributes.figures ?? 0);
1101
+ if (left && right) {
1102
+ return { type: "equalRounded", left, right, roundingMode, figures, source: node.source };
1103
+ }
1104
+ }
1105
+ const numericCompareOperator = numericCompareOperatorFor(node.localName);
1106
+ if (numericCompareOperator) {
1107
+ const [left, right] = childElements(node)
1108
+ .map(parseExpression)
1109
+ .filter((expression) => expression !== undefined);
1110
+ if (left && right) {
1111
+ return {
1112
+ type: "numericCompare",
1113
+ operator: numericCompareOperator,
1114
+ left,
1115
+ right,
1116
+ source: node.source,
1117
+ };
1118
+ }
1119
+ }
1120
+ const durationCompareOperator = durationCompareOperatorFor(node.localName);
1121
+ if (durationCompareOperator) {
1122
+ const [left, right] = childElements(node)
1123
+ .map(parseExpression)
1124
+ .filter((expression) => expression !== undefined);
1125
+ if (left && right) {
1126
+ return {
1127
+ type: "durationCompare",
1128
+ operator: durationCompareOperator,
1129
+ left,
1130
+ right,
1131
+ source: node.source,
1132
+ };
1133
+ }
1134
+ }
1135
+ if (node.localName === "qti-string-match") {
1136
+ const [left, right] = childElements(node)
1137
+ .map(parseExpression)
1138
+ .filter((expression) => expression !== undefined);
1139
+ if (left && right) {
1140
+ return {
1141
+ type: "stringMatch",
1142
+ left,
1143
+ right,
1144
+ caseSensitive: node.attributes["case-sensitive"] !== "false",
1145
+ substring: node.attributes.substring === "true",
1146
+ source: node.source,
1147
+ };
1148
+ }
1149
+ }
1150
+ if (node.localName === "qti-substring") {
1151
+ const [left, right] = childElements(node)
1152
+ .map(parseExpression)
1153
+ .filter((expression) => expression !== undefined);
1154
+ if (left && right) {
1155
+ return {
1156
+ type: "substring",
1157
+ left,
1158
+ right,
1159
+ caseSensitive: node.attributes["case-sensitive"] !== "false",
1160
+ source: node.source,
1161
+ };
1162
+ }
1163
+ }
1164
+ if (node.localName === "qti-pattern-match") {
1165
+ const expression = parseFirstExpression(node);
1166
+ if (expression) {
1167
+ return {
1168
+ type: "patternMatch",
1169
+ expression,
1170
+ pattern: node.attributes.pattern ?? "",
1171
+ source: node.source,
1172
+ };
1173
+ }
1174
+ }
1175
+ if (node.localName === "qti-field-value") {
1176
+ const expression = parseFirstExpression(node);
1177
+ if (expression) {
1178
+ return {
1179
+ type: "fieldValue",
1180
+ fieldIdentifier: node.attributes["field-identifier"] ?? "",
1181
+ expression,
1182
+ source: node.source,
1183
+ };
1184
+ }
1185
+ }
1186
+ if (node.localName === "qti-member") {
1187
+ const [value, collection] = childElements(node)
1188
+ .map(parseExpression)
1189
+ .filter((expression) => expression !== undefined);
1190
+ if (value && collection)
1191
+ return { type: "member", value, collection, source: node.source };
1192
+ }
1193
+ if (node.localName === "qti-delete") {
1194
+ const [value, collection] = childElements(node)
1195
+ .map(parseExpression)
1196
+ .filter((expression) => expression !== undefined);
1197
+ if (value && collection)
1198
+ return { type: "delete", value, collection, source: node.source };
1199
+ }
1200
+ if (node.localName === "qti-contains") {
1201
+ const [collection, values] = childElements(node)
1202
+ .map(parseExpression)
1203
+ .filter((expression) => expression !== undefined);
1204
+ if (collection && values)
1205
+ return { type: "contains", collection, values, source: node.source };
1206
+ }
1207
+ if (node.localName === "qti-gcd" || node.localName === "qti-lcm") {
1208
+ return {
1209
+ type: node.localName === "qti-gcd" ? "gcd" : "lcm",
1210
+ expressions: childElements(node)
1211
+ .map(parseExpression)
1212
+ .filter((expression) => expression !== undefined),
1213
+ source: node.source,
1214
+ };
1215
+ }
1216
+ if (node.localName === "qti-inside") {
1217
+ const expression = parseFirstExpression(node);
1218
+ if (expression) {
1219
+ return {
1220
+ type: "inside",
1221
+ expression,
1222
+ shape: parseShape(node.attributes.shape),
1223
+ coords: parseCoords(node.attributes.coords),
1224
+ attributes: node.attributes,
1225
+ source: node.source,
1226
+ };
1227
+ }
1228
+ }
1229
+ if (node.localName === "qti-math-constant") {
1230
+ return { type: "mathConstant", name: node.attributes.name ?? "", source: node.source };
1231
+ }
1232
+ if (node.localName === "qti-math-operator") {
1233
+ return {
1234
+ type: "mathOperator",
1235
+ name: node.attributes.name ?? "",
1236
+ expressions: childElements(node)
1237
+ .map(parseExpression)
1238
+ .filter((expression) => expression !== undefined),
1239
+ source: node.source,
1240
+ };
1241
+ }
1242
+ if (node.localName === "qti-repeat") {
1243
+ return {
1244
+ type: "repeat",
1245
+ numberRepeats: node.attributes["number-repeats"] ?? "",
1246
+ expressions: childElements(node)
1247
+ .map(parseExpression)
1248
+ .filter((expression) => expression !== undefined),
1249
+ source: node.source,
1250
+ };
1251
+ }
1252
+ if (node.localName === "qti-stats-operator") {
1253
+ const expression = parseFirstExpression(node);
1254
+ if (expression) {
1255
+ return {
1256
+ type: "statsOperator",
1257
+ name: node.attributes.name ?? "",
1258
+ expression,
1259
+ source: node.source,
1260
+ };
1261
+ }
1262
+ }
1263
+ if (node.localName === "qti-custom-operator") {
1264
+ return {
1265
+ type: "customOperator",
1266
+ definition: node.attributes.definition,
1267
+ className: node.attributes.class,
1268
+ attributes: node.attributes,
1269
+ expressions: childElements(node)
1270
+ .map(parseExpression)
1271
+ .filter((expression) => expression !== undefined),
1272
+ source: node.source,
1273
+ };
1274
+ }
1275
+ if (node.localName === "qti-match") {
1276
+ const variable = childElements(node, "qti-variable")[0];
1277
+ const correct = childElements(node, "qti-correct")[0];
1278
+ if (variable && correct) {
1279
+ return {
1280
+ type: "matchCorrect",
1281
+ identifier: variable?.attributes.identifier ?? "",
1282
+ correctIdentifier: correct?.attributes.identifier ?? "",
1283
+ source: node.source,
1284
+ };
1285
+ }
1286
+ const [left, right] = childElements(node)
1287
+ .map(parseExpression)
1288
+ .filter((expression) => expression !== undefined);
1289
+ if (left && right)
1290
+ return { type: "match", left, right, source: node.source };
1291
+ }
1292
+ return undefined;
1293
+ }
1294
+ function numericCompareOperatorFor(localName) {
1295
+ if (localName === "qti-lt")
1296
+ return "lt";
1297
+ if (localName === "qti-lte")
1298
+ return "lte";
1299
+ if (localName === "qti-gt")
1300
+ return "gt";
1301
+ if (localName === "qti-gte")
1302
+ return "gte";
1303
+ return undefined;
1304
+ }
1305
+ function durationCompareOperatorFor(localName) {
1306
+ if (localName === "qti-duration-lt")
1307
+ return "lt";
1308
+ if (localName === "qti-duration-gte")
1309
+ return "gte";
1310
+ return undefined;
1311
+ }
1312
+ function coerceValue(value, baseType) {
1313
+ if (baseType === "integer")
1314
+ return Number.parseInt(value, 10);
1315
+ if (baseType === "float")
1316
+ return Number.parseFloat(value);
1317
+ if (baseType === "boolean")
1318
+ return value === "true";
1319
+ return value;
1320
+ }
1321
+ function parseCardinality(value) {
1322
+ if (value === "multiple" || value === "ordered" || value === "record")
1323
+ return value;
1324
+ return "single";
1325
+ }
1326
+ function normalizeValueForCardinality(value, cardinality) {
1327
+ if ((cardinality === "multiple" || cardinality === "ordered") &&
1328
+ value !== null &&
1329
+ !Array.isArray(value) &&
1330
+ !isRecordValue(value)) {
1331
+ return [value];
1332
+ }
1333
+ return value;
1334
+ }
1335
+ function isRecordValue(value) {
1336
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1337
+ }
1338
+ //# sourceMappingURL=parser.js.map