@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.
- package/README.md +59 -3
- package/dist/adaptive-turn-materializer.d.ts +14 -0
- package/dist/adaptive-turn-materializer.d.ts.map +1 -0
- package/dist/adaptive-turn-materializer.js +56 -0
- package/dist/adaptive-turn-materializer.js.map +1 -0
- package/dist/adaptive-turn.d.ts +23 -0
- package/dist/adaptive-turn.d.ts.map +1 -0
- package/dist/adaptive-turn.js +60 -0
- package/dist/adaptive-turn.js.map +1 -0
- package/dist/assert-never.d.ts +3 -0
- package/dist/assert-never.d.ts.map +1 -0
- package/dist/assert-never.js +5 -0
- package/dist/assert-never.js.map +1 -0
- package/dist/attempt-state-constants.d.ts +6 -0
- package/dist/attempt-state-constants.d.ts.map +1 -0
- package/dist/attempt-state-constants.js +6 -0
- package/dist/attempt-state-constants.js.map +1 -0
- package/dist/attempt-state.d.ts +5 -0
- package/dist/attempt-state.d.ts.map +1 -0
- package/dist/attempt-state.js +190 -0
- package/dist/attempt-state.js.map +1 -0
- package/dist/catalog.js +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/content-text.d.ts.map +1 -1
- package/dist/content-text.js +3 -0
- package/dist/content-text.js.map +1 -1
- package/dist/custom-operators.d.ts +13 -0
- package/dist/custom-operators.d.ts.map +1 -0
- package/dist/custom-operators.js +2 -0
- package/dist/custom-operators.js.map +1 -0
- package/dist/delivery-redaction.d.ts +50 -0
- package/dist/delivery-redaction.d.ts.map +1 -0
- package/dist/delivery-redaction.js +176 -0
- package/dist/delivery-redaction.js.map +1 -0
- package/dist/delivery-security.d.ts +3 -18
- package/dist/delivery-security.d.ts.map +1 -1
- package/dist/delivery-security.js +35 -189
- package/dist/delivery-security.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/parser-custom-interactions.js +1 -1
- package/dist/parser-custom-interactions.js.map +1 -1
- package/dist/parser-declarations.d.ts.map +1 -1
- package/dist/parser-declarations.js +4 -4
- package/dist/parser-declarations.js.map +1 -1
- package/dist/parser-item-metadata.d.ts.map +1 -1
- package/dist/parser-item-metadata.js +8 -7
- package/dist/parser-item-metadata.js.map +1 -1
- package/dist/parser-processing.js +2 -2
- package/dist/parser-processing.js.map +1 -1
- package/dist/parser-values.d.ts +5 -1
- package/dist/parser-values.d.ts.map +1 -1
- package/dist/parser-values.js +33 -0
- package/dist/parser-values.js.map +1 -1
- package/dist/processing-evaluator-boolean.d.ts +8 -0
- package/dist/processing-evaluator-boolean.d.ts.map +1 -0
- package/dist/processing-evaluator-boolean.js +47 -0
- package/dist/processing-evaluator-boolean.js.map +1 -0
- package/dist/processing-evaluator-collection.d.ts +12 -0
- package/dist/processing-evaluator-collection.d.ts.map +1 -0
- package/dist/processing-evaluator-collection.js +35 -0
- package/dist/processing-evaluator-collection.js.map +1 -0
- package/dist/processing-evaluator-comparison.d.ts +8 -0
- package/dist/processing-evaluator-comparison.d.ts.map +1 -0
- package/dist/processing-evaluator-comparison.js +88 -0
- package/dist/processing-evaluator-comparison.js.map +1 -0
- package/dist/processing-evaluator-custom.d.ts +8 -0
- package/dist/processing-evaluator-custom.d.ts.map +1 -0
- package/dist/processing-evaluator-custom.js +14 -0
- package/dist/processing-evaluator-custom.js.map +1 -0
- package/dist/processing-evaluator-geometry.d.ts +8 -0
- package/dist/processing-evaluator-geometry.d.ts.map +1 -0
- package/dist/processing-evaluator-geometry.js +21 -0
- package/dist/processing-evaluator-geometry.js.map +1 -0
- package/dist/processing-evaluator-numeric.d.ts +8 -0
- package/dist/processing-evaluator-numeric.d.ts.map +1 -0
- package/dist/processing-evaluator-numeric.js +112 -0
- package/dist/processing-evaluator-numeric.js.map +1 -0
- package/dist/processing-evaluator-string.d.ts +8 -0
- package/dist/processing-evaluator-string.d.ts.map +1 -0
- package/dist/processing-evaluator-string.js +33 -0
- package/dist/processing-evaluator-string.js.map +1 -0
- package/dist/processing-evaluator-variable.d.ts +8 -0
- package/dist/processing-evaluator-variable.d.ts.map +1 -0
- package/dist/processing-evaluator-variable.js +39 -0
- package/dist/processing-evaluator-variable.js.map +1 -0
- package/dist/processing-evaluator.d.ts +22 -0
- package/dist/processing-evaluator.d.ts.map +1 -0
- package/dist/processing-evaluator.js +137 -0
- package/dist/processing-evaluator.js.map +1 -0
- package/dist/processing-expression-children.d.ts +3 -0
- package/dist/processing-expression-children.d.ts.map +1 -0
- package/dist/processing-expression-children.js +68 -0
- package/dist/processing-expression-children.js.map +1 -0
- package/dist/processing-mapping.d.ts +13 -0
- package/dist/processing-mapping.d.ts.map +1 -0
- package/dist/processing-mapping.js +120 -0
- package/dist/processing-mapping.js.map +1 -0
- package/dist/processing-operators.d.ts +11 -0
- package/dist/processing-operators.d.ts.map +1 -0
- package/dist/processing-operators.js +178 -0
- package/dist/processing-operators.js.map +1 -0
- package/dist/processing-random.d.ts +2 -0
- package/dist/processing-random.d.ts.map +1 -0
- package/dist/processing-random.js +16 -0
- package/dist/processing-random.js.map +1 -0
- package/dist/processing-state.d.ts +8 -0
- package/dist/processing-state.d.ts.map +1 -0
- package/dist/processing-state.js +44 -0
- package/dist/processing-state.js.map +1 -0
- package/dist/processing-templates.d.ts +3 -0
- package/dist/processing-templates.d.ts.map +1 -0
- package/dist/processing-templates.js +12 -0
- package/dist/processing-templates.js.map +1 -0
- package/dist/processing-values.d.ts +15 -0
- package/dist/processing-values.d.ts.map +1 -0
- package/dist/processing-values.js +150 -0
- package/dist/processing-values.js.map +1 -0
- package/dist/processing-variables.d.ts +7 -0
- package/dist/processing-variables.d.ts.map +1 -0
- package/dist/processing-variables.js +40 -0
- package/dist/processing-variables.js.map +1 -0
- package/dist/serializer-processing-expressions.d.ts.map +1 -1
- package/dist/serializer-processing-expressions.js +2 -8
- package/dist/serializer-processing-expressions.js.map +1 -1
- package/dist/serializer-processing-xml.js +2 -2
- package/dist/serializer-processing-xml.js.map +1 -1
- package/dist/serializer-processing.d.ts.map +1 -1
- package/dist/serializer-processing.js +3 -0
- package/dist/serializer-processing.js.map +1 -1
- package/dist/server-scoring.d.ts +4 -14
- package/dist/server-scoring.d.ts.map +1 -1
- package/dist/server-scoring.js +19 -157
- package/dist/server-scoring.js.map +1 -1
- package/dist/session.d.ts +5 -14
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +76 -1158
- package/dist/session.js.map +1 -1
- package/dist/shared-vocabulary-coverage-policy.js +1 -1
- package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
- package/dist/shared-vocabulary-generated-families.js +1 -1
- package/dist/shared-vocabulary-generated-families.js.map +1 -1
- package/dist/shared-vocabulary-support.js +3 -3
- package/dist/shared-vocabulary-support.js.map +1 -1
- package/dist/shared-vocabulary.d.ts.map +1 -1
- package/dist/shared-vocabulary.js +4 -3
- package/dist/shared-vocabulary.js.map +1 -1
- package/dist/support-evidence.d.ts +22 -0
- package/dist/support-evidence.d.ts.map +1 -0
- package/dist/support-evidence.js +93 -0
- package/dist/support-evidence.js.map +1 -0
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +38 -47
- package/dist/support.js.map +1 -1
- package/dist/trusted-item-session.d.ts +49 -0
- package/dist/trusted-item-session.d.ts.map +1 -0
- package/dist/trusted-item-session.js +261 -0
- package/dist/trusted-item-session.js.map +1 -0
- package/dist/tts.d.ts.map +1 -1
- package/dist/tts.js +16 -2
- package/dist/tts.js.map +1 -1
- package/dist/validation-geometry.d.ts +8 -0
- package/dist/validation-geometry.d.ts.map +1 -0
- package/dist/validation-geometry.js +187 -0
- package/dist/validation-geometry.js.map +1 -0
- package/dist/validation-interactions.d.ts +3 -0
- package/dist/validation-interactions.d.ts.map +1 -0
- package/dist/validation-interactions.js +644 -0
- package/dist/validation-interactions.js.map +1 -0
- package/dist/validation-primitives.d.ts +11 -0
- package/dist/validation-primitives.d.ts.map +1 -0
- package/dist/validation-primitives.js +48 -0
- package/dist/validation-primitives.js.map +1 -0
- package/dist/validation-processing.d.ts +4 -0
- package/dist/validation-processing.d.ts.map +1 -0
- package/dist/validation-processing.js +569 -0
- package/dist/validation-processing.js.map +1 -0
- package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
- package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
- package/dist/validation-shared-vocabulary-layout.js +102 -0
- package/dist/validation-shared-vocabulary-layout.js.map +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +27 -1641
- package/dist/validation.js.map +1 -1
- package/dist/xml.js +1 -1
- package/dist/xml.js.map +1 -1
- package/package.json +4 -2
- package/src/adaptive-turn-materializer.ts +85 -0
- package/src/adaptive-turn.ts +109 -0
- package/src/assert-never.ts +4 -0
- package/src/attempt-state-constants.ts +5 -0
- package/src/attempt-state.ts +245 -0
- package/src/catalog.ts +1 -1
- package/src/content-text.ts +3 -0
- package/src/custom-operators.ts +12 -0
- package/src/delivery-redaction.ts +268 -0
- package/src/delivery-security.ts +59 -245
- package/src/index.ts +9 -0
- package/src/parser-custom-interactions.ts +1 -1
- package/src/parser-declarations.ts +4 -3
- package/src/parser-item-metadata.ts +8 -7
- package/src/parser-processing.ts +2 -2
- package/src/parser-values.ts +36 -0
- package/src/processing-evaluator-boolean.ts +48 -0
- package/src/processing-evaluator-collection.ts +50 -0
- package/src/processing-evaluator-comparison.ts +106 -0
- package/src/processing-evaluator-custom.ts +20 -0
- package/src/processing-evaluator-geometry.ts +26 -0
- package/src/processing-evaluator-numeric.ts +155 -0
- package/src/processing-evaluator-string.ts +60 -0
- package/src/processing-evaluator-variable.ts +81 -0
- package/src/processing-evaluator.ts +173 -0
- package/src/processing-expression-children.ts +69 -0
- package/src/processing-mapping.ts +153 -0
- package/src/processing-operators.ts +184 -0
- package/src/processing-random.ts +16 -0
- package/src/processing-state.ts +69 -0
- package/src/processing-templates.ts +12 -0
- package/src/processing-values.ts +162 -0
- package/src/processing-variables.ts +83 -0
- package/src/serializer-processing-expressions.ts +2 -12
- package/src/serializer-processing-xml.ts +2 -2
- package/src/serializer-processing.ts +3 -0
- package/src/server-scoring.ts +30 -226
- package/src/session.ts +128 -2006
- package/src/shared-vocabulary-coverage-policy.ts +1 -1
- package/src/shared-vocabulary-generated-families.ts +1 -1
- package/src/shared-vocabulary-support.ts +3 -3
- package/src/shared-vocabulary.ts +3 -2
- package/src/support-evidence.ts +107 -0
- package/src/support.ts +68 -54
- package/src/trusted-item-session.ts +429 -0
- package/src/tts.ts +29 -15
- package/src/validation-geometry.ts +204 -0
- package/src/validation-interactions.ts +831 -0
- package/src/validation-primitives.ts +63 -0
- package/src/validation-processing.ts +785 -0
- package/src/validation-shared-vocabulary-layout.ts +117 -0
- package/src/validation.ts +44 -2077
- package/src/xml.ts +1 -1
- package/src/interaction-test-fixtures.ts +0 -44
- package/src/serializer-processing.fixtures.ts +0 -469
package/dist/session.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
]);
|
|
1
|
+
import { qtiValueToString } from "./value-format.js";
|
|
2
|
+
import { createEvaluationContext } from "./processing-evaluator.js";
|
|
3
|
+
import { seededRandom } from "./processing-random.js";
|
|
4
|
+
import { getResponseDeclaration } from "./processing-variables.js";
|
|
5
|
+
import { lookupOutcomeValue, mapOrMatchResponse } from "./processing-mapping.js";
|
|
6
|
+
import { responseProcessingTemplateKind } from "./processing-templates.js";
|
|
7
|
+
import { assertCompatiblePriorState } from "./attempt-state.js";
|
|
8
|
+
export { assertQtiAttemptStateV1, isQtiAttemptStateV1 } from "./attempt-state.js";
|
|
9
|
+
import { COMPLETION_COMPLETED, COMPLETION_NOT_ATTEMPTED, COMPLETION_STATUS, COMPLETION_UNKNOWN, } from "./attempt-state-constants.js";
|
|
10
|
+
import { cloneDiagnostics, clonePortableCustomState, clonePortableCustomStateRecord, cloneValue, cloneValueRecord, serialize, } from "./processing-state.js";
|
|
11
|
+
import { booleanValue, normalizeValueForCardinality, qtiMatchValues } from "./processing-values.js";
|
|
13
12
|
export function visibleModalFeedback(item, outcomes) {
|
|
14
13
|
return item.modalFeedback.filter((feedback) => {
|
|
15
14
|
if (feedback.showHide === "hide")
|
|
@@ -37,6 +36,7 @@ export function createItemSession(document, priorState, options = {}) {
|
|
|
37
36
|
let status = priorState?.status ?? "initialized";
|
|
38
37
|
const random = seededRandom(options.randomSeed ?? document.item.identifier);
|
|
39
38
|
const customOperators = options.customOperators ?? {};
|
|
39
|
+
const allowedUndeclaredResponseIdentifiers = new Set(options.allowedUndeclaredResponseIdentifiers ?? []);
|
|
40
40
|
const portableCustomResponseIdentifiers = new Set(document.item.interactions
|
|
41
41
|
.filter((interaction) => interaction.type === "portableCustom")
|
|
42
42
|
.map((interaction) => interaction.responseIdentifier)
|
|
@@ -57,11 +57,16 @@ export function createItemSession(document, priorState, options = {}) {
|
|
|
57
57
|
const baseResponses = cloneValueRecord(responses);
|
|
58
58
|
const baseResponseDefaults = cloneValueRecord(responseDefaults);
|
|
59
59
|
const baseOutcomes = cloneValueRecord(outcomes);
|
|
60
|
-
|
|
60
|
+
const evaluation = createEvaluationContext(document, responses, outcomes, templateValues, correctResponses, random, customOperators, { allowedUndeclaredResponseIdentifiers });
|
|
61
|
+
const processingContext = {
|
|
62
|
+
responseDefaults,
|
|
63
|
+
evaluation,
|
|
64
|
+
};
|
|
65
|
+
applyTemplateProcessing(processingContext, new Set(), baseResponses, baseResponseDefaults, baseOutcomes);
|
|
61
66
|
if (priorState) {
|
|
62
67
|
Object.assign(templateValues, priorTemplateValues);
|
|
63
68
|
resetCorrectResponses(document, correctResponses);
|
|
64
|
-
applyTemplateProcessing(
|
|
69
|
+
applyTemplateProcessing(processingContext, new Set(Object.keys(priorTemplateValues)), baseResponses, baseResponseDefaults, baseOutcomes);
|
|
65
70
|
}
|
|
66
71
|
const defaultOutcomes = cloneValueRecord(outcomes);
|
|
67
72
|
Object.assign(responses, priorResponses);
|
|
@@ -102,7 +107,7 @@ export function createItemSession(document, priorState, options = {}) {
|
|
|
102
107
|
resetRecord(outcomes, cloneValueRecord(defaultOutcomes));
|
|
103
108
|
outcomes[COMPLETION_STATUS] = completionStatus;
|
|
104
109
|
}
|
|
105
|
-
applyResponseProcessing(
|
|
110
|
+
applyResponseProcessing(processingContext);
|
|
106
111
|
if (outcomes[COMPLETION_STATUS] === COMPLETION_COMPLETED)
|
|
107
112
|
status = "completed";
|
|
108
113
|
validationMessages = diagnostics;
|
|
@@ -125,186 +130,29 @@ export function createItemSession(document, priorState, options = {}) {
|
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
|
-
export function isQtiAttemptStateV1(value) {
|
|
129
|
-
return attemptStateErrors(value).length === 0;
|
|
130
|
-
}
|
|
131
|
-
export function assertQtiAttemptStateV1(value) {
|
|
132
|
-
const [firstError] = attemptStateErrors(value);
|
|
133
|
-
if (firstError)
|
|
134
|
-
throw new Error(firstError);
|
|
135
|
-
}
|
|
136
133
|
function resetRecord(target, source) {
|
|
137
134
|
for (const key of Object.keys(target)) {
|
|
138
135
|
delete target[key];
|
|
139
136
|
}
|
|
140
137
|
Object.assign(target, source);
|
|
141
138
|
}
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
assertQtiAttemptStateV1(priorState);
|
|
146
|
-
if (priorState.itemIdentifier !== document.item.identifier) {
|
|
147
|
-
throw new Error(`Cannot restore state for ${priorState.itemIdentifier} into ${document.item.identifier}.`);
|
|
148
|
-
}
|
|
149
|
-
const responseIdentifiers = new Set(document.item.responseDeclarations.map((declaration) => declaration.identifier));
|
|
150
|
-
const outcomeIdentifiers = new Set([
|
|
151
|
-
...document.item.outcomeDeclarations.map((declaration) => declaration.identifier),
|
|
152
|
-
COMPLETION_STATUS,
|
|
153
|
-
]);
|
|
154
|
-
const templateIdentifiers = new Set(document.item.templateDeclarations.map((declaration) => declaration.identifier));
|
|
155
|
-
const interactionStateIdentifiers = new Set(document.item.interactions
|
|
156
|
-
.filter((interaction) => interaction.type === "portableCustom")
|
|
157
|
-
.map((interaction) => interaction.responseIdentifier)
|
|
158
|
-
.filter((identifier) => Boolean(identifier)));
|
|
159
|
-
assertKnownStateIdentifiers("response", priorState.responses, responseIdentifiers);
|
|
160
|
-
assertKnownStateIdentifiers("outcome", priorState.outcomes, outcomeIdentifiers);
|
|
161
|
-
assertKnownStateIdentifiers("template", priorState.templateValues ?? {}, templateIdentifiers);
|
|
162
|
-
assertKnownPortableCustomStateIdentifiers(priorState.interactionStates ?? {}, interactionStateIdentifiers);
|
|
163
|
-
for (const message of priorState.validationMessages) {
|
|
164
|
-
if (message.path && !responseIdentifiers.has(message.path)) {
|
|
165
|
-
throw new Error(`Cannot restore validation message for unknown response ${message.path}.`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
for (const declaration of document.item.responseDeclarations) {
|
|
169
|
-
assertRestoredValueMatchesDeclaration("response", declaration, priorState.responses);
|
|
170
|
-
}
|
|
171
|
-
for (const declaration of document.item.outcomeDeclarations) {
|
|
172
|
-
assertRestoredValueMatchesDeclaration("outcome", declaration, priorState.outcomes);
|
|
173
|
-
}
|
|
174
|
-
for (const declaration of document.item.templateDeclarations) {
|
|
175
|
-
assertRestoredValueMatchesDeclaration("template", declaration, priorState.templateValues ?? {});
|
|
176
|
-
}
|
|
177
|
-
const completionStatus = priorState.outcomes[COMPLETION_STATUS];
|
|
178
|
-
if (completionStatus !== undefined &&
|
|
179
|
-
completionStatus !== COMPLETION_NOT_ATTEMPTED &&
|
|
180
|
-
completionStatus !== COMPLETION_UNKNOWN &&
|
|
181
|
-
completionStatus !== COMPLETION_COMPLETED &&
|
|
182
|
-
completionStatus !== "incomplete") {
|
|
183
|
-
throw new Error(`Cannot restore unsupported completionStatus ${qtiValueToString(completionStatus)}.`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function assertKnownStateIdentifiers(kind, record, allowed) {
|
|
187
|
-
const unknown = Object.keys(record).find((identifier) => !allowed.has(identifier));
|
|
188
|
-
if (unknown)
|
|
189
|
-
throw new Error(`Cannot restore unknown ${kind} identifier ${unknown}.`);
|
|
190
|
-
}
|
|
191
|
-
function assertKnownPortableCustomStateIdentifiers(record, allowed) {
|
|
192
|
-
const unknown = Object.keys(record).find((identifier) => !allowed.has(identifier));
|
|
193
|
-
if (unknown)
|
|
194
|
-
throw new Error(`Cannot restore unknown interaction state identifier ${unknown}.`);
|
|
195
|
-
}
|
|
196
|
-
function assertRestoredValueMatchesDeclaration(kind, declaration, record) {
|
|
197
|
-
if (!(declaration.identifier in record))
|
|
198
|
-
return;
|
|
199
|
-
const value = record[declaration.identifier] ?? null;
|
|
200
|
-
const error = restoredValueError(declaration, value);
|
|
201
|
-
if (error)
|
|
202
|
-
throw new Error(`Cannot restore ${kind} ${declaration.identifier}: ${error}.`);
|
|
203
|
-
}
|
|
204
|
-
function restoredValueError(declaration, value) {
|
|
205
|
-
if (value === null)
|
|
206
|
-
return undefined;
|
|
207
|
-
if (declaration.cardinality === "record") {
|
|
208
|
-
return isRecordValue(value) ? undefined : "expected record value";
|
|
209
|
-
}
|
|
210
|
-
if (isRecordValue(value))
|
|
211
|
-
return "expected scalar value";
|
|
212
|
-
if (declaration.cardinality === "single" && Array.isArray(value)) {
|
|
213
|
-
return "expected single value";
|
|
214
|
-
}
|
|
215
|
-
if ((declaration.cardinality === "multiple" || declaration.cardinality === "ordered") &&
|
|
216
|
-
!Array.isArray(value)) {
|
|
217
|
-
return `expected ${declaration.cardinality} value container`;
|
|
218
|
-
}
|
|
219
|
-
if (!declaration.baseType)
|
|
220
|
-
return undefined;
|
|
221
|
-
for (const entry of restoredValueEntries(value)) {
|
|
222
|
-
if (!restoredScalarMatchesBaseType(entry, declaration.baseType)) {
|
|
223
|
-
return `value ${String(entry)} is not valid for base-type ${declaration.baseType}`;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return undefined;
|
|
227
|
-
}
|
|
228
|
-
function restoredValueEntries(value) {
|
|
229
|
-
if (value === null || isRecordValue(value))
|
|
230
|
-
return [];
|
|
231
|
-
return Array.isArray(value) ? value : [value];
|
|
232
|
-
}
|
|
233
|
-
function restoredScalarMatchesBaseType(value, baseType) {
|
|
234
|
-
if (!baseType)
|
|
235
|
-
return true;
|
|
236
|
-
if (baseType === "integer") {
|
|
237
|
-
return typeof value === "number" ? Number.isInteger(value) : /^-?\d+$/.test(String(value));
|
|
238
|
-
}
|
|
239
|
-
if (baseType === "float") {
|
|
240
|
-
return typeof value === "number" ? Number.isFinite(value) : Number.isFinite(Number(value));
|
|
241
|
-
}
|
|
242
|
-
if (baseType === "boolean")
|
|
243
|
-
return typeof value === "boolean" || value === "true" || value === "false";
|
|
244
|
-
if (baseType === "point")
|
|
245
|
-
return pointValueIsValid(value);
|
|
246
|
-
if (baseType === "pair" || baseType === "directedPair")
|
|
247
|
-
return pairValueIsValid(value);
|
|
248
|
-
if (baseType === "identifier")
|
|
249
|
-
return typeof value === "string" && value.trim().length > 0 && !/\s/.test(value);
|
|
250
|
-
return typeof value === "string";
|
|
251
|
-
}
|
|
252
|
-
function pointValueIsValid(value) {
|
|
253
|
-
const parts = String(value).trim().split(/\s+/);
|
|
254
|
-
return parts.length === 2 && parts.every((part) => Number.isFinite(Number(part)));
|
|
255
|
-
}
|
|
256
|
-
function pairValueIsValid(value) {
|
|
257
|
-
const parts = String(value).trim().split(/\s+/);
|
|
258
|
-
return parts.length === 2 && parts.every((part) => part.length > 0);
|
|
259
|
-
}
|
|
260
|
-
function attemptStateErrors(value) {
|
|
261
|
-
if (!isRecord(value))
|
|
262
|
-
return ["QTI attempt state must be an object."];
|
|
263
|
-
const schema = value.schema;
|
|
264
|
-
if (schema !== ATTEMPT_STATE_SCHEMA) {
|
|
265
|
-
return [`Unsupported QTI attempt state schema ${String(schema)}.`];
|
|
266
|
-
}
|
|
267
|
-
const errors = [];
|
|
268
|
-
if (typeof value.itemIdentifier !== "string" || value.itemIdentifier.length === 0) {
|
|
269
|
-
errors.push("QTI attempt state itemIdentifier must be a non-empty string.");
|
|
270
|
-
}
|
|
271
|
-
if (typeof value.status !== "string" || !ATTEMPT_STATUSES.has(value.status)) {
|
|
272
|
-
errors.push(`QTI attempt state status ${String(value.status)} is not supported.`);
|
|
273
|
-
}
|
|
274
|
-
if (!isQtiValueRecord(value.responses)) {
|
|
275
|
-
errors.push("QTI attempt state responses must be a record of QTI values.");
|
|
276
|
-
}
|
|
277
|
-
if (!isQtiValueRecord(value.outcomes)) {
|
|
278
|
-
errors.push("QTI attempt state outcomes must be a record of QTI values.");
|
|
279
|
-
}
|
|
280
|
-
if (value.templateValues !== undefined && !isQtiValueRecord(value.templateValues)) {
|
|
281
|
-
errors.push("QTI attempt state templateValues must be a record of QTI values.");
|
|
282
|
-
}
|
|
283
|
-
if (value.interactionStates !== undefined &&
|
|
284
|
-
!isPortableCustomStateRecord(value.interactionStates)) {
|
|
285
|
-
errors.push("QTI attempt state interactionStates must be a record of JSON values.");
|
|
286
|
-
}
|
|
287
|
-
if (!isDiagnosticArray(value.validationMessages)) {
|
|
288
|
-
errors.push("QTI attempt state validationMessages must be an array of diagnostics.");
|
|
289
|
-
}
|
|
290
|
-
return errors;
|
|
291
|
-
}
|
|
292
|
-
function applyTemplateProcessing(document, templateValues, responses, responseDefaults, outcomes, correctResponses, random, customOperators, preservedTemplateIdentifiers = new Set(), baseResponses = cloneValueRecord(responses), baseResponseDefaults = cloneValueRecord(responseDefaults), baseOutcomes = cloneValueRecord(outcomes)) {
|
|
293
|
-
const rules = document.item.templateProcessing?.rules ?? [];
|
|
139
|
+
function applyTemplateProcessing(context, preservedTemplateIdentifiers = new Set(), baseResponses = cloneValueRecord(context.evaluation.responses), baseResponseDefaults = cloneValueRecord(context.responseDefaults), baseOutcomes = cloneValueRecord(context.evaluation.outcomes)) {
|
|
140
|
+
const { evaluation } = context;
|
|
141
|
+
const rules = evaluation.document.item.templateProcessing?.rules ?? [];
|
|
294
142
|
let restarts = 0;
|
|
295
143
|
for (let index = 0; index < rules.length; index += 1) {
|
|
296
144
|
const rule = rules[index];
|
|
297
|
-
const shouldExit = applyTemplateRule(
|
|
145
|
+
const shouldExit = applyTemplateRule(context, rule, preservedTemplateIdentifiers);
|
|
298
146
|
if (shouldExit)
|
|
299
147
|
return;
|
|
300
148
|
if (rule.type === "templateConstraint") {
|
|
301
|
-
const satisfied =
|
|
149
|
+
const satisfied = evaluateProcessingBoolean(context, rule.expression);
|
|
302
150
|
if (!satisfied) {
|
|
303
|
-
resetTemplateValues(document, templateValues);
|
|
304
|
-
resetRecord(responses, cloneValueRecord(baseResponses));
|
|
305
|
-
resetRecord(responseDefaults, cloneValueRecord(baseResponseDefaults));
|
|
306
|
-
resetRecord(outcomes, cloneValueRecord(baseOutcomes));
|
|
307
|
-
resetCorrectResponses(document, correctResponses);
|
|
151
|
+
resetTemplateValues(evaluation.document, evaluation.templateValues);
|
|
152
|
+
resetRecord(evaluation.responses, cloneValueRecord(baseResponses));
|
|
153
|
+
resetRecord(context.responseDefaults, cloneValueRecord(baseResponseDefaults));
|
|
154
|
+
resetRecord(evaluation.outcomes, cloneValueRecord(baseOutcomes));
|
|
155
|
+
resetCorrectResponses(evaluation.document, evaluation.correctResponses);
|
|
308
156
|
restarts += 1;
|
|
309
157
|
if (restarts <= 100)
|
|
310
158
|
index = -1;
|
|
@@ -325,1047 +173,117 @@ function resetCorrectResponses(document, correctResponses) {
|
|
|
325
173
|
correctResponses[declaration.identifier] = cloneValue(declaration.correctResponse);
|
|
326
174
|
}
|
|
327
175
|
}
|
|
328
|
-
function
|
|
176
|
+
function evaluateProcessingBoolean(context, expression) {
|
|
177
|
+
return expression ? booleanValue(context.evaluation.evaluate(expression)) : false;
|
|
178
|
+
}
|
|
179
|
+
function resolveConditionalRules(context, condition) {
|
|
180
|
+
if (evaluateProcessingBoolean(context, condition.ifExpression))
|
|
181
|
+
return condition.thenRules;
|
|
182
|
+
for (const elseIf of condition.elseIfs) {
|
|
183
|
+
if (evaluateProcessingBoolean(context, elseIf.expression))
|
|
184
|
+
return elseIf.rules;
|
|
185
|
+
}
|
|
186
|
+
return condition.elseRules;
|
|
187
|
+
}
|
|
188
|
+
function applyTemplateRule(context, rule, preservedTemplateIdentifiers) {
|
|
189
|
+
const { evaluation } = context;
|
|
329
190
|
if (rule.type === "exitTemplate")
|
|
330
191
|
return true;
|
|
331
192
|
if (rule.type === "templateConstraint")
|
|
332
193
|
return false;
|
|
333
194
|
if (rule.type === "templateCondition") {
|
|
334
|
-
|
|
335
|
-
? rule.thenRules
|
|
336
|
-
: undefined;
|
|
337
|
-
if (!branch) {
|
|
338
|
-
for (const elseIf of rule.elseIfs) {
|
|
339
|
-
if (evaluateBoolean(elseIf.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators)) {
|
|
340
|
-
branch = elseIf.rules;
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
branch ??= rule.elseRules;
|
|
195
|
+
const branch = resolveConditionalRules(context, rule);
|
|
346
196
|
for (const branchRule of branch) {
|
|
347
|
-
const shouldExit = applyTemplateRule(
|
|
197
|
+
const shouldExit = applyTemplateRule(context, branchRule, preservedTemplateIdentifiers);
|
|
348
198
|
if (shouldExit)
|
|
349
199
|
return true;
|
|
350
200
|
}
|
|
351
201
|
return false;
|
|
352
202
|
}
|
|
353
|
-
const value =
|
|
203
|
+
const value = evaluation.evaluate(rule.expression);
|
|
354
204
|
if (rule.type === "setTemplateValue") {
|
|
355
205
|
if (preservedTemplateIdentifiers.has(rule.identifier))
|
|
356
206
|
return false;
|
|
357
|
-
templateValues[rule.identifier] = value;
|
|
207
|
+
evaluation.templateValues[rule.identifier] = value;
|
|
358
208
|
return false;
|
|
359
209
|
}
|
|
360
210
|
if (rule.type === "setDefaultValue") {
|
|
361
|
-
const responseDeclaration = getResponseDeclaration(document, rule.identifier);
|
|
211
|
+
const responseDeclaration = getResponseDeclaration(evaluation.document, rule.identifier);
|
|
362
212
|
if (responseDeclaration) {
|
|
363
213
|
const normalized = normalizeValueForCardinality(value, responseDeclaration.cardinality);
|
|
364
214
|
if (normalized === null) {
|
|
365
|
-
delete responseDefaults[rule.identifier];
|
|
215
|
+
delete context.responseDefaults[rule.identifier];
|
|
366
216
|
}
|
|
367
217
|
else {
|
|
368
|
-
responseDefaults[rule.identifier] = normalized;
|
|
218
|
+
context.responseDefaults[rule.identifier] = normalized;
|
|
369
219
|
}
|
|
370
220
|
return false;
|
|
371
221
|
}
|
|
372
|
-
const outcomeDeclaration = document.item.outcomeDeclarations.find((declaration) => declaration.identifier === rule.identifier);
|
|
222
|
+
const outcomeDeclaration = evaluation.document.item.outcomeDeclarations.find((declaration) => declaration.identifier === rule.identifier);
|
|
373
223
|
if (outcomeDeclaration) {
|
|
374
|
-
outcomes[rule.identifier] = value;
|
|
224
|
+
evaluation.outcomes[rule.identifier] = value;
|
|
375
225
|
}
|
|
376
226
|
return false;
|
|
377
227
|
}
|
|
378
|
-
const declaration = getResponseDeclaration(document, rule.identifier);
|
|
228
|
+
const declaration = getResponseDeclaration(evaluation.document, rule.identifier);
|
|
379
229
|
if (declaration)
|
|
380
|
-
correctResponses[rule.identifier] = normalizeValueForCardinality(value, declaration.cardinality);
|
|
230
|
+
evaluation.correctResponses[rule.identifier] = normalizeValueForCardinality(value, declaration.cardinality);
|
|
381
231
|
return false;
|
|
382
232
|
}
|
|
383
|
-
function applyResponseProcessing(
|
|
384
|
-
const
|
|
233
|
+
function applyResponseProcessing(context) {
|
|
234
|
+
const { evaluation } = context;
|
|
235
|
+
const processing = evaluation.document.item.responseProcessing;
|
|
385
236
|
if (processing?.rules.length) {
|
|
386
|
-
applyResponseRules(processing.rules
|
|
237
|
+
applyResponseRules(context, processing.rules);
|
|
387
238
|
return;
|
|
388
239
|
}
|
|
389
|
-
const templateKind =
|
|
240
|
+
const templateKind = processing?.template
|
|
241
|
+
? (responseProcessingTemplateKind(processing.template) ?? "unsupported")
|
|
242
|
+
: undefined;
|
|
390
243
|
if (templateKind === "unsupported") {
|
|
391
244
|
return;
|
|
392
245
|
}
|
|
393
246
|
if (templateKind === "mapResponse" || templateKind === "mapResponsePoint") {
|
|
394
|
-
const declaration = getResponseDeclaration(document, "RESPONSE");
|
|
395
|
-
outcomes.SCORE = declaration
|
|
396
|
-
? mapOrMatchResponse(declaration, responses.RESPONSE ?? null, correctResponses.RESPONSE ?? null)
|
|
247
|
+
const declaration = getResponseDeclaration(evaluation.document, "RESPONSE");
|
|
248
|
+
evaluation.outcomes.SCORE = declaration
|
|
249
|
+
? mapOrMatchResponse(declaration, evaluation.responses.RESPONSE ?? null, evaluation.correctResponses.RESPONSE ?? null)
|
|
397
250
|
: 0;
|
|
398
251
|
return;
|
|
399
252
|
}
|
|
400
253
|
if (templateKind === "matchCorrect") {
|
|
401
|
-
const declaration = getResponseDeclaration(document, "RESPONSE");
|
|
254
|
+
const declaration = getResponseDeclaration(evaluation.document, "RESPONSE");
|
|
402
255
|
const matches = declaration
|
|
403
|
-
? qtiMatchValues(responses.RESPONSE ?? null, correctResponses.RESPONSE ?? null, declaration.cardinality === "ordered")
|
|
256
|
+
? qtiMatchValues(evaluation.responses.RESPONSE ?? null, evaluation.correctResponses.RESPONSE ?? null, declaration.cardinality === "ordered")
|
|
404
257
|
: null;
|
|
405
|
-
outcomes.SCORE = matches === true ? 1 : 0;
|
|
258
|
+
evaluation.outcomes.SCORE = matches === true ? 1 : 0;
|
|
406
259
|
}
|
|
407
260
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
return undefined;
|
|
411
|
-
const path = template.split(/[?#]/, 1)[0] ?? "";
|
|
412
|
-
const name = path.slice(path.lastIndexOf("/") + 1).replace(/\.xml$/i, "");
|
|
413
|
-
if (name === "match_correct")
|
|
414
|
-
return "matchCorrect";
|
|
415
|
-
if (name === "map_response")
|
|
416
|
-
return "mapResponse";
|
|
417
|
-
if (name === "map_response_point")
|
|
418
|
-
return "mapResponsePoint";
|
|
419
|
-
return "unsupported";
|
|
420
|
-
}
|
|
421
|
-
function applyResponseRules(rules, document, responses, outcomes, templateValues, correctResponses, random, customOperators) {
|
|
261
|
+
function applyResponseRules(context, rules) {
|
|
262
|
+
const { evaluation } = context;
|
|
422
263
|
for (const rule of rules) {
|
|
423
264
|
if (rule.type === "exitResponse")
|
|
424
265
|
return true;
|
|
425
266
|
if (rule.type === "responseCondition") {
|
|
426
|
-
const shouldExit = applyResponseCondition(rule.condition
|
|
267
|
+
const shouldExit = applyResponseCondition(context, rule.condition);
|
|
427
268
|
if (shouldExit)
|
|
428
269
|
return true;
|
|
429
270
|
continue;
|
|
430
271
|
}
|
|
431
272
|
if (rule.type === "responseProcessingFragment") {
|
|
432
|
-
const shouldExit = applyResponseRules(rule.rules
|
|
273
|
+
const shouldExit = applyResponseRules(context, rule.rules);
|
|
433
274
|
if (shouldExit)
|
|
434
275
|
return true;
|
|
435
276
|
continue;
|
|
436
277
|
}
|
|
437
278
|
if (rule.type === "lookupOutcomeValue") {
|
|
438
|
-
outcomes[rule.identifier] = lookupOutcomeValue(document, rule.identifier,
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
outcomes[rule.identifier] = evaluateValue(rule.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
442
|
-
}
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
function applyResponseCondition(condition, document, responses, outcomes, templateValues, correctResponses, random, customOperators) {
|
|
446
|
-
let branch = evaluateBoolean(condition.ifExpression, document, responses, outcomes, templateValues, correctResponses, random, customOperators)
|
|
447
|
-
? condition.thenRules
|
|
448
|
-
: undefined;
|
|
449
|
-
if (!branch) {
|
|
450
|
-
for (const elseIf of condition.elseIfs) {
|
|
451
|
-
if (evaluateBoolean(elseIf.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators)) {
|
|
452
|
-
branch = elseIf.rules;
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
branch ??= condition.elseRules;
|
|
458
|
-
return applyResponseRules(branch, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
459
|
-
}
|
|
460
|
-
function evaluateBoolean(expression, document, responses, outcomes = {}, templateValues = {}, correctResponses = {}, random = Math.random, customOperators = {}) {
|
|
461
|
-
if (!expression)
|
|
462
|
-
return false;
|
|
463
|
-
return booleanValue(evaluateValue(expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
464
|
-
}
|
|
465
|
-
function evaluateValue(expression, document, responses, outcomes = {}, templateValues = {}, correctResponses = {}, random = Math.random, customOperators = {}) {
|
|
466
|
-
if (expression.type === "baseValue")
|
|
467
|
-
return expression.value;
|
|
468
|
-
if (expression.type === "null")
|
|
469
|
-
return null;
|
|
470
|
-
if (expression.type === "isNull")
|
|
471
|
-
return isNullResponse(responses[expression.identifier] ?? null);
|
|
472
|
-
if (expression.type === "matchCorrect") {
|
|
473
|
-
const declaration = getResponseDeclaration(document, expression.correctIdentifier);
|
|
474
|
-
return declaration
|
|
475
|
-
? qtiMatchValues(responses[expression.identifier] ?? null, correctResponses[expression.correctIdentifier] ?? null, declaration.cardinality === "ordered")
|
|
476
|
-
: false;
|
|
477
|
-
}
|
|
478
|
-
if (expression.type === "match") {
|
|
479
|
-
const left = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
480
|
-
const right = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
481
|
-
return qtiMatchValues(left, right, expressionIsOrdered(expression.left, document) ||
|
|
482
|
-
expressionIsOrdered(expression.right, document));
|
|
483
|
-
}
|
|
484
|
-
if (expression.type === "mapResponse") {
|
|
485
|
-
const declaration = getResponseDeclaration(document, expression.identifier);
|
|
486
|
-
return declaration
|
|
487
|
-
? mapOrMatchResponse(declaration, responses[expression.identifier] ?? null, correctResponses[expression.identifier] ?? null)
|
|
488
|
-
: 0;
|
|
489
|
-
}
|
|
490
|
-
if (expression.type === "mapResponsePoint") {
|
|
491
|
-
const declaration = getResponseDeclaration(document, expression.identifier);
|
|
492
|
-
return declaration?.areaMapping
|
|
493
|
-
? scoreAreaMapping(responses[expression.identifier] ?? null, declaration.areaMapping)
|
|
494
|
-
: 0;
|
|
495
|
-
}
|
|
496
|
-
if (expression.type === "correct") {
|
|
497
|
-
return correctResponses[expression.identifier] ?? null;
|
|
498
|
-
}
|
|
499
|
-
if (expression.type === "default") {
|
|
500
|
-
return defaultValueForIdentifier(document, expression.identifier);
|
|
501
|
-
}
|
|
502
|
-
if (expression.type === "variable") {
|
|
503
|
-
return (responses[expression.identifier] ??
|
|
504
|
-
outcomes[expression.identifier] ??
|
|
505
|
-
templateValues[expression.identifier] ??
|
|
506
|
-
null);
|
|
507
|
-
}
|
|
508
|
-
if (expression.type === "randomInteger") {
|
|
509
|
-
const step = expression.step > 0 ? expression.step : 1;
|
|
510
|
-
const count = Math.floor((expression.max - expression.min) / step) + 1;
|
|
511
|
-
return expression.min + Math.floor(random() * count) * step;
|
|
512
|
-
}
|
|
513
|
-
if (expression.type === "randomFloat") {
|
|
514
|
-
return expression.min + random() * (expression.max - expression.min);
|
|
515
|
-
}
|
|
516
|
-
if (expression.type === "random") {
|
|
517
|
-
if (expression.values.length === 0)
|
|
518
|
-
return null;
|
|
519
|
-
const index = Math.floor(random() * expression.values.length);
|
|
520
|
-
return evaluateValue(expression.values[index], document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
521
|
-
}
|
|
522
|
-
if (expression.type === "multiple" || expression.type === "ordered") {
|
|
523
|
-
const values = expression.expressions.flatMap((item) => valueContainer(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators)));
|
|
524
|
-
return values.length > 0 ? values : null;
|
|
525
|
-
}
|
|
526
|
-
if (expression.type === "index") {
|
|
527
|
-
const values = valueContainer(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
528
|
-
const n = indexValue(expression.n, outcomes, templateValues);
|
|
529
|
-
if (n === undefined || n < 1 || n > values.length)
|
|
530
|
-
return null;
|
|
531
|
-
return values[n - 1] ?? null;
|
|
532
|
-
}
|
|
533
|
-
if (expression.type === "containerSize") {
|
|
534
|
-
return valueContainer(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators)).length;
|
|
535
|
-
}
|
|
536
|
-
if (expression.type === "sum") {
|
|
537
|
-
const values = evaluateNumericOperands(expression.expressions, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
538
|
-
return values ? values.reduce((sum, value) => sum + value, 0) : null;
|
|
539
|
-
}
|
|
540
|
-
if (expression.type === "product") {
|
|
541
|
-
const values = evaluateNumericOperands(expression.expressions, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
542
|
-
return values ? values.reduce((product, value) => product * value, 1) : null;
|
|
543
|
-
}
|
|
544
|
-
if (expression.type === "min" || expression.type === "max") {
|
|
545
|
-
const values = evaluateNumericOperands(expression.expressions, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
546
|
-
if (!values || values.length === 0)
|
|
547
|
-
return null;
|
|
548
|
-
return expression.type === "min" ? Math.min(...values) : Math.max(...values);
|
|
549
|
-
}
|
|
550
|
-
if (expression.type === "subtract") {
|
|
551
|
-
const values = evaluateNumericOperands([expression.left, expression.right], document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
552
|
-
return values && values.length === 2 ? values[0] - values[1] : null;
|
|
553
|
-
}
|
|
554
|
-
if (expression.type === "divide") {
|
|
555
|
-
const dividendValue = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
556
|
-
const divisorValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
557
|
-
if (dividendValue === null || divisorValue === null)
|
|
558
|
-
return null;
|
|
559
|
-
const dividend = numericValueOrNull(dividendValue);
|
|
560
|
-
const divisor = numericValueOrNull(divisorValue);
|
|
561
|
-
if (dividend === null || divisor === null || divisor === 0)
|
|
562
|
-
return null;
|
|
563
|
-
const quotient = dividend / divisor;
|
|
564
|
-
return Number.isFinite(quotient) ? quotient : null;
|
|
565
|
-
}
|
|
566
|
-
if (expression.type === "power") {
|
|
567
|
-
const values = evaluateNumericOperands([expression.left, expression.right], document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
568
|
-
if (!values || values.length !== 2)
|
|
569
|
-
return null;
|
|
570
|
-
const value = Math.pow(values[0], values[1]);
|
|
571
|
-
return Number.isFinite(value) ? value : null;
|
|
572
|
-
}
|
|
573
|
-
if (expression.type === "integerDivide") {
|
|
574
|
-
const dividendValue = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
575
|
-
const divisorValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
576
|
-
if (dividendValue === null || divisorValue === null)
|
|
577
|
-
return null;
|
|
578
|
-
const dividend = numericValueOrNull(dividendValue);
|
|
579
|
-
const divisor = numericValueOrNull(divisorValue);
|
|
580
|
-
if (dividend === null || divisor === null || divisor === 0)
|
|
581
|
-
return null;
|
|
582
|
-
return Math.floor(dividend / divisor);
|
|
583
|
-
}
|
|
584
|
-
if (expression.type === "integerModulus") {
|
|
585
|
-
const dividendValue = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
586
|
-
const divisorValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
587
|
-
if (dividendValue === null || divisorValue === null)
|
|
588
|
-
return null;
|
|
589
|
-
const dividend = numericValueOrNull(dividendValue);
|
|
590
|
-
const divisor = numericValueOrNull(divisorValue);
|
|
591
|
-
if (dividend === null || divisor === null || divisor === 0)
|
|
592
|
-
return null;
|
|
593
|
-
return dividend - Math.floor(dividend / divisor) * divisor;
|
|
594
|
-
}
|
|
595
|
-
if (expression.type === "round") {
|
|
596
|
-
const value = numericValueOrNull(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
597
|
-
return value === null ? null : Math.round(value);
|
|
598
|
-
}
|
|
599
|
-
if (expression.type === "roundTo") {
|
|
600
|
-
const value = numericValueOrNull(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
601
|
-
if (value === null)
|
|
602
|
-
return null;
|
|
603
|
-
return expression.roundingMode === "decimalPlaces"
|
|
604
|
-
? roundToDecimalPlaces(value, expression.figures)
|
|
605
|
-
: roundToSignificantFigures(value, expression.figures);
|
|
606
|
-
}
|
|
607
|
-
if (expression.type === "truncate") {
|
|
608
|
-
const value = numericValueOrNull(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
609
|
-
return value === null ? null : Math.trunc(value);
|
|
610
|
-
}
|
|
611
|
-
if (expression.type === "integerToFloat") {
|
|
612
|
-
const value = evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
613
|
-
return numericValueOrNull(value);
|
|
614
|
-
}
|
|
615
|
-
if (expression.type === "and") {
|
|
616
|
-
let sawNull = false;
|
|
617
|
-
for (const item of expression.expressions) {
|
|
618
|
-
const value = booleanValueOrNull(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
619
|
-
if (value === false)
|
|
620
|
-
return false;
|
|
621
|
-
if (value === null)
|
|
622
|
-
sawNull = true;
|
|
623
|
-
}
|
|
624
|
-
return sawNull ? null : true;
|
|
625
|
-
}
|
|
626
|
-
if (expression.type === "anyN") {
|
|
627
|
-
const min = indexValue(expression.min, outcomes, templateValues) ?? 0;
|
|
628
|
-
const max = indexValue(expression.max, outcomes, templateValues) ?? 0;
|
|
629
|
-
const values = expression.expressions.map((item) => evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
630
|
-
const trueCount = values.filter((value) => value === true).length;
|
|
631
|
-
const nullCount = values.filter((value) => value === null).length;
|
|
632
|
-
if (min > max || trueCount > max || trueCount + nullCount < min)
|
|
633
|
-
return false;
|
|
634
|
-
if (trueCount >= min && trueCount <= max)
|
|
635
|
-
return true;
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
if (expression.type === "or") {
|
|
639
|
-
let sawNull = false;
|
|
640
|
-
for (const item of expression.expressions) {
|
|
641
|
-
const value = booleanValueOrNull(evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
642
|
-
if (value === true)
|
|
643
|
-
return true;
|
|
644
|
-
if (value === null)
|
|
645
|
-
sawNull = true;
|
|
646
|
-
}
|
|
647
|
-
return sawNull ? null : false;
|
|
648
|
-
}
|
|
649
|
-
if (expression.type === "not") {
|
|
650
|
-
const value = booleanValueOrNull(evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
651
|
-
return value === null ? null : !value;
|
|
652
|
-
}
|
|
653
|
-
if (expression.type === "equal") {
|
|
654
|
-
const left = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
655
|
-
const right = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
656
|
-
return left === null || right === null ? null : valuesEqual(left, right);
|
|
657
|
-
}
|
|
658
|
-
if (expression.type === "equalRounded") {
|
|
659
|
-
const left = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
660
|
-
const right = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
661
|
-
if (left === null || right === null)
|
|
662
|
-
return null;
|
|
663
|
-
const leftNumber = numericValueOrNull(left);
|
|
664
|
-
const rightNumber = numericValueOrNull(right);
|
|
665
|
-
if (leftNumber === null || rightNumber === null)
|
|
666
|
-
return null;
|
|
667
|
-
const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, expression.figures);
|
|
668
|
-
const roundedRight = roundWithMode(rightNumber, expression.roundingMode, expression.figures);
|
|
669
|
-
return roundedLeft === null || roundedRight === null ? null : roundedLeft === roundedRight;
|
|
670
|
-
}
|
|
671
|
-
if (expression.type === "numericCompare") {
|
|
672
|
-
const leftValue = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
673
|
-
const rightValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
674
|
-
if (leftValue === null || rightValue === null)
|
|
675
|
-
return null;
|
|
676
|
-
const left = numericValueOrNull(leftValue);
|
|
677
|
-
const right = numericValueOrNull(rightValue);
|
|
678
|
-
if (left === null || right === null)
|
|
679
|
-
return null;
|
|
680
|
-
if (expression.operator === "lt")
|
|
681
|
-
return left < right;
|
|
682
|
-
if (expression.operator === "lte")
|
|
683
|
-
return left <= right;
|
|
684
|
-
if (expression.operator === "gt")
|
|
685
|
-
return left > right;
|
|
686
|
-
return left >= right;
|
|
687
|
-
}
|
|
688
|
-
if (expression.type === "durationCompare") {
|
|
689
|
-
const leftValue = evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
690
|
-
const rightValue = evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
691
|
-
const left = durationSeconds(leftValue);
|
|
692
|
-
const right = durationSeconds(rightValue);
|
|
693
|
-
if (left === null || right === null)
|
|
694
|
-
return null;
|
|
695
|
-
return expression.operator === "lt" ? left < right : left >= right;
|
|
696
|
-
}
|
|
697
|
-
if (expression.type === "stringMatch") {
|
|
698
|
-
return stringMatch(evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators), evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators), expression.caseSensitive, expression.substring);
|
|
699
|
-
}
|
|
700
|
-
if (expression.type === "substring") {
|
|
701
|
-
return stringMatch(evaluateValue(expression.right, document, responses, outcomes, templateValues, correctResponses, random, customOperators), evaluateValue(expression.left, document, responses, outcomes, templateValues, correctResponses, random, customOperators), expression.caseSensitive, true);
|
|
702
|
-
}
|
|
703
|
-
if (expression.type === "patternMatch") {
|
|
704
|
-
const value = evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
705
|
-
if (value === null)
|
|
706
|
-
return null;
|
|
707
|
-
const patternValue = responses[expression.pattern] ??
|
|
708
|
-
outcomes[expression.pattern] ??
|
|
709
|
-
templateValues[expression.pattern] ??
|
|
710
|
-
expression.pattern;
|
|
711
|
-
try {
|
|
712
|
-
return new RegExp(typeof patternValue === "string" ? patternValue : qtiValueToString(patternValue)).test(qtiValueToString(value));
|
|
713
|
-
}
|
|
714
|
-
catch {
|
|
715
|
-
return null;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
if (expression.type === "fieldValue") {
|
|
719
|
-
const value = evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
720
|
-
return isRecordValue(value) ? (value[expression.fieldIdentifier] ?? null) : null;
|
|
721
|
-
}
|
|
722
|
-
if (expression.type === "member") {
|
|
723
|
-
const value = evaluateValue(expression.value, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
724
|
-
const collection = evaluateValue(expression.collection, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
725
|
-
if (value === null || collection === null)
|
|
726
|
-
return null;
|
|
727
|
-
const values = valueContainer(collection);
|
|
728
|
-
return values.some((item) => valuesEqual(item, value));
|
|
729
|
-
}
|
|
730
|
-
if (expression.type === "delete") {
|
|
731
|
-
const value = evaluateValue(expression.value, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
732
|
-
const collectionValue = evaluateValue(expression.collection, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
733
|
-
if (value === null || collectionValue === null)
|
|
734
|
-
return null;
|
|
735
|
-
const collection = valueContainer(collectionValue);
|
|
736
|
-
if (collection.length === 0)
|
|
737
|
-
return null;
|
|
738
|
-
const filtered = collection.filter((item) => !valuesEqual(item, value));
|
|
739
|
-
return filtered.length > 0 ? filtered : null;
|
|
740
|
-
}
|
|
741
|
-
if (expression.type === "contains") {
|
|
742
|
-
const collectionValue = evaluateValue(expression.collection, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
743
|
-
const valuesValue = evaluateValue(expression.values, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
744
|
-
if (collectionValue === null || valuesValue === null)
|
|
745
|
-
return null;
|
|
746
|
-
const collection = valueContainer(collectionValue);
|
|
747
|
-
const values = valueContainer(valuesValue);
|
|
748
|
-
if (collection.length === 0 || values.length === 0)
|
|
749
|
-
return null;
|
|
750
|
-
return containsValues(collection, values);
|
|
751
|
-
}
|
|
752
|
-
if (expression.type === "gcd" || expression.type === "lcm") {
|
|
753
|
-
const values = expression.expressions.flatMap((item) => {
|
|
754
|
-
const value = evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
755
|
-
return value === null ? [null] : valueContainer(value);
|
|
756
|
-
});
|
|
757
|
-
if (values.length === 0 || values.some((value) => value === null))
|
|
758
|
-
return null;
|
|
759
|
-
const integers = values.map((value) => Math.trunc(numericValue(value)));
|
|
760
|
-
return expression.type === "gcd" ? generalizedGcd(integers) : generalizedLcm(integers);
|
|
761
|
-
}
|
|
762
|
-
if (expression.type === "inside") {
|
|
763
|
-
const value = evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
764
|
-
if (value === null)
|
|
765
|
-
return null;
|
|
766
|
-
if (expression.shape === "default")
|
|
767
|
-
return true;
|
|
768
|
-
return valueContainer(value).some((pointValue) => {
|
|
769
|
-
const point = parsePoint(String(pointValue));
|
|
770
|
-
return point
|
|
771
|
-
? pointInsideArea(point, {
|
|
772
|
-
shape: expression.shape,
|
|
773
|
-
coords: expression.coords,
|
|
774
|
-
mappedValue: 0,
|
|
775
|
-
attributes: {},
|
|
776
|
-
})
|
|
777
|
-
: false;
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
if (expression.type === "mathConstant") {
|
|
781
|
-
if (expression.name === "pi")
|
|
782
|
-
return Math.PI;
|
|
783
|
-
if (expression.name === "e")
|
|
784
|
-
return Math.E;
|
|
785
|
-
return null;
|
|
786
|
-
}
|
|
787
|
-
if (expression.type === "mathOperator") {
|
|
788
|
-
const values = expression.expressions.map((item) => evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators));
|
|
789
|
-
if (values.length === 0 || values.some((value) => value === null))
|
|
790
|
-
return null;
|
|
791
|
-
return mathOperatorValue(expression.name, values.map(numericValue));
|
|
792
|
-
}
|
|
793
|
-
if (expression.type === "repeat") {
|
|
794
|
-
const repeats = indexValue(expression.numberRepeats, outcomes, templateValues);
|
|
795
|
-
if (repeats === undefined || repeats < 1)
|
|
796
|
-
return null;
|
|
797
|
-
const container = [];
|
|
798
|
-
for (let repeat = 0; repeat < repeats; repeat += 1) {
|
|
799
|
-
for (const item of expression.expressions) {
|
|
800
|
-
const value = evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
801
|
-
container.push(...valueContainer(value));
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
return container.length > 0 ? container : null;
|
|
805
|
-
}
|
|
806
|
-
if (expression.type === "statsOperator") {
|
|
807
|
-
const value = evaluateValue(expression.expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
808
|
-
if (value === null)
|
|
809
|
-
return null;
|
|
810
|
-
const values = valueContainer(value).map(numericValue);
|
|
811
|
-
return statsOperatorValue(expression.name, values);
|
|
812
|
-
}
|
|
813
|
-
if (expression.type === "customOperator") {
|
|
814
|
-
const operatorKey = expression.definition ?? expression.className ?? "";
|
|
815
|
-
const handler = customOperators[operatorKey];
|
|
816
|
-
if (!handler)
|
|
817
|
-
return null;
|
|
818
|
-
return handler({
|
|
819
|
-
definition: expression.definition,
|
|
820
|
-
className: expression.className,
|
|
821
|
-
attributes: expression.attributes,
|
|
822
|
-
values: expression.expressions.map((item) => evaluateValue(item, document, responses, outcomes, templateValues, correctResponses, random, customOperators)),
|
|
823
|
-
expression,
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
function getResponseDeclaration(document, identifier) {
|
|
829
|
-
return document.item.responseDeclarations.find((declaration) => declaration.identifier === identifier);
|
|
830
|
-
}
|
|
831
|
-
function expressionIsOrdered(expression, document) {
|
|
832
|
-
if (expression.type === "ordered")
|
|
833
|
-
return true;
|
|
834
|
-
if ((expression.type === "variable" ||
|
|
835
|
-
expression.type === "correct" ||
|
|
836
|
-
expression.type === "default" ||
|
|
837
|
-
expression.type === "isNull") &&
|
|
838
|
-
variableCardinality(document, expression.identifier) === "ordered") {
|
|
839
|
-
return true;
|
|
840
|
-
}
|
|
841
|
-
return false;
|
|
842
|
-
}
|
|
843
|
-
function variableCardinality(document, identifier) {
|
|
844
|
-
return (document.item.responseDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
845
|
-
?.cardinality ??
|
|
846
|
-
document.item.outcomeDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
847
|
-
?.cardinality ??
|
|
848
|
-
document.item.templateDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
849
|
-
?.cardinality);
|
|
850
|
-
}
|
|
851
|
-
function defaultValueForIdentifier(document, identifier) {
|
|
852
|
-
return (document.item.responseDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
853
|
-
?.defaultValue ??
|
|
854
|
-
document.item.outcomeDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
855
|
-
?.defaultValue ??
|
|
856
|
-
document.item.templateDeclarations.find((declaration) => declaration.identifier === identifier)
|
|
857
|
-
?.defaultValue ??
|
|
858
|
-
null);
|
|
859
|
-
}
|
|
860
|
-
function lookupOutcomeValue(document, identifier, value) {
|
|
861
|
-
const declaration = document.item.outcomeDeclarations.find((outcome) => outcome.identifier === identifier);
|
|
862
|
-
const lookupTable = declaration?.lookupTable;
|
|
863
|
-
if (!lookupTable)
|
|
864
|
-
return null;
|
|
865
|
-
if (value === null)
|
|
866
|
-
return lookupTable.defaultValue;
|
|
867
|
-
const numeric = numericValue(value);
|
|
868
|
-
if (lookupTable.type === "match") {
|
|
869
|
-
return (lookupTable.entries.find((entry) => entry.sourceValue === numeric)?.targetValue ??
|
|
870
|
-
lookupTable.defaultValue);
|
|
871
|
-
}
|
|
872
|
-
const entry = [...lookupTable.entries]
|
|
873
|
-
.sort((left, right) => left.sourceValue - right.sourceValue)
|
|
874
|
-
.find((candidate) => numeric < candidate.sourceValue ||
|
|
875
|
-
(candidate.includeBoundary !== false && numeric === candidate.sourceValue));
|
|
876
|
-
return entry?.targetValue ?? lookupTable.defaultValue;
|
|
877
|
-
}
|
|
878
|
-
function mapOrMatchResponse(declaration, response, correctResponse) {
|
|
879
|
-
if (declaration.areaMapping)
|
|
880
|
-
return scoreAreaMapping(response, declaration.areaMapping);
|
|
881
|
-
if (declaration.mapping)
|
|
882
|
-
return scoreMapping(response, declaration.mapping);
|
|
883
|
-
return valuesEqual(response, correctResponse, declaration.cardinality === "ordered") ? 1 : 0;
|
|
884
|
-
}
|
|
885
|
-
function scoreAreaMapping(response, areaMapping) {
|
|
886
|
-
const points = Array.isArray(response)
|
|
887
|
-
? response.map(qtiScalarToString)
|
|
888
|
-
: response === null
|
|
889
|
-
? []
|
|
890
|
-
: qtiValueToStringList(response);
|
|
891
|
-
let score = 0;
|
|
892
|
-
for (const point of points) {
|
|
893
|
-
const parsed = parsePoint(String(point));
|
|
894
|
-
if (!parsed) {
|
|
895
|
-
score += areaMapping.defaultValue;
|
|
279
|
+
evaluation.outcomes[rule.identifier] = lookupOutcomeValue(evaluation.document, rule.identifier, evaluation.evaluate(rule.expression));
|
|
896
280
|
continue;
|
|
897
281
|
}
|
|
898
|
-
|
|
899
|
-
score += entry?.mappedValue ?? areaMapping.defaultValue;
|
|
900
|
-
}
|
|
901
|
-
return clampMappedScore(score, areaMapping.attributes);
|
|
902
|
-
}
|
|
903
|
-
function parsePoint(value) {
|
|
904
|
-
const [x, y] = value
|
|
905
|
-
.trim()
|
|
906
|
-
.split(/[,\s]+/)
|
|
907
|
-
.map((part) => Number(part));
|
|
908
|
-
if (x === undefined || y === undefined || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
909
|
-
return undefined;
|
|
910
|
-
}
|
|
911
|
-
return { x, y };
|
|
912
|
-
}
|
|
913
|
-
function pointInsideArea(point, entry) {
|
|
914
|
-
if (entry.shape === "circle") {
|
|
915
|
-
const [cx, cy, radius] = entry.coords;
|
|
916
|
-
if (cx === undefined || cy === undefined || radius === undefined)
|
|
917
|
-
return false;
|
|
918
|
-
return Math.hypot(point.x - cx, point.y - cy) <= radius;
|
|
919
|
-
}
|
|
920
|
-
if (entry.shape === "rect") {
|
|
921
|
-
const [left, top, right, bottom] = entry.coords;
|
|
922
|
-
if (left === undefined || top === undefined || right === undefined || bottom === undefined) {
|
|
923
|
-
return false;
|
|
924
|
-
}
|
|
925
|
-
return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
|
|
926
|
-
}
|
|
927
|
-
if (entry.shape === "poly") {
|
|
928
|
-
return pointInsidePolygon(point, entry.coords);
|
|
282
|
+
evaluation.outcomes[rule.identifier] = evaluation.evaluate(rule.expression);
|
|
929
283
|
}
|
|
930
284
|
return false;
|
|
931
285
|
}
|
|
932
|
-
function
|
|
933
|
-
|
|
934
|
-
return false;
|
|
935
|
-
let inside = false;
|
|
936
|
-
for (let index = 0, previous = coords.length - 2; index < coords.length; index += 2) {
|
|
937
|
-
const xi = coords[index];
|
|
938
|
-
const yi = coords[index + 1];
|
|
939
|
-
const xj = coords[previous];
|
|
940
|
-
const yj = coords[previous + 1];
|
|
941
|
-
const intersects = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
|
|
942
|
-
if (intersects)
|
|
943
|
-
inside = !inside;
|
|
944
|
-
previous = index;
|
|
945
|
-
}
|
|
946
|
-
return inside;
|
|
947
|
-
}
|
|
948
|
-
function isNullResponse(response) {
|
|
949
|
-
return response === null || response === "" || (Array.isArray(response) && response.length === 0);
|
|
950
|
-
}
|
|
951
|
-
function serialize(itemIdentifier, status, responses, outcomes, templateValues, interactionStates, validationMessages) {
|
|
952
|
-
return {
|
|
953
|
-
schema: ATTEMPT_STATE_SCHEMA,
|
|
954
|
-
itemIdentifier,
|
|
955
|
-
status,
|
|
956
|
-
responses: cloneValueRecord(responses),
|
|
957
|
-
outcomes: cloneValueRecord(outcomes),
|
|
958
|
-
templateValues: cloneValueRecord(templateValues),
|
|
959
|
-
interactionStates: clonePortableCustomStateRecord(interactionStates),
|
|
960
|
-
validationMessages: cloneDiagnostics(validationMessages),
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
function cloneValueRecord(record) {
|
|
964
|
-
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, cloneValue(value)]));
|
|
965
|
-
}
|
|
966
|
-
function clonePortableCustomStateRecord(record) {
|
|
967
|
-
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, clonePortableCustomState(value)]));
|
|
968
|
-
}
|
|
969
|
-
function cloneValue(value) {
|
|
970
|
-
if (Array.isArray(value))
|
|
971
|
-
return [...value];
|
|
972
|
-
if (isRecordValue(value))
|
|
973
|
-
return cloneValueRecord(value);
|
|
974
|
-
return value;
|
|
975
|
-
}
|
|
976
|
-
function clonePortableCustomState(value) {
|
|
977
|
-
if (Array.isArray(value))
|
|
978
|
-
return value.map(clonePortableCustomState);
|
|
979
|
-
if (isPortableCustomStateObject(value))
|
|
980
|
-
return clonePortableCustomStateRecord(value);
|
|
981
|
-
return value;
|
|
982
|
-
}
|
|
983
|
-
function cloneDiagnostics(diagnostics) {
|
|
984
|
-
return diagnostics.map((diagnostic) => ({
|
|
985
|
-
...diagnostic,
|
|
986
|
-
source: diagnostic.source ? { ...diagnostic.source } : undefined,
|
|
987
|
-
}));
|
|
988
|
-
}
|
|
989
|
-
function scoreMapping(response, mapping) {
|
|
990
|
-
const values = Object.fromEntries(mapping.entries
|
|
991
|
-
.filter((entry) => entry.mapKey !== undefined)
|
|
992
|
-
.map((entry) => [entry.mapKey, entry.mappedValue]));
|
|
993
|
-
if (Array.isArray(response)) {
|
|
994
|
-
const score = response.reduce((sum, value) => sum + (values[String(value)] ?? mapping.defaultValue), 0);
|
|
995
|
-
return clampMappedScore(score, mapping.attributes);
|
|
996
|
-
}
|
|
997
|
-
const score = response === null || isRecordValue(response)
|
|
998
|
-
? 0
|
|
999
|
-
: (values[String(response)] ?? mapping.defaultValue);
|
|
1000
|
-
return clampMappedScore(score, mapping.attributes);
|
|
1001
|
-
}
|
|
1002
|
-
function clampMappedScore(score, attributes) {
|
|
1003
|
-
const lower = numericBound(attributes["lower-bound"]);
|
|
1004
|
-
const upper = numericBound(attributes["upper-bound"]);
|
|
1005
|
-
let clamped = score;
|
|
1006
|
-
if (lower !== undefined)
|
|
1007
|
-
clamped = Math.max(clamped, lower);
|
|
1008
|
-
if (upper !== undefined)
|
|
1009
|
-
clamped = Math.min(clamped, upper);
|
|
1010
|
-
return clamped;
|
|
1011
|
-
}
|
|
1012
|
-
function numericBound(value) {
|
|
1013
|
-
if (value === undefined)
|
|
1014
|
-
return undefined;
|
|
1015
|
-
const numberValue = Number(value);
|
|
1016
|
-
return Number.isFinite(numberValue) ? numberValue : undefined;
|
|
1017
|
-
}
|
|
1018
|
-
function valuesEqual(actual, expected, ordered = false) {
|
|
1019
|
-
if (isRecordValue(actual) || isRecordValue(expected)) {
|
|
1020
|
-
if (!isRecordValue(actual) || !isRecordValue(expected))
|
|
1021
|
-
return false;
|
|
1022
|
-
const actualKeys = Object.keys(actual).sort();
|
|
1023
|
-
const expectedKeys = Object.keys(expected).sort();
|
|
1024
|
-
return (valuesEqual(actualKeys, expectedKeys, true) &&
|
|
1025
|
-
actualKeys.every((key) => valuesEqual(actual[key] ?? null, expected[key] ?? null, ordered)));
|
|
1026
|
-
}
|
|
1027
|
-
if (Array.isArray(actual) || Array.isArray(expected)) {
|
|
1028
|
-
const actualValues = valueContainer(actual);
|
|
1029
|
-
const expectedValues = Array.isArray(expected) ? expected : expected === null ? [] : [expected];
|
|
1030
|
-
if (actualValues.length !== expectedValues.length)
|
|
1031
|
-
return false;
|
|
1032
|
-
if (ordered)
|
|
1033
|
-
return actualValues.every((value, index) => scalarValuesEqual(value, expectedValues[index]));
|
|
1034
|
-
const sortedExpected = [...expectedValues].sort(compareScalarValues);
|
|
1035
|
-
return [...actualValues]
|
|
1036
|
-
.sort(compareScalarValues)
|
|
1037
|
-
.every((value, index) => scalarValuesEqual(value, sortedExpected[index]));
|
|
1038
|
-
}
|
|
1039
|
-
return scalarValuesEqual(actual, expected);
|
|
1040
|
-
}
|
|
1041
|
-
function qtiMatchValues(actual, expected, ordered = false) {
|
|
1042
|
-
if (actual === null || expected === null)
|
|
1043
|
-
return null;
|
|
1044
|
-
return valuesEqual(actual, expected, ordered);
|
|
1045
|
-
}
|
|
1046
|
-
function scalarValuesEqual(actual, expected) {
|
|
1047
|
-
if (typeof actual === "boolean" && typeof expected === "string") {
|
|
1048
|
-
return String(actual) === expected;
|
|
1049
|
-
}
|
|
1050
|
-
if (typeof actual === "string" && typeof expected === "boolean") {
|
|
1051
|
-
return actual === String(expected);
|
|
1052
|
-
}
|
|
1053
|
-
return actual === expected;
|
|
1054
|
-
}
|
|
1055
|
-
function normalizeValueForCardinality(value, cardinality) {
|
|
1056
|
-
if ((cardinality === "multiple" || cardinality === "ordered") &&
|
|
1057
|
-
value !== null &&
|
|
1058
|
-
!Array.isArray(value) &&
|
|
1059
|
-
!isRecordValue(value)) {
|
|
1060
|
-
return [value];
|
|
1061
|
-
}
|
|
1062
|
-
return value;
|
|
1063
|
-
}
|
|
1064
|
-
function valueContainer(value) {
|
|
1065
|
-
if (value === null)
|
|
1066
|
-
return [];
|
|
1067
|
-
if (isRecordValue(value))
|
|
1068
|
-
return [];
|
|
1069
|
-
return Array.isArray(value) ? value.filter((item) => item !== null) : [value];
|
|
1070
|
-
}
|
|
1071
|
-
function isRecordValue(value) {
|
|
1072
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1073
|
-
}
|
|
1074
|
-
function isQtiValueRecord(value) {
|
|
1075
|
-
if (!isRecord(value))
|
|
1076
|
-
return false;
|
|
1077
|
-
return Object.values(value).every(isQtiValue);
|
|
1078
|
-
}
|
|
1079
|
-
function isPortableCustomState(value) {
|
|
1080
|
-
return isQtiPortableCustomStateValue(value);
|
|
1081
|
-
}
|
|
1082
|
-
function isPortableCustomStateObject(value) {
|
|
1083
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1084
|
-
}
|
|
1085
|
-
function isPortableCustomStateRecord(value) {
|
|
1086
|
-
if (!isRecord(value))
|
|
1087
|
-
return false;
|
|
1088
|
-
return Object.values(value).every(isPortableCustomState);
|
|
1089
|
-
}
|
|
1090
|
-
function isDiagnosticArray(value) {
|
|
1091
|
-
if (!Array.isArray(value))
|
|
1092
|
-
return false;
|
|
1093
|
-
return value.every((item) => {
|
|
1094
|
-
if (!isRecord(item))
|
|
1095
|
-
return false;
|
|
1096
|
-
if (typeof item.code !== "string" || item.code.length === 0)
|
|
1097
|
-
return false;
|
|
1098
|
-
if (item.severity !== "info" && item.severity !== "warning" && item.severity !== "error") {
|
|
1099
|
-
return false;
|
|
1100
|
-
}
|
|
1101
|
-
if (typeof item.message !== "string" || item.message.length === 0)
|
|
1102
|
-
return false;
|
|
1103
|
-
return item.path === undefined || typeof item.path === "string";
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
function isRecord(value) {
|
|
1107
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1108
|
-
}
|
|
1109
|
-
function indexValue(n, outcomes, templateValues) {
|
|
1110
|
-
const parsed = Number(n);
|
|
1111
|
-
if (Number.isInteger(parsed))
|
|
1112
|
-
return parsed;
|
|
1113
|
-
const value = outcomes[n] ?? templateValues[n] ?? null;
|
|
1114
|
-
const numeric = numericValueOrNull(value);
|
|
1115
|
-
return numeric !== null && Number.isInteger(numeric) ? numeric : undefined;
|
|
1116
|
-
}
|
|
1117
|
-
function evaluateNumericOperands(expressions, document, responses, outcomes, templateValues, correctResponses, random, customOperators) {
|
|
1118
|
-
const numericValues = [];
|
|
1119
|
-
for (const expression of expressions) {
|
|
1120
|
-
const value = evaluateValue(expression, document, responses, outcomes, templateValues, correctResponses, random, customOperators);
|
|
1121
|
-
if (value === null || isRecordValue(value))
|
|
1122
|
-
return null;
|
|
1123
|
-
const values = Array.isArray(value) ? value : [value];
|
|
1124
|
-
for (const item of values) {
|
|
1125
|
-
const numeric = numericValueOrNull(item);
|
|
1126
|
-
if (numeric === null)
|
|
1127
|
-
return null;
|
|
1128
|
-
numericValues.push(numeric);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
return numericValues;
|
|
1132
|
-
}
|
|
1133
|
-
function containsValues(collection, values) {
|
|
1134
|
-
const remaining = [...collection];
|
|
1135
|
-
for (const value of values) {
|
|
1136
|
-
const index = remaining.findIndex((candidate) => valuesEqual(candidate, value));
|
|
1137
|
-
if (index === -1)
|
|
1138
|
-
return false;
|
|
1139
|
-
remaining.splice(index, 1);
|
|
1140
|
-
}
|
|
1141
|
-
return true;
|
|
1142
|
-
}
|
|
1143
|
-
function compareScalarValues(left, right) {
|
|
1144
|
-
return String(left).localeCompare(String(right));
|
|
1145
|
-
}
|
|
1146
|
-
function numericValue(value) {
|
|
1147
|
-
if (typeof value === "number")
|
|
1148
|
-
return value;
|
|
1149
|
-
if (typeof value === "boolean")
|
|
1150
|
-
return value ? 1 : 0;
|
|
1151
|
-
if (typeof value === "string")
|
|
1152
|
-
return Number(value);
|
|
1153
|
-
return 0;
|
|
1154
|
-
}
|
|
1155
|
-
function numericValueOrNull(value) {
|
|
1156
|
-
if (typeof value === "number")
|
|
1157
|
-
return Number.isFinite(value) ? value : null;
|
|
1158
|
-
if (typeof value === "boolean")
|
|
1159
|
-
return value ? 1 : 0;
|
|
1160
|
-
if (typeof value === "string") {
|
|
1161
|
-
const parsed = Number(value);
|
|
1162
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
1163
|
-
}
|
|
1164
|
-
return null;
|
|
1165
|
-
}
|
|
1166
|
-
function durationSeconds(value) {
|
|
1167
|
-
if (value === null || Array.isArray(value) || isRecordValue(value))
|
|
1168
|
-
return null;
|
|
1169
|
-
if (typeof value === "number")
|
|
1170
|
-
return Number.isFinite(value) ? value : null;
|
|
1171
|
-
const raw = String(value).trim();
|
|
1172
|
-
if (raw.length === 0)
|
|
1173
|
-
return null;
|
|
1174
|
-
const numeric = Number(raw);
|
|
1175
|
-
if (Number.isFinite(numeric))
|
|
1176
|
-
return numeric;
|
|
1177
|
-
const match = /^P(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/i.exec(raw);
|
|
1178
|
-
if (!match)
|
|
1179
|
-
return null;
|
|
1180
|
-
const [, days, hours, minutes, seconds] = match;
|
|
1181
|
-
if (!days && !hours && !minutes && !seconds)
|
|
1182
|
-
return null;
|
|
1183
|
-
const total = Number(days ?? 0) * 86_400 +
|
|
1184
|
-
Number(hours ?? 0) * 3_600 +
|
|
1185
|
-
Number(minutes ?? 0) * 60 +
|
|
1186
|
-
Number(seconds ?? 0);
|
|
1187
|
-
return Number.isFinite(total) ? total : null;
|
|
1188
|
-
}
|
|
1189
|
-
function booleanValue(value) {
|
|
1190
|
-
if (typeof value === "boolean")
|
|
1191
|
-
return value;
|
|
1192
|
-
if (typeof value === "number")
|
|
1193
|
-
return value !== 0;
|
|
1194
|
-
if (typeof value === "string")
|
|
1195
|
-
return value.length > 0 && value !== "false";
|
|
1196
|
-
if (Array.isArray(value))
|
|
1197
|
-
return value.length > 0;
|
|
1198
|
-
return false;
|
|
1199
|
-
}
|
|
1200
|
-
function booleanValueOrNull(value) {
|
|
1201
|
-
if (value === null)
|
|
1202
|
-
return null;
|
|
1203
|
-
return booleanValue(value);
|
|
1204
|
-
}
|
|
1205
|
-
function roundToDecimalPlaces(value, figures) {
|
|
1206
|
-
const factor = 10 ** figures;
|
|
1207
|
-
return Math.round(value * factor) / factor;
|
|
1208
|
-
}
|
|
1209
|
-
function roundToSignificantFigures(value, figures) {
|
|
1210
|
-
if (value === 0 || figures <= 0)
|
|
1211
|
-
return 0;
|
|
1212
|
-
const factor = 10 ** (figures - 1 - Math.floor(Math.log10(Math.abs(value))));
|
|
1213
|
-
return Math.round(value * factor) / factor;
|
|
1214
|
-
}
|
|
1215
|
-
function roundWithMode(value, roundingMode, figures) {
|
|
1216
|
-
if (!Number.isFinite(value))
|
|
1217
|
-
return null;
|
|
1218
|
-
if (roundingMode === "decimalPlaces")
|
|
1219
|
-
return roundToDecimalPlaces(value, figures);
|
|
1220
|
-
if (roundingMode === "significantFigures")
|
|
1221
|
-
return roundToSignificantFigures(value, figures);
|
|
1222
|
-
return null;
|
|
1223
|
-
}
|
|
1224
|
-
function generalizedGcd(values) {
|
|
1225
|
-
let result = 0;
|
|
1226
|
-
for (const value of values) {
|
|
1227
|
-
result = gcd(result, Math.abs(value));
|
|
1228
|
-
}
|
|
1229
|
-
return result;
|
|
1230
|
-
}
|
|
1231
|
-
function generalizedLcm(values) {
|
|
1232
|
-
if (values.some((value) => value === 0))
|
|
1233
|
-
return 0;
|
|
1234
|
-
return values.reduce((result, value) => lcm(result, Math.abs(value)), 1);
|
|
1235
|
-
}
|
|
1236
|
-
function gcd(left, right) {
|
|
1237
|
-
let a = Math.abs(left);
|
|
1238
|
-
let b = Math.abs(right);
|
|
1239
|
-
while (b !== 0) {
|
|
1240
|
-
const next = a % b;
|
|
1241
|
-
a = b;
|
|
1242
|
-
b = next;
|
|
1243
|
-
}
|
|
1244
|
-
return a;
|
|
1245
|
-
}
|
|
1246
|
-
function lcm(left, right) {
|
|
1247
|
-
return Math.abs(left * right) / gcd(left, right);
|
|
1248
|
-
}
|
|
1249
|
-
function mathOperatorValue(name, values) {
|
|
1250
|
-
const [first, second] = values;
|
|
1251
|
-
if (first === undefined)
|
|
1252
|
-
return null;
|
|
1253
|
-
switch (name) {
|
|
1254
|
-
case "abs":
|
|
1255
|
-
return finiteOrNull(Math.abs(first));
|
|
1256
|
-
case "acos":
|
|
1257
|
-
return Math.abs(first) > 1 ? null : finiteOrNull(Math.acos(first));
|
|
1258
|
-
case "acot":
|
|
1259
|
-
return finiteOrNull(Math.PI / 2 - Math.atan(1 / first));
|
|
1260
|
-
case "acsc":
|
|
1261
|
-
return Math.abs(first) < 1 ? null : finiteOrNull(Math.PI / 2 - Math.asin(1 / first));
|
|
1262
|
-
case "asec":
|
|
1263
|
-
return Math.abs(first) < 1 ? null : finiteOrNull(Math.PI / 2 - Math.acos(1 / first));
|
|
1264
|
-
case "asin":
|
|
1265
|
-
return Math.abs(first) > 1 ? null : finiteOrNull(Math.asin(first));
|
|
1266
|
-
case "atan":
|
|
1267
|
-
return finiteOrNull(Math.atan(first));
|
|
1268
|
-
case "atan2":
|
|
1269
|
-
return second === undefined ? null : finiteOrNull(Math.atan2(first, second));
|
|
1270
|
-
case "ceil":
|
|
1271
|
-
return finiteOrNull(Math.ceil(first));
|
|
1272
|
-
case "cos":
|
|
1273
|
-
return finiteOrNull(Math.cos(first));
|
|
1274
|
-
case "cosh":
|
|
1275
|
-
return finiteOrNull(Math.cosh(first));
|
|
1276
|
-
case "cot":
|
|
1277
|
-
return finiteOrNull(1 / Math.tan(first));
|
|
1278
|
-
case "coth":
|
|
1279
|
-
return finiteOrNull(1 / Math.tanh(first));
|
|
1280
|
-
case "csc":
|
|
1281
|
-
return finiteOrNull(1 / Math.sin(first));
|
|
1282
|
-
case "csch":
|
|
1283
|
-
return finiteOrNull(1 / Math.sinh(first));
|
|
1284
|
-
case "exp":
|
|
1285
|
-
return finiteOrNull(Math.exp(first));
|
|
1286
|
-
case "floor":
|
|
1287
|
-
return finiteOrNull(Math.floor(first));
|
|
1288
|
-
case "ln":
|
|
1289
|
-
return first < 0 ? null : finiteOrNull(Math.log(first));
|
|
1290
|
-
case "log":
|
|
1291
|
-
return first < 0 ? null : finiteOrNull(Math.log10(first));
|
|
1292
|
-
case "sec":
|
|
1293
|
-
return finiteOrNull(1 / Math.cos(first));
|
|
1294
|
-
case "sech":
|
|
1295
|
-
return finiteOrNull(1 / Math.cosh(first));
|
|
1296
|
-
case "signum":
|
|
1297
|
-
return finiteOrNull(Math.sign(first));
|
|
1298
|
-
case "sin":
|
|
1299
|
-
return finiteOrNull(Math.sin(first));
|
|
1300
|
-
case "sinh":
|
|
1301
|
-
return finiteOrNull(Math.sinh(first));
|
|
1302
|
-
case "tan":
|
|
1303
|
-
return finiteOrNull(Math.tan(first));
|
|
1304
|
-
case "tanh":
|
|
1305
|
-
return finiteOrNull(Math.tanh(first));
|
|
1306
|
-
case "toDegrees":
|
|
1307
|
-
return finiteOrNull((first * 180) / Math.PI);
|
|
1308
|
-
case "toRadians":
|
|
1309
|
-
return finiteOrNull((first * Math.PI) / 180);
|
|
1310
|
-
default:
|
|
1311
|
-
return null;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
function statsOperatorValue(name, values) {
|
|
1315
|
-
if (values.length === 0)
|
|
1316
|
-
return 0;
|
|
1317
|
-
const meanValue = mean(values);
|
|
1318
|
-
const squareDiffs = values.map((value) => (value - meanValue) ** 2);
|
|
1319
|
-
switch (name) {
|
|
1320
|
-
case "mean":
|
|
1321
|
-
return meanValue;
|
|
1322
|
-
case "sampleVariance":
|
|
1323
|
-
return meanWithDivisor(squareDiffs, values.length > 1 ? values.length - 1 : 1);
|
|
1324
|
-
case "sampleSD":
|
|
1325
|
-
return Math.sqrt(meanWithDivisor(squareDiffs, values.length > 1 ? values.length - 1 : 1));
|
|
1326
|
-
case "popVariance":
|
|
1327
|
-
return mean(squareDiffs);
|
|
1328
|
-
case "popSD":
|
|
1329
|
-
return Math.sqrt(mean(squareDiffs));
|
|
1330
|
-
default:
|
|
1331
|
-
return null;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
function mean(values) {
|
|
1335
|
-
return meanWithDivisor(values, values.length);
|
|
1336
|
-
}
|
|
1337
|
-
function meanWithDivisor(values, divisor) {
|
|
1338
|
-
if (values.length === 0)
|
|
1339
|
-
return 0;
|
|
1340
|
-
return values.reduce((sum, value) => sum + value, 0) / divisor;
|
|
1341
|
-
}
|
|
1342
|
-
function finiteOrNull(value) {
|
|
1343
|
-
return Number.isFinite(value) ? value : null;
|
|
1344
|
-
}
|
|
1345
|
-
function stringMatch(left, right, caseSensitive, substring) {
|
|
1346
|
-
if (left === null || right === null)
|
|
1347
|
-
return null;
|
|
1348
|
-
let actual = qtiValueToString(left);
|
|
1349
|
-
let expected = qtiValueToString(right);
|
|
1350
|
-
if (!caseSensitive) {
|
|
1351
|
-
actual = actual.toLocaleLowerCase();
|
|
1352
|
-
expected = expected.toLocaleLowerCase();
|
|
1353
|
-
}
|
|
1354
|
-
return substring ? actual.includes(expected) : actual === expected;
|
|
1355
|
-
}
|
|
1356
|
-
function seededRandom(seed) {
|
|
1357
|
-
let state = typeof seed === "number" ? seed >>> 0 : hashSeed(seed);
|
|
1358
|
-
return () => {
|
|
1359
|
-
state = (1664525 * state + 1013904223) >>> 0;
|
|
1360
|
-
return state / 0x100000000;
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
function hashSeed(seed) {
|
|
1364
|
-
let hash = 2166136261;
|
|
1365
|
-
for (let index = 0; index < seed.length; index += 1) {
|
|
1366
|
-
hash ^= seed.charCodeAt(index);
|
|
1367
|
-
hash = Math.imul(hash, 16777619);
|
|
1368
|
-
}
|
|
1369
|
-
return hash >>> 0;
|
|
286
|
+
function applyResponseCondition(context, condition) {
|
|
287
|
+
return applyResponseRules(context, resolveConditionalRules(context, condition));
|
|
1370
288
|
}
|
|
1371
289
|
//# sourceMappingURL=session.js.map
|