@longsightgroup/qti3-core 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -8
- package/dist/delivery-security.d.ts +26 -0
- package/dist/delivery-security.d.ts.map +1 -0
- package/dist/delivery-security.js +213 -0
- package/dist/delivery-security.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +21 -3
- package/dist/parser.js.map +1 -1
- package/dist/server-scoring.d.ts +27 -0
- package/dist/server-scoring.d.ts.map +1 -0
- package/dist/server-scoring.js +162 -0
- package/dist/server-scoring.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +192 -105
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +136 -3
- package/dist/validation.js.map +1 -1
- package/dist/value-format.d.ts +11 -0
- package/dist/value-format.d.ts.map +1 -0
- package/dist/value-format.js +107 -0
- package/dist/value-format.js.map +1 -0
- package/dist/xml.d.ts +12 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +200 -50
- package/dist/xml.js.map +1 -1
- package/package.json +2 -2
- package/src/delivery-security.ts +283 -0
- package/src/index.ts +25 -0
- package/src/parser.ts +23 -3
- package/src/server-scoring.ts +244 -0
- package/src/session.ts +336 -291
- package/src/types.ts +3 -0
- package/src/validation.ts +151 -2
- package/src/value-format.ts +103 -0
- package/src/xml.ts +224 -52
package/src/session.ts
CHANGED
|
@@ -17,6 +17,13 @@ import type {
|
|
|
17
17
|
QtiValue,
|
|
18
18
|
QtiVariableDeclaration,
|
|
19
19
|
} from "./types.js";
|
|
20
|
+
import {
|
|
21
|
+
isQtiPortableCustomStateValue,
|
|
22
|
+
isQtiValue,
|
|
23
|
+
qtiScalarToString,
|
|
24
|
+
qtiValueToString,
|
|
25
|
+
qtiValueToStringList,
|
|
26
|
+
} from "./value-format.js";
|
|
20
27
|
|
|
21
28
|
export interface QtiCustomOperatorContext {
|
|
22
29
|
definition?: string | undefined;
|
|
@@ -64,8 +71,9 @@ export function visibleModalFeedback(
|
|
|
64
71
|
return item.modalFeedback.filter((feedback) => {
|
|
65
72
|
if (feedback.showHide === "hide") return false;
|
|
66
73
|
const outcome = outcomes[feedback.outcomeIdentifier];
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
const outcomeValue: QtiValue = outcome === undefined ? null : outcome;
|
|
75
|
+
if (Array.isArray(outcomeValue)) return outcomeValue.includes(feedback.identifier);
|
|
76
|
+
return qtiValueToString(outcomeValue) === feedback.identifier;
|
|
69
77
|
});
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -84,6 +92,7 @@ export function createItemSession(
|
|
|
84
92
|
);
|
|
85
93
|
let validationMessages = cloneDiagnostics(priorState?.validationMessages ?? []);
|
|
86
94
|
const responses: Record<string, QtiValue> = {};
|
|
95
|
+
const responseDefaults: Record<string, QtiValue> = {};
|
|
87
96
|
const outcomes: Record<string, QtiValue> = {};
|
|
88
97
|
const templateValues: Record<string, QtiValue> = {};
|
|
89
98
|
const interactionStates: Record<string, QtiPortableCustomStateValue> = {};
|
|
@@ -100,8 +109,8 @@ export function createItemSession(
|
|
|
100
109
|
|
|
101
110
|
for (const declaration of document.item.responseDeclarations) {
|
|
102
111
|
correctResponses[declaration.identifier] = cloneValue(declaration.correctResponse);
|
|
103
|
-
if (declaration.defaultValue !== null
|
|
104
|
-
|
|
112
|
+
if (declaration.defaultValue !== null) {
|
|
113
|
+
responseDefaults[declaration.identifier] = cloneValue(declaration.defaultValue);
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
for (const declaration of document.item.templateDeclarations) {
|
|
@@ -112,18 +121,21 @@ export function createItemSession(
|
|
|
112
121
|
}
|
|
113
122
|
outcomes[COMPLETION_STATUS] = COMPLETION_NOT_ATTEMPTED;
|
|
114
123
|
const baseResponses = cloneValueRecord(responses);
|
|
124
|
+
const baseResponseDefaults = cloneValueRecord(responseDefaults);
|
|
115
125
|
const baseOutcomes = cloneValueRecord(outcomes);
|
|
116
126
|
|
|
117
127
|
applyTemplateProcessing(
|
|
118
128
|
document,
|
|
119
129
|
templateValues,
|
|
120
130
|
responses,
|
|
131
|
+
responseDefaults,
|
|
121
132
|
outcomes,
|
|
122
133
|
correctResponses,
|
|
123
134
|
random,
|
|
124
135
|
customOperators,
|
|
125
136
|
new Set(),
|
|
126
137
|
baseResponses,
|
|
138
|
+
baseResponseDefaults,
|
|
127
139
|
baseOutcomes,
|
|
128
140
|
);
|
|
129
141
|
if (priorState) {
|
|
@@ -133,12 +145,14 @@ export function createItemSession(
|
|
|
133
145
|
document,
|
|
134
146
|
templateValues,
|
|
135
147
|
responses,
|
|
148
|
+
responseDefaults,
|
|
136
149
|
outcomes,
|
|
137
150
|
correctResponses,
|
|
138
151
|
random,
|
|
139
152
|
customOperators,
|
|
140
153
|
new Set(Object.keys(priorTemplateValues)),
|
|
141
154
|
baseResponses,
|
|
155
|
+
baseResponseDefaults,
|
|
142
156
|
baseOutcomes,
|
|
143
157
|
);
|
|
144
158
|
}
|
|
@@ -174,7 +188,7 @@ export function createItemSession(
|
|
|
174
188
|
},
|
|
175
189
|
score() {
|
|
176
190
|
const diagnostics: QtiDiagnostic[] = [];
|
|
177
|
-
if (document.item.adaptive || status !== "initialized"
|
|
191
|
+
if (document.item.adaptive || status !== "initialized") {
|
|
178
192
|
startAttempt();
|
|
179
193
|
}
|
|
180
194
|
const completionStatus = outcomes[COMPLETION_STATUS] ?? COMPLETION_NOT_ATTEMPTED;
|
|
@@ -218,6 +232,9 @@ export function createItemSession(
|
|
|
218
232
|
};
|
|
219
233
|
|
|
220
234
|
function startAttempt(): void {
|
|
235
|
+
for (const [identifier, value] of Object.entries(responseDefaults)) {
|
|
236
|
+
if (responses[identifier] === undefined) responses[identifier] = cloneValue(value);
|
|
237
|
+
}
|
|
221
238
|
if (status === "initialized" || status === "suspended") status = "interacting";
|
|
222
239
|
if (outcomes[COMPLETION_STATUS] === COMPLETION_NOT_ATTEMPTED) {
|
|
223
240
|
outcomes[COMPLETION_STATUS] = COMPLETION_UNKNOWN;
|
|
@@ -297,7 +314,9 @@ function assertCompatiblePriorState(
|
|
|
297
314
|
completionStatus !== COMPLETION_COMPLETED &&
|
|
298
315
|
completionStatus !== "incomplete"
|
|
299
316
|
) {
|
|
300
|
-
throw new Error(
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Cannot restore unsupported completionStatus ${qtiValueToString(completionStatus)}.`,
|
|
319
|
+
);
|
|
301
320
|
}
|
|
302
321
|
}
|
|
303
322
|
|
|
@@ -431,12 +450,14 @@ function applyTemplateProcessing(
|
|
|
431
450
|
document: QtiDocument,
|
|
432
451
|
templateValues: Record<string, QtiValue>,
|
|
433
452
|
responses: Record<string, QtiValue>,
|
|
453
|
+
responseDefaults: Record<string, QtiValue>,
|
|
434
454
|
outcomes: Record<string, QtiValue>,
|
|
435
455
|
correctResponses: Record<string, QtiValue>,
|
|
436
456
|
random: () => number,
|
|
437
457
|
customOperators: QtiCustomOperatorRegistry,
|
|
438
458
|
preservedTemplateIdentifiers = new Set<string>(),
|
|
439
459
|
baseResponses: Record<string, QtiValue> = cloneValueRecord(responses),
|
|
460
|
+
baseResponseDefaults: Record<string, QtiValue> = cloneValueRecord(responseDefaults),
|
|
440
461
|
baseOutcomes: Record<string, QtiValue> = cloneValueRecord(outcomes),
|
|
441
462
|
): void {
|
|
442
463
|
const rules = document.item.templateProcessing?.rules ?? [];
|
|
@@ -448,6 +469,7 @@ function applyTemplateProcessing(
|
|
|
448
469
|
document,
|
|
449
470
|
templateValues,
|
|
450
471
|
responses,
|
|
472
|
+
responseDefaults,
|
|
451
473
|
outcomes,
|
|
452
474
|
correctResponses,
|
|
453
475
|
random,
|
|
@@ -469,6 +491,7 @@ function applyTemplateProcessing(
|
|
|
469
491
|
if (!satisfied) {
|
|
470
492
|
resetTemplateValues(document, templateValues);
|
|
471
493
|
resetRecord(responses, cloneValueRecord(baseResponses));
|
|
494
|
+
resetRecord(responseDefaults, cloneValueRecord(baseResponseDefaults));
|
|
472
495
|
resetRecord(outcomes, cloneValueRecord(baseOutcomes));
|
|
473
496
|
resetCorrectResponses(document, correctResponses);
|
|
474
497
|
restarts += 1;
|
|
@@ -504,6 +527,7 @@ function applyTemplateRule(
|
|
|
504
527
|
document: QtiDocument,
|
|
505
528
|
templateValues: Record<string, QtiValue>,
|
|
506
529
|
responses: Record<string, QtiValue>,
|
|
530
|
+
responseDefaults: Record<string, QtiValue>,
|
|
507
531
|
outcomes: Record<string, QtiValue>,
|
|
508
532
|
correctResponses: Record<string, QtiValue>,
|
|
509
533
|
random: () => number,
|
|
@@ -552,6 +576,7 @@ function applyTemplateRule(
|
|
|
552
576
|
document,
|
|
553
577
|
templateValues,
|
|
554
578
|
responses,
|
|
579
|
+
responseDefaults,
|
|
555
580
|
outcomes,
|
|
556
581
|
correctResponses,
|
|
557
582
|
random,
|
|
@@ -583,7 +608,11 @@ function applyTemplateRule(
|
|
|
583
608
|
const responseDeclaration = getResponseDeclaration(document, rule.identifier);
|
|
584
609
|
if (responseDeclaration) {
|
|
585
610
|
const normalized = normalizeValueForCardinality(value, responseDeclaration.cardinality);
|
|
586
|
-
|
|
611
|
+
if (normalized === null) {
|
|
612
|
+
delete responseDefaults[rule.identifier];
|
|
613
|
+
} else {
|
|
614
|
+
responseDefaults[rule.identifier] = normalized;
|
|
615
|
+
}
|
|
587
616
|
return false;
|
|
588
617
|
}
|
|
589
618
|
const outcomeDeclaration = document.item.outcomeDeclarations.find(
|
|
@@ -628,33 +657,45 @@ function applyResponseProcessing(
|
|
|
628
657
|
return;
|
|
629
658
|
}
|
|
630
659
|
|
|
631
|
-
const
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
660
|
+
const templateKind = responseProcessingTemplateKind(processing?.template);
|
|
661
|
+
if (templateKind === "unsupported") {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (templateKind === "mapResponse" || templateKind === "mapResponsePoint") {
|
|
665
|
+
const declaration = getResponseDeclaration(document, "RESPONSE");
|
|
666
|
+
outcomes.SCORE = declaration
|
|
667
|
+
? mapOrMatchResponse(
|
|
668
|
+
declaration,
|
|
669
|
+
responses.RESPONSE ?? null,
|
|
670
|
+
correctResponses.RESPONSE ?? null,
|
|
671
|
+
)
|
|
672
|
+
: 0;
|
|
642
673
|
return;
|
|
643
674
|
}
|
|
644
675
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
676
|
+
if (templateKind === "matchCorrect") {
|
|
677
|
+
const declaration = getResponseDeclaration(document, "RESPONSE");
|
|
678
|
+
const matches = declaration
|
|
679
|
+
? qtiMatchValues(
|
|
680
|
+
responses.RESPONSE ?? null,
|
|
681
|
+
correctResponses.RESPONSE ?? null,
|
|
682
|
+
declaration.cardinality === "ordered",
|
|
683
|
+
)
|
|
684
|
+
: null;
|
|
685
|
+
outcomes.SCORE = matches === true ? 1 : 0;
|
|
656
686
|
}
|
|
657
|
-
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function responseProcessingTemplateKind(
|
|
690
|
+
template: string | undefined,
|
|
691
|
+
): "matchCorrect" | "mapResponse" | "mapResponsePoint" | "unsupported" | undefined {
|
|
692
|
+
if (!template) return undefined;
|
|
693
|
+
const path = template.split(/[?#]/, 1)[0] ?? "";
|
|
694
|
+
const name = path.slice(path.lastIndexOf("/") + 1).replace(/\.xml$/i, "");
|
|
695
|
+
if (name === "match_correct") return "matchCorrect";
|
|
696
|
+
if (name === "map_response") return "mapResponse";
|
|
697
|
+
if (name === "map_response_point") return "mapResponsePoint";
|
|
698
|
+
return "unsupported";
|
|
658
699
|
}
|
|
659
700
|
|
|
660
701
|
function applyResponseRules(
|
|
@@ -823,7 +864,7 @@ function evaluateValue(
|
|
|
823
864
|
if (expression.type === "matchCorrect") {
|
|
824
865
|
const declaration = getResponseDeclaration(document, expression.correctIdentifier);
|
|
825
866
|
return declaration
|
|
826
|
-
?
|
|
867
|
+
? qtiMatchValues(
|
|
827
868
|
responses[expression.identifier] ?? null,
|
|
828
869
|
correctResponses[expression.correctIdentifier] ?? null,
|
|
829
870
|
declaration.cardinality === "ordered",
|
|
@@ -831,27 +872,29 @@ function evaluateValue(
|
|
|
831
872
|
: false;
|
|
832
873
|
}
|
|
833
874
|
if (expression.type === "match") {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
875
|
+
const left = evaluateValue(
|
|
876
|
+
expression.left,
|
|
877
|
+
document,
|
|
878
|
+
responses,
|
|
879
|
+
outcomes,
|
|
880
|
+
templateValues,
|
|
881
|
+
correctResponses,
|
|
882
|
+
random,
|
|
883
|
+
customOperators,
|
|
884
|
+
);
|
|
885
|
+
const right = evaluateValue(
|
|
886
|
+
expression.right,
|
|
887
|
+
document,
|
|
888
|
+
responses,
|
|
889
|
+
outcomes,
|
|
890
|
+
templateValues,
|
|
891
|
+
correctResponses,
|
|
892
|
+
random,
|
|
893
|
+
customOperators,
|
|
894
|
+
);
|
|
895
|
+
return qtiMatchValues(
|
|
896
|
+
left,
|
|
897
|
+
right,
|
|
855
898
|
expressionIsOrdered(expression.left, document) ||
|
|
856
899
|
expressionIsOrdered(expression.right, document),
|
|
857
900
|
);
|
|
@@ -957,89 +1000,57 @@ function evaluateValue(
|
|
|
957
1000
|
).length;
|
|
958
1001
|
}
|
|
959
1002
|
if (expression.type === "sum") {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
templateValues,
|
|
970
|
-
correctResponses,
|
|
971
|
-
random,
|
|
972
|
-
customOperators,
|
|
973
|
-
),
|
|
974
|
-
),
|
|
975
|
-
0,
|
|
1003
|
+
const values = evaluateNumericOperands(
|
|
1004
|
+
expression.expressions,
|
|
1005
|
+
document,
|
|
1006
|
+
responses,
|
|
1007
|
+
outcomes,
|
|
1008
|
+
templateValues,
|
|
1009
|
+
correctResponses,
|
|
1010
|
+
random,
|
|
1011
|
+
customOperators,
|
|
976
1012
|
);
|
|
1013
|
+
return values ? values.reduce((sum, value) => sum + value, 0) : null;
|
|
977
1014
|
}
|
|
978
1015
|
if (expression.type === "product") {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
templateValues,
|
|
989
|
-
correctResponses,
|
|
990
|
-
random,
|
|
991
|
-
customOperators,
|
|
992
|
-
),
|
|
993
|
-
),
|
|
994
|
-
1,
|
|
1016
|
+
const values = evaluateNumericOperands(
|
|
1017
|
+
expression.expressions,
|
|
1018
|
+
document,
|
|
1019
|
+
responses,
|
|
1020
|
+
outcomes,
|
|
1021
|
+
templateValues,
|
|
1022
|
+
correctResponses,
|
|
1023
|
+
random,
|
|
1024
|
+
customOperators,
|
|
995
1025
|
);
|
|
1026
|
+
return values ? values.reduce((product, value) => product * value, 1) : null;
|
|
996
1027
|
}
|
|
997
1028
|
if (expression.type === "min" || expression.type === "max") {
|
|
998
|
-
const values =
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
random,
|
|
1008
|
-
customOperators,
|
|
1009
|
-
),
|
|
1010
|
-
),
|
|
1029
|
+
const values = evaluateNumericOperands(
|
|
1030
|
+
expression.expressions,
|
|
1031
|
+
document,
|
|
1032
|
+
responses,
|
|
1033
|
+
outcomes,
|
|
1034
|
+
templateValues,
|
|
1035
|
+
correctResponses,
|
|
1036
|
+
random,
|
|
1037
|
+
customOperators,
|
|
1011
1038
|
);
|
|
1012
|
-
if (values.length === 0) return null;
|
|
1013
|
-
|
|
1014
|
-
return expression.type === "min" ? Math.min(...numericValues) : Math.max(...numericValues);
|
|
1039
|
+
if (!values || values.length === 0) return null;
|
|
1040
|
+
return expression.type === "min" ? Math.min(...values) : Math.max(...values);
|
|
1015
1041
|
}
|
|
1016
1042
|
if (expression.type === "subtract") {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
random,
|
|
1027
|
-
customOperators,
|
|
1028
|
-
),
|
|
1029
|
-
) -
|
|
1030
|
-
numericValue(
|
|
1031
|
-
evaluateValue(
|
|
1032
|
-
expression.right,
|
|
1033
|
-
document,
|
|
1034
|
-
responses,
|
|
1035
|
-
outcomes,
|
|
1036
|
-
templateValues,
|
|
1037
|
-
correctResponses,
|
|
1038
|
-
random,
|
|
1039
|
-
customOperators,
|
|
1040
|
-
),
|
|
1041
|
-
)
|
|
1043
|
+
const values = evaluateNumericOperands(
|
|
1044
|
+
[expression.left, expression.right],
|
|
1045
|
+
document,
|
|
1046
|
+
responses,
|
|
1047
|
+
outcomes,
|
|
1048
|
+
templateValues,
|
|
1049
|
+
correctResponses,
|
|
1050
|
+
random,
|
|
1051
|
+
customOperators,
|
|
1042
1052
|
);
|
|
1053
|
+
return values && values.length === 2 ? values[0]! - values[1]! : null;
|
|
1043
1054
|
}
|
|
1044
1055
|
if (expression.type === "divide") {
|
|
1045
1056
|
const dividendValue = evaluateValue(
|
|
@@ -1063,38 +1074,25 @@ function evaluateValue(
|
|
|
1063
1074
|
customOperators,
|
|
1064
1075
|
);
|
|
1065
1076
|
if (dividendValue === null || divisorValue === null) return null;
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1077
|
+
const dividend = numericValueOrNull(dividendValue);
|
|
1078
|
+
const divisor = numericValueOrNull(divisorValue);
|
|
1079
|
+
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1080
|
+
const quotient = dividend / divisor;
|
|
1069
1081
|
return Number.isFinite(quotient) ? quotient : null;
|
|
1070
1082
|
}
|
|
1071
1083
|
if (expression.type === "power") {
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
random,
|
|
1082
|
-
customOperators,
|
|
1083
|
-
),
|
|
1084
|
-
),
|
|
1085
|
-
numericValue(
|
|
1086
|
-
evaluateValue(
|
|
1087
|
-
expression.right,
|
|
1088
|
-
document,
|
|
1089
|
-
responses,
|
|
1090
|
-
outcomes,
|
|
1091
|
-
templateValues,
|
|
1092
|
-
correctResponses,
|
|
1093
|
-
random,
|
|
1094
|
-
customOperators,
|
|
1095
|
-
),
|
|
1096
|
-
),
|
|
1084
|
+
const values = evaluateNumericOperands(
|
|
1085
|
+
[expression.left, expression.right],
|
|
1086
|
+
document,
|
|
1087
|
+
responses,
|
|
1088
|
+
outcomes,
|
|
1089
|
+
templateValues,
|
|
1090
|
+
correctResponses,
|
|
1091
|
+
random,
|
|
1092
|
+
customOperators,
|
|
1097
1093
|
);
|
|
1094
|
+
if (!values || values.length !== 2) return null;
|
|
1095
|
+
const value = Math.pow(values[0]!, values[1]!);
|
|
1098
1096
|
return Number.isFinite(value) ? value : null;
|
|
1099
1097
|
}
|
|
1100
1098
|
if (expression.type === "integerDivide") {
|
|
@@ -1119,9 +1117,10 @@ function evaluateValue(
|
|
|
1119
1117
|
customOperators,
|
|
1120
1118
|
);
|
|
1121
1119
|
if (dividendValue === null || divisorValue === null) return null;
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1120
|
+
const dividend = numericValueOrNull(dividendValue);
|
|
1121
|
+
const divisor = numericValueOrNull(divisorValue);
|
|
1122
|
+
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1123
|
+
return Math.floor(dividend / divisor);
|
|
1125
1124
|
}
|
|
1126
1125
|
if (expression.type === "integerModulus") {
|
|
1127
1126
|
const dividendValue = evaluateValue(
|
|
@@ -1145,29 +1144,28 @@ function evaluateValue(
|
|
|
1145
1144
|
customOperators,
|
|
1146
1145
|
);
|
|
1147
1146
|
if (dividendValue === null || divisorValue === null) return null;
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1147
|
+
const dividend = numericValueOrNull(dividendValue);
|
|
1148
|
+
const divisor = numericValueOrNull(divisorValue);
|
|
1149
|
+
if (dividend === null || divisor === null || divisor === 0) return null;
|
|
1151
1150
|
return dividend - Math.floor(dividend / divisor) * divisor;
|
|
1152
1151
|
}
|
|
1153
1152
|
if (expression.type === "round") {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
customOperators,
|
|
1165
|
-
),
|
|
1153
|
+
const value = numericValueOrNull(
|
|
1154
|
+
evaluateValue(
|
|
1155
|
+
expression.expression,
|
|
1156
|
+
document,
|
|
1157
|
+
responses,
|
|
1158
|
+
outcomes,
|
|
1159
|
+
templateValues,
|
|
1160
|
+
correctResponses,
|
|
1161
|
+
random,
|
|
1162
|
+
customOperators,
|
|
1166
1163
|
),
|
|
1167
1164
|
);
|
|
1165
|
+
return value === null ? null : Math.round(value);
|
|
1168
1166
|
}
|
|
1169
1167
|
if (expression.type === "roundTo") {
|
|
1170
|
-
const value =
|
|
1168
|
+
const value = numericValueOrNull(
|
|
1171
1169
|
evaluateValue(
|
|
1172
1170
|
expression.expression,
|
|
1173
1171
|
document,
|
|
@@ -1179,25 +1177,25 @@ function evaluateValue(
|
|
|
1179
1177
|
customOperators,
|
|
1180
1178
|
),
|
|
1181
1179
|
);
|
|
1180
|
+
if (value === null) return null;
|
|
1182
1181
|
return expression.roundingMode === "decimalPlaces"
|
|
1183
1182
|
? roundToDecimalPlaces(value, expression.figures)
|
|
1184
1183
|
: roundToSignificantFigures(value, expression.figures);
|
|
1185
1184
|
}
|
|
1186
1185
|
if (expression.type === "truncate") {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
customOperators,
|
|
1198
|
-
),
|
|
1186
|
+
const value = numericValueOrNull(
|
|
1187
|
+
evaluateValue(
|
|
1188
|
+
expression.expression,
|
|
1189
|
+
document,
|
|
1190
|
+
responses,
|
|
1191
|
+
outcomes,
|
|
1192
|
+
templateValues,
|
|
1193
|
+
correctResponses,
|
|
1194
|
+
random,
|
|
1195
|
+
customOperators,
|
|
1199
1196
|
),
|
|
1200
1197
|
);
|
|
1198
|
+
return value === null ? null : Math.trunc(value);
|
|
1201
1199
|
}
|
|
1202
1200
|
if (expression.type === "integerToFloat") {
|
|
1203
1201
|
const value = evaluateValue(
|
|
@@ -1210,11 +1208,12 @@ function evaluateValue(
|
|
|
1210
1208
|
random,
|
|
1211
1209
|
customOperators,
|
|
1212
1210
|
);
|
|
1213
|
-
return
|
|
1211
|
+
return numericValueOrNull(value);
|
|
1214
1212
|
}
|
|
1215
1213
|
if (expression.type === "and") {
|
|
1216
|
-
|
|
1217
|
-
|
|
1214
|
+
let sawNull = false;
|
|
1215
|
+
for (const item of expression.expressions) {
|
|
1216
|
+
const value = booleanValueOrNull(
|
|
1218
1217
|
evaluateValue(
|
|
1219
1218
|
item,
|
|
1220
1219
|
document,
|
|
@@ -1225,8 +1224,11 @@ function evaluateValue(
|
|
|
1225
1224
|
random,
|
|
1226
1225
|
customOperators,
|
|
1227
1226
|
),
|
|
1228
|
-
)
|
|
1229
|
-
|
|
1227
|
+
);
|
|
1228
|
+
if (value === false) return false;
|
|
1229
|
+
if (value === null) sawNull = true;
|
|
1230
|
+
}
|
|
1231
|
+
return sawNull ? null : true;
|
|
1230
1232
|
}
|
|
1231
1233
|
if (expression.type === "anyN") {
|
|
1232
1234
|
const min = indexValue(expression.min, outcomes, templateValues) ?? 0;
|
|
@@ -1250,8 +1252,9 @@ function evaluateValue(
|
|
|
1250
1252
|
return null;
|
|
1251
1253
|
}
|
|
1252
1254
|
if (expression.type === "or") {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
+
let sawNull = false;
|
|
1256
|
+
for (const item of expression.expressions) {
|
|
1257
|
+
const value = booleanValueOrNull(
|
|
1255
1258
|
evaluateValue(
|
|
1256
1259
|
item,
|
|
1257
1260
|
document,
|
|
@@ -1262,11 +1265,14 @@ function evaluateValue(
|
|
|
1262
1265
|
random,
|
|
1263
1266
|
customOperators,
|
|
1264
1267
|
),
|
|
1265
|
-
)
|
|
1266
|
-
|
|
1268
|
+
);
|
|
1269
|
+
if (value === true) return true;
|
|
1270
|
+
if (value === null) sawNull = true;
|
|
1271
|
+
}
|
|
1272
|
+
return sawNull ? null : false;
|
|
1267
1273
|
}
|
|
1268
1274
|
if (expression.type === "not") {
|
|
1269
|
-
|
|
1275
|
+
const value = booleanValueOrNull(
|
|
1270
1276
|
evaluateValue(
|
|
1271
1277
|
expression.expression,
|
|
1272
1278
|
document,
|
|
@@ -1278,30 +1284,30 @@ function evaluateValue(
|
|
|
1278
1284
|
customOperators,
|
|
1279
1285
|
),
|
|
1280
1286
|
);
|
|
1287
|
+
return value === null ? null : !value;
|
|
1281
1288
|
}
|
|
1282
1289
|
if (expression.type === "equal") {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
customOperators,
|
|
1293
|
-
),
|
|
1294
|
-
evaluateValue(
|
|
1295
|
-
expression.right,
|
|
1296
|
-
document,
|
|
1297
|
-
responses,
|
|
1298
|
-
outcomes,
|
|
1299
|
-
templateValues,
|
|
1300
|
-
correctResponses,
|
|
1301
|
-
random,
|
|
1302
|
-
customOperators,
|
|
1303
|
-
),
|
|
1290
|
+
const left = evaluateValue(
|
|
1291
|
+
expression.left,
|
|
1292
|
+
document,
|
|
1293
|
+
responses,
|
|
1294
|
+
outcomes,
|
|
1295
|
+
templateValues,
|
|
1296
|
+
correctResponses,
|
|
1297
|
+
random,
|
|
1298
|
+
customOperators,
|
|
1304
1299
|
);
|
|
1300
|
+
const right = evaluateValue(
|
|
1301
|
+
expression.right,
|
|
1302
|
+
document,
|
|
1303
|
+
responses,
|
|
1304
|
+
outcomes,
|
|
1305
|
+
templateValues,
|
|
1306
|
+
correctResponses,
|
|
1307
|
+
random,
|
|
1308
|
+
customOperators,
|
|
1309
|
+
);
|
|
1310
|
+
return left === null || right === null ? null : valuesEqual(left, right);
|
|
1305
1311
|
}
|
|
1306
1312
|
if (expression.type === "equalRounded") {
|
|
1307
1313
|
const left = evaluateValue(
|
|
@@ -1325,16 +1331,11 @@ function evaluateValue(
|
|
|
1325
1331
|
customOperators,
|
|
1326
1332
|
);
|
|
1327
1333
|
if (left === null || right === null) return null;
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
);
|
|
1333
|
-
const roundedRight = roundWithMode(
|
|
1334
|
-
numericValue(right),
|
|
1335
|
-
expression.roundingMode,
|
|
1336
|
-
expression.figures,
|
|
1337
|
-
);
|
|
1334
|
+
const leftNumber = numericValueOrNull(left);
|
|
1335
|
+
const rightNumber = numericValueOrNull(right);
|
|
1336
|
+
if (leftNumber === null || rightNumber === null) return null;
|
|
1337
|
+
const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, expression.figures);
|
|
1338
|
+
const roundedRight = roundWithMode(rightNumber, expression.roundingMode, expression.figures);
|
|
1338
1339
|
return roundedLeft === null || roundedRight === null ? null : roundedLeft === roundedRight;
|
|
1339
1340
|
}
|
|
1340
1341
|
if (expression.type === "numericCompare") {
|
|
@@ -1359,8 +1360,9 @@ function evaluateValue(
|
|
|
1359
1360
|
customOperators,
|
|
1360
1361
|
);
|
|
1361
1362
|
if (leftValue === null || rightValue === null) return null;
|
|
1362
|
-
const left =
|
|
1363
|
-
const right =
|
|
1363
|
+
const left = numericValueOrNull(leftValue);
|
|
1364
|
+
const right = numericValueOrNull(rightValue);
|
|
1365
|
+
if (left === null || right === null) return null;
|
|
1364
1366
|
if (expression.operator === "lt") return left < right;
|
|
1365
1367
|
if (expression.operator === "lte") return left <= right;
|
|
1366
1368
|
if (expression.operator === "gt") return left > right;
|
|
@@ -1462,7 +1464,9 @@ function evaluateValue(
|
|
|
1462
1464
|
templateValues[expression.pattern] ??
|
|
1463
1465
|
expression.pattern;
|
|
1464
1466
|
try {
|
|
1465
|
-
return new RegExp(
|
|
1467
|
+
return new RegExp(
|
|
1468
|
+
typeof patternValue === "string" ? patternValue : qtiValueToString(patternValue),
|
|
1469
|
+
).test(qtiValueToString(value));
|
|
1466
1470
|
} catch {
|
|
1467
1471
|
return null;
|
|
1468
1472
|
}
|
|
@@ -1501,8 +1505,9 @@ function evaluateValue(
|
|
|
1501
1505
|
random,
|
|
1502
1506
|
customOperators,
|
|
1503
1507
|
);
|
|
1508
|
+
if (value === null || collection === null) return null;
|
|
1504
1509
|
const values = valueContainer(collection);
|
|
1505
|
-
return
|
|
1510
|
+
return values.some((item) => valuesEqual(item, value));
|
|
1506
1511
|
}
|
|
1507
1512
|
if (expression.type === "delete") {
|
|
1508
1513
|
const value = evaluateValue(
|
|
@@ -1515,47 +1520,46 @@ function evaluateValue(
|
|
|
1515
1520
|
random,
|
|
1516
1521
|
customOperators,
|
|
1517
1522
|
);
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
customOperators,
|
|
1528
|
-
),
|
|
1523
|
+
const collectionValue = evaluateValue(
|
|
1524
|
+
expression.collection,
|
|
1525
|
+
document,
|
|
1526
|
+
responses,
|
|
1527
|
+
outcomes,
|
|
1528
|
+
templateValues,
|
|
1529
|
+
correctResponses,
|
|
1530
|
+
random,
|
|
1531
|
+
customOperators,
|
|
1529
1532
|
);
|
|
1530
|
-
if (value === null ||
|
|
1533
|
+
if (value === null || collectionValue === null) return null;
|
|
1534
|
+
const collection = valueContainer(collectionValue);
|
|
1535
|
+
if (collection.length === 0) return null;
|
|
1531
1536
|
const filtered = collection.filter((item) => !valuesEqual(item, value));
|
|
1532
1537
|
return filtered.length > 0 ? filtered : null;
|
|
1533
1538
|
}
|
|
1534
1539
|
if (expression.type === "contains") {
|
|
1535
|
-
const
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
customOperators,
|
|
1545
|
-
),
|
|
1540
|
+
const collectionValue = evaluateValue(
|
|
1541
|
+
expression.collection,
|
|
1542
|
+
document,
|
|
1543
|
+
responses,
|
|
1544
|
+
outcomes,
|
|
1545
|
+
templateValues,
|
|
1546
|
+
correctResponses,
|
|
1547
|
+
random,
|
|
1548
|
+
customOperators,
|
|
1546
1549
|
);
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
customOperators,
|
|
1557
|
-
),
|
|
1550
|
+
const valuesValue = evaluateValue(
|
|
1551
|
+
expression.values,
|
|
1552
|
+
document,
|
|
1553
|
+
responses,
|
|
1554
|
+
outcomes,
|
|
1555
|
+
templateValues,
|
|
1556
|
+
correctResponses,
|
|
1557
|
+
random,
|
|
1558
|
+
customOperators,
|
|
1558
1559
|
);
|
|
1560
|
+
if (collectionValue === null || valuesValue === null) return null;
|
|
1561
|
+
const collection = valueContainer(collectionValue);
|
|
1562
|
+
const values = valueContainer(valuesValue);
|
|
1559
1563
|
if (collection.length === 0 || values.length === 0) return null;
|
|
1560
1564
|
return containsValues(collection, values);
|
|
1561
1565
|
}
|
|
@@ -1769,7 +1773,11 @@ function scoreAreaMapping(
|
|
|
1769
1773
|
response: QtiValue,
|
|
1770
1774
|
areaMapping: NonNullable<QtiResponseDeclaration["areaMapping"]>,
|
|
1771
1775
|
): number {
|
|
1772
|
-
const points = Array.isArray(response)
|
|
1776
|
+
const points = Array.isArray(response)
|
|
1777
|
+
? response.map(qtiScalarToString)
|
|
1778
|
+
: response === null
|
|
1779
|
+
? []
|
|
1780
|
+
: qtiValueToStringList(response);
|
|
1773
1781
|
let score = 0;
|
|
1774
1782
|
for (const point of points) {
|
|
1775
1783
|
const parsed = parsePoint(String(point));
|
|
@@ -1907,7 +1915,10 @@ function scoreMapping(
|
|
|
1907
1915
|
);
|
|
1908
1916
|
return clampMappedScore(score, mapping.attributes);
|
|
1909
1917
|
}
|
|
1910
|
-
const score =
|
|
1918
|
+
const score =
|
|
1919
|
+
response === null || isRecordValue(response)
|
|
1920
|
+
? 0
|
|
1921
|
+
: (values[String(response)] ?? mapping.defaultValue);
|
|
1911
1922
|
return clampMappedScore(score, mapping.attributes);
|
|
1912
1923
|
}
|
|
1913
1924
|
|
|
@@ -1950,6 +1961,11 @@ function valuesEqual(actual: QtiValue, expected: QtiValue, ordered = false): boo
|
|
|
1950
1961
|
return scalarValuesEqual(actual, expected);
|
|
1951
1962
|
}
|
|
1952
1963
|
|
|
1964
|
+
function qtiMatchValues(actual: QtiValue, expected: QtiValue, ordered = false): boolean | null {
|
|
1965
|
+
if (actual === null || expected === null) return null;
|
|
1966
|
+
return valuesEqual(actual, expected, ordered);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1953
1969
|
function scalarValuesEqual(actual: QtiValue, expected: QtiValue): boolean {
|
|
1954
1970
|
if (typeof actual === "boolean" && typeof expected === "string") {
|
|
1955
1971
|
return String(actual) === expected;
|
|
@@ -1985,33 +2001,13 @@ function isRecordValue(value: QtiValue): value is QtiRecordValue {
|
|
|
1985
2001
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1986
2002
|
}
|
|
1987
2003
|
|
|
1988
|
-
function isQtiValue(value: unknown): value is QtiValue {
|
|
1989
|
-
if (value === null) return true;
|
|
1990
|
-
if (isQtiScalarValue(value)) return true;
|
|
1991
|
-
if (Array.isArray(value)) return value.every(isQtiScalarValue);
|
|
1992
|
-
return isQtiValueRecord(value);
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
function isQtiScalarValue(value: unknown): value is QtiScalarValue {
|
|
1996
|
-
return (
|
|
1997
|
-
typeof value === "string" ||
|
|
1998
|
-
typeof value === "boolean" ||
|
|
1999
|
-
(typeof value === "number" && Number.isFinite(value))
|
|
2000
|
-
);
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
2004
|
function isQtiValueRecord(value: unknown): value is Record<string, QtiValue> {
|
|
2004
2005
|
if (!isRecord(value)) return false;
|
|
2005
2006
|
return Object.values(value).every(isQtiValue);
|
|
2006
2007
|
}
|
|
2007
2008
|
|
|
2008
2009
|
function isPortableCustomState(value: unknown): value is QtiPortableCustomStateValue {
|
|
2009
|
-
|
|
2010
|
-
if (typeof value === "string" || typeof value === "boolean") return true;
|
|
2011
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
2012
|
-
if (Array.isArray(value)) return value.every(isPortableCustomState);
|
|
2013
|
-
if (isRecord(value)) return Object.values(value).every(isPortableCustomState);
|
|
2014
|
-
return false;
|
|
2010
|
+
return isQtiPortableCustomStateValue(value);
|
|
2015
2011
|
}
|
|
2016
2012
|
|
|
2017
2013
|
function isPortableCustomStateObject(
|
|
@@ -2052,8 +2048,41 @@ function indexValue(
|
|
|
2052
2048
|
const parsed = Number(n);
|
|
2053
2049
|
if (Number.isInteger(parsed)) return parsed;
|
|
2054
2050
|
const value = outcomes[n] ?? templateValues[n] ?? null;
|
|
2055
|
-
const numeric =
|
|
2056
|
-
return Number.isInteger(numeric) ? numeric : undefined;
|
|
2051
|
+
const numeric = numericValueOrNull(value);
|
|
2052
|
+
return numeric !== null && Number.isInteger(numeric) ? numeric : undefined;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function evaluateNumericOperands(
|
|
2056
|
+
expressions: QtiProcessingExpression[],
|
|
2057
|
+
document: QtiDocument,
|
|
2058
|
+
responses: Record<string, QtiValue>,
|
|
2059
|
+
outcomes: Record<string, QtiValue>,
|
|
2060
|
+
templateValues: Record<string, QtiValue>,
|
|
2061
|
+
correctResponses: Record<string, QtiValue>,
|
|
2062
|
+
random: () => number,
|
|
2063
|
+
customOperators: QtiCustomOperatorRegistry,
|
|
2064
|
+
): number[] | null {
|
|
2065
|
+
const numericValues: number[] = [];
|
|
2066
|
+
for (const expression of expressions) {
|
|
2067
|
+
const value = evaluateValue(
|
|
2068
|
+
expression,
|
|
2069
|
+
document,
|
|
2070
|
+
responses,
|
|
2071
|
+
outcomes,
|
|
2072
|
+
templateValues,
|
|
2073
|
+
correctResponses,
|
|
2074
|
+
random,
|
|
2075
|
+
customOperators,
|
|
2076
|
+
);
|
|
2077
|
+
if (value === null || isRecordValue(value)) return null;
|
|
2078
|
+
const values = Array.isArray(value) ? value : [value];
|
|
2079
|
+
for (const item of values) {
|
|
2080
|
+
const numeric = numericValueOrNull(item);
|
|
2081
|
+
if (numeric === null) return null;
|
|
2082
|
+
numericValues.push(numeric);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return numericValues;
|
|
2057
2086
|
}
|
|
2058
2087
|
|
|
2059
2088
|
function containsValues(collection: QtiScalarValue[], values: QtiScalarValue[]): boolean {
|
|
@@ -2077,6 +2106,16 @@ function numericValue(value: QtiValue): number {
|
|
|
2077
2106
|
return 0;
|
|
2078
2107
|
}
|
|
2079
2108
|
|
|
2109
|
+
function numericValueOrNull(value: QtiValue): number | null {
|
|
2110
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
2111
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
2112
|
+
if (typeof value === "string") {
|
|
2113
|
+
const parsed = Number(value);
|
|
2114
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2115
|
+
}
|
|
2116
|
+
return null;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2080
2119
|
function durationSeconds(value: QtiValue): number | null {
|
|
2081
2120
|
if (value === null || Array.isArray(value) || isRecordValue(value)) return null;
|
|
2082
2121
|
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
@@ -2107,6 +2146,11 @@ function booleanValue(value: QtiValue): boolean {
|
|
|
2107
2146
|
return false;
|
|
2108
2147
|
}
|
|
2109
2148
|
|
|
2149
|
+
function booleanValueOrNull(value: QtiValue): boolean | null {
|
|
2150
|
+
if (value === null) return null;
|
|
2151
|
+
return booleanValue(value);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2110
2154
|
function roundToDecimalPlaces(value: number, figures: number): number {
|
|
2111
2155
|
const factor = 10 ** figures;
|
|
2112
2156
|
return Math.round(value * factor) / factor;
|
|
@@ -2256,9 +2300,10 @@ function stringMatch(
|
|
|
2256
2300
|
right: QtiValue,
|
|
2257
2301
|
caseSensitive: boolean,
|
|
2258
2302
|
substring: boolean,
|
|
2259
|
-
): boolean {
|
|
2260
|
-
|
|
2261
|
-
let
|
|
2303
|
+
): boolean | null {
|
|
2304
|
+
if (left === null || right === null) return null;
|
|
2305
|
+
let actual = qtiValueToString(left);
|
|
2306
|
+
let expected = qtiValueToString(right);
|
|
2262
2307
|
if (!caseSensitive) {
|
|
2263
2308
|
actual = actual.toLocaleLowerCase();
|
|
2264
2309
|
expected = expected.toLocaleLowerCase();
|