@longsightgroup/qti3-core 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +41 -10
  2. package/dist/content-text.d.ts +14 -0
  3. package/dist/content-text.d.ts.map +1 -0
  4. package/dist/content-text.js +58 -0
  5. package/dist/content-text.js.map +1 -0
  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 +32 -1086
  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 +50 -0
  58. package/dist/shared-vocabulary-authoring.d.ts.map +1 -0
  59. package/dist/shared-vocabulary-authoring.js +302 -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 +96 -56
  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 +159 -10
  83. package/dist/support.js.map +1 -1
  84. package/dist/types.d.ts +38 -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 -249
  92. package/dist/xml.js.map +1 -1
  93. package/package.json +1 -4
  94. package/src/content-text.ts +80 -0
  95. package/src/index.ts +67 -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 +59 -1230
  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 +409 -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 +207 -0
  114. package/src/shared-vocabulary-support.ts +127 -101
  115. package/src/shared-vocabulary.ts +18 -0
  116. package/src/support.ts +174 -14
  117. package/src/types.ts +48 -7
  118. package/src/validation.ts +33 -341
  119. package/src/xml.ts +378 -255
package/dist/parser.js CHANGED
@@ -1,4 +1,10 @@
1
- import { getInteractionSupport, interactionNameToType, processingSupport } from "./support.js";
1
+ import { appendContentTextNode, flatTextFromContent } from "./content-text.js";
2
+ import { parseOutcomeDeclaration, parseResponseDeclaration, parseTemplateDeclaration, } from "./parser-declarations.js";
3
+ import { firstChildElement, parseCatalogInfo, parseCatalogReferences, parseCompanionMaterialsInfo, parseModalFeedback, parseStylesheet, } from "./parser-item-metadata.js";
4
+ import { parseXmlBoolean } from "./parser-values.js";
5
+ import { parseInteractionCustomPayload } from "./parser-custom-interactions.js";
6
+ import { parseResponseProcessing, parseTemplateProcessing } from "./parser-processing.js";
7
+ import { interactionNameToType, interactionRegistryDiagnostics, interactionRegistryStatus, processingSupport, } from "./support.js";
2
8
  import { validateAssessmentItem } from "./validation.js";
3
9
  import { childElements, descendants, parseXmlTree, textContent } from "./xml.js";
4
10
  const qtiAssessmentItemNamespace = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0";
@@ -79,7 +85,8 @@ function parseAssessmentItem(node, diagnostics) {
79
85
  interactions.push(...descendants(node, isInteractionElement).map((interactionNode) => parseInteraction(interactionNode, diagnostics, responseDeclarationMap)));
80
86
  }
81
87
  const modalFeedback = childElements(node, "qti-modal-feedback").map(parseModalFeedback);
82
- const catalogInfo = parseCatalogInfo(childElements(node, "qti-catalog-info")[0]);
88
+ const catalogInfo = parseCatalogInfo(firstChildElement(node, "qti-catalog-info", diagnostics, "item.child.duplicate"));
89
+ const companionMaterials = parseCompanionMaterialsInfo(firstChildElement(node, "qti-companion-materials-info", diagnostics, "item.child.duplicate"), diagnostics);
83
90
  const catalogReferences = itemBody ? parseCatalogReferences(itemBody) : [];
84
91
  const stylesheets = childElements(node, "qti-stylesheet").map(parseStylesheet);
85
92
  const prompt = itemBody ? childElements(itemBody, "qti-prompt")[0] : undefined;
@@ -100,6 +107,7 @@ function parseAssessmentItem(node, diagnostics) {
100
107
  interactions,
101
108
  modalFeedback,
102
109
  catalogInfo,
110
+ companionMaterials,
103
111
  catalogReferences,
104
112
  stylesheets,
105
113
  body,
@@ -137,45 +145,14 @@ function diagnoseProcessingElements(processingNode, diagnostics) {
137
145
  });
138
146
  }
139
147
  }
140
- function parseStylesheet(node) {
141
- return {
142
- href: node.attributes.href ?? "",
143
- type: node.attributes.type,
144
- media: node.attributes.media,
145
- title: node.attributes.title,
146
- attributes: node.attributes,
147
- source: node.source,
148
- };
149
- }
150
- function parseCatalogReferences(node) {
151
- const references = [
152
- ...(node.attributes["data-catalog-idref"] ? [node] : []),
153
- ...descendants(node, (child) => Boolean(child.attributes["data-catalog-idref"])),
154
- ];
155
- return references.map((reference) => ({
156
- idref: reference.attributes["data-catalog-idref"] ?? "",
157
- source: reference.source,
158
- }));
159
- }
160
148
  function isInteractionElement(node) {
161
149
  return interactionNameToType.has(node.localName) || /^qti-.+-interaction$/.test(node.localName);
162
150
  }
