@longsightgroup/qti3-core 0.10.1 → 0.10.3

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 +21 -2
  2. package/dist/attempt-state.d.ts.map +1 -1
  3. package/dist/attempt-state.js +18 -0
  4. package/dist/attempt-state.js.map +1 -1
  5. package/dist/content-text.d.ts +4 -0
  6. package/dist/content-text.d.ts.map +1 -1
  7. package/dist/content-text.js +112 -9
  8. package/dist/content-text.js.map +1 -1
  9. package/dist/index.d.ts +4 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/media-definition.d.ts +55 -0
  14. package/dist/media-definition.d.ts.map +1 -0
  15. package/dist/media-definition.js +97 -0
  16. package/dist/media-definition.js.map +1 -0
  17. package/dist/parser-declarations.d.ts +4 -4
  18. package/dist/parser-declarations.d.ts.map +1 -1
  19. package/dist/parser-declarations.js +87 -46
  20. package/dist/parser-declarations.js.map +1 -1
  21. package/dist/parser-item-metadata.d.ts.map +1 -1
  22. package/dist/parser-item-metadata.js +4 -3
  23. package/dist/parser-item-metadata.js.map +1 -1
  24. package/dist/parser-processing.js +10 -9
  25. package/dist/parser-processing.js.map +1 -1
  26. package/dist/parser-values.d.ts +2 -0
  27. package/dist/parser-values.d.ts.map +1 -1
  28. package/dist/parser-values.js +7 -0
  29. package/dist/parser-values.js.map +1 -1
  30. package/dist/parser.js +16 -13
  31. package/dist/parser.js.map +1 -1
  32. package/dist/processing-evaluator-collection.d.ts.map +1 -1
  33. package/dist/processing-evaluator-collection.js +8 -2
  34. package/dist/processing-evaluator-collection.js.map +1 -1
  35. package/dist/processing-evaluator-comparison.d.ts.map +1 -1
  36. package/dist/processing-evaluator-comparison.js +6 -0
  37. package/dist/processing-evaluator-comparison.js.map +1 -1
  38. package/dist/processing-evaluator-numeric.d.ts.map +1 -1
  39. package/dist/processing-evaluator-numeric.js +8 -1
  40. package/dist/processing-evaluator-numeric.js.map +1 -1
  41. package/dist/processing-expression-children.d.ts.map +1 -1
  42. package/dist/processing-expression-children.js +2 -1
  43. package/dist/processing-expression-children.js.map +1 -1
  44. package/dist/processing-limits.d.ts +3 -0
  45. package/dist/processing-limits.d.ts.map +1 -0
  46. package/dist/processing-limits.js +3 -0
  47. package/dist/processing-limits.js.map +1 -0
  48. package/dist/processing-mapping.d.ts.map +1 -1
  49. package/dist/processing-mapping.js +13 -2
  50. package/dist/processing-mapping.js.map +1 -1
  51. package/dist/processing-operators.d.ts +2 -0
  52. package/dist/processing-operators.d.ts.map +1 -1
  53. package/dist/processing-operators.js +34 -5
  54. package/dist/processing-operators.js.map +1 -1
  55. package/dist/response-validation-policy.d.ts.map +1 -1
  56. package/dist/response-validation-policy.js +5 -6
  57. package/dist/response-validation-policy.js.map +1 -1
  58. package/dist/serializer-processing-expressions.js +1 -1
  59. package/dist/serializer-processing-expressions.js.map +1 -1
  60. package/dist/slider-definition.d.ts +59 -0
  61. package/dist/slider-definition.d.ts.map +1 -0
  62. package/dist/slider-definition.js +198 -0
  63. package/dist/slider-definition.js.map +1 -0
  64. package/dist/support-evidence.d.ts +6 -2
  65. package/dist/support-evidence.d.ts.map +1 -1
  66. package/dist/support-evidence.js +15 -11
  67. package/dist/support-evidence.js.map +1 -1
  68. package/dist/support.d.ts.map +1 -1
  69. package/dist/support.js +96 -100
  70. package/dist/support.js.map +1 -1
  71. package/dist/types.d.ts +3 -0
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/validation-declaration-entries.d.ts +13 -0
  74. package/dist/validation-declaration-entries.d.ts.map +1 -0
  75. package/dist/validation-declaration-entries.js +136 -0
  76. package/dist/validation-declaration-entries.js.map +1 -0
  77. package/dist/validation-interactions.d.ts.map +1 -1
  78. package/dist/validation-interactions.js +14 -46
  79. package/dist/validation-interactions.js.map +1 -1
  80. package/dist/validation-primitives.js +2 -2
  81. package/dist/validation-primitives.js.map +1 -1
  82. package/dist/validation-processing.d.ts.map +1 -1
  83. package/dist/validation-processing.js +125 -1
  84. package/dist/validation-processing.js.map +1 -1
  85. package/dist/validation.d.ts.map +1 -1
  86. package/dist/validation.js +8 -134
  87. package/dist/validation.js.map +1 -1
  88. package/dist/xml.d.ts.map +1 -1
  89. package/dist/xml.js +3 -2
  90. package/dist/xml.js.map +1 -1
  91. package/package.json +1 -1
  92. package/src/attempt-state.ts +20 -0
  93. package/src/content-text.ts +135 -11
  94. package/src/index.ts +25 -0
  95. package/src/media-definition.ts +174 -0
  96. package/src/parser-declarations.ts +134 -40
  97. package/src/parser-item-metadata.ts +4 -3
  98. package/src/parser-processing.ts +10 -9
  99. package/src/parser-values.ts +7 -0
  100. package/src/parser.ts +16 -13
  101. package/src/processing-evaluator-collection.ts +7 -2
  102. package/src/processing-evaluator-comparison.ts +6 -0
  103. package/src/processing-evaluator-numeric.ts +5 -1
  104. package/src/processing-expression-children.ts +2 -1
  105. package/src/processing-limits.ts +2 -0
  106. package/src/processing-mapping.ts +10 -2
  107. package/src/processing-operators.ts +38 -5
  108. package/src/response-validation-policy.ts +5 -5
  109. package/src/serializer-processing-expressions.ts +1 -1
  110. package/src/slider-definition.ts +363 -0
  111. package/src/support-evidence.ts +15 -17
  112. package/src/support.ts +106 -102
  113. package/src/types.ts +8 -1
  114. package/src/validation-declaration-entries.ts +162 -0
  115. package/src/validation-interactions.ts +14 -64
  116. package/src/validation-primitives.ts +2 -2
  117. package/src/validation-processing.ts +145 -1
  118. package/src/validation.ts +11 -151
  119. package/src/xml.ts +3 -4
