@longsightgroup/qti3-core 0.9.2 → 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 (219) hide show
  1. package/README.md +20 -3
  2. package/dist/assert-never.d.ts +3 -0
  3. package/dist/assert-never.d.ts.map +1 -0
  4. package/dist/assert-never.js +5 -0
  5. package/dist/assert-never.js.map +1 -0
  6. package/dist/attempt-state-constants.d.ts +6 -0
  7. package/dist/attempt-state-constants.d.ts.map +1 -0
  8. package/dist/attempt-state-constants.js +6 -0
  9. package/dist/attempt-state-constants.js.map +1 -0
  10. package/dist/attempt-state.d.ts +5 -0
  11. package/dist/attempt-state.d.ts.map +1 -0
  12. package/dist/attempt-state.js +190 -0
  13. package/dist/attempt-state.js.map +1 -0
  14. package/dist/catalog.js +1 -1
  15. package/dist/catalog.js.map +1 -1
  16. package/dist/content-text.d.ts.map +1 -1
  17. package/dist/content-text.js +3 -0
  18. package/dist/content-text.js.map +1 -1
  19. package/dist/custom-operators.d.ts +13 -0
  20. package/dist/custom-operators.d.ts.map +1 -0
  21. package/dist/custom-operators.js +2 -0
  22. package/dist/custom-operators.js.map +1 -0
  23. package/dist/delivery-redaction.js +1 -1
  24. package/dist/delivery-redaction.js.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/parser-custom-interactions.js +1 -1
  30. package/dist/parser-custom-interactions.js.map +1 -1
  31. package/dist/parser-declarations.d.ts.map +1 -1
  32. package/dist/parser-declarations.js +4 -4
  33. package/dist/parser-declarations.js.map +1 -1
  34. package/dist/parser-item-metadata.d.ts.map +1 -1
  35. package/dist/parser-item-metadata.js +8 -7
  36. package/dist/parser-item-metadata.js.map +1 -1
  37. package/dist/parser-processing.js +2 -2
  38. package/dist/parser-processing.js.map +1 -1
  39. package/dist/parser-values.d.ts +5 -1
  40. package/dist/parser-values.d.ts.map +1 -1
  41. package/dist/parser-values.js +33 -0
  42. package/dist/parser-values.js.map +1 -1
  43. package/dist/processing-evaluator-boolean.d.ts +8 -0
  44. package/dist/processing-evaluator-boolean.d.ts.map +1 -0
  45. package/dist/processing-evaluator-boolean.js +47 -0
  46. package/dist/processing-evaluator-boolean.js.map +1 -0
  47. package/dist/processing-evaluator-collection.d.ts +12 -0
  48. package/dist/processing-evaluator-collection.d.ts.map +1 -0
  49. package/dist/processing-evaluator-collection.js +35 -0
  50. package/dist/processing-evaluator-collection.js.map +1 -0
  51. package/dist/processing-evaluator-comparison.d.ts +8 -0
  52. package/dist/processing-evaluator-comparison.d.ts.map +1 -0
  53. package/dist/processing-evaluator-comparison.js +88 -0
  54. package/dist/processing-evaluator-comparison.js.map +1 -0
  55. package/dist/processing-evaluator-custom.d.ts +8 -0
  56. package/dist/processing-evaluator-custom.d.ts.map +1 -0
  57. package/dist/processing-evaluator-custom.js +14 -0
  58. package/dist/processing-evaluator-custom.js.map +1 -0
  59. package/dist/processing-evaluator-geometry.d.ts +8 -0
  60. package/dist/processing-evaluator-geometry.d.ts.map +1 -0
  61. package/dist/processing-evaluator-geometry.js +21 -0
  62. package/dist/processing-evaluator-geometry.js.map +1 -0
  63. package/dist/processing-evaluator-numeric.d.ts +8 -0
  64. package/dist/processing-evaluator-numeric.d.ts.map +1 -0
  65. package/dist/processing-evaluator-numeric.js +112 -0
  66. package/dist/processing-evaluator-numeric.js.map +1 -0
  67. package/dist/processing-evaluator-string.d.ts +8 -0
  68. package/dist/processing-evaluator-string.d.ts.map +1 -0
  69. package/dist/processing-evaluator-string.js +33 -0
  70. package/dist/processing-evaluator-string.js.map +1 -0
  71. package/dist/processing-evaluator-variable.d.ts +8 -0
  72. package/dist/processing-evaluator-variable.d.ts.map +1 -0
  73. package/dist/processing-evaluator-variable.js +39 -0
  74. package/dist/processing-evaluator-variable.js.map +1 -0
  75. package/dist/processing-evaluator.d.ts +22 -0
  76. package/dist/processing-evaluator.d.ts.map +1 -0
  77. package/dist/processing-evaluator.js +137 -0
  78. package/dist/processing-evaluator.js.map +1 -0
  79. package/dist/processing-expression-children.d.ts +3 -0
  80. package/dist/processing-expression-children.d.ts.map +1 -0
  81. package/dist/processing-expression-children.js +68 -0
  82. package/dist/processing-expression-children.js.map +1 -0
  83. package/dist/processing-mapping.d.ts +13 -0
  84. package/dist/processing-mapping.d.ts.map +1 -0
  85. package/dist/processing-mapping.js +120 -0
  86. package/dist/processing-mapping.js.map +1 -0
  87. package/dist/processing-operators.d.ts +11 -0
  88. package/dist/processing-operators.d.ts.map +1 -0
  89. package/dist/processing-operators.js +178 -0
  90. package/dist/processing-operators.js.map +1 -0
  91. package/dist/processing-random.d.ts +2 -0
  92. package/dist/processing-random.d.ts.map +1 -0
  93. package/dist/processing-random.js +16 -0
  94. package/dist/processing-random.js.map +1 -0
  95. package/dist/processing-state.d.ts +8 -0
  96. package/dist/processing-state.d.ts.map +1 -0
  97. package/dist/processing-state.js +44 -0
  98. package/dist/processing-state.js.map +1 -0
  99. package/dist/processing-templates.d.ts +3 -0
  100. package/dist/processing-templates.d.ts.map +1 -0
  101. package/dist/processing-templates.js +12 -0
  102. package/dist/processing-templates.js.map +1 -0
  103. package/dist/processing-values.d.ts +15 -0
  104. package/dist/processing-values.d.ts.map +1 -0
  105. package/dist/processing-values.js +150 -0
  106. package/dist/processing-values.js.map +1 -0
  107. package/dist/processing-variables.d.ts +7 -0
  108. package/dist/processing-variables.d.ts.map +1 -0
  109. package/dist/processing-variables.js +40 -0
  110. package/dist/processing-variables.js.map +1 -0
  111. package/dist/serializer-processing-expressions.d.ts.map +1 -1
  112. package/dist/serializer-processing-expressions.js +2 -8
  113. package/dist/serializer-processing-expressions.js.map +1 -1
  114. package/dist/serializer-processing-xml.js +2 -2
  115. package/dist/serializer-processing-xml.js.map +1 -1
  116. package/dist/serializer-processing.d.ts.map +1 -1
  117. package/dist/serializer-processing.js +3 -0
  118. package/dist/serializer-processing.js.map +1 -1
  119. package/dist/session.d.ts +5 -14
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +76 -1158
  122. package/dist/session.js.map +1 -1
  123. package/dist/shared-vocabulary-coverage-policy.js +1 -1
  124. package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
  125. package/dist/shared-vocabulary-generated-families.js +1 -1
  126. package/dist/shared-vocabulary-generated-families.js.map +1 -1
  127. package/dist/shared-vocabulary-support.js +3 -3
  128. package/dist/shared-vocabulary-support.js.map +1 -1
  129. package/dist/shared-vocabulary.d.ts.map +1 -1
  130. package/dist/shared-vocabulary.js +4 -3
  131. package/dist/shared-vocabulary.js.map +1 -1
  132. package/dist/support-evidence.d.ts +22 -0
  133. package/dist/support-evidence.d.ts.map +1 -0
  134. package/dist/support-evidence.js +93 -0
  135. package/dist/support-evidence.js.map +1 -0
  136. package/dist/support.d.ts.map +1 -1
  137. package/dist/support.js +30 -43
  138. package/dist/support.js.map +1 -1
  139. package/dist/trusted-item-session.d.ts.map +1 -1
  140. package/dist/trusted-item-session.js +6 -5
  141. package/dist/trusted-item-session.js.map +1 -1
  142. package/dist/tts.d.ts.map +1 -1
  143. package/dist/tts.js +16 -2
  144. package/dist/tts.js.map +1 -1
  145. package/dist/validation-geometry.d.ts +8 -0
  146. package/dist/validation-geometry.d.ts.map +1 -0
  147. package/dist/validation-geometry.js +187 -0
  148. package/dist/validation-geometry.js.map +1 -0
  149. package/dist/validation-interactions.d.ts +3 -0
  150. package/dist/validation-interactions.d.ts.map +1 -0
  151. package/dist/validation-interactions.js +644 -0
  152. package/dist/validation-interactions.js.map +1 -0
  153. package/dist/validation-primitives.d.ts +11 -0
  154. package/dist/validation-primitives.d.ts.map +1 -0
  155. package/dist/validation-primitives.js +48 -0
  156. package/dist/validation-primitives.js.map +1 -0
  157. package/dist/validation-processing.d.ts +4 -0
  158. package/dist/validation-processing.d.ts.map +1 -0
  159. package/dist/validation-processing.js +569 -0
  160. package/dist/validation-processing.js.map +1 -0
  161. package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
  162. package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
  163. package/dist/validation-shared-vocabulary-layout.js +102 -0
  164. package/dist/validation-shared-vocabulary-layout.js.map +1 -0
  165. package/dist/validation.d.ts.map +1 -1
  166. package/dist/validation.js +27 -1641
  167. package/dist/validation.js.map +1 -1
  168. package/dist/xml.js +1 -1
  169. package/dist/xml.js.map +1 -1
  170. package/package.json +1 -1
  171. package/src/assert-never.ts +4 -0
  172. package/src/attempt-state-constants.ts +5 -0
  173. package/src/attempt-state.ts +245 -0
  174. package/src/catalog.ts +1 -1
  175. package/src/content-text.ts +3 -0
  176. package/src/custom-operators.ts +12 -0
  177. package/src/delivery-redaction.ts +1 -1
  178. package/src/index.ts +2 -0
  179. package/src/parser-custom-interactions.ts +1 -1
  180. package/src/parser-declarations.ts +4 -3
  181. package/src/parser-item-metadata.ts +8 -7
  182. package/src/parser-processing.ts +2 -2
  183. package/src/parser-values.ts +36 -0
  184. package/src/processing-evaluator-boolean.ts +48 -0
  185. package/src/processing-evaluator-collection.ts +50 -0
  186. package/src/processing-evaluator-comparison.ts +106 -0
  187. package/src/processing-evaluator-custom.ts +20 -0
  188. package/src/processing-evaluator-geometry.ts +26 -0
  189. package/src/processing-evaluator-numeric.ts +155 -0
  190. package/src/processing-evaluator-string.ts +60 -0
  191. package/src/processing-evaluator-variable.ts +81 -0
  192. package/src/processing-evaluator.ts +173 -0
  193. package/src/processing-expression-children.ts +69 -0
  194. package/src/processing-mapping.ts +153 -0
  195. package/src/processing-operators.ts +184 -0
  196. package/src/processing-random.ts +16 -0
  197. package/src/processing-state.ts +69 -0
  198. package/src/processing-templates.ts +12 -0
  199. package/src/processing-values.ts +162 -0
  200. package/src/processing-variables.ts +83 -0
  201. package/src/serializer-processing-expressions.ts +2 -12
  202. package/src/serializer-processing-xml.ts +2 -2
  203. package/src/serializer-processing.ts +3 -0
  204. package/src/session.ts +128 -2006
  205. package/src/shared-vocabulary-coverage-policy.ts +1 -1
  206. package/src/shared-vocabulary-generated-families.ts +1 -1
  207. package/src/shared-vocabulary-support.ts +3 -3
  208. package/src/shared-vocabulary.ts +3 -2
  209. package/src/support-evidence.ts +107 -0
  210. package/src/support.ts +59 -50
  211. package/src/trusted-item-session.ts +6 -4
  212. package/src/tts.ts +29 -15
  213. package/src/validation-geometry.ts +204 -0
  214. package/src/validation-interactions.ts +831 -0
  215. package/src/validation-primitives.ts +63 -0
  216. package/src/validation-processing.ts +785 -0
  217. package/src/validation-shared-vocabulary-layout.ts +117 -0
  218. package/src/validation.ts +44 -2077
  219. package/src/xml.ts +1 -1
