@longsightgroup/qti3-core 0.9.1 → 0.9.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 (244) hide show
  1. package/README.md +59 -3
  2. package/dist/adaptive-turn-materializer.d.ts +14 -0
  3. package/dist/adaptive-turn-materializer.d.ts.map +1 -0
  4. package/dist/adaptive-turn-materializer.js +56 -0
  5. package/dist/adaptive-turn-materializer.js.map +1 -0
  6. package/dist/adaptive-turn.d.ts +23 -0
  7. package/dist/adaptive-turn.d.ts.map +1 -0
  8. package/dist/adaptive-turn.js +60 -0
  9. package/dist/adaptive-turn.js.map +1 -0
  10. package/dist/assert-never.d.ts +3 -0
  11. package/dist/assert-never.d.ts.map +1 -0
  12. package/dist/assert-never.js +5 -0
  13. package/dist/assert-never.js.map +1 -0
  14. package/dist/attempt-state-constants.d.ts +6 -0
  15. package/dist/attempt-state-constants.d.ts.map +1 -0
  16. package/dist/attempt-state-constants.js +6 -0
  17. package/dist/attempt-state-constants.js.map +1 -0
  18. package/dist/attempt-state.d.ts +5 -0
  19. package/dist/attempt-state.d.ts.map +1 -0
  20. package/dist/attempt-state.js +190 -0
  21. package/dist/attempt-state.js.map +1 -0
  22. package/dist/catalog.js +1 -1
  23. package/dist/catalog.js.map +1 -1
  24. package/dist/content-text.d.ts.map +1 -1
  25. package/dist/content-text.js +3 -0
  26. package/dist/content-text.js.map +1 -1
  27. package/dist/custom-operators.d.ts +13 -0
  28. package/dist/custom-operators.d.ts.map +1 -0
  29. package/dist/custom-operators.js +2 -0
  30. package/dist/custom-operators.js.map +1 -0
  31. package/dist/delivery-redaction.d.ts +50 -0
  32. package/dist/delivery-redaction.d.ts.map +1 -0
  33. package/dist/delivery-redaction.js +176 -0
  34. package/dist/delivery-redaction.js.map +1 -0
  35. package/dist/delivery-security.d.ts +3 -18
  36. package/dist/delivery-security.d.ts.map +1 -1
  37. package/dist/delivery-security.js +35 -189
  38. package/dist/delivery-security.js.map +1 -1
  39. package/dist/index.d.ts +4 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/parser-custom-interactions.js +1 -1
  44. package/dist/parser-custom-interactions.js.map +1 -1
  45. package/dist/parser-declarations.d.ts.map +1 -1
  46. package/dist/parser-declarations.js +4 -4
  47. package/dist/parser-declarations.js.map +1 -1
  48. package/dist/parser-item-metadata.d.ts.map +1 -1
  49. package/dist/parser-item-metadata.js +8 -7
  50. package/dist/parser-item-metadata.js.map +1 -1
  51. package/dist/parser-processing.js +2 -2
  52. package/dist/parser-processing.js.map +1 -1
  53. package/dist/parser-values.d.ts +5 -1
  54. package/dist/parser-values.d.ts.map +1 -1
  55. package/dist/parser-values.js +33 -0
  56. package/dist/parser-values.js.map +1 -1
  57. package/dist/processing-evaluator-boolean.d.ts +8 -0
  58. package/dist/processing-evaluator-boolean.d.ts.map +1 -0
  59. package/dist/processing-evaluator-boolean.js +47 -0
  60. package/dist/processing-evaluator-boolean.js.map +1 -0
  61. package/dist/processing-evaluator-collection.d.ts +12 -0
  62. package/dist/processing-evaluator-collection.d.ts.map +1 -0
  63. package/dist/processing-evaluator-collection.js +35 -0
  64. package/dist/processing-evaluator-collection.js.map +1 -0
  65. package/dist/processing-evaluator-comparison.d.ts +8 -0
  66. package/dist/processing-evaluator-comparison.d.ts.map +1 -0
  67. package/dist/processing-evaluator-comparison.js +88 -0
  68. package/dist/processing-evaluator-comparison.js.map +1 -0
  69. package/dist/processing-evaluator-custom.d.ts +8 -0
  70. package/dist/processing-evaluator-custom.d.ts.map +1 -0
  71. package/dist/processing-evaluator-custom.js +14 -0
  72. package/dist/processing-evaluator-custom.js.map +1 -0
  73. package/dist/processing-evaluator-geometry.d.ts +8 -0
  74. package/dist/processing-evaluator-geometry.d.ts.map +1 -0
  75. package/dist/processing-evaluator-geometry.js +21 -0
  76. package/dist/processing-evaluator-geometry.js.map +1 -0
  77. package/dist/processing-evaluator-numeric.d.ts +8 -0
  78. package/dist/processing-evaluator-numeric.d.ts.map +1 -0
  79. package/dist/processing-evaluator-numeric.js +112 -0
  80. package/dist/processing-evaluator-numeric.js.map +1 -0
  81. package/dist/processing-evaluator-string.d.ts +8 -0
  82. package/dist/processing-evaluator-string.d.ts.map +1 -0
  83. package/dist/processing-evaluator-string.js +33 -0
  84. package/dist/processing-evaluator-string.js.map +1 -0
  85. package/dist/processing-evaluator-variable.d.ts +8 -0
  86. package/dist/processing-evaluator-variable.d.ts.map +1 -0
  87. package/dist/processing-evaluator-variable.js +39 -0
  88. package/dist/processing-evaluator-variable.js.map +1 -0
  89. package/dist/processing-evaluator.d.ts +22 -0
  90. package/dist/processing-evaluator.d.ts.map +1 -0
  91. package/dist/processing-evaluator.js +137 -0
  92. package/dist/processing-evaluator.js.map +1 -0
  93. package/dist/processing-expression-children.d.ts +3 -0
  94. package/dist/processing-expression-children.d.ts.map +1 -0
  95. package/dist/processing-expression-children.js +68 -0
  96. package/dist/processing-expression-children.js.map +1 -0
  97. package/dist/processing-mapping.d.ts +13 -0
  98. package/dist/processing-mapping.d.ts.map +1 -0
  99. package/dist/processing-mapping.js +120 -0
  100. package/dist/processing-mapping.js.map +1 -0
  101. package/dist/processing-operators.d.ts +11 -0
  102. package/dist/processing-operators.d.ts.map +1 -0
  103. package/dist/processing-operators.js +178 -0
  104. package/dist/processing-operators.js.map +1 -0
  105. package/dist/processing-random.d.ts +2 -0
  106. package/dist/processing-random.d.ts.map +1 -0
  107. package/dist/processing-random.js +16 -0
  108. package/dist/processing-random.js.map +1 -0
  109. package/dist/processing-state.d.ts +8 -0
  110. package/dist/processing-state.d.ts.map +1 -0
  111. package/dist/processing-state.js +44 -0
  112. package/dist/processing-state.js.map +1 -0
  113. package/dist/processing-templates.d.ts +3 -0
  114. package/dist/processing-templates.d.ts.map +1 -0
  115. package/dist/processing-templates.js +12 -0
  116. package/dist/processing-templates.js.map +1 -0
  117. package/dist/processing-values.d.ts +15 -0
  118. package/dist/processing-values.d.ts.map +1 -0
  119. package/dist/processing-values.js +150 -0
  120. package/dist/processing-values.js.map +1 -0
  121. package/dist/processing-variables.d.ts +7 -0
  122. package/dist/processing-variables.d.ts.map +1 -0
  123. package/dist/processing-variables.js +40 -0
  124. package/dist/processing-variables.js.map +1 -0
  125. package/dist/serializer-processing-expressions.d.ts.map +1 -1
  126. package/dist/serializer-processing-expressions.js +2 -8
  127. package/dist/serializer-processing-expressions.js.map +1 -1
  128. package/dist/serializer-processing-xml.js +2 -2
  129. package/dist/serializer-processing-xml.js.map +1 -1
  130. package/dist/serializer-processing.d.ts.map +1 -1
  131. package/dist/serializer-processing.js +3 -0
  132. package/dist/serializer-processing.js.map +1 -1
  133. package/dist/server-scoring.d.ts +4 -14
  134. package/dist/server-scoring.d.ts.map +1 -1
  135. package/dist/server-scoring.js +19 -157
  136. package/dist/server-scoring.js.map +1 -1
  137. package/dist/session.d.ts +5 -14
  138. package/dist/session.d.ts.map +1 -1
  139. package/dist/session.js +76 -1158
  140. package/dist/session.js.map +1 -1
  141. package/dist/shared-vocabulary-coverage-policy.js +1 -1
  142. package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
  143. package/dist/shared-vocabulary-generated-families.js +1 -1
  144. package/dist/shared-vocabulary-generated-families.js.map +1 -1
  145. package/dist/shared-vocabulary-support.js +3 -3
  146. package/dist/shared-vocabulary-support.js.map +1 -1
  147. package/dist/shared-vocabulary.d.ts.map +1 -1
  148. package/dist/shared-vocabulary.js +4 -3
  149. package/dist/shared-vocabulary.js.map +1 -1
  150. package/dist/support-evidence.d.ts +22 -0
  151. package/dist/support-evidence.d.ts.map +1 -0
  152. package/dist/support-evidence.js +93 -0
  153. package/dist/support-evidence.js.map +1 -0
  154. package/dist/support.d.ts.map +1 -1
  155. package/dist/support.js +38 -47
  156. package/dist/support.js.map +1 -1
  157. package/dist/trusted-item-session.d.ts +49 -0
  158. package/dist/trusted-item-session.d.ts.map +1 -0
  159. package/dist/trusted-item-session.js +261 -0
  160. package/dist/trusted-item-session.js.map +1 -0
  161. package/dist/tts.d.ts.map +1 -1
  162. package/dist/tts.js +16 -2
  163. package/dist/tts.js.map +1 -1
  164. package/dist/validation-geometry.d.ts +8 -0
  165. package/dist/validation-geometry.d.ts.map +1 -0
  166. package/dist/validation-geometry.js +187 -0
  167. package/dist/validation-geometry.js.map +1 -0
  168. package/dist/validation-interactions.d.ts +3 -0
  169. package/dist/validation-interactions.d.ts.map +1 -0
  170. package/dist/validation-interactions.js +644 -0
  171. package/dist/validation-interactions.js.map +1 -0
  172. package/dist/validation-primitives.d.ts +11 -0
  173. package/dist/validation-primitives.d.ts.map +1 -0
  174. package/dist/validation-primitives.js +48 -0
  175. package/dist/validation-primitives.js.map +1 -0
  176. package/dist/validation-processing.d.ts +4 -0
  177. package/dist/validation-processing.d.ts.map +1 -0
  178. package/dist/validation-processing.js +569 -0
  179. package/dist/validation-processing.js.map +1 -0
  180. package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
  181. package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
  182. package/dist/validation-shared-vocabulary-layout.js +102 -0
  183. package/dist/validation-shared-vocabulary-layout.js.map +1 -0
  184. package/dist/validation.d.ts.map +1 -1
  185. package/dist/validation.js +27 -1641
  186. package/dist/validation.js.map +1 -1
  187. package/dist/xml.js +1 -1
  188. package/dist/xml.js.map +1 -1
  189. package/package.json +4 -2
  190. package/src/adaptive-turn-materializer.ts +85 -0
  191. package/src/adaptive-turn.ts +109 -0
  192. package/src/assert-never.ts +4 -0
  193. package/src/attempt-state-constants.ts +5 -0
  194. package/src/attempt-state.ts +245 -0
  195. package/src/catalog.ts +1 -1
  196. package/src/content-text.ts +3 -0
  197. package/src/custom-operators.ts +12 -0
  198. package/src/delivery-redaction.ts +268 -0
  199. package/src/delivery-security.ts +59 -245
  200. package/src/index.ts +9 -0
  201. package/src/parser-custom-interactions.ts +1 -1
  202. package/src/parser-declarations.ts +4 -3
  203. package/src/parser-item-metadata.ts +8 -7
  204. package/src/parser-processing.ts +2 -2
  205. package/src/parser-values.ts +36 -0
  206. package/src/processing-evaluator-boolean.ts +48 -0
  207. package/src/processing-evaluator-collection.ts +50 -0
  208. package/src/processing-evaluator-comparison.ts +106 -0
  209. package/src/processing-evaluator-custom.ts +20 -0
  210. package/src/processing-evaluator-geometry.ts +26 -0
  211. package/src/processing-evaluator-numeric.ts +155 -0
  212. package/src/processing-evaluator-string.ts +60 -0
  213. package/src/processing-evaluator-variable.ts +81 -0
  214. package/src/processing-evaluator.ts +173 -0
  215. package/src/processing-expression-children.ts +69 -0
  216. package/src/processing-mapping.ts +153 -0
  217. package/src/processing-operators.ts +184 -0
  218. package/src/processing-random.ts +16 -0
  219. package/src/processing-state.ts +69 -0
  220. package/src/processing-templates.ts +12 -0
  221. package/src/processing-values.ts +162 -0
  222. package/src/processing-variables.ts +83 -0
  223. package/src/serializer-processing-expressions.ts +2 -12
  224. package/src/serializer-processing-xml.ts +2 -2
  225. package/src/serializer-processing.ts +3 -0
  226. package/src/server-scoring.ts +30 -226
  227. package/src/session.ts +128 -2006
  228. package/src/shared-vocabulary-coverage-policy.ts +1 -1
  229. package/src/shared-vocabulary-generated-families.ts +1 -1
  230. package/src/shared-vocabulary-support.ts +3 -3
  231. package/src/shared-vocabulary.ts +3 -2
  232. package/src/support-evidence.ts +107 -0
  233. package/src/support.ts +68 -54
  234. package/src/trusted-item-session.ts +429 -0
  235. package/src/tts.ts +29 -15
  236. package/src/validation-geometry.ts +204 -0
  237. package/src/validation-interactions.ts +831 -0
  238. package/src/validation-primitives.ts +63 -0
  239. package/src/validation-processing.ts +785 -0
  240. package/src/validation-shared-vocabulary-layout.ts +117 -0
  241. package/src/validation.ts +44 -2077
  242. package/src/xml.ts +1 -1
  243. package/src/interaction-test-fixtures.ts +0 -44
  244. package/src/serializer-processing.fixtures.ts +0 -469