163
- function parseModalFeedback(node) {
164
- const showHide = node.attributes["show-hide"] === "hide" ? "hide" : "show";
165
- return {
166
- identifier: node.attributes.identifier ?? "",
167
- outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
168
- showHide,
169
- text: textContent(node),
170
- source: node.source,
171
- };
172
- }
173
151
  function parseContentChildren(node, diagnostics, responseDeclarationMap, interactions) {
174
152
  const content = [];
175
153
  for (const entry of node.content) {
176
154
  if (typeof entry === "string") {
177
- if (entry.length > 0)
178
- content.push({ kind: "text", text: entry, source: node.source });
155
+ appendContentTextNode(content, entry, node.source);
179
156
  continue;
180
157
  }
181
158
  const parsed = parseContentNode(entry, diagnostics, responseDeclarationMap, interactions);
@@ -225,116 +202,6 @@ function parseContentNode(node, diagnostics, responseDeclarationMap, interaction
225
202
  source: node.source,
226
203
  };
227
204
  }
228
- function parseCatalogInfo(node) {
229
- if (!node)
230
- return undefined;
231
- return {
232
- catalogs: childElements(node, "qti-catalog").map((catalog) => ({
233
- id: catalog.attributes.id ?? "",
234
- attributes: catalog.attributes,
235
- cards: childElements(catalog, "qti-card").map(parseCatalogCard),
236
- source: catalog.source,
237
- })),
238
- source: node.source,
239
- };
240
- }
241
- function parseCatalogCard(node) {
242
- return {
243
- support: node.attributes.support ?? "",
244
- htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
245
- fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
246
- entries: childElements(node, "qti-card-entry").map(parseCatalogCardEntry),
247
- attributes: node.attributes,
248
- source: node.source,
249
- };
250
- }
251
- function parseCatalogCardEntry(node) {
252
- return {
253
- language: node.attributes["xml:lang"] ?? node.attributes.lang,
254
- default: node.attributes.default === "true",
255
- htmlContent: parseCatalogHtmlContent(childElements(node, "qti-html-content")[0]),
256
- fileHrefs: childElements(node, "qti-file-href").map(parseCatalogFileHref),
257
- attributes: node.attributes,
258
- source: node.source,
259
- };
260
- }
261
- function parseCatalogHtmlContent(node) {
262
- if (!node)
263
- return undefined;
264
- return {
265
- text: textContent(node),
266
- children: parseCatalogHtmlChildren(node),
267
- attributes: node.attributes,
268
- source: node.source,
269
- };
270
- }
271
- function parseCatalogHtmlChildren(node) {
272
- const content = [];
273
- for (const entry of node.content) {
274
- if (typeof entry === "string") {
275
- if (entry.length > 0)
276
- content.push({ kind: "text", text: entry, source: node.source });
277
- continue;
278
- }
279
- content.push({
280
- kind: "element",
281
- qtiName: entry.localName,
282
- attributes: entry.attributes,
283
- children: parseCatalogHtmlChildren(entry),
284
- source: entry.source,
285
- });
286
- }
287
- return content;
288
- }
289
- function parseCatalogFileHref(node) {
290
- return {
291
- href: textContent(node).trim(),
292
- mimeType: node.attributes["mime-type"],
293
- attributes: node.attributes,
294
- source: node.source,
295
- };
296
- }
297
- function parseResponseDeclaration(node) {
298
- const cardinality = parseCardinality(node.attributes.cardinality);
299
- const baseType = node.attributes["base-type"];
300
- return {
301
- kind: "response",
302
- identifier: node.attributes.identifier ?? "",
303
- cardinality,
304
- baseType,
305
- defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
306
- correctResponse: normalizeValueForCardinality(parseVariableValue(childElements(node, "qti-correct-response")[0], baseType), cardinality),
307
- mapping: parseMapping(childElements(node, "qti-mapping")[0]),
308
- areaMapping: parseAreaMapping(childElements(node, "qti-area-mapping")[0]),
309
- attributes: node.attributes,
310
- source: node.source,
311
- };
312
- }
313
- function parseOutcomeDeclaration(node) {
314
- const baseType = node.attributes["base-type"];
315
- return {
316
- kind: "outcome",
317
- identifier: node.attributes.identifier ?? "",
318
- cardinality: parseCardinality(node.attributes.cardinality),
319
- baseType,
320
- defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
321
- lookupTable: parseLookupTable(node, baseType),
322
- attributes: node.attributes,
323
- source: node.source,
324
- };
325
- }
326
- function parseTemplateDeclaration(node) {
327
- const baseType = node.attributes["base-type"];
328
- return {
329
- kind: "template",
330
- identifier: node.attributes.identifier ?? "",
331
- cardinality: parseCardinality(node.attributes.cardinality),
332
- baseType,
333
- defaultValue: parseVariableValue(childElements(node, "qti-default-value")[0], baseType),
334
- attributes: node.attributes,
335
- source: node.source,
336
- };
337
- }
338
205
  function parseInteraction(node, diagnostics, responseDeclarationMap) {
339
206
  const interactionType = interactionNameToType.get(node.localName);
340
207
  const responseIdentifier = node.attributes["response-identifier"];
@@ -342,25 +209,8 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
342
209
  ? responseDeclarationMap.get(responseIdentifier)
343
210
  : undefined;
344
211
  const prompt = childElements(node, "qti-prompt")[0];
345
- if (!interactionType) {
346
- diagnostics.push({
347
- code: "interaction.unsupported",
348
- severity: "warning",
349
- message: `${node.localName} is not currently in the support registry.`,
350
- path: node.source.path,
351
- source: node.source,
352
- });
353
- }
354
- const support = getInteractionSupport(node.localName);
355
- if (support?.support === "deprecated") {
356
- diagnostics.push({
357
- code: "interaction.deprecated",
358
- severity: "warning",
359
- message: `${node.localName} is deprecated. ${support.notes ?? ""}`.trim(),
360
- path: node.source.path,
361
- source: node.source,
362
- });
363
- }
212
+ const registryStatus = interactionRegistryStatus(node.localName);
213
+ diagnostics.push(...interactionRegistryDiagnostics(node.localName, node.source));
364
214
  const objectNode = interactionType === "positionObject"
365
215
  ? positionObjectInteractionObject(node)
366
216
  : interactionType === "media"
@@ -368,13 +218,22 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
368
218
  : interactionType === "drawing"
369
219
  ? drawingInteractionObject(node)
370
220
  : descendants(node, (child) => child.localName === "object" || child.localName === "img")[0];
221
+ const promptContent = prompt
222
+ ? parseContentChildren(prompt, diagnostics, responseDeclarationMap, [])
223
+ : undefined;
371
224
  return {
372
225
  type: interactionType ?? "custom",
226
+ registryStatus,
373
227
  qtiName: node.localName,
374
228
  responseIdentifier,
375
229
  responseCardinality: responseDeclaration?.cardinality,
376
230
  responseBaseType: responseDeclaration?.baseType,
377
- prompt: prompt ? textContent(prompt) : undefined,
231
+ prompt: promptContent
232
+ ? flatTextFromContent(promptContent, { excludeAnnotations: true }) || undefined
233
+ : prompt
234
+ ? textContent(prompt)
235
+ : undefined,
236
+ promptContent: promptContent && promptContent.length > 0 ? promptContent : undefined,
378
237
  promptAttributes: prompt?.attributes,
379
238
  promptSource: prompt?.source,
380
239
  contextText: inlineInteractionContext(node, interactionType),
@@ -382,10 +241,8 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
382
241
  positionObjectStage: interactionType === "positionObject"
383
242
  ? parseObjectAsset(positionObjectStageObject(node))
384
243
  : undefined,
385
- portableCustom: interactionType === "portableCustom"
386
- ? parsePortableCustomDefinition(node, diagnostics)
387
- : undefined,
388
- choices: parseChoices(node),
244
+ ...parseInteractionCustomPayload(node, interactionType, diagnostics, isInteractionElement),
245
+ choices: parseChoices(node, diagnostics, responseDeclarationMap),
389
246
  hottextSegments: interactionType === "hottext" ? parseHottextSegments(node) : undefined,
390
247
  gapMatchSegments: interactionType === "gapMatch" || interactionType === "graphicGapMatch"
391
248
  ? parseGapMatchSegments(node)
@@ -399,135 +256,6 @@ function parseInteraction(node, diagnostics, responseDeclarationMap) {
399
256
  source: node.source,
400
257
  };
401
258
  }
402
- function parsePortableCustomDefinition(node, diagnostics) {
403
- const markup = firstChildElement(node, "qti-interaction-markup", diagnostics);
404
- const modules = firstChildElement(node, "qti-interaction-modules", diagnostics);
405
- return {
406
- responseIdentifier: node.attributes["response-identifier"],
407
- customInteractionTypeIdentifier: node.attributes["custom-interaction-type-identifier"],
408
- module: node.attributes.module,
409
- interactionModules: modules ? parsePortableCustomInteractionModules(modules) : undefined,
410
- interactionMarkup: markup ? parsePortableCustomMarkupChildren(markup, diagnostics) : [],
411
- interactionMarkupRaw: markup ? serializeXmlContent(markup) : undefined,
412
- templateVariables: childElements(node, "qti-template-variable").map((variable) => parsePortableCustomVariableBinding(variable, "template")),
413
- contextVariables: childElements(node, "qti-context-variable").map((variable) => parsePortableCustomVariableBinding(variable, "context")),
414
- stylesheets: childElements(node, "qti-stylesheet").map(parseStylesheet),
415
- catalogInfo: parseCatalogInfo(childElements(node, "qti-catalog-info")[0]),
416
- dataAttributes: Object.fromEntries(Object.entries(node.attributes).filter(([name]) => name.startsWith("data-"))),
417
- attributes: node.attributes,
418
- source: node.source,
419
- };
420
- }
421
- function firstChildElement(node, name, diagnostics) {
422
- const matches = childElements(node, name);
423
- if (matches.length > 1) {
424
- diagnostics.push({
425
- code: "interaction.portableCustom.child.duplicate",
426
- severity: "error",
427
- message: `${node.localName} allows at most one ${name} child.`,
428
- path: matches[1]?.source?.path ?? node.source?.path,
429
- source: matches[1]?.source ?? node.source,
430
- });
431
- }
432
- return matches[0];
433
- }
434
- function parsePortableCustomMarkupChildren(node, diagnostics) {
435
- const content = [];
436
- for (const entry of node.content) {
437
- if (typeof entry === "string") {
438
- if (entry.length > 0)
439
- content.push({ kind: "text", text: entry, source: node.source });
440
- continue;
441
- }
442
- content.push(parsePortableCustomMarkupNode(entry, diagnostics));
443
- }
444
- return content;
445
- }
446
- function parsePortableCustomMarkupNode(node, diagnostics) {
447
- if (isInteractionElement(node)) {
448
- diagnostics.push({
449
- code: "interaction.portableCustom.markupInteraction",
450
- severity: "error",
451
- message: `qti-interaction-markup must not contain nested QTI interaction ${node.localName}.`,
452
- path: node.source?.path,
453
- source: node.source,
454
- });
455
- }
456
- if (node.localName === "qti-printed-variable") {
457
- return {
458
- kind: "printedVariable",
459
- identifier: node.attributes.identifier ?? "",
460
- format: node.attributes.format,
461
- attributes: node.attributes,
462
- source: node.source,
463
- };
464
- }
465
- if (node.localName === "qti-feedback-block" || node.localName === "qti-feedback-inline") {
466
- return {
467
- kind: "feedback",
468
- feedbackType: node.localName === "qti-feedback-block" ? "block" : "inline",
469
- identifier: node.attributes.identifier ?? "",
470
- outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
471
- showHide: node.attributes["show-hide"] === "hide" ? "hide" : "show",
472
- attributes: node.attributes,
473
- children: parsePortableCustomMarkupChildren(node, diagnostics),
474
- source: node.source,
475
- };
476
- }
477
- return {
478
- kind: "element",
479
- qtiName: node.localName,
480
- attributes: node.attributes,
481
- children: parsePortableCustomMarkupChildren(node, diagnostics),
482
- source: node.source,
483
- };
484
- }
485
- function parsePortableCustomInteractionModules(node) {
486
- return {
487
- primaryConfiguration: node.attributes["primary-configuration"],
488
- secondaryConfiguration: node.attributes["secondary-configuration"] ?? node.attributes["fallback-configuration"],
489
- modules: childElements(node, "qti-interaction-module").map(parsePortableCustomInteractionModule),
490
- attributes: node.attributes,
491
- source: node.source,
492
- };
493
- }
494
- function parsePortableCustomInteractionModule(node) {
495
- return {
496
- id: node.attributes.id,
497
- primaryPath: node.attributes["primary-path"],
498
- fallbackPath: node.attributes["fallback-path"],
499
- attributes: node.attributes,
500
- source: node.source,
501
- };
502
- }
503
- function parsePortableCustomVariableBinding(node, kind) {
504
- return {
505
- kind,
506
- identifier: node.attributes.identifier ?? node.attributes["template-identifier"],
507
- variableIdentifier: node.attributes["variable-identifier"],
508
- attributes: node.attributes,
509
- source: node.source,
510
- };
511
- }
512
- function serializeXmlContent(node) {
513
- return node.content
514
- .map((entry) => (typeof entry === "string" ? escapeXmlText(entry) : serializeXmlNode(entry)))
515
- .join("");
516
- }
517
- function serializeXmlNode(node) {
518
- const attributes = Object.entries(node.attributes)
519
- .map(([name, value]) => ` ${name}="${escapeXmlAttribute(value)}"`)
520
- .join("");
521
- if (node.content.length === 0)
522
- return `<${node.name}${attributes}/>`;
523
- return `<${node.name}${attributes}>${serializeXmlContent(node)}</${node.name}>`;
524
- }
525
- function escapeXmlText(value) {
526
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
527
- }
528
- function escapeXmlAttribute(value) {
529
- return escapeXmlText(value).replaceAll('"', "&quot;");
530
- }
531
259
  function positionObjectInteractionObject(node) {
532
260
  return childElements(node).find((child) => child.localName === "object" || child.localName === "img");
533
261
  }
@@ -798,7 +526,7 @@ function assetTypeFromData(data) {
798
526
  return "video/*";
799
527
  return undefined;
800
528
  }
801
- function parseChoices(node) {
529
+ function parseChoices(node, diagnostics, responseDeclarationMap) {
802
530
  const choiceNames = new Set([
803
531
  "qti-simple-choice",
804
532
  "qti-simple-associable-choice",
@@ -813,13 +541,18 @@ function parseChoices(node) {
813
541
  return descendants(node, (child) => choiceNames.has(child.localName)).map((choice, index) => {
814
542
  const identifier = choice.attributes.identifier ?? "";
815
543
  const asset = parseChoiceAsset(choice);
544
+ const content = parseContentChildren(choice, diagnostics, responseDeclarationMap, []);
545
+ const flatChoiceText = content.length > 0
546
+ ? flatTextFromContent(content, { excludeAnnotations: true })
547
+ : textContent(choice);
816
548
  return {
817
549
  identifier,
818
- text: textContent(choice) ||
550
+ text: flatChoiceText ||
819
551
  choice.attributes["object-label"] ||
820
552
  asset?.text ||
821
553
  identifier ||
822
554
  `Choice ${index + 1}`,
555
+ content: content.length > 0 ? content : undefined,
823
556
  asset,
824
557
  role: choiceRole(choice),
825
558
  qtiName: choice.localName,
@@ -869,791 +602,4 @@ function matchSetIndex(node) {
869
602
  const siblings = node.parent?.children.filter((sibling) => sibling.localName === "qti-simple-match-set") ?? [];
870
603
  return siblings.indexOf(node);
871
604
  }
872
- function parseVariableValue(node, baseType) {
873
- if (!node)
874
- return null;
875
- const valueNodes = childElements(node, "qti-value");
876
- const entries = valueNodes.map((valueNode) => ({
877
- fieldIdentifier: valueNode.attributes["field-identifier"],
878
- value: coerceValue(textContent(valueNode), valueNode.attributes["base-type"] ?? baseType),
879
- }));
880
- const recordEntries = entries.filter((entry) => Boolean(entry.fieldIdentifier));
881
- if (recordEntries.length > 0) {
882
- return Object.fromEntries(recordEntries.map((entry) => [entry.fieldIdentifier, entry.value]));
883
- }
884
- if (entries.length === 0) {
885
- const text = textContent(node);
886
- return text.length > 0 ? coerceValue(text, baseType) : null;
887
- }
888
- if (entries.length === 1)
889
- return entries[0]?.value ?? null;
890
- return entries.map((entry) => entry.value);
891
- }
892
- function parseMapping(node) {
893
- if (!node)
894
- return undefined;
895
- return {
896
- defaultValue: Number(node.attributes["default-value"] ?? 0),
897
- attributes: node.attributes,
898
- source: node.source,
899
- entries: childElements(node, "qti-map-entry").map((entry) => ({
900
- mapKey: entry.attributes["map-key"],
901
- mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
902
- attributes: entry.attributes,
903
- source: entry.source,
904
- })),
905
- };
906
- }
907
- function parseAreaMapping(node) {
908
- if (!node)
909
- return undefined;
910
- return {
911
- defaultValue: Number(node.attributes["default-value"] ?? 0),
912
- attributes: node.attributes,
913
- source: node.source,
914
- entries: childElements(node, "qti-area-map-entry").map((entry) => ({
915
- shape: parseShape(entry.attributes.shape),
916
- coords: parseCoords(entry.attributes.coords),
917
- mappedValue: Number(entry.attributes["mapped-value"] ?? 0),
918
- attributes: entry.attributes,
919
- source: entry.source,
920
- })),
921
- };
922
- }
923
- function parseLookupTable(node, baseType) {
924
- const matchTable = childElements(node, "qti-match-table")[0];
925
- if (matchTable)
926
- return parseMatchTable(matchTable, baseType);
927
- const interpolationTable = childElements(node, "qti-interpolation-table")[0];
928
- if (interpolationTable)
929
- return parseInterpolationTable(interpolationTable, baseType);
930
- return undefined;
931
- }
932
- function parseMatchTable(node, baseType) {
933
- return {
934
- type: "match",
935
- defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
936
- attributes: node.attributes,
937
- source: node.source,
938
- entries: childElements(node, "qti-match-table-entry").map((entry) => ({
939
- sourceValue: Number(entry.attributes["source-value"]),
940
- targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
941
- attributes: entry.attributes,
942
- source: entry.source,
943
- })),
944
- };
945
- }
946
- function parseInterpolationTable(node, baseType) {
947
- return {
948
- type: "interpolation",
949
- defaultValue: parseLookupValue(node.attributes["default-value"], baseType),
950
- attributes: node.attributes,
951
- source: node.source,
952
- entries: childElements(node, "qti-interpolation-table-entry").map((entry) => ({
953
- sourceValue: Number(entry.attributes["source-value"]),
954
- targetValue: parseLookupValue(entry.attributes["target-value"], baseType),
955
- includeBoundary: entry.attributes["include-boundary"] !== "false",
956
- attributes: entry.attributes,
957
- source: entry.source,
958
- })),
959
- };
960
- }
961
- function parseLookupValue(value, baseType) {
962
- return value === undefined ? null : coerceValue(value, baseType);
963
- }
964
- function parseShape(shape) {
965
- if (shape === "circle" || shape === "rect" || shape === "poly")
966
- return shape;
967
- return "default";
968
- }
969
- function parseCoords(value) {
970
- return (value ?? "")
971
- .split(",")
972
- .map((part) => Number(part.trim()))
973
- .filter((part) => Number.isFinite(part));
974
- }
975
- function parseResponseProcessing(node) {
976
- if (!node)
977
- return undefined;
978
- return {
979
- template: node.attributes.template,
980
- rules: parseResponseRules(node),
981
- conditions: responseConditionsFromChildren(node),
982
- };
983
- }
984
- function parseTemplateProcessing(node) {
985
- if (!node)
986
- return undefined;
987
- return {
988
- rules: parseTemplateRules(node),
989
- };
990
- }
991
- function parseTemplateRules(node) {
992
- return childElements(node)
993
- .map(parseTemplateRule)
994
- .filter((rule) => rule !== undefined);
995
- }
996
- function parseTemplateRule(node) {
997
- if (node.localName === "qti-set-template-value") {
998
- return {
999
- type: "setTemplateValue",
1000
- identifier: node.attributes.identifier ?? "",
1001
- expression: parseFirstExpression(node) ?? {
1002
- type: "baseValue",
1003
- value: null,
1004
- source: node.source,
1005
- },
1006
- source: node.source,
1007
- };
1008
- }
1009
- if (node.localName === "qti-set-default-value") {
1010
- return {
1011
- type: "setDefaultValue",
1012
- identifier: node.attributes.identifier ?? "",
1013
- expression: parseFirstExpression(node) ?? {
1014
- type: "baseValue",
1015
- value: null,
1016
- source: node.source,
1017
- },
1018
- source: node.source,
1019
- };
1020
- }
1021
- if (node.localName === "qti-set-correct-response") {
1022
- return {
1023
- type: "setCorrectResponse",
1024
- identifier: node.attributes.identifier ?? "",
1025
- expression: parseFirstExpression(node) ?? {
1026
- type: "baseValue",
1027
- value: null,
1028
- source: node.source,
1029
- },
1030
- source: node.source,
1031
- };
1032
- }
1033
- if (node.localName === "qti-template-condition") {
1034
- const templateIf = childElements(node, "qti-template-if")[0];
1035
- const templateElse = childElements(node, "qti-template-else")[0];
1036
- return {
1037
- type: "templateCondition",
1038
- ifExpression: templateIf ? parseFirstExpression(templateIf) : undefined,
1039
- thenRules: templateIf ? parseTemplateRules(templateIf) : [],
1040
- elseIfs: childElements(node, "qti-template-else-if").map((branch) => ({
1041
- expression: parseFirstExpression(branch),
1042
- rules: parseTemplateRules(branch),
1043
- })),
1044
- elseRules: templateElse ? parseTemplateRules(templateElse) : [],
1045
- source: node.source,
1046
- };
1047
- }
1048
- if (node.localName === "qti-exit-template") {
1049
- return {
1050
- type: "exitTemplate",
1051
- source: node.source,
1052
- };
1053
- }
1054
- if (node.localName === "qti-template-constraint") {
1055
- return {
1056
- type: "templateConstraint",
1057
- expression: parseFirstExpression(node) ?? {
1058
- type: "baseValue",
1059
- value: null,
1060
- source: node.source,
1061
- },
1062
- source: node.source,
1063
- };
1064
- }
1065
- return undefined;
1066
- }
1067
- function responseConditionsFromChildren(node) {
1068
- return childElements(node).flatMap((child) => {
1069
- if (child.localName === "qti-response-condition")
1070
- return [parseResponseCondition(child)];
1071
- if (child.localName === "qti-response-processing-fragment") {
1072
- return responseConditionsFromChildren(child);
1073
- }
1074
- return [];
1075
- });
1076
- }
1077
- function parseResponseCondition(node) {
1078
- const responseIf = childElements(node, "qti-response-if")[0];
1079
- const responseElse = childElements(node, "qti-response-else")[0];
1080
- return {
1081
- ifExpression: responseIf ? parseFirstExpression(responseIf) : undefined,
1082
- thenRules: responseIf ? parseResponseRules(responseIf) : [],
1083
- elseIfs: childElements(node, "qti-response-else-if").map((branch) => ({
1084
- expression: parseFirstExpression(branch),
1085
- rules: parseResponseRules(branch),
1086
- })),
1087
- elseRules: responseElse ? parseResponseRules(responseElse) : [],
1088
- };
1089
- }
1090
- function parseResponseRules(node) {
1091
- return childElements(node)
1092
- .map(parseResponseRule)
1093
- .filter((rule) => rule !== undefined);
1094
- }
1095
- function parseResponseRule(node) {
1096
- if (node.localName === "qti-response-condition") {
1097
- return {
1098
- type: "responseCondition",
1099
- condition: parseResponseCondition(node),
1100
- source: node.source,
1101
- };
1102
- }
1103
- if (node.localName === "qti-set-outcome-value")
1104
- return parseSetOutcomeValue(node);
1105
- if (node.localName === "qti-lookup-outcome-value")
1106
- return parseLookupOutcomeValue(node);
1107
- if (node.localName === "qti-exit-response") {
1108
- return { type: "exitResponse", source: node.source };
1109
- }
1110
- if (node.localName === "qti-response-processing-fragment") {
1111
- return {
1112
- type: "responseProcessingFragment",
1113
- rules: parseResponseRules(node),
1114
- source: node.source,
1115
- };
1116
- }
1117
- return undefined;
1118
- }
1119
- function parseLookupOutcomeValue(node) {
1120
- return {
1121
- type: "lookupOutcomeValue",
1122
- identifier: node.attributes.identifier ?? "",
1123
- expression: parseFirstExpression(node) ?? {
1124
- type: "baseValue",
1125
- value: null,
1126
- source: node.source,
1127
- },
1128
- source: node.source,
1129
- };
1130
- }
1131
- function parseSetOutcomeValue(setNode) {
1132
- return {
1133
- type: "setOutcomeValue",
1134
- identifier: setNode.attributes.identifier ?? "",
1135
- expression: parseFirstExpression(setNode) ?? {
1136
- type: "baseValue",
1137
- value: null,
1138
- source: setNode.source,
1139
- },
1140
- source: setNode.source,
1141
- };
1142
- }
1143
- function parseFirstExpression(node) {
1144
- for (const child of node.children) {
1145
- const expression = parseExpression(child);
1146
- if (expression)
1147
- return expression;
1148
- }
1149
- return undefined;
1150
- }
1151
- function parseExpression(node) {
1152
- if (node.localName === "qti-base-value") {
1153
- const rawValue = textContent(node);
1154
- return {
1155
- type: "baseValue",
1156
- value: coerceValue(rawValue, node.attributes["base-type"]),
1157
- rawValue,
1158
- baseType: node.attributes["base-type"],
1159
- source: node.source,
1160
- };
1161
- }
1162
- if (node.localName === "qti-null") {
1163
- return { type: "null", source: node.source };
1164
- }
1165
- if (node.localName === "qti-is-null") {
1166
- const variable = childElements(node, "qti-variable")[0];
1167
- return {
1168
- type: "isNull",
1169
- identifier: variable?.attributes.identifier ?? "",
1170
- source: node.source,
1171
- };
1172
- }
1173
- if (node.localName === "qti-map-response") {
1174
- return {
1175
- type: "mapResponse",
1176
- identifier: node.attributes.identifier ?? "",
1177
- source: node.source,
1178
- };
1179
- }
1180
- if (node.localName === "qti-map-response-point") {
1181
- return {
1182
- type: "mapResponsePoint",
1183
- identifier: node.attributes.identifier ?? "",
1184
- source: node.source,
1185
- };
1186
- }
1187
- if (node.localName === "qti-correct") {
1188
- return {
1189
- type: "correct",
1190
- identifier: node.attributes.identifier ?? "",
1191
- source: node.source,
1192
- };
1193
- }
1194
- if (node.localName === "qti-default") {
1195
- return {
1196
- type: "default",
1197
- identifier: node.attributes.identifier ?? "",
1198
- source: node.source,
1199
- };
1200
- }
1201
- if (node.localName === "qti-variable") {
1202
- return { type: "variable", identifier: node.attributes.identifier ?? "", source: node.source };
1203
- }
1204
- if (node.localName === "qti-random-integer") {
1205
- return {
1206
- type: "randomInteger",
1207
- min: Number(node.attributes.min ?? 0),
1208
- max: Number(node.attributes.max ?? 0),
1209
- step: Number(node.attributes.step ?? 1),
1210
- attributes: node.attributes,
1211
- source: node.source,
1212
- };
1213
- }
1214
- if (node.localName === "qti-random-float") {
1215
- return {
1216
- type: "randomFloat",
1217
- min: Number(node.attributes.min ?? 0),
1218
- max: Number(node.attributes.max ?? 0),
1219
- attributes: node.attributes,
1220
- source: node.source,
1221
- };
1222
- }
1223
- if (node.localName === "qti-random") {
1224
- const multiple = childElements(node, "qti-multiple")[0];
1225
- return {
1226
- type: "random",
1227
- values: childElements(multiple ?? node)
1228
- .map(parseExpression)
1229
- .filter((expression) => expression !== undefined),
1230
- source: node.source,
1231
- };
1232
- }
1233
- if (node.localName === "qti-multiple") {
1234
- return {
1235
- type: "multiple",
1236
- expressions: childElements(node)
1237
- .map(parseExpression)
1238
- .filter((expression) => expression !== undefined),
1239
- source: node.source,
1240
- };
1241
- }
1242
- if (node.localName === "qti-ordered") {
1243
- return {
1244
- type: "ordered",
1245
- expressions: childElements(node)
1246
- .map(parseExpression)
1247
- .filter((expression) => expression !== undefined),
1248
- source: node.source,
1249
- };
1250
- }
1251
- if (node.localName === "qti-index") {
1252
- const expression = parseFirstExpression(node);
1253
- if (expression) {
1254
- return { type: "index", expression, n: node.attributes.n ?? "", source: node.source };
1255
- }
1256
- }
1257
- if (node.localName === "qti-container-size") {
1258
- const expression = parseFirstExpression(node);
1259
- if (expression)
1260
- return { type: "containerSize", expression, source: node.source };
1261
- }
1262
- if (node.localName === "qti-sum") {
1263
- return {
1264
- type: "sum",
1265
- expressions: childElements(node)
1266
- .map(parseExpression)
1267
- .filter((expression) => expression !== undefined),
1268
- source: node.source,
1269
- };
1270
- }
1271
- if (node.localName === "qti-product") {
1272
- return {
1273
- type: "product",
1274
- expressions: childElements(node)
1275
- .map(parseExpression)
1276
- .filter((expression) => expression !== undefined),
1277
- source: node.source,
1278
- };
1279
- }
1280
- if (node.localName === "qti-min") {
1281
- return {
1282
- type: "min",
1283
- expressions: childElements(node)
1284
- .map(parseExpression)
1285
- .filter((expression) => expression !== undefined),
1286
- source: node.source,
1287
- };
1288
- }
1289
- if (node.localName === "qti-max") {
1290
- return {
1291
- type: "max",
1292
- expressions: childElements(node)
1293
- .map(parseExpression)
1294
- .filter((expression) => expression !== undefined),
1295
- source: node.source,
1296
- };
1297
- }
1298
- if (node.localName === "qti-subtract") {
1299
- const expressions = childElements(node)
1300
- .map(parseExpression)
1301
- .filter((expression) => expression !== undefined);
1302
- const [left, right] = expressions;
1303
- if (left && right)
1304
- return { type: "subtract", left, right, source: node.source };
1305
- }
1306
- if (node.localName === "qti-divide") {
1307
- const expressions = childElements(node)
1308
- .map(parseExpression)
1309
- .filter((expression) => expression !== undefined);
1310
- const [left, right] = expressions;
1311
- if (left && right)
1312
- return { type: "divide", left, right, source: node.source };
1313
- }
1314
- if (node.localName === "qti-power") {
1315
- const expressions = childElements(node)
1316
- .map(parseExpression)
1317
- .filter((expression) => expression !== undefined);
1318
- const [left, right] = expressions;
1319
- if (left && right)
1320
- return { type: "power", left, right, source: node.source };
1321
- }
1322
- if (node.localName === "qti-integer-divide") {
1323
- const expressions = childElements(node)
1324
- .map(parseExpression)
1325
- .filter((expression) => expression !== undefined);
1326
- const [left, right] = expressions;
1327
- if (left && right)
1328
- return { type: "integerDivide", left, right, source: node.source };
1329
- }
1330
- if (node.localName === "qti-integer-modulus") {
1331
- const expressions = childElements(node)
1332
- .map(parseExpression)
1333
- .filter((expression) => expression !== undefined);
1334
- const [left, right] = expressions;
1335
- if (left && right)
1336
- return { type: "integerModulus", left, right, source: node.source };
1337
- }
1338
- if (node.localName === "qti-round") {
1339
- const expression = parseFirstExpression(node);
1340
- if (expression)
1341
- return { type: "round", expression, source: node.source };
1342
- }
1343
- if (node.localName === "qti-round-to") {
1344
- const expression = parseFirstExpression(node);
1345
- const roundingMode = node.attributes["rounding-mode"];
1346
- const figures = Number(node.attributes.figures ?? 0);
1347
- if (expression &&
1348
- (roundingMode === "decimalPlaces" || roundingMode === "significantFigures") &&
1349
- Number.isInteger(figures) &&
1350
- (roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0)) {
1351
- return { type: "roundTo", expression, roundingMode, figures, source: node.source };
1352
- }
1353
- }
1354
- if (node.localName === "qti-truncate") {
1355
- const expression = parseFirstExpression(node);
1356
- if (expression)
1357
- return { type: "truncate", expression, source: node.source };
1358
- }
1359
- if (node.localName === "qti-integer-to-float") {
1360
- const expression = parseFirstExpression(node);
1361
- if (expression)
1362
- return { type: "integerToFloat", expression, source: node.source };
1363
- }
1364
- if (node.localName === "qti-and") {
1365
- return {
1366
- type: "and",
1367
- expressions: childElements(node)
1368
- .map(parseExpression)
1369
- .filter((expression) => expression !== undefined),
1370
- source: node.source,
1371
- };
1372
- }
1373
- if (node.localName === "qti-any-n") {
1374
- return {
1375
- type: "anyN",
1376
- min: node.attributes.min ?? "",
1377
- max: node.attributes.max ?? "",
1378
- expressions: childElements(node)
1379
- .map(parseExpression)
1380
- .filter((expression) => expression !== undefined),
1381
- source: node.source,
1382
- };
1383
- }
1384
- if (node.localName === "qti-or") {
1385
- return {
1386
- type: "or",
1387
- expressions: childElements(node)
1388
- .map(parseExpression)
1389
- .filter((expression) => expression !== undefined),
1390
- source: node.source,
1391
- };
1392
- }
1393
- if (node.localName === "qti-not") {
1394
- const expression = parseFirstExpression(node);
1395
- if (expression)
1396
- return { type: "not", expression, source: node.source };
1397
- }
1398
- if (node.localName === "qti-equal") {
1399
- const [left, right] = childElements(node)
1400
- .map(parseExpression)
1401
- .filter((expression) => expression !== undefined);
1402
- if (left && right)
1403
- return { type: "equal", left, right, source: node.source };
1404
- }
1405
- if (node.localName === "qti-equal-rounded") {
1406
- const [left, right] = childElements(node)
1407
- .map(parseExpression)
1408
- .filter((expression) => expression !== undefined);
1409
- const roundingMode = node.attributes["rounding-mode"] ?? "";
1410
- const figures = Number(node.attributes.figures ?? 0);
1411
- if (left && right) {
1412
- return { type: "equalRounded", left, right, roundingMode, figures, source: node.source };
1413
- }
1414
- }
1415
- const numericCompareOperator = numericCompareOperatorFor(node.localName);
1416
- if (numericCompareOperator) {
1417
- const [left, right] = childElements(node)
1418
- .map(parseExpression)
1419
- .filter((expression) => expression !== undefined);
1420
- if (left && right) {
1421
- return {
1422
- type: "numericCompare",
1423
- operator: numericCompareOperator,
1424
- left,
1425
- right,
1426
- source: node.source,
1427
- };
1428
- }
1429
- }
1430
- const durationCompareOperator = durationCompareOperatorFor(node.localName);
1431
- if (durationCompareOperator) {
1432
- const [left, right] = childElements(node)
1433
- .map(parseExpression)
1434
- .filter((expression) => expression !== undefined);
1435
- if (left && right) {
1436
- return {
1437
- type: "durationCompare",
1438
- operator: durationCompareOperator,
1439
- left,
1440
- right,
1441
- source: node.source,
1442
- };
1443
- }
1444
- }
1445
- if (node.localName === "qti-string-match") {
1446
- const [left, right] = childElements(node)
1447
- .map(parseExpression)
1448
- .filter((expression) => expression !== undefined);
1449
- if (left && right) {
1450
- return {
1451
- type: "stringMatch",
1452
- left,
1453
- right,
1454
- caseSensitive: node.attributes["case-sensitive"] !== "false",
1455
- substring: node.attributes.substring === "true",
1456
- source: node.source,
1457
- };
1458
- }
1459
- }
1460
- if (node.localName === "qti-substring") {
1461
- const [left, right] = childElements(node)
1462
- .map(parseExpression)
1463
- .filter((expression) => expression !== undefined);
1464
- if (left && right) {
1465
- return {
1466
- type: "substring",
1467
- left,
1468
- right,
1469
- caseSensitive: node.attributes["case-sensitive"] !== "false",
1470
- source: node.source,
1471
- };
1472
- }
1473
- }
1474
- if (node.localName === "qti-pattern-match") {
1475
- const expression = parseFirstExpression(node);
1476
- if (expression) {
1477
- return {
1478
- type: "patternMatch",
1479
- expression,
1480
- pattern: node.attributes.pattern ?? "",
1481
- source: node.source,
1482
- };
1483
- }
1484
- }
1485
- if (node.localName === "qti-field-value") {
1486
- const expression = parseFirstExpression(node);
1487
- if (expression) {
1488
- return {
1489
- type: "fieldValue",
1490
- fieldIdentifier: node.attributes["field-identifier"] ?? "",
1491
- expression,
1492
- source: node.source,
1493
- };
1494
- }
1495
- }
1496
- if (node.localName === "qti-member") {
1497
- const [value, collection] = childElements(node)
1498
- .map(parseExpression)
1499
- .filter((expression) => expression !== undefined);
1500
- if (value && collection)
1501
- return { type: "member", value, collection, source: node.source };
1502
- }
1503
- if (node.localName === "qti-delete") {
1504
- const [value, collection] = childElements(node)
1505
- .map(parseExpression)
1506
- .filter((expression) => expression !== undefined);
1507
- if (value && collection)
1508
- return { type: "delete", value, collection, source: node.source };
1509
- }
1510
- if (node.localName === "qti-contains") {
1511
- const [collection, values] = childElements(node)
1512
- .map(parseExpression)
1513
- .filter((expression) => expression !== undefined);
1514
- if (collection && values)
1515
- return { type: "contains", collection, values, source: node.source };
1516
- }
1517
- if (node.localName === "qti-gcd" || node.localName === "qti-lcm") {
1518
- return {
1519
- type: node.localName === "qti-gcd" ? "gcd" : "lcm",
1520
- expressions: childElements(node)
1521
- .map(parseExpression)
1522
- .filter((expression) => expression !== undefined),
1523
- source: node.source,
1524
- };
1525
- }
1526
- if (node.localName === "qti-inside") {
1527
- const expression = parseFirstExpression(node);
1528
- if (expression) {
1529
- return {
1530
- type: "inside",
1531
- expression,
1532
- shape: parseShape(node.attributes.shape),
1533
- coords: parseCoords(node.attributes.coords),
1534
- attributes: node.attributes,
1535
- source: node.source,
1536
- };
1537
- }
1538
- }
1539
- if (node.localName === "qti-math-constant") {
1540
- return { type: "mathConstant", name: node.attributes.name ?? "", source: node.source };
1541
- }
1542
- if (node.localName === "qti-math-operator") {
1543
- return {
1544
- type: "mathOperator",
1545
- name: node.attributes.name ?? "",
1546
- expressions: childElements(node)
1547
- .map(parseExpression)
1548
- .filter((expression) => expression !== undefined),
1549
- source: node.source,
1550
- };
1551
- }
1552
- if (node.localName === "qti-repeat") {
1553
- return {
1554
- type: "repeat",
1555
- numberRepeats: node.attributes["number-repeats"] ?? "",
1556
- expressions: childElements(node)
1557
- .map(parseExpression)
1558
- .filter((expression) => expression !== undefined),
1559
- source: node.source,
1560
- };
1561
- }
1562
- if (node.localName === "qti-stats-operator") {
1563
- const expression = parseFirstExpression(node);
1564
- if (expression) {
1565
- return {
1566
- type: "statsOperator",
1567
- name: node.attributes.name ?? "",
1568
- expression,
1569
- source: node.source,
1570
- };
1571
- }
1572
- }
1573
- if (node.localName === "qti-custom-operator") {
1574
- return {
1575
- type: "customOperator",
1576
- definition: node.attributes.definition,
1577
- className: node.attributes.class,
1578
- attributes: node.attributes,
1579
- expressions: childElements(node)
1580
- .map(parseExpression)
1581
- .filter((expression) => expression !== undefined),
1582
- source: node.source,
1583
- };
1584
- }
1585
- if (node.localName === "qti-match") {
1586
- const variable = childElements(node, "qti-variable")[0];
1587
- const correct = childElements(node, "qti-correct")[0];
1588
- if (variable && correct) {
1589
- return {
1590
- type: "matchCorrect",
1591
- identifier: variable?.attributes.identifier ?? "",
1592
- correctIdentifier: correct?.attributes.identifier ?? "",
1593
- source: node.source,
1594
- };
1595
- }
1596
- const [left, right] = childElements(node)
1597
- .map(parseExpression)
1598
- .filter((expression) => expression !== undefined);
1599
- if (left && right)
1600
- return { type: "match", left, right, source: node.source };
1601
- }
1602
- return undefined;
1603
- }
1604
- function numericCompareOperatorFor(localName) {
1605
- if (localName === "qti-lt")
1606
- return "lt";
1607
- if (localName === "qti-lte")
1608
- return "lte";
1609
- if (localName === "qti-gt")
1610
- return "gt";
1611
- if (localName === "qti-gte")
1612
- return "gte";
1613
- return undefined;
1614
- }
1615
- function durationCompareOperatorFor(localName) {
1616
- if (localName === "qti-duration-lt")
1617
- return "lt";
1618
- if (localName === "qti-duration-gte")
1619
- return "gte";
1620
- return undefined;
1621
- }
1622
- function coerceValue(value, baseType) {
1623
- if (baseType === "integer")
1624
- return Number.parseInt(value, 10);
1625
- if (baseType === "float")
1626
- return Number.parseFloat(value);
1627
- if (baseType === "boolean") {
1628
- if (value === "true")
1629
- return true;
1630
- if (value === "false")
1631
- return false;
1632
- }
1633
- return value;
1634
- }
1635
- function parseCardinality(value) {
1636
- if (value === "multiple" || value === "ordered" || value === "record")
1637
- return value;
1638
- return "single";
1639
- }
1640
- function parseXmlBoolean(value) {
1641
- if (value === "true" || value === "1")
1642
- return true;
1643
- if (value === "false" || value === "0")
1644
- return false;
1645
- return undefined;
1646
- }
1647
- function normalizeValueForCardinality(value, cardinality) {
1648
- if ((cardinality === "multiple" || cardinality === "ordered") &&
1649
- value !== null &&
1650
- !Array.isArray(value) &&
1651
- !isRecordValue(value)) {
1652
- return [value];
1653
- }
1654
- return value;
1655
- }
1656
- function isRecordValue(value) {
1657
- return typeof value === "object" && value !== null && !Array.isArray(value);
1658
- }
1659
605
  //# sourceMappingURL=parser.js.map