@@ -0,0 +1,69 @@
1
+ import type {
2
+ QtiAttemptStateV1,
3
+ QtiAttemptStatus,
4
+ QtiDiagnostic,
5
+ QtiPortableCustomStateValue,
6
+ QtiValue,
7
+ } from "./types.js";
8
+ import { ATTEMPT_STATE_SCHEMA } from "./attempt-state-constants.js";
9
+ import { isRecordValue } from "./processing-values.js";
10
+
11
+ export function serialize(
12
+ itemIdentifier: string,
13
+ status: QtiAttemptStatus,
14
+ responses: Record<string, QtiValue>,
15
+ outcomes: Record<string, QtiValue>,
16
+ templateValues: Record<string, QtiValue>,
17
+ interactionStates: Record<string, QtiPortableCustomStateValue>,
18
+ validationMessages: QtiDiagnostic[],
19
+ ): QtiAttemptStateV1 {
20
+ return {
21
+ schema: ATTEMPT_STATE_SCHEMA,
22
+ itemIdentifier,
23
+ status,
24
+ responses: cloneValueRecord(responses),
25
+ outcomes: cloneValueRecord(outcomes),
26
+ templateValues: cloneValueRecord(templateValues),
27
+ interactionStates: clonePortableCustomStateRecord(interactionStates),
28
+ validationMessages: cloneDiagnostics(validationMessages),
29
+ };
30
+ }
31
+
32
+ export function cloneValueRecord(record: Record<string, QtiValue>): Record<string, QtiValue> {
33
+ return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, cloneValue(value)]));
34
+ }
35
+
36
+ export function clonePortableCustomStateRecord(
37
+ record: Record<string, QtiPortableCustomStateValue>,
38
+ ): Record<string, QtiPortableCustomStateValue> {
39
+ return Object.fromEntries(
40
+ Object.entries(record).map(([key, value]) => [key, clonePortableCustomState(value)]),
41
+ );
42
+ }
43
+
44
+ export function cloneValue(value: QtiValue): QtiValue {
45
+ if (Array.isArray(value)) return [...value];
46
+ if (isRecordValue(value)) return cloneValueRecord(value);
47
+ return value;
48
+ }
49
+
50
+ export function clonePortableCustomState(
51
+ value: QtiPortableCustomStateValue,
52
+ ): QtiPortableCustomStateValue {
53
+ if (Array.isArray(value)) return value.map(clonePortableCustomState);
54
+ if (isPortableCustomStateObject(value)) return clonePortableCustomStateRecord(value);
55
+ return value;
56
+ }
57
+
58
+ function isPortableCustomStateObject(
59
+ value: QtiPortableCustomStateValue,
60
+ ): value is { [key: string]: QtiPortableCustomStateValue } {
61
+ return typeof value === "object" && value !== null && !Array.isArray(value);
62
+ }
63
+
64
+ export function cloneDiagnostics(diagnostics: QtiDiagnostic[]): QtiDiagnostic[] {
65
+ return diagnostics.map((diagnostic) => ({
66
+ ...diagnostic,
67
+ source: diagnostic.source ? { ...diagnostic.source } : undefined,
68
+ }));
69
+ }
@@ -0,0 +1,12 @@
1
+ export type ResponseProcessingTemplateKind = "matchCorrect" | "mapResponse" | "mapResponsePoint";
2
+
3
+ export function responseProcessingTemplateKind(
4
+ template: string,
5
+ ): ResponseProcessingTemplateKind | undefined {
6
+ const path = template.split(/[?#]/, 1)[0] ?? "";
7
+ const name = path.slice(path.lastIndexOf("/") + 1).replace(/\.xml$/i, "");
8
+ if (name === "match_correct") return "matchCorrect";
9
+ if (name === "map_response") return "mapResponse";
10
+ if (name === "map_response_point") return "mapResponsePoint";
11
+ return undefined;
12
+ }
@@ -0,0 +1,162 @@
1
+ import type { QtiRecordValue, QtiResponseDeclaration, QtiScalarValue, QtiValue } from "./types.js";
2
+ import { qtiValueToString } from "./value-format.js";
3
+
4
+ export function isNullResponse(response: QtiValue): boolean {
5
+ return response === null || response === "" || (Array.isArray(response) && response.length === 0);
6
+ }
7
+
8
+ export function valuesEqual(actual: QtiValue, expected: QtiValue, ordered = false): boolean {
9
+ if (isRecordValue(actual) || isRecordValue(expected)) {
10
+ if (!isRecordValue(actual) || !isRecordValue(expected)) return false;
11
+ const actualKeys = Object.keys(actual).toSorted();
12
+ const expectedKeys = Object.keys(expected).toSorted();
13
+ return (
14
+ valuesEqual(actualKeys, expectedKeys, true) &&
15
+ actualKeys.every((key) => valuesEqual(actual[key] ?? null, expected[key] ?? null, ordered))
16
+ );
17
+ }
18
+ if (Array.isArray(actual) || Array.isArray(expected)) {
19
+ const actualValues = valueContainer(actual);
20
+ const expectedValues = Array.isArray(expected) ? expected : expected === null ? [] : [expected];
21
+ if (actualValues.length !== expectedValues.length) return false;
22
+ if (ordered)
23
+ return actualValues.every((value, index) => scalarValuesEqual(value, expectedValues[index]!));
24
+ const sortedExpected = [...expectedValues].toSorted(compareScalarValues);
25
+ return [...actualValues]
26
+ .toSorted(compareScalarValues)
27
+ .every((value, index) => scalarValuesEqual(value, sortedExpected[index]!));
28
+ }
29
+ return scalarValuesEqual(actual, expected);
30
+ }
31
+
32
+ export function qtiMatchValues(
33
+ actual: QtiValue,
34
+ expected: QtiValue,
35
+ ordered = false,
36
+ ): boolean | null {
37
+ if (actual === null || expected === null) return null;
38
+ return valuesEqual(actual, expected, ordered);
39
+ }
40
+
41
+ function scalarValuesEqual(actual: QtiValue, expected: QtiValue): boolean {
42
+ if (typeof actual === "boolean" && typeof expected === "string") {
43
+ return String(actual) === expected;
44
+ }
45
+ if (typeof actual === "string" && typeof expected === "boolean") {
46
+ return actual === String(expected);
47
+ }
48
+ if (typeof actual === "number" && typeof expected === "string") {
49
+ return String(actual) === expected;
50
+ }
51
+ if (typeof actual === "string" && typeof expected === "number") {
52
+ return actual === String(expected);
53
+ }
54
+ return actual === expected;
55
+ }
56
+
57
+ export function normalizeValueForCardinality(
58
+ value: QtiValue,
59
+ cardinality: QtiResponseDeclaration["cardinality"],
60
+ ): QtiValue {
61
+ if (
62
+ (cardinality === "multiple" || cardinality === "ordered") &&
63
+ value !== null &&
64
+ !Array.isArray(value) &&
65
+ !isRecordValue(value)
66
+ ) {
67
+ return [value];
68
+ }
69
+ return value;
70
+ }
71
+
72
+ export function valueContainer(value: QtiValue): QtiScalarValue[] {
73
+ if (value === null) return [];
74
+ if (isRecordValue(value)) return [];
75
+ return Array.isArray(value) ? value : [value];
76
+ }
77
+
78
+ export function isRecordValue(value: QtiValue): value is QtiRecordValue {
79
+ return typeof value === "object" && value !== null && !Array.isArray(value);
80
+ }
81
+
82
+ export function containsValues(collection: QtiScalarValue[], values: QtiScalarValue[]): boolean {
83
+ const remaining = [...collection];
84
+ for (const value of values) {
85
+ const index = remaining.findIndex((candidate) => valuesEqual(candidate, value));
86
+ if (index === -1) return false;
87
+ remaining.splice(index, 1);
88
+ }
89
+ return true;
90
+ }
91
+
92
+ function compareScalarValues(left: QtiScalarValue, right: QtiScalarValue): number {
93
+ return String(left).localeCompare(String(right));
94
+ }
95
+
96
+ export function numericValue(value: QtiValue): number {
97
+ if (typeof value === "number") return value;
98
+ if (typeof value === "boolean") return value ? 1 : 0;
99
+ if (typeof value === "string") return Number(value);
100
+ return 0;
101
+ }
102
+
103
+ export function numericValueOrNull(value: QtiValue): number | null {
104
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
105
+ if (typeof value === "boolean") return value ? 1 : 0;
106
+ if (typeof value === "string") {
107
+ const parsed = Number(value);
108
+ return Number.isFinite(parsed) ? parsed : null;
109
+ }
110
+ return null;
111
+ }
112
+
113
+ export function durationSeconds(value: QtiValue): number | null {
114
+ if (value === null || Array.isArray(value) || isRecordValue(value)) return null;
115
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
116
+ const raw = String(value).trim();
117
+ if (raw.length === 0) return null;
118
+ const numeric = Number(raw);
119
+ if (Number.isFinite(numeric)) return numeric;
120
+ const match =
121
+ /^P(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/i.exec(
122
+ raw,
123
+ );
124
+ if (!match) return null;
125
+ const [, days, hours, minutes, seconds] = match;
126
+ if (!days && !hours && !minutes && !seconds) return null;
127
+ const total =
128
+ Number(days ?? 0) * 86_400 +
129
+ Number(hours ?? 0) * 3_600 +
130
+ Number(minutes ?? 0) * 60 +
131
+ Number(seconds ?? 0);
132
+ return Number.isFinite(total) ? total : null;
133
+ }
134
+
135
+ export function booleanValue(value: QtiValue): boolean {
136
+ if (typeof value === "boolean") return value;
137
+ if (typeof value === "number") return value !== 0;
138
+ if (typeof value === "string") return value.length > 0 && value !== "false";
139
+ if (Array.isArray(value)) return value.length > 0;
140
+ return false;
141
+ }
142
+
143
+ export function booleanValueOrNull(value: QtiValue): boolean | null {
144
+ if (value === null) return null;
145
+ return booleanValue(value);
146
+ }
147
+
148
+ export function stringMatch(
149
+ left: QtiValue,
150
+ right: QtiValue,
151
+ caseSensitive: boolean,
152
+ substring: boolean,
153
+ ): boolean | null {
154
+ if (left === null || right === null) return null;
155
+ let actual = qtiValueToString(left);
156
+ let expected = qtiValueToString(right);
157
+ if (!caseSensitive) {
158
+ actual = actual.toLocaleLowerCase();
159
+ expected = expected.toLocaleLowerCase();
160
+ }
161
+ return substring ? actual.includes(expected) : actual === expected;
162
+ }
@@ -0,0 +1,83 @@
1
+ import type {
2
+ QtiDocument,
3
+ QtiOutcomeDeclaration,
4
+ QtiProcessingExpression,
5
+ QtiResponseDeclaration,
6
+ QtiTemplateDeclaration,
7
+ QtiValue,
8
+ } from "./types.js";
9
+
10
+ export function getResponseDeclaration(
11
+ document: QtiDocument,
12
+ identifier: string,
13
+ ): QtiResponseDeclaration | undefined {
14
+ return document.item.responseDeclarations.find(
15
+ (declaration) => declaration.identifier === identifier,
16
+ );
17
+ }
18
+
19
+ export function resolveVariableValue(
20
+ document: QtiDocument,
21
+ identifier: string,
22
+ responses: Record<string, QtiValue>,
23
+ outcomes: Record<string, QtiValue>,
24
+ templateValues: Record<string, QtiValue>,
25
+ ): QtiValue {
26
+ return (
27
+ resolveOptionalVariableValue(document, identifier, responses, outcomes, templateValues) ?? null
28
+ );
29
+ }
30
+
31
+ export function resolveOptionalVariableValue(
32
+ document: QtiDocument,
33
+ identifier: string,
34
+ responses: Record<string, QtiValue>,
35
+ outcomes: Record<string, QtiValue>,
36
+ templateValues: Record<string, QtiValue>,
37
+ ): QtiValue | undefined {
38
+ const declaration = resolveVariableDeclaration(document, identifier);
39
+ if (!declaration) return undefined;
40
+ if (declaration.kind === "response") return responses[identifier] ?? null;
41
+ if (declaration.kind === "outcome") return outcomes[identifier] ?? null;
42
+ return templateValues[identifier] ?? null;
43
+ }
44
+
45
+ export function defaultValueForIdentifier(document: QtiDocument, identifier: string): QtiValue {
46
+ return resolveVariableDeclaration(document, identifier)?.defaultValue ?? null;
47
+ }
48
+
49
+ export function expressionIsOrdered(
50
+ expression: QtiProcessingExpression,
51
+ document: QtiDocument,
52
+ ): boolean {
53
+ if (expression.type === "ordered") return true;
54
+ if (
55
+ (expression.type === "variable" ||
56
+ expression.type === "correct" ||
57
+ expression.type === "default" ||
58
+ expression.type === "isNull") &&
59
+ variableCardinality(document, expression.identifier) === "ordered"
60
+ ) {
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+
66
+ function variableCardinality(document: QtiDocument, identifier: string): string | undefined {
67
+ return resolveVariableDeclaration(document, identifier)?.cardinality;
68
+ }
69
+
70
+ function resolveVariableDeclaration(
71
+ document: QtiDocument,
72
+ identifier: string,
73
+ ): QtiResponseDeclaration | QtiOutcomeDeclaration | QtiTemplateDeclaration | undefined {
74
+ return (
75
+ document.item.responseDeclarations.find(
76
+ (declaration) => declaration.identifier === identifier,
77
+ ) ??
78
+ document.item.outcomeDeclarations.find(
79
+ (declaration) => declaration.identifier === identifier,
80
+ ) ??
81
+ document.item.templateDeclarations.find((declaration) => declaration.identifier === identifier)
82
+ );
83
+ }
@@ -11,6 +11,7 @@ import {
11
11
  type UnaryExpressionType,
12
12
  } from "./processing-expression-tags.js";
13
13
  import { escapeXmlText } from "./xml.js";
14
+ import { assertNever } from "./assert-never.js";
14
15
  import {
15
16
  addSerializerDiagnostic,
16
17
  knownAttributesWithBagFallback,
@@ -274,7 +275,7 @@ export function serializeExpression(
274
275
  indent,
275
276
  );
276
277
  default:
277
- return unsupportedExpression(expression, context);
278
+ return assertNever(expression);
278
279
  }
279
280
  }
280
281
 
@@ -443,14 +444,3 @@ function isSerializableBaseValue(value: QtiValue): value is string | number | bo
443
444
  (typeof value === "number" && Number.isFinite(value))
444
445
  );
445
446
  }
446
-
447
- function unsupportedExpression(
448
- expression: QtiProcessingExpression,
449
- context: SerializationContext,
450
- ): string[] {
451
- addSerializerDiagnostic(context, "responseProcessing.serialize.unsupportedExpression", {
452
- message: `Unsupported response-processing expression type: ${expression.type}.`,
453
- source: expression.source,
454
- });
455
- return [];
456
- }
@@ -41,13 +41,13 @@ export function knownAttributesWithBagFallback(
41
41
  });
42
42
  const extras = Object.entries(bag ?? {})
43
43
  .filter(([name]) => !knownNames.has(name))
44
- .sort(([left], [right]) => left.localeCompare(right));
44
+ .toSorted(([left], [right]) => left.localeCompare(right));
45
45
  return [...resolved, ...extras];
46
46
  }
47
47
 
48
48
  export function sortedBagAttributes(bag: Record<string, string> | undefined): XmlAttribute[] {
49
49
  return Object.entries(bag ?? {})
50
- .sort(([left], [right]) => left.localeCompare(right))
50
+ .toSorted(([left], [right]) => left.localeCompare(right))
51
51
  .map(([name, value]) => [name, value] as const);
52
52
  }
53
53
 
@@ -13,6 +13,7 @@ import type {
13
13
  QtiResponseProcessing,
14
14
  QtiResponseRule,
15
15
  } from "./types.js";
16
+ import { assertNever } from "./assert-never.js";
16
17
 
17
18
  export interface QtiSerializeResponseProcessingResult {
18
19
  ok: boolean;
@@ -114,6 +115,8 @@ function serializeResponseRule(
114
115
  rule.rules.flatMap((child) => serializeResponseRule(child, context, indent + 1)),
115
116
  indent,
116
117
  );
118
+ default:
119
+ return assertNever(rule);
117
120
  }
118
121
  }
119
122