@@ -0,0 +1,106 @@
1
+ import type { QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+ import type { EvaluationContext } from "./processing-evaluator.js";
4
+ import {
5
+ containsValues,
6
+ durationSeconds,
7
+ numericValueOrNull,
8
+ qtiMatchValues,
9
+ valuesEqual,
10
+ valueContainer,
11
+ } from "./processing-values.js";
12
+ import { expressionIsOrdered } from "./processing-variables.js";
13
+ import { roundWithMode } from "./processing-operators.js";
14
+
15
+ type ComparisonExpression = Extract<
16
+ QtiProcessingExpression,
17
+ {
18
+ type:
19
+ | "match"
20
+ | "equal"
21
+ | "equalRounded"
22
+ | "numericCompare"
23
+ | "durationCompare"
24
+ | "member"
25
+ | "delete"
26
+ | "contains";
27
+ }
28
+ >;
29
+
30
+ export function evaluateComparisonExpression(
31
+ expression: ComparisonExpression,
32
+ context: EvaluationContext,
33
+ ): QtiValue {
34
+ switch (expression.type) {
35
+ case "match": {
36
+ const left = context.evaluate(expression.left);
37
+ const right = context.evaluate(expression.right);
38
+ return qtiMatchValues(
39
+ left,
40
+ right,
41
+ expressionIsOrdered(expression.left, context.document) ||
42
+ expressionIsOrdered(expression.right, context.document),
43
+ );
44
+ }
45
+ case "equal": {
46
+ const left = context.evaluate(expression.left);
47
+ const right = context.evaluate(expression.right);
48
+ return left === null || right === null ? null : valuesEqual(left, right);
49
+ }
50
+ case "equalRounded": {
51
+ const left = context.evaluate(expression.left);
52
+ const right = context.evaluate(expression.right);
53
+ if (left === null || right === null) return null;
54
+ const leftNumber = numericValueOrNull(left);
55
+ const rightNumber = numericValueOrNull(right);
56
+ if (leftNumber === null || rightNumber === null) return null;
57
+ const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, expression.figures);
58
+ const roundedRight = roundWithMode(rightNumber, expression.roundingMode, expression.figures);
59
+ return roundedLeft === null || roundedRight === null ? null : roundedLeft === roundedRight;
60
+ }
61
+ case "numericCompare": {
62
+ const leftValue = context.evaluate(expression.left);
63
+ const rightValue = context.evaluate(expression.right);
64
+ if (leftValue === null || rightValue === null) return null;
65
+ const left = numericValueOrNull(leftValue);
66
+ const right = numericValueOrNull(rightValue);
67
+ if (left === null || right === null) return null;
68
+ if (expression.operator === "lt") return left < right;
69
+ if (expression.operator === "lte") return left <= right;
70
+ if (expression.operator === "gt") return left > right;
71
+ return left >= right;
72
+ }
73
+ case "durationCompare": {
74
+ const left = durationSeconds(context.evaluate(expression.left));
75
+ const right = durationSeconds(context.evaluate(expression.right));
76
+ if (left === null || right === null) return null;
77
+ return expression.operator === "lt" ? left < right : left >= right;
78
+ }
79
+ case "member": {
80
+ const value = context.evaluate(expression.value);
81
+ const collection = context.evaluate(expression.collection);
82
+ if (value === null || collection === null) return null;
83
+ return valueContainer(collection).some((item) => valuesEqual(item, value));
84
+ }
85
+ case "delete": {
86
+ const value = context.evaluate(expression.value);
87
+ const collectionValue = context.evaluate(expression.collection);
88
+ if (value === null || collectionValue === null) return null;
89
+ const collection = valueContainer(collectionValue);
90
+ if (collection.length === 0) return null;
91
+ const filtered = collection.filter((item) => !valuesEqual(item, value));
92
+ return filtered.length > 0 ? filtered : null;
93
+ }
94
+ case "contains": {
95
+ const collectionValue = context.evaluate(expression.collection);
96
+ const valuesValue = context.evaluate(expression.values);
97
+ if (collectionValue === null || valuesValue === null) return null;
98
+ const collection = valueContainer(collectionValue);
99
+ const values = valueContainer(valuesValue);
100
+ if (collection.length === 0 || values.length === 0) return null;
101
+ return containsValues(collection, values);
102
+ }
103
+ default:
104
+ return assertNever(expression);
105
+ }
106
+ }
@@ -0,0 +1,20 @@
1
+ import type { QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import type { EvaluationContext } from "./processing-evaluator.js";
3
+
4
+ type CustomOperatorExpression = Extract<QtiProcessingExpression, { type: "customOperator" }>;
5
+
6
+ export function evaluateCustomOperatorExpression(
7
+ expression: CustomOperatorExpression,
8
+ context: EvaluationContext,
9
+ ): QtiValue {
10
+ const operatorKey = expression.definition ?? expression.className ?? "";
11
+ const handler = context.customOperators[operatorKey];
12
+ if (!handler) return null;
13
+ return handler({
14
+ definition: expression.definition,
15
+ className: expression.className,
16
+ attributes: expression.attributes,
17
+ values: expression.expressions.map((item) => context.evaluate(item)),
18
+ expression,
19
+ });
20
+ }
@@ -0,0 +1,26 @@
1
+ import type { QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import type { EvaluationContext } from "./processing-evaluator.js";
3
+ import { parsePoint, pointInsideArea } from "./processing-mapping.js";
4
+ import { valueContainer } from "./processing-values.js";
5
+
6
+ type GeometryExpression = Extract<QtiProcessingExpression, { type: "inside" }>;
7
+
8
+ export function evaluateGeometryExpression(
9
+ expression: GeometryExpression,
10
+ context: EvaluationContext,
11
+ ): QtiValue {
12
+ const value = context.evaluate(expression.expression);
13
+ if (value === null) return null;
14
+ if (expression.shape === "default") return true;
15
+ return valueContainer(value).some((pointValue) => {
16
+ const point = parsePoint(String(pointValue));
17
+ return point
18
+ ? pointInsideArea(point, {
19
+ shape: expression.shape,
20
+ coords: expression.coords,
21
+ mappedValue: 0,
22
+ attributes: {},
23
+ })
24
+ : false;
25
+ });
26
+ }
@@ -0,0 +1,155 @@
1
+ import type { QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+ import type { EvaluationContext } from "./processing-evaluator.js";
4
+ import {
5
+ generalizedGcd,
6
+ generalizedLcm,
7
+ mathOperatorValue,
8
+ roundToDecimalPlaces,
9
+ roundToSignificantFigures,
10
+ statsOperatorValue,
11
+ } from "./processing-operators.js";
12
+ import {
13
+ isRecordValue,
14
+ numericValue,
15
+ numericValueOrNull,
16
+ valueContainer,
17
+ } from "./processing-values.js";
18
+
19
+ type NumericExpression = Extract<
20
+ QtiProcessingExpression,
21
+ {
22
+ type:
23
+ | "sum"
24
+ | "product"
25
+ | "min"
26
+ | "max"
27
+ | "subtract"
28
+ | "divide"
29
+ | "power"
30
+ | "integerDivide"
31
+ | "integerModulus"
32
+ | "round"
33
+ | "roundTo"
34
+ | "truncate"
35
+ | "integerToFloat"
36
+ | "gcd"
37
+ | "lcm"
38
+ | "mathConstant"
39
+ | "mathOperator"
40
+ | "statsOperator";
41
+ }
42
+ >;
43
+
44
+ export function evaluateNumericExpression(
45
+ expression: NumericExpression,
46
+ context: EvaluationContext,
47
+ ): QtiValue {
48
+ switch (expression.type) {
49
+ case "sum": {
50
+ const values = context.numericOperands(expression.expressions);
51
+ return values ? values.reduce((sum, value) => sum + value, 0) : null;
52
+ }
53
+ case "product": {
54
+ const values = context.numericOperands(expression.expressions);
55
+ return values ? values.reduce((product, value) => product * value, 1) : null;
56
+ }
57
+ case "min":
58
+ case "max": {
59
+ const values = context.numericOperands(expression.expressions);
60
+ if (!values || values.length === 0) return null;
61
+ return expression.type === "min" ? Math.min(...values) : Math.max(...values);
62
+ }
63
+ case "subtract": {
64
+ const values = context.numericOperands([expression.left, expression.right]);
65
+ return values && values.length === 2 ? values[0]! - values[1]! : null;
66
+ }
67
+ case "divide":
68
+ return divide(context.evaluate(expression.left), context.evaluate(expression.right));
69
+ case "power": {
70
+ const values = context.numericOperands([expression.left, expression.right]);
71
+ if (!values || values.length !== 2) return null;
72
+ const value = Math.pow(values[0]!, values[1]!);
73
+ return Number.isFinite(value) ? value : null;
74
+ }
75
+ case "integerDivide": {
76
+ const values = integerOperands(
77
+ context.evaluate(expression.left),
78
+ context.evaluate(expression.right),
79
+ );
80
+ return values ? Math.floor(values.dividend / values.divisor) : null;
81
+ }
82
+ case "integerModulus": {
83
+ const values = integerOperands(
84
+ context.evaluate(expression.left),
85
+ context.evaluate(expression.right),
86
+ );
87
+ return values
88
+ ? values.dividend - Math.floor(values.dividend / values.divisor) * values.divisor
89
+ : null;
90
+ }
91
+ case "round": {
92
+ const value = numericValueOrNull(context.evaluate(expression.expression));
93
+ return value === null ? null : Math.round(value);
94
+ }
95
+ case "roundTo": {
96
+ const value = numericValueOrNull(context.evaluate(expression.expression));
97
+ if (value === null) return null;
98
+ return expression.roundingMode === "decimalPlaces"
99
+ ? roundToDecimalPlaces(value, expression.figures)
100
+ : roundToSignificantFigures(value, expression.figures);
101
+ }
102
+ case "truncate": {
103
+ const value = numericValueOrNull(context.evaluate(expression.expression));
104
+ return value === null ? null : Math.trunc(value);
105
+ }
106
+ case "integerToFloat":
107
+ return numericValueOrNull(context.evaluate(expression.expression));
108
+ case "gcd":
109
+ case "lcm": {
110
+ const values = expression.expressions.flatMap((item) => {
111
+ const value = context.evaluate(item);
112
+ return value === null ? [null] : valueContainer(value);
113
+ });
114
+ if (values.length === 0 || values.some((value) => value === null || isRecordValue(value))) {
115
+ return null;
116
+ }
117
+ const integers = values.map((value) => Math.trunc(numericValue(value)));
118
+ return expression.type === "gcd" ? generalizedGcd(integers) : generalizedLcm(integers);
119
+ }
120
+ case "mathConstant":
121
+ if (expression.name === "pi") return Math.PI;
122
+ if (expression.name === "e") return Math.E;
123
+ return null;
124
+ case "mathOperator": {
125
+ const values = expression.expressions.map((item) => context.evaluate(item));
126
+ if (values.length === 0 || values.some((value) => value === null)) return null;
127
+ return mathOperatorValue(expression.name, values.map(numericValue));
128
+ }
129
+ case "statsOperator": {
130
+ const value = context.evaluate(expression.expression);
131
+ if (value === null) return null;
132
+ return statsOperatorValue(expression.name, valueContainer(value).map(numericValue));
133
+ }
134
+ default:
135
+ return assertNever(expression);
136
+ }
137
+ }
138
+
139
+ function divide(dividendValue: QtiValue, divisorValue: QtiValue): QtiValue {
140
+ const values = integerOperands(dividendValue, divisorValue);
141
+ if (!values) return null;
142
+ const quotient = values.dividend / values.divisor;
143
+ return Number.isFinite(quotient) ? quotient : null;
144
+ }
145
+
146
+ function integerOperands(
147
+ dividendValue: QtiValue,
148
+ divisorValue: QtiValue,
149
+ ): { dividend: number; divisor: number } | undefined {
150
+ if (dividendValue === null || divisorValue === null) return undefined;
151
+ const dividend = numericValueOrNull(dividendValue);
152
+ const divisor = numericValueOrNull(divisorValue);
153
+ if (dividend === null || divisor === null || divisor === 0) return undefined;
154
+ return { dividend, divisor };
155
+ }
@@ -0,0 +1,60 @@
1
+ import type { QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+ import type { EvaluationContext } from "./processing-evaluator.js";
4
+ import { qtiValueToString } from "./value-format.js";
5
+ import { isRecordValue, stringMatch } from "./processing-values.js";
6
+ import { resolveOptionalVariableValue } from "./processing-variables.js";
7
+
8
+ type StringExpression = Extract<
9
+ QtiProcessingExpression,
10
+ { type: "stringMatch" | "substring" | "patternMatch" | "fieldValue" }
11
+ >;
12
+
13
+ export function evaluateStringExpression(
14
+ expression: StringExpression,
15
+ context: EvaluationContext,
16
+ ): QtiValue {
17
+ switch (expression.type) {
18
+ case "stringMatch":
19
+ return stringMatch(
20
+ context.evaluate(expression.left),
21
+ context.evaluate(expression.right),
22
+ expression.caseSensitive,
23
+ expression.substring,
24
+ );
25
+ case "substring":
26
+ return stringMatch(
27
+ context.evaluate(expression.right),
28
+ context.evaluate(expression.left),
29
+ expression.caseSensitive,
30
+ true,
31
+ );
32
+ case "patternMatch": {
33
+ const value = context.evaluate(expression.expression);
34
+ if (value === null) return null;
35
+ const patternValue =
36
+ resolveOptionalVariableValue(
37
+ context.document,
38
+ expression.pattern,
39
+ context.responses,
40
+ context.outcomes,
41
+ context.templateValues,
42
+ ) ??
43
+ context.undeclaredResponseValue(expression.pattern) ??
44
+ expression.pattern;
45
+ try {
46
+ return new RegExp(
47
+ typeof patternValue === "string" ? patternValue : qtiValueToString(patternValue),
48
+ ).test(qtiValueToString(value));
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ case "fieldValue": {
54
+ const value = context.evaluate(expression.expression);
55
+ return isRecordValue(value) ? (value[expression.fieldIdentifier] ?? null) : null;
56
+ }
57
+ default:
58
+ return assertNever(expression);
59
+ }
60
+ }
@@ -0,0 +1,81 @@
1
+ import type { QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+ import type { EvaluationContext } from "./processing-evaluator.js";
4
+ import { mapOrMatchResponse, scoreAreaMapping } from "./processing-mapping.js";
5
+ import { isNullResponse, qtiMatchValues } from "./processing-values.js";
6
+ import {
7
+ defaultValueForIdentifier,
8
+ getResponseDeclaration,
9
+ resolveVariableValue,
10
+ } from "./processing-variables.js";
11
+
12
+ type VariableExpression = Extract<
13
+ QtiProcessingExpression,
14
+ {
15
+ type:
16
+ | "isNull"
17
+ | "matchCorrect"
18
+ | "mapResponse"
19
+ | "mapResponsePoint"
20
+ | "correct"
21
+ | "default"
22
+ | "variable";
23
+ }
24
+ >;
25
+
26
+ export function evaluateVariableExpression(
27
+ expression: VariableExpression,
28
+ context: EvaluationContext,
29
+ ): QtiValue {
30
+ switch (expression.type) {
31
+ case "isNull":
32
+ return isNullResponse(context.responses[expression.identifier] ?? null);
33
+ case "matchCorrect": {
34
+ const declaration = getResponseDeclaration(context.document, expression.correctIdentifier);
35
+ return declaration
36
+ ? qtiMatchValues(
37
+ context.responses[expression.identifier] ?? null,
38
+ context.correctResponses[expression.correctIdentifier] ?? null,
39
+ declaration.cardinality === "ordered",
40
+ )
41
+ : false;
42
+ }
43
+ case "mapResponse": {
44
+ const declaration = getResponseDeclaration(context.document, expression.identifier);
45
+ return declaration
46
+ ? mapOrMatchResponse(
47
+ declaration,
48
+ context.responses[expression.identifier] ?? null,
49
+ context.correctResponses[expression.identifier] ?? null,
50
+ )
51
+ : 0;
52
+ }
53
+ case "mapResponsePoint": {
54
+ const declaration = getResponseDeclaration(context.document, expression.identifier);
55
+ return declaration?.areaMapping
56
+ ? scoreAreaMapping(
57
+ context.responses[expression.identifier] ?? null,
58
+ declaration.areaMapping,
59
+ )
60
+ : 0;
61
+ }
62
+ case "correct":
63
+ return context.correctResponses[expression.identifier] ?? null;
64
+ case "default":
65
+ return defaultValueForIdentifier(context.document, expression.identifier);
66
+ case "variable":
67
+ return (
68
+ resolveVariableValue(
69
+ context.document,
70
+ expression.identifier,
71
+ context.responses,
72
+ context.outcomes,
73
+ context.templateValues,
74
+ ) ??
75
+ context.undeclaredResponseValue(expression.identifier) ??
76
+ null
77
+ );
78
+ default:
79
+ return assertNever(expression);
80
+ }
81
+ }
@@ -0,0 +1,173 @@
1
+ import type { QtiDocument, QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+ import type { QtiCustomOperatorRegistry } from "./custom-operators.js";
4
+ import { evaluateBooleanExpression } from "./processing-evaluator-boolean.js";
5
+ import {
6
+ evaluateCollectionExpression,
7
+ evaluateRepeatExpression,
8
+ } from "./processing-evaluator-collection.js";
9
+ import { evaluateComparisonExpression } from "./processing-evaluator-comparison.js";
10
+ import { evaluateCustomOperatorExpression } from "./processing-evaluator-custom.js";
11
+ import { evaluateGeometryExpression } from "./processing-evaluator-geometry.js";
12
+ import { evaluateNumericExpression } from "./processing-evaluator-numeric.js";
13
+ import { evaluateStringExpression } from "./processing-evaluator-string.js";
14
+ import { evaluateVariableExpression } from "./processing-evaluator-variable.js";
15
+ import { isRecordValue, numericValueOrNull } from "./processing-values.js";
16
+
17
+ export interface EvaluationContext {
18
+ document: QtiDocument;
19
+ responses: Record<string, QtiValue>;
20
+ outcomes: Record<string, QtiValue>;
21
+ templateValues: Record<string, QtiValue>;
22
+ correctResponses: Record<string, QtiValue>;
23
+ allowedUndeclaredResponseIdentifiers: ReadonlySet<string>;
24
+ random: () => number;
25
+ customOperators: QtiCustomOperatorRegistry;
26
+ evaluate(expression: QtiProcessingExpression): QtiValue;
27
+ indexValue(identifierOrInteger: string): number | undefined;
28
+ numericOperands(expressions: QtiProcessingExpression[]): number[] | null;
29
+ undeclaredResponseValue(identifier: string): QtiValue | undefined;
30
+ }
31
+
32
+ export interface EvaluationOptions {
33
+ allowedUndeclaredResponseIdentifiers?: ReadonlySet<string> | readonly string[] | undefined;
34
+ }
35
+
36
+ export function createEvaluationContext(
37
+ document: QtiDocument,
38
+ responses: Record<string, QtiValue>,
39
+ outcomes: Record<string, QtiValue>,
40
+ templateValues: Record<string, QtiValue>,
41
+ correctResponses: Record<string, QtiValue>,
42
+ random: () => number,
43
+ customOperators: QtiCustomOperatorRegistry,
44
+ options: EvaluationOptions = {},
45
+ ): EvaluationContext {
46
+ const allowedUndeclaredResponseIdentifiers =
47
+ options.allowedUndeclaredResponseIdentifiers instanceof Set
48
+ ? options.allowedUndeclaredResponseIdentifiers
49
+ : new Set(options.allowedUndeclaredResponseIdentifiers ?? []);
50
+ const context: EvaluationContext = {
51
+ document,
52
+ responses,
53
+ outcomes,
54
+ templateValues,
55
+ correctResponses,
56
+ allowedUndeclaredResponseIdentifiers,
57
+ random,
58
+ customOperators,
59
+ evaluate(expression) {
60
+ return evaluateProcessingExpression(expression, context);
61
+ },
62
+ indexValue(identifierOrInteger) {
63
+ const parsed = Number(identifierOrInteger);
64
+ if (Number.isInteger(parsed)) return parsed;
65
+ const value = outcomes[identifierOrInteger] ?? templateValues[identifierOrInteger] ?? null;
66
+ const numeric = numericValueOrNull(value);
67
+ return numeric !== null && Number.isInteger(numeric) ? numeric : undefined;
68
+ },
69
+ numericOperands(expressions) {
70
+ const numericValues: number[] = [];
71
+ for (const expression of expressions) {
72
+ const value = context.evaluate(expression);
73
+ if (value === null || isRecordValue(value)) return null;
74
+ const values = Array.isArray(value) ? value : [value];
75
+ for (const item of values) {
76
+ const numeric = numericValueOrNull(item);
77
+ if (numeric === null) return null;
78
+ numericValues.push(numeric);
79
+ }
80
+ }
81
+ return numericValues;
82
+ },
83
+ undeclaredResponseValue(identifier) {
84
+ return allowedUndeclaredResponseIdentifiers.has(identifier) && identifier in responses
85
+ ? (responses[identifier] ?? null)
86
+ : undefined;
87
+ },
88
+ };
89
+ return context;
90
+ }
91
+
92
+ export function evaluateProcessingExpression(
93
+ expression: QtiProcessingExpression,
94
+ context: EvaluationContext,
95
+ ): QtiValue {
96
+ switch (expression.type) {
97
+ case "baseValue":
98
+ return expression.value;
99
+ case "null":
100
+ return null;
101
+ case "randomInteger": {
102
+ const step = expression.step > 0 ? expression.step : 1;
103
+ const count = Math.floor((expression.max - expression.min) / step) + 1;
104
+ return expression.min + Math.floor(context.random() * count) * step;
105
+ }
106
+ case "randomFloat":
107
+ return expression.min + context.random() * (expression.max - expression.min);
108
+ case "random":
109
+ if (expression.values.length === 0) return null;
110
+ return context.evaluate(
111
+ expression.values[Math.floor(context.random() * expression.values.length)]!,
112
+ );
113
+ case "isNull":
114
+ case "matchCorrect":
115
+ case "mapResponse":
116
+ case "mapResponsePoint":
117
+ case "correct":
118
+ case "default":
119
+ case "variable":
120
+ return evaluateVariableExpression(expression, context);
121
+ case "multiple":
122
+ case "ordered":
123
+ case "index":
124
+ case "containerSize":
125
+ return evaluateCollectionExpression(expression, context);
126
+ case "sum":
127
+ case "product":
128
+ case "min":
129
+ case "max":
130
+ case "subtract":
131
+ case "divide":
132
+ case "power":
133
+ case "integerDivide":
134
+ case "integerModulus":
135
+ case "round":
136
+ case "roundTo":
137
+ case "truncate":
138
+ case "integerToFloat":
139
+ case "gcd":
140
+ case "lcm":
141
+ case "mathConstant":
142
+ case "mathOperator":
143
+ case "statsOperator":
144
+ return evaluateNumericExpression(expression, context);
145
+ case "and":
146
+ case "anyN":
147
+ case "or":
148
+ case "not":
149
+ return evaluateBooleanExpression(expression, context);
150
+ case "match":
151
+ case "equal":
152
+ case "equalRounded":
153
+ case "numericCompare":
154
+ case "durationCompare":
155
+ case "member":
156
+ case "delete":
157
+ case "contains":
158
+ return evaluateComparisonExpression(expression, context);
159
+ case "stringMatch":
160
+ case "substring":
161
+ case "patternMatch":
162
+ case "fieldValue":
163
+ return evaluateStringExpression(expression, context);
164
+ case "inside":
165
+ return evaluateGeometryExpression(expression, context);
166
+ case "repeat":
167
+ return evaluateRepeatExpression(expression, context);
168
+ case "customOperator":
169
+ return evaluateCustomOperatorExpression(expression, context);
170
+ default:
171
+ return assertNever(expression);
172
+ }
173
+ }
@@ -0,0 +1,69 @@
1
+ import type { QtiProcessingExpression } from "./types.js";
2
+ import { assertNever } from "./assert-never.js";
3
+
4
+ export function expressionChildren(expression: QtiProcessingExpression): QtiProcessingExpression[] {
5
+ switch (expression.type) {
6
+ case "random":
7
+ return expression.values;
8
+ case "multiple":
9
+ case "ordered":
10
+ case "sum":
11
+ case "product":
12
+ case "min":
13
+ case "max":
14
+ case "and":
15
+ case "anyN":
16
+ case "or":
17
+ case "gcd":
18
+ case "lcm":
19
+ case "mathOperator":
20
+ case "repeat":
21
+ case "customOperator":
22
+ return expression.expressions;
23
+ case "subtract":
24
+ case "divide":
25
+ case "power":
26
+ case "integerDivide":
27
+ case "integerModulus":
28
+ case "match":
29
+ case "equal":
30
+ case "equalRounded":
31
+ case "numericCompare":
32
+ case "durationCompare":
33
+ case "stringMatch":
34
+ case "substring":
35
+ return [expression.left, expression.right];
36
+ case "not":
37
+ case "round":
38
+ case "roundTo":
39
+ case "truncate":
40
+ case "integerToFloat":
41
+ case "index":
42
+ case "containerSize":
43
+ case "patternMatch":
44
+ case "fieldValue":
45
+ case "inside":
46
+ case "statsOperator":
47
+ return [expression.expression];
48
+ case "member":
49
+ case "delete":
50
+ return [expression.value, expression.collection];
51
+ case "contains":
52
+ return [expression.collection, expression.values];
53
+ case "baseValue":
54
+ case "correct":
55
+ case "default":
56
+ case "isNull":
57
+ case "mapResponse":
58
+ case "mapResponsePoint":
59
+ case "matchCorrect":
60
+ case "mathConstant":
61
+ case "null":
62
+ case "randomFloat":
63
+ case "randomInteger":
64
+ case "variable":
65
+ return [];
66
+ default:
67
+ return assertNever(expression);
68
+ }
69
+ }