@conform-ed/qti-react 0.0.12 → 0.0.13
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/dist/index.js +4566 -212
- package/package.json +3 -1
- package/src/capability.ts +24 -0
- package/src/content-model.ts +104 -5
- package/src/graphic.ts +103 -0
- package/src/index.ts +139 -3
- package/src/interactions/associate.ts +22 -0
- package/src/interactions/drawing.ts +24 -0
- package/src/interactions/end-attempt.ts +19 -0
- package/src/interactions/extended-text.ts +21 -0
- package/src/interactions/gap-match.ts +22 -0
- package/src/interactions/graphic.ts +104 -0
- package/src/interactions/hottext.ts +21 -0
- package/src/interactions/index.ts +57 -2
- package/src/interactions/match.ts +27 -0
- package/src/interactions/media.ts +24 -0
- package/src/interactions/order.ts +21 -0
- package/src/interactions/slider.ts +24 -0
- package/src/interactions/upload.ts +19 -0
- package/src/normalized-item.ts +561 -0
- package/src/pci/index.ts +22 -0
- package/src/pci/interaction.ts +42 -0
- package/src/pci/markup.ts +102 -0
- package/src/pci/mount.ts +135 -0
- package/src/pci/registry.ts +240 -0
- package/src/pci/response.ts +138 -0
- package/src/pci/skin.ts +87 -0
- package/src/reference-skin/associate.ts +98 -0
- package/src/reference-skin/choice.ts +44 -0
- package/src/reference-skin/content.ts +30 -0
- package/src/reference-skin/drawing.ts +150 -0
- package/src/reference-skin/end-attempt.ts +27 -0
- package/src/reference-skin/extended-text.ts +35 -0
- package/src/reference-skin/gap-match.ts +69 -0
- package/src/reference-skin/graphic-associate.ts +123 -0
- package/src/reference-skin/graphic-base.ts +142 -0
- package/src/reference-skin/graphic-gap-match.ts +143 -0
- package/src/reference-skin/graphic-order.ts +76 -0
- package/src/reference-skin/hotspot.ts +43 -0
- package/src/reference-skin/hottext.ts +42 -0
- package/src/reference-skin/index.ts +75 -0
- package/src/reference-skin/inline-choice.ts +42 -0
- package/src/reference-skin/match.ts +80 -0
- package/src/reference-skin/media.ts +74 -0
- package/src/reference-skin/order.ts +79 -0
- package/src/reference-skin/position-object.ts +84 -0
- package/src/reference-skin/select-point.ts +87 -0
- package/src/reference-skin/slider.ts +41 -0
- package/src/reference-skin/text-entry.ts +31 -0
- package/src/reference-skin/upload.ts +46 -0
- package/src/response-processing.ts +178 -29
- package/src/rp/evaluate.ts +828 -0
- package/src/rp/index.ts +30 -0
- package/src/rp/interpreter.ts +251 -0
- package/src/rp/template-processing.ts +295 -0
- package/src/rp/templates.ts +190 -0
- package/src/rp/types.ts +161 -0
- package/src/rp/values.ts +198 -0
- package/src/runtime.ts +474 -28
- package/src/store.ts +155 -5
- package/src/test/controller.ts +806 -0
- package/src/test/index.ts +25 -0
- package/src/test/session-store.ts +244 -0
- package/src/test/types.ts +203 -0
- package/src/types.ts +27 -1
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Standard-template scoring helpers, spec-strict by default (ADR-0004). `match_correct`
|
|
3
|
+
* is an exact match and mapping entries default to caseSensitive=true, per spec. The
|
|
4
|
+
* optional `normalize` parameter is the Response Normalization hook: a consumer-
|
|
5
|
+
* configured transform applied to both sides of string comparisons (off by default,
|
|
6
|
+
* always off in conformance runs). The RP interpreter (`src/rp/`) reuses these for its
|
|
7
|
+
* `mapResponse` operator; `scoreResponse` also backs the per-interaction feedback
|
|
8
|
+
* chrome in the runtime. Pure functions: deterministic given (declaration, response),
|
|
9
|
+
* so scoring is replayable and runs fully offline in the headless core.
|
|
8
10
|
*/
|
|
9
11
|
|
|
12
|
+
import { parsePoint, pointInShape } from "./graphic";
|
|
13
|
+
import type { ResponseNormalization } from "./rp/types";
|
|
10
14
|
import type { ResponseDeclarationView, ResponseValue, ScoreResult } from "./types";
|
|
11
15
|
|
|
12
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Lowercase + strip combining diacritics. Exported as a ready-made Response
|
|
18
|
+
* Normalization for language-learning leniency ("cafe" ≈ "Café") — a documented
|
|
19
|
+
* deviation a consumer must opt into.
|
|
20
|
+
*/
|
|
13
21
|
export function foldString(value: string): string {
|
|
14
22
|
return value
|
|
15
23
|
.normalize("NFD")
|
|
@@ -22,30 +30,86 @@ function asList(response: ResponseValue): string[] {
|
|
|
22
30
|
return [];
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
// Records are not list-shaped; heuristic scoring treats them as empty.
|
|
34
|
+
return typeof response === "string" ? [response] : Array.isArray(response) ? [...response] : [];
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
function isStringBaseType(declaration: ResponseDeclarationView): boolean {
|
|
29
38
|
return declaration.baseType === "string" || declaration.baseType === undefined;
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Pair values serialize as two space-separated identifiers ("A B"). A `pair` is
|
|
43
|
+
* unordered within the pair ("A B" ≡ "B A"); a `directedPair` is ordered.
|
|
44
|
+
*/
|
|
45
|
+
function pairsEqual(a: string, b: string, directed: boolean): boolean {
|
|
46
|
+
const [a1, a2] = a.trim().split(/\s+/u);
|
|
47
|
+
const [b1, b2] = b.trim().split(/\s+/u);
|
|
48
|
+
|
|
49
|
+
if (a1 === undefined || a2 === undefined || b1 === undefined || b2 === undefined) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (a1 === b1 && a2 === b2) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return !directed && a1 === b2 && a2 === b1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const numericBaseTypes = new Set(["float", "integer"]);
|
|
61
|
+
|
|
62
|
+
/** Value equality for one declared baseType, used by both match_correct and map_response. */
|
|
63
|
+
function makeValueComparator(
|
|
64
|
+
declaration: ResponseDeclarationView,
|
|
65
|
+
normalize?: ResponseNormalization,
|
|
66
|
+
): (a: string, b: string) => boolean {
|
|
67
|
+
if (declaration.baseType === "pair") {
|
|
68
|
+
return (a, b) => pairsEqual(a, b, false);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (declaration.baseType === "directedPair") {
|
|
72
|
+
return (a, b) => pairsEqual(a, b, true);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (declaration.baseType !== undefined && numericBaseTypes.has(declaration.baseType)) {
|
|
76
|
+
return (a, b) => a.trim() !== "" && b.trim() !== "" && Number(a) === Number(b);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (declaration.baseType === "point") {
|
|
80
|
+
return (a, b) => {
|
|
81
|
+
const pointA = parsePoint(a);
|
|
82
|
+
const pointB = parsePoint(b);
|
|
83
|
+
|
|
84
|
+
return pointA !== null && pointB !== null && pointA.x === pointB.x && pointA.y === pointB.y;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (normalize && isStringBaseType(declaration)) {
|
|
89
|
+
return (a, b) => normalize(a, declaration) === normalize(b, declaration);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (a, b) => a === b;
|
|
34
93
|
}
|
|
35
94
|
|
|
36
95
|
/**
|
|
37
96
|
* `match_correct`: true when the response exactly matches `correctResponse`, respecting
|
|
38
|
-
* cardinality
|
|
39
|
-
*
|
|
97
|
+
* cardinality and baseType. Spec-strict: no case or diacritic folding unless the
|
|
98
|
+
* consumer passes a Response Normalization. Returns false when no correctResponse
|
|
99
|
+
* exists.
|
|
40
100
|
*/
|
|
41
|
-
export function matchCorrect(
|
|
101
|
+
export function matchCorrect(
|
|
102
|
+
declaration: ResponseDeclarationView,
|
|
103
|
+
response: ResponseValue,
|
|
104
|
+
normalize?: ResponseNormalization,
|
|
105
|
+
): boolean {
|
|
42
106
|
const correct = declaration.correctResponse;
|
|
43
107
|
|
|
44
108
|
if (!correct) {
|
|
45
109
|
return false;
|
|
46
110
|
}
|
|
47
111
|
|
|
48
|
-
const
|
|
112
|
+
const equals = makeValueComparator(declaration, normalize);
|
|
49
113
|
const expected = correct.values.map((entry) => entry.value);
|
|
50
114
|
const actual = asList(response);
|
|
51
115
|
|
|
@@ -54,14 +118,14 @@ export function matchCorrect(declaration: ResponseDeclarationView, response: Res
|
|
|
54
118
|
}
|
|
55
119
|
|
|
56
120
|
if (declaration.cardinality === "ordered") {
|
|
57
|
-
return expected.every((value, index) =>
|
|
121
|
+
return expected.every((value, index) => equals(value, actual[index] ?? ""));
|
|
58
122
|
}
|
|
59
123
|
|
|
60
124
|
// single + multiple: order-independent set match
|
|
61
125
|
const remaining = [...actual];
|
|
62
126
|
|
|
63
127
|
for (const value of expected) {
|
|
64
|
-
const matchIndex = remaining.findIndex((candidate) =>
|
|
128
|
+
const matchIndex = remaining.findIndex((candidate) => equals(candidate, value));
|
|
65
129
|
|
|
66
130
|
if (matchIndex === -1) {
|
|
67
131
|
return false;
|
|
@@ -89,24 +153,48 @@ function clamp(value: number, lower: number | undefined, upper: number | undefin
|
|
|
89
153
|
|
|
90
154
|
/**
|
|
91
155
|
* `map_response`: sum the mapped values of the response's members, each member mapped at
|
|
92
|
-
* most once.
|
|
93
|
-
*
|
|
94
|
-
*
|
|
156
|
+
* most once. Per spec, entries default to caseSensitive=true; `caseSensitive: false`
|
|
157
|
+
* lowercases both sides. A Response Normalization, when configured, applies on top for
|
|
158
|
+
* string base types. Applies the mapping's `defaultValue` to unmatched members and
|
|
159
|
+
* clamps to [lowerBound, upperBound]. Returns 0 when no mapping exists.
|
|
95
160
|
*/
|
|
96
|
-
export function mapResponse(
|
|
161
|
+
export function mapResponse(
|
|
162
|
+
declaration: ResponseDeclarationView,
|
|
163
|
+
response: ResponseValue,
|
|
164
|
+
normalize?: ResponseNormalization,
|
|
165
|
+
): number {
|
|
97
166
|
const mapping = declaration.mapping;
|
|
98
167
|
|
|
99
168
|
if (!mapping) {
|
|
100
169
|
return 0;
|
|
101
170
|
}
|
|
102
171
|
|
|
172
|
+
const isPairType = declaration.baseType === "pair" || declaration.baseType === "directedPair";
|
|
173
|
+
const applyNormalize = normalize && isStringBaseType(declaration) ? normalize : undefined;
|
|
103
174
|
const defaultValue = mapping.defaultValue ?? 0;
|
|
104
175
|
let total = 0;
|
|
105
176
|
|
|
106
177
|
for (const member of asList(response)) {
|
|
107
|
-
const entry = mapping.mapEntries.find((candidate) =>
|
|
108
|
-
|
|
109
|
-
|
|
178
|
+
const entry = mapping.mapEntries.find((candidate) => {
|
|
179
|
+
if (isPairType) {
|
|
180
|
+
return pairsEqual(candidate.mapKey, member, declaration.baseType === "directedPair");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let key = candidate.mapKey;
|
|
184
|
+
let candidateMember = member;
|
|
185
|
+
|
|
186
|
+
if (candidate.caseSensitive === false) {
|
|
187
|
+
key = key.toLocaleLowerCase();
|
|
188
|
+
candidateMember = candidateMember.toLocaleLowerCase();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (applyNormalize) {
|
|
192
|
+
key = applyNormalize(key, declaration);
|
|
193
|
+
candidateMember = applyNormalize(candidateMember, declaration);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return key === candidateMember;
|
|
197
|
+
});
|
|
110
198
|
|
|
111
199
|
total += entry ? entry.mappedValue : defaultValue;
|
|
112
200
|
}
|
|
@@ -115,13 +203,74 @@ export function mapResponse(declaration: ResponseDeclarationView, response: Resp
|
|
|
115
203
|
}
|
|
116
204
|
|
|
117
205
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
206
|
+
* `map_response_point`: sum the mapped values of the areas hit by the response's point
|
|
207
|
+
* members. Per spec each area counts at most once regardless of how many points land in
|
|
208
|
+
* it; points hitting no area add the mapping's `defaultValue`. Clamps to
|
|
209
|
+
* [lowerBound, upperBound]. Returns 0 when no areaMapping exists.
|
|
121
210
|
*/
|
|
122
|
-
export function
|
|
211
|
+
export function mapResponsePoint(declaration: ResponseDeclarationView, response: ResponseValue): number {
|
|
212
|
+
const areaMapping = declaration.areaMapping;
|
|
213
|
+
|
|
214
|
+
if (!areaMapping) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const defaultValue = areaMapping.defaultValue ?? 0;
|
|
219
|
+
const usedAreas = new Set<number>();
|
|
220
|
+
let total = 0;
|
|
221
|
+
|
|
222
|
+
for (const member of asList(response)) {
|
|
223
|
+
const point = parsePoint(member);
|
|
224
|
+
|
|
225
|
+
if (!point) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const areaIndex = areaMapping.areaMapEntries.findIndex(
|
|
230
|
+
(entry, index) => !usedAreas.has(index) && pointInShape(entry.shape, entry.coords, point),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (areaIndex === -1) {
|
|
234
|
+
total += defaultValue;
|
|
235
|
+
} else {
|
|
236
|
+
usedAreas.add(areaIndex);
|
|
237
|
+
total += areaMapping.areaMapEntries[areaIndex]!.mappedValue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return clamp(total, areaMapping.lowerBound, areaMapping.upperBound);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Apply the appropriate standard template: `map_response_point` when an areaMapping is
|
|
246
|
+
* declared, `map_response` when a mapping is declared, otherwise `match_correct`. `maxScore` is the mapping upper bound (or the sum of
|
|
247
|
+
* positive mapped values) for mapped items, else 1 for match_correct. This heuristic
|
|
248
|
+
* backs the per-interaction feedback chrome; item outcomes of record come from the RP
|
|
249
|
+
* interpreter when the item declares `responseProcessing`.
|
|
250
|
+
*/
|
|
251
|
+
export function scoreResponse(
|
|
252
|
+
declaration: ResponseDeclarationView,
|
|
253
|
+
response: ResponseValue,
|
|
254
|
+
normalize?: ResponseNormalization,
|
|
255
|
+
): ScoreResult {
|
|
256
|
+
if (declaration.areaMapping) {
|
|
257
|
+
const score = mapResponsePoint(declaration, response);
|
|
258
|
+
const positiveSum = declaration.areaMapping.areaMapEntries.reduce(
|
|
259
|
+
(sum, entry) => sum + Math.max(entry.mappedValue, 0),
|
|
260
|
+
0,
|
|
261
|
+
);
|
|
262
|
+
const maxScore = declaration.areaMapping.upperBound ?? positiveSum;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
identifier: declaration.identifier,
|
|
266
|
+
score,
|
|
267
|
+
maxScore,
|
|
268
|
+
correct: maxScore > 0 && score >= maxScore,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
123
272
|
if (declaration.mapping) {
|
|
124
|
-
const score = mapResponse(declaration, response);
|
|
273
|
+
const score = mapResponse(declaration, response, normalize);
|
|
125
274
|
const positiveSum = declaration.mapping.mapEntries.reduce((sum, entry) => sum + Math.max(entry.mappedValue, 0), 0);
|
|
126
275
|
const maxScore = declaration.mapping.upperBound ?? positiveSum;
|
|
127
276
|
|
|
@@ -133,7 +282,7 @@ export function scoreResponse(declaration: ResponseDeclarationView, response: Re
|
|
|
133
282
|
};
|
|
134
283
|
}
|
|
135
284
|
|
|
136
|
-
const correct = matchCorrect(declaration, response);
|
|
285
|
+
const correct = matchCorrect(declaration, response, normalize);
|
|
137
286
|
|
|
138
287
|
return {
|
|
139
288
|
identifier: declaration.identifier,
|