@longsightgroup/qti3-core 0.7.3 → 0.8.1

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