@@ -19,6 +19,7 @@ import {
19
19
  type ParsedCompanionMaterialChildQtiName,
20
20
  pushCompanionMaterialParseWarning,
21
21
  } from "./companion-materials.js";
22
+ import { visibleTextContent } from "./content-text.js";
22
23
  import { parseXmlBoolean } from "./parser-values.js";
23
24
  import { childElements, descendants, textContent, type XmlNode } from "./xml.js";
24
25
 
@@ -75,7 +76,7 @@ export function parseModalFeedback(node: XmlNode): QtiModalFeedback {
75
76
  identifier: node.attributes.identifier ?? "",
76
77
  outcomeIdentifier: node.attributes["outcome-identifier"] ?? "",
77
78
  showHide,
78
- text: textContent(node),
79
+ text: visibleTextContent(node),
79
80
  source: node.source,
80
81
  };
81
82
  }
@@ -119,7 +120,7 @@ function parseCatalogCardEntry(node: XmlNode): QtiCatalogCardEntry {
119
120
  function parseCatalogHtmlContent(node: XmlNode | undefined): QtiCatalogHtmlContent | undefined {
120
121
  if (!node) return undefined;
121
122
  return {
122
- text: textContent(node),
123
+ text: visibleTextContent(node),
123
124
  children: parseCatalogHtmlChildren(node),
124
125
  attributes: node.attributes,
125
126
  source: node.source,
@@ -202,7 +203,7 @@ function parsePhysicalMaterial(
202
203
  node: XmlNode,
203
204
  diagnostics: QtiDiagnostic[],
204
205
  ): QtiPhysicalMaterial | undefined {
205
- const text = textContent(node).trim();
206
+ const text = visibleTextContent(node);
206
207
  if (text.length === 0) {
207
208
  return pushCompanionMaterialParseWarning(
208
209
  diagnostics,
@@ -646,15 +646,16 @@ function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
646
646
  }
647
647
 
648
648
  if (node.localName === "qti-stats-operator") {
649
- const expression = parseFirstExpression(node);
650
- if (expression) {
651
- return {
652
- type: "statsOperator",
653
- name: node.attributes.name ?? "",
654
- expression,
655
- source: node.source,
656
- };
657
- }
649
+ const expressions = childElements(node)
650
+ .map(parseExpression)
651
+ .filter((expression): expression is QtiProcessingExpression => expression !== undefined);
652
+ return {
653
+ type: "statsOperator",
654
+ name: node.attributes.name ?? "",
655
+ expression: expressions[0] ?? { type: "null", source: node.source },
656
+ expressions,
657
+ source: node.source,
658
+ };
658
659
  }
659
660
 
660
661
  if (node.localName === "qti-custom-operator") {
@@ -66,6 +66,13 @@ export function parseXmlBoolean(value: string | undefined): boolean | undefined
66
66
  return undefined;
67
67
  }
68
68
 
69
+ /** Parse a non-empty finite numeric XML attribute value. */
70
+ export function parseFiniteNumber(value: string | undefined): number | undefined {
71
+ if (value === undefined || value.trim().length === 0) return undefined;
72
+ const parsed = Number(value);
73
+ return Number.isFinite(parsed) ? parsed : undefined;
74
+ }
75
+
69
76
  export function normalizeValueForCardinality(
70
77
  value: QtiValue,
71
78
  cardinality: QtiCardinality,
package/src/parser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { appendContentTextNode, flatTextFromContent } from "./content-text.js";
1
+ import { appendContentTextNode, flatTextFromContent, visibleTextContent } from "./content-text.js";
2
2
  import {
3
3
  parseOutcomeDeclaration,
4
4
  parseResponseDeclaration,
@@ -36,7 +36,7 @@ import type {
36
36
  QtiResponseDeclaration,
37
37
  } from "./types.js";
38
38
  import { validateAssessmentItem } from "./validation.js";
39
- import { childElements, descendants, parseXmlTree, textContent, type XmlNode } from "./xml.js";
39
+ import { childElements, descendants, parseXmlTree, type XmlNode } from "./xml.js";
40
40
 
41
41
  const qtiAssessmentItemNamespace = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0";
42
42
  const supportedProcessingNames = new Set(processingSupport.map((entry) => entry.qtiName));
@@ -109,16 +109,16 @@ export function parseQtiXml(xml: string): QtiParseResult {
109
109
  function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAssessmentItem {
110
110
  const identifier = node.attributes.identifier ?? "";
111
111
  const responseDeclarations = childElements(node, "qti-response-declaration").map(
112
- parseResponseDeclaration,
112
+ (declarationNode) => parseResponseDeclaration(declarationNode, diagnostics),
113
113
  );
114
114
  const responseDeclarationMap = new Map(
115
115
  responseDeclarations.map((declaration) => [declaration.identifier, declaration]),
116
116
  );
117
117
  const outcomeDeclarations = childElements(node, "qti-outcome-declaration").map(
118
- parseOutcomeDeclaration,
118
+ (declarationNode) => parseOutcomeDeclaration(declarationNode, diagnostics),
119
119
  );
120
120
  const templateDeclarations = childElements(node, "qti-template-declaration").map(
121
- parseTemplateDeclaration,
121
+ (declarationNode) => parseTemplateDeclaration(declarationNode, diagnostics),
122
122
  );
123
123
  const templateProcessing = parseTemplateProcessing(
124
124
  childElements(node, "qti-template-processing")[0],
@@ -166,7 +166,7 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
166
166
  adaptive: parseXmlBoolean(node.attributes.adaptive) === true,
167
167
  timeDependent: parseXmlBoolean(node.attributes["time-dependent"]),
168
168
  attributes: node.attributes,
169
- prompt: prompt ? textContent(prompt) : undefined,
169
+ prompt: prompt ? visibleTextContent(prompt) : undefined,
170
170
  itemBodyAttributes: itemBody?.attributes,
171
171
  itemBodySource: itemBody?.source,
172
172
  responseDeclarations,
@@ -181,7 +181,10 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
181
181
  catalogReferences,
182
182
  stylesheets,
183
183
  body,
184
- bodyText: textContent(node),
184
+ bodyText: flatTextFromContent(body, {
185
+ excludeAnnotations: true,
186
+ interactionText: (interactionIndex) => interactions[interactionIndex]?.prompt ?? "",
187
+ }),
185
188
  source: node.source,
186
189
  };
187
190
  }
@@ -334,7 +337,7 @@ function parseInteraction(
334
337
  prompt: promptContent
335
338
  ? flatTextFromContent(promptContent, { excludeAnnotations: true }) || undefined
336
339
  : prompt
337
- ? textContent(prompt)
340
+ ? visibleTextContent(prompt)
338
341
  : undefined,
339
342
  promptContent: promptContent && promptContent.length > 0 ? promptContent : undefined,
340
343
  promptAttributes: prompt?.attributes,
@@ -357,7 +360,7 @@ function parseInteraction(
357
360
  source: child.source,
358
361
  })),
359
362
  attributes: node.attributes,
360
- text: textContent(node),
363
+ text: visibleTextContent(node),
361
364
  source: node.source,
362
365
  };
363
366
  }
@@ -417,7 +420,7 @@ function parseHottextSegments(node: XmlNode): QtiInteraction["hottextSegments"]
417
420
  segments.push({
418
421
  kind: "hottext",
419
422
  identifier: entry.attributes.identifier ?? "",
420
- text: textContent(entry),
423
+ text: visibleTextContent(entry),
421
424
  attributes: entry.attributes,
422
425
  source: entry.source,
423
426
  });
@@ -475,7 +478,7 @@ function inlineInteractionContext(
475
478
  if (interactionType !== "inlineChoice" && interactionType !== "textEntry") return undefined;
476
479
  const parent = node.parent;
477
480
  if (!parent) return undefined;
478
- return normalizeInlineContext(parent.text) ?? normalizeInlineContext(textContent(parent));
481
+ return normalizeInlineContext(parent.text) ?? normalizeInlineContext(visibleTextContent(parent));
479
482
  }
480
483
 
481
484
  function normalizeInlineContext(value: string): string | undefined {
@@ -505,7 +508,7 @@ function parseObjectAsset(node: XmlNode | undefined): QtiObjectAsset | undefined
505
508
  node.attributes.height ?? pictureImage?.attributes.height ?? inferredSvgDimensions?.height,
506
509
  sources,
507
510
  tracks,
508
- text: textContent(node) || node.attributes.alt || pictureImage?.attributes.alt || "",
511
+ text: visibleTextContent(node) || node.attributes.alt || pictureImage?.attributes.alt || "",
509
512
  attributes: node.attributes,
510
513
  source: node.source,
511
514
  };
@@ -670,7 +673,7 @@ function parseChoices(
670
673
  const flatChoiceText =
671
674
  content.length > 0
672
675
  ? flatTextFromContent(content, { excludeAnnotations: true })
673
- : textContent(choice);
676
+ : visibleTextContent(choice);
674
677
  return {
675
678
  identifier,
676
679
  text:
@@ -1,6 +1,7 @@
1
1
  import type { QtiProcessingExpression, QtiScalarValue, QtiValue } from "./types.js";
2
2
  import { assertNever } from "./assert-never.js";
3
3
  import type { EvaluationContext } from "./processing-evaluator.js";
4
+ import { MAX_QTI_REPEAT_RESULT_ELEMENTS } from "./processing-limits.js";
4
5
  import { valueContainer } from "./processing-values.js";
5
6
 
6
7
  type CollectionExpression = Extract<
@@ -39,11 +40,15 @@ export function evaluateRepeatExpression(
39
40
  context: EvaluationContext,
40
41
  ): QtiValue {
41
42
  const repeats = context.indexValue(expression.numberRepeats);
42
- if (repeats === undefined || repeats < 1) return null;
43
+ if (repeats === undefined || repeats < 1 || repeats > MAX_QTI_REPEAT_RESULT_ELEMENTS) {
44
+ return null;
45
+ }
43
46
  const container: QtiScalarValue[] = [];
44
47
  for (let repeat = 0; repeat < repeats; repeat += 1) {
45
48
  for (const item of expression.expressions) {
46
- container.push(...valueContainer(context.evaluate(item)));
49
+ const values = valueContainer(context.evaluate(item));
50
+ if (values.length > MAX_QTI_REPEAT_RESULT_ELEMENTS - container.length) return null;
51
+ for (const value of values) container.push(value);
47
52
  }
48
53
  }
49
54
  return container.length > 0 ? container : null;
@@ -48,6 +48,12 @@ export function evaluateComparisonExpression(
48
48
  return left === null || right === null ? null : valuesEqual(left, right);
49
49
  }
50
50
  case "equalRounded": {
51
+ const validRounding =
52
+ Number.isInteger(expression.figures) &&
53
+ (expression.roundingMode === "decimalPlaces"
54
+ ? expression.figures >= 0
55
+ : expression.roundingMode === "significantFigures" && expression.figures > 0);
56
+ if (!validRounding) return null;
51
57
  const left = context.evaluate(expression.left);
52
58
  const right = context.evaluate(expression.right);
53
59
  if (left === null || right === null) return null;
@@ -47,10 +47,12 @@ export function evaluateNumericExpression(
47
47
  ): QtiValue {
48
48
  switch (expression.type) {
49
49
  case "sum": {
50
+ if (expression.expressions.length === 0) return null;
50
51
  const values = context.numericOperands(expression.expressions);
51
52
  return values ? values.reduce((sum, value) => sum + value, 0) : null;
52
53
  }
53
54
  case "product": {
55
+ if (expression.expressions.length === 0) return null;
54
56
  const values = context.numericOperands(expression.expressions);
55
57
  return values ? values.reduce((product, value) => product * value, 1) : null;
56
58
  }
@@ -127,7 +129,9 @@ export function evaluateNumericExpression(
127
129
  return mathOperatorValue(expression.name, values.map(numericValue));
128
130
  }
129
131
  case "statsOperator": {
130
- const value = context.evaluate(expression.expression);
132
+ const expressions = expression.expressions ?? [expression.expression];
133
+ if (expressions.length !== 1) return null;
134
+ const value = context.evaluate(expressions[0]!);
131
135
  if (value === null) return null;
132
136
  return statsOperatorValue(expression.name, valueContainer(value).map(numericValue));
133
137
  }
@@ -43,8 +43,9 @@ export function expressionChildren(expression: QtiProcessingExpression): QtiProc
43
43
  case "patternMatch":
44
44
  case "fieldValue":
45
45
  case "inside":
46
- case "statsOperator":
47
46
  return [expression.expression];
47
+ case "statsOperator":
48
+ return expression.expressions ?? [expression.expression];
48
49
  case "member":
49
50
  case "delete":
50
51
  return [expression.value, expression.collection];
@@ -0,0 +1,2 @@
1
+ /** Maximum scalar elements a single qti-repeat expression may produce. */
2
+ export const MAX_QTI_REPEAT_RESULT_ELEMENTS = 10_000;
@@ -49,6 +49,7 @@ export function scoreAreaMapping(
49
49
  : response === null
50
50
  ? []
51
51
  : qtiValueToStringList(response);
52
+ const matchedAreaIndexes = new Set<number>();
52
53
  let score = 0;
53
54
  for (const point of points) {
54
55
  const parsed = parsePoint(point);
@@ -56,8 +57,15 @@ export function scoreAreaMapping(
56
57
  score += areaMapping.defaultValue;
57
58
  continue;
58
59
  }
59
- const entry = areaMapping.entries.find((candidate) => pointInsideArea(parsed, candidate));
60
- score += entry?.mappedValue ?? areaMapping.defaultValue;
60
+ let matchedArea = false;
61
+ for (const [index, entry] of areaMapping.entries.entries()) {
62
+ if (!pointInsideArea(parsed, entry)) continue;
63
+ matchedArea = true;
64
+ if (matchedAreaIndexes.has(index)) continue;
65
+ matchedAreaIndexes.add(index);
66
+ score += entry.mappedValue;
67
+ }
68
+ if (!matchedArea) score += areaMapping.defaultValue;
61
69
  }
62
70
  return clampMappedScore(score, areaMapping.attributes);
63
71
  }
@@ -39,15 +39,45 @@ export const STATS_OPERATOR_NAMES = [
39
39
  "popSD",
40
40
  ] as const;
41
41
 
42
+ /** Round decimal ties away from zero without an intermediate scaled binary float. */
42
43
  export function roundToDecimalPlaces(value: number, figures: number): number {
43
- const factor = 10 ** figures;
44
- return Math.round(value * factor) / factor;
44
+ if (!Number.isFinite(value) || value === 0) return value;
45
+ const decimal = decimalDigits(value);
46
+ const retainedDigits = decimal.exponent + figures + 1;
47
+ return roundDecimalDigits(value, decimal, retainedDigits);
45
48
  }
46
49
 
50
+ /** Round significant decimal digits, with half-up ties on the absolute magnitude. */
47
51
  export function roundToSignificantFigures(value: number, figures: number): number {
48
52
  if (value === 0 || figures <= 0) return 0;
49
- const factor = 10 ** (figures - 1 - Math.floor(Math.log10(Math.abs(value))));
50
- return Math.round(value * factor) / factor;
53
+ if (!Number.isFinite(value)) return Number.NaN;
54
+ return roundDecimalDigits(value, decimalDigits(value), figures);
55
+ }
56
+
57
+ interface DecimalDigits {
58
+ digits: string;
59
+ exponent: number;
60
+ }
61
+
62
+ function decimalDigits(value: number): DecimalDigits {
63
+ const [coefficient = "", exponent = "0"] = Math.abs(value).toExponential().split("e");
64
+ return {
65
+ digits: coefficient.replace(".", ""),
66
+ exponent: Number(exponent),
67
+ };
68
+ }
69
+
70
+ function roundDecimalDigits(value: number, decimal: DecimalDigits, retainedDigits: number): number {
71
+ if (retainedDigits >= decimal.digits.length) return value;
72
+ if (retainedDigits < 0) return value < 0 ? -0 : 0;
73
+
74
+ const roundingDigit = decimal.digits[retainedDigits] ?? "0";
75
+ const retained = retainedDigits === 0 ? 0n : BigInt(decimal.digits.slice(0, retainedDigits));
76
+ const rounded = roundingDigit >= "5" ? retained + 1n : retained;
77
+ if (rounded === 0n) return value < 0 ? -0 : 0;
78
+
79
+ const unitExponent = decimal.exponent - retainedDigits + 1;
80
+ return Number(`${value < 0 ? "-" : ""}${rounded}e${unitExponent}`);
51
81
  }
52
82
 
53
83
  export function roundWithMode(value: number, roundingMode: string, figures: number): number | null {
@@ -151,7 +181,10 @@ export function mathOperatorValue(name: string, values: number[]): QtiValue {
151
181
  }
152
182
 
153
183
  export function statsOperatorValue(name: string, values: number[]): QtiValue {
154
- if (values.length === 0) return 0;
184
+ // QTI 3.0.1 defines the input as a numeric container but does not define empty-container
185
+ // results, so this implementation conservatively returns NULL.
186
+ // https://www.imsglobal.org/sites/default/files/spec/qti/v3/info/index.html#OpStatsOp
187
+ if (values.length === 0) return null;
155
188
  const meanValue = mean(values);
156
189
  const squareDiffs = values.map((value) => (value - meanValue) ** 2);
157
190
  switch (name) {
@@ -1,4 +1,5 @@
1
1
  import type { QtiChoice, QtiDiagnostic, QtiInteraction, QtiValue } from "./types.js";
2
+ import { parseQtiMediaDefinition } from "./media-definition.js";
2
3
  import { parseXmlBoolean } from "./parser-values.js";
3
4
  import { parseNonNegativeInteger } from "./validation-primitives.js";
4
5
  import { qtiValueToIdentifierList } from "./value-format.js";
@@ -75,14 +76,13 @@ export function mediaPlayCount(value: QtiValue): number {
75
76
  }
76
77
 
77
78
  function minimumMediaPlays(interaction: QtiInteraction): number {
78
- const parsed = parseNonNegativeInteger(interaction.attributes["min-plays"]);
79
- if (parsed !== undefined) return parsed;
80
- return interactionRequiresResponse(interaction) ? 1 : 0;
79
+ const definition = parseQtiMediaDefinition(interaction);
80
+ return definition.ok ? definition.value.minPlays : 0;
81
81
  }
82
82
 
83
83
  function maximumMediaPlays(interaction: QtiInteraction): number | undefined {
84
- const parsed = parseNonNegativeInteger(interaction.attributes["max-plays"]);
85
- return parsed === undefined || parsed <= 0 ? undefined : parsed;
84
+ const definition = parseQtiMediaDefinition(interaction);
85
+ return definition.ok ? definition.value.maxPlays : undefined;
86
86
  }
87
87
 
88
88
  export { maximumMediaPlays, minimumMediaPlays };
@@ -262,7 +262,7 @@ export function serializeExpression(
262
262
  return renderExpressionContainer(
263
263
  "qti-stats-operator",
264
264
  [["name", expression.name]],
265
- [expression.expression],
265
+ expression.expressions ?? [expression.expression],
266
266
  context,
267
267
  indent,
268
268
  );