@conform-ed/qti-react 0.0.13 → 0.0.15
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/capability.d.ts +17 -0
- package/dist/content-model.d.ts +42 -0
- package/dist/graphic.d.ts +23 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +155 -162
- package/dist/interactions/associate.d.ts +2 -0
- package/dist/interactions/choice.d.ts +2 -0
- package/dist/interactions/drawing.d.ts +2 -0
- package/dist/interactions/end-attempt.d.ts +2 -0
- package/dist/interactions/extended-text.d.ts +2 -0
- package/dist/interactions/gap-match.d.ts +2 -0
- package/dist/interactions/graphic.d.ts +13 -0
- package/dist/interactions/hottext.d.ts +2 -0
- package/dist/interactions/index.d.ts +18 -0
- package/dist/interactions/inline-choice.d.ts +2 -0
- package/dist/interactions/match.d.ts +2 -0
- package/dist/interactions/media.d.ts +2 -0
- package/dist/interactions/order.d.ts +2 -0
- package/dist/interactions/slider.d.ts +2 -0
- package/dist/interactions/text-entry.d.ts +2 -0
- package/dist/interactions/upload.d.ts +2 -0
- package/dist/normalized-item.d.ts +30 -0
- package/dist/pci/index.d.ts +6 -0
- package/dist/pci/interaction.d.ts +8 -0
- package/dist/pci/markup.d.ts +10 -0
- package/dist/pci/mount.d.ts +50 -0
- package/dist/pci/registry.d.ts +53 -0
- package/dist/pci/response.d.ts +11 -0
- package/dist/pci/skin.d.ts +12 -0
- package/dist/reference-skin/associate.d.ts +8 -0
- package/dist/reference-skin/choice.d.ts +8 -0
- package/dist/reference-skin/content.d.ts +6 -0
- package/dist/reference-skin/drawing.d.ts +9 -0
- package/dist/reference-skin/end-attempt.d.ts +7 -0
- package/dist/reference-skin/extended-text.d.ts +6 -0
- package/dist/reference-skin/gap-match.d.ts +8 -0
- package/dist/reference-skin/graphic-associate.d.ts +8 -0
- package/dist/reference-skin/graphic-base.d.ts +39 -0
- package/dist/reference-skin/graphic-gap-match.d.ts +8 -0
- package/dist/reference-skin/graphic-order.d.ts +8 -0
- package/dist/reference-skin/hotspot.d.ts +8 -0
- package/dist/reference-skin/hottext.d.ts +8 -0
- package/dist/reference-skin/index.d.ts +30 -0
- package/dist/reference-skin/inline-choice.d.ts +7 -0
- package/dist/reference-skin/match.d.ts +8 -0
- package/dist/reference-skin/media.d.ts +9 -0
- package/dist/reference-skin/order.d.ts +8 -0
- package/dist/reference-skin/position-object.d.ts +9 -0
- package/dist/reference-skin/select-point.d.ts +8 -0
- package/dist/reference-skin/slider.d.ts +8 -0
- package/dist/reference-skin/text-entry.d.ts +6 -0
- package/dist/reference-skin/upload.d.ts +8 -0
- package/dist/response-processing.d.ts +48 -0
- package/dist/rp/evaluate.d.ts +35 -0
- package/dist/rp/index.d.ts +4 -0
- package/dist/rp/interpreter.d.ts +15 -0
- package/dist/rp/template-processing.d.ts +49 -0
- package/dist/rp/templates.d.ts +8 -0
- package/dist/rp/types.d.ts +158 -0
- package/dist/rp/values.d.ts +27 -0
- package/dist/runtime.d.ts +164 -0
- package/dist/store.d.ts +61 -0
- package/dist/test/controller.d.ts +11 -0
- package/dist/test/index.d.ts +3 -0
- package/dist/test/session-store.d.ts +46 -0
- package/dist/test/types.d.ts +194 -0
- package/dist/types.d.ts +58 -0
- package/package.json +6 -6
- package/src/interactions/associate.ts +2 -2
- package/src/interactions/choice.ts +2 -2
- package/src/interactions/drawing.ts +2 -2
- package/src/interactions/end-attempt.ts +2 -2
- package/src/interactions/extended-text.ts +2 -2
- package/src/interactions/gap-match.ts +2 -2
- package/src/interactions/graphic.ts +7 -7
- package/src/interactions/hottext.ts +2 -2
- package/src/interactions/index.ts +0 -1
- package/src/interactions/inline-choice.ts +2 -2
- package/src/interactions/match.ts +2 -2
- package/src/interactions/media.ts +2 -2
- package/src/interactions/order.ts +2 -2
- package/src/interactions/slider.ts +2 -2
- package/src/interactions/text-entry.ts +2 -2
- package/src/interactions/upload.ts +2 -2
- package/src/normalized-item.ts +6 -4
- package/src/pci/interaction.ts +2 -2
- package/src/pci/mount.ts +12 -5
- package/src/pci/skin.ts +0 -1
- package/src/reference-skin/drawing.ts +25 -15
- package/src/reference-skin/index.ts +2 -3
- package/src/rp/evaluate.ts +22 -23
- package/src/rp/interpreter.ts +9 -6
- package/src/rp/template-processing.ts +8 -13
- package/src/rp/types.ts +12 -6
- package/src/rp/values.ts +19 -6
- package/src/runtime.ts +9 -7
- package/src/store.ts +14 -8
- package/src/test/controller.ts +10 -7
- package/src/test/session-store.ts +0 -1
package/src/pci/mount.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
import type { BodyNode } from "../runtime";
|
|
10
10
|
import type { ResponseDeclarationView, ResponseValue } from "../types";
|
|
11
|
-
|
|
12
11
|
import { serializePciMarkup } from "./markup";
|
|
13
12
|
import type { PciConfiguration, PciInstance, PciModule, PciModuleRegistry } from "./registry";
|
|
14
13
|
import { pciResponseToValue, valueToPciResponse } from "./response";
|
|
@@ -92,10 +91,18 @@ export async function mountPci(options: PciMountOptions): Promise<PciMountHandle
|
|
|
92
91
|
const { container, node, registry } = options;
|
|
93
92
|
const module = await resolveModule(node, registry);
|
|
94
93
|
|
|
94
|
+
// Each mount owns a root element inside the container, and teardown removes only
|
|
95
|
+
// that root: mounts on a shared container must stay independent because React
|
|
96
|
+
// StrictMode double-invokes the host effect — the cancelled first mount's teardown
|
|
97
|
+
// ran concurrently with the second mount and must not destroy its DOM.
|
|
98
|
+
const mountRoot = container.ownerDocument!.createElement("div");
|
|
99
|
+
mountRoot.setAttribute("data-qti-pci-mount", "");
|
|
100
|
+
container.appendChild(mountRoot);
|
|
101
|
+
|
|
95
102
|
const markupHost = container.ownerDocument!.createElement("div");
|
|
96
103
|
markupHost.className = "qti-interaction-markup";
|
|
97
104
|
markupHost.innerHTML = serializePciMarkup(node.interactionMarkup?.content);
|
|
98
|
-
|
|
105
|
+
mountRoot.appendChild(markupHost);
|
|
99
106
|
|
|
100
107
|
let resolveReady!: (instance: PciInstance) => void;
|
|
101
108
|
const ready = new Promise<PciInstance>((resolve) => {
|
|
@@ -110,12 +117,12 @@ export async function mountPci(options: PciMountOptions): Promise<PciMountHandle
|
|
|
110
117
|
: {}),
|
|
111
118
|
status: "interacting",
|
|
112
119
|
onready: (instance) => resolveReady(instance),
|
|
113
|
-
ondone: (
|
|
120
|
+
ondone: (_instance, response, state) => options.ondone?.(pciResponseToValue(response), state),
|
|
114
121
|
};
|
|
115
122
|
|
|
116
123
|
// The spec delivers the instance via onready; implementations commonly also return
|
|
117
124
|
// it from getInstance. Accept either, first one wins.
|
|
118
|
-
const returned = module.getInstance(
|
|
125
|
+
const returned = module.getInstance(mountRoot, configuration, options.state);
|
|
119
126
|
|
|
120
127
|
if (returned) {
|
|
121
128
|
resolveReady(returned);
|
|
@@ -129,7 +136,7 @@ export async function mountPci(options: PciMountOptions): Promise<PciMountHandle
|
|
|
129
136
|
getState: () => instance.getState?.(),
|
|
130
137
|
unmount: () => {
|
|
131
138
|
instance.oncompleted?.();
|
|
132
|
-
|
|
139
|
+
mountRoot.remove();
|
|
133
140
|
},
|
|
134
141
|
};
|
|
135
142
|
}
|
package/src/pci/skin.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import { createElement, useEffect, useRef, useState, type ReactNode } from "react";
|
|
9
9
|
|
|
10
10
|
import type { InteractionRenderProps, InteractionSkin } from "../runtime";
|
|
11
|
-
|
|
12
11
|
import { mountPci, type PciInteractionNode, type PciMountHandle } from "./mount";
|
|
13
12
|
import type { PciModuleRegistry } from "./registry";
|
|
14
13
|
|
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
* Items using drawing are typically scored externally, not by client RP.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createElement,
|
|
10
|
+
useCallback,
|
|
11
|
+
useEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
type PointerEvent as ReactPointerEvent,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
} from "react";
|
|
9
16
|
|
|
10
17
|
import type { BodyNode, InteractionRenderProps } from "../runtime";
|
|
11
|
-
|
|
12
18
|
import type { ObjectView } from "./graphic-base";
|
|
13
19
|
|
|
14
20
|
interface DrawingNodeView {
|
|
@@ -30,28 +36,32 @@ export function DrawingReferenceSkin(props: InteractionRenderProps): ReactNode {
|
|
|
30
36
|
propsRef.current = props;
|
|
31
37
|
|
|
32
38
|
// Paint the stage image as the drawing background (and again after a clear).
|
|
33
|
-
const
|
|
34
|
-
|
|
39
|
+
const stageData = node.object.data;
|
|
40
|
+
const paintBackground = useCallback(
|
|
41
|
+
(canvas: HTMLCanvasElement): void => {
|
|
42
|
+
const context = canvas.getContext("2d");
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
if (!context) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
const image = new Image();
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
image.onload = () => {
|
|
53
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
54
|
+
};
|
|
55
|
+
image.src = propsRef.current.resolveAsset(stageData);
|
|
56
|
+
},
|
|
57
|
+
[stageData],
|
|
58
|
+
);
|
|
49
59
|
|
|
50
60
|
useEffect(() => {
|
|
51
61
|
if (canvasRef.current) {
|
|
52
62
|
paintBackground(canvasRef.current);
|
|
53
63
|
}
|
|
54
|
-
}, [
|
|
64
|
+
}, [paintBackground]);
|
|
55
65
|
|
|
56
66
|
const pointerPosition = (event: ReactPointerEvent<HTMLCanvasElement>): { x: number; y: number } => {
|
|
57
67
|
const canvas = event.currentTarget;
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { SkinRegistry } from "../runtime";
|
|
9
|
-
|
|
10
9
|
import { AssociateReferenceSkin } from "./associate";
|
|
11
10
|
import { ChoiceReferenceSkin } from "./choice";
|
|
12
11
|
import { DrawingReferenceSkin } from "./drawing";
|
|
@@ -17,13 +16,13 @@ import { GraphicAssociateReferenceSkin } from "./graphic-associate";
|
|
|
17
16
|
import { GraphicGapMatchReferenceSkin } from "./graphic-gap-match";
|
|
18
17
|
import { GraphicOrderReferenceSkin } from "./graphic-order";
|
|
19
18
|
import { HotspotReferenceSkin } from "./hotspot";
|
|
20
|
-
import { PositionObjectReferenceSkin } from "./position-object";
|
|
21
|
-
import { SelectPointReferenceSkin } from "./select-point";
|
|
22
19
|
import { HottextReferenceSkin } from "./hottext";
|
|
23
20
|
import { InlineChoiceReferenceSkin } from "./inline-choice";
|
|
24
21
|
import { MatchReferenceSkin } from "./match";
|
|
25
22
|
import { MediaReferenceSkin } from "./media";
|
|
26
23
|
import { OrderReferenceSkin } from "./order";
|
|
24
|
+
import { PositionObjectReferenceSkin } from "./position-object";
|
|
25
|
+
import { SelectPointReferenceSkin } from "./select-point";
|
|
27
26
|
import { SliderReferenceSkin } from "./slider";
|
|
28
27
|
import { TextEntryReferenceSkin } from "./text-entry";
|
|
29
28
|
import { UploadReferenceSkin } from "./upload";
|
package/src/rp/evaluate.ts
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
import { parseCoords, parsePoint, pointInShape } from "../graphic";
|
|
10
10
|
import { mapResponse, mapResponsePoint } from "../response-processing";
|
|
11
11
|
import type { ResponseDeclarationView, ResponseValue } from "../types";
|
|
12
|
-
|
|
13
12
|
import type { CustomOperatorImplementation, ResponseNormalization, RpExpressionView } from "./types";
|
|
14
13
|
import {
|
|
15
14
|
booleanValue,
|
|
16
15
|
coerceScalar,
|
|
17
16
|
floatValue,
|
|
17
|
+
rpValue,
|
|
18
18
|
scalarsEqual,
|
|
19
19
|
singleBoolean,
|
|
20
20
|
singleNumber,
|
|
@@ -27,19 +27,19 @@ export interface EvalEnv {
|
|
|
27
27
|
readonly lookupVariable: (identifier: string) => MaybeRpValue;
|
|
28
28
|
readonly responseDeclaration: (identifier: string) => ResponseDeclarationView | undefined;
|
|
29
29
|
readonly responseValue: (identifier: string) => ResponseValue;
|
|
30
|
-
readonly normalization?: ResponseNormalization;
|
|
30
|
+
readonly normalization?: ResponseNormalization | undefined;
|
|
31
31
|
/** Seeded PRNG in [0, 1); present only in template processing. */
|
|
32
|
-
readonly random?: () => number;
|
|
32
|
+
readonly random?: (() => number) | undefined;
|
|
33
33
|
/** `testVariables` aggregation; present only in test-level outcome processing. */
|
|
34
34
|
readonly testVariables?: (expression: RpExpressionView) => MaybeRpValue;
|
|
35
35
|
/** The `number*` item-session aggregates; present only in test-level outcome processing. */
|
|
36
36
|
readonly testAggregate?: (expression: RpExpressionView) => MaybeRpValue;
|
|
37
37
|
/** Registered vendor operators by class; unregistered classes stay unsupported. */
|
|
38
|
-
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation
|
|
38
|
+
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Expression kinds legal everywhere (deterministic). */
|
|
42
|
-
export const deterministicExpressionKinds = new Set([
|
|
42
|
+
export const deterministicExpressionKinds: ReadonlySet<string> = new Set([
|
|
43
43
|
"and",
|
|
44
44
|
"baseValue",
|
|
45
45
|
"correct",
|
|
@@ -86,11 +86,14 @@ export const deterministicExpressionKinds = new Set([
|
|
|
86
86
|
]);
|
|
87
87
|
|
|
88
88
|
/** Expression kinds requiring the seeded PRNG (template processing only). */
|
|
89
|
-
export const randomExpressionKinds = new Set(["random", "randomFloat", "randomInteger"]);
|
|
89
|
+
export const randomExpressionKinds: ReadonlySet<string> = new Set(["random", "randomFloat", "randomInteger"]);
|
|
90
90
|
|
|
91
91
|
export class RpUnsupportedError extends Error {
|
|
92
|
-
|
|
92
|
+
readonly kindName: string;
|
|
93
|
+
|
|
94
|
+
constructor(kindName: string) {
|
|
93
95
|
super(`Unsupported response-processing construct: ${kindName}`);
|
|
96
|
+
this.kindName = kindName;
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -190,7 +193,7 @@ export function evaluateExpression(expression: RpExpressionView, env: EvalEnv):
|
|
|
190
193
|
const baseType = expression.baseType;
|
|
191
194
|
const value = expression.value;
|
|
192
195
|
|
|
193
|
-
return value === undefined ? null :
|
|
196
|
+
return value === undefined ? null : rpValue("single", [coerceScalar(value, baseType)], baseType);
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
case "variable":
|
|
@@ -203,11 +206,11 @@ export function evaluateExpression(expression: RpExpressionView, env: EvalEnv):
|
|
|
203
206
|
return null;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
return
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
return rpValue(
|
|
210
|
+
declaration.cardinality,
|
|
211
|
+
declaration.correctResponse.values.map((entry) => coerceScalar(entry.value, declaration.baseType)),
|
|
212
|
+
declaration.baseType,
|
|
213
|
+
);
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
case "mapResponse": {
|
|
@@ -260,9 +263,7 @@ export function evaluateExpression(expression: RpExpressionView, env: EvalEnv):
|
|
|
260
263
|
const value = operand === undefined ? null : evaluate(operand);
|
|
261
264
|
const field = value?.fields?.find((entry) => entry.name === expression.fieldIdentifier);
|
|
262
265
|
|
|
263
|
-
return field === undefined
|
|
264
|
-
? null
|
|
265
|
-
: { cardinality: "single", ...(field.baseType ? { baseType: field.baseType } : {}), values: [field.value] };
|
|
266
|
+
return field === undefined ? null : rpValue("single", [field.value], field.baseType);
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
case "and":
|
|
@@ -392,7 +393,7 @@ export function evaluateExpression(expression: RpExpressionView, env: EvalEnv):
|
|
|
392
393
|
return null; // out of range is null, per spec
|
|
393
394
|
}
|
|
394
395
|
|
|
395
|
-
return
|
|
396
|
+
return rpValue("single", [member], container.baseType);
|
|
396
397
|
}
|
|
397
398
|
|
|
398
399
|
case "mathConstant": {
|
|
@@ -603,9 +604,7 @@ export function evaluateExpression(expression: RpExpressionView, env: EvalEnv):
|
|
|
603
604
|
const remaining = container.values.filter((member) => !scalarsEqual(member, scalar, baseType, env.normalization));
|
|
604
605
|
|
|
605
606
|
// An empty container is NULL, per the QTI value model.
|
|
606
|
-
return remaining.length === 0
|
|
607
|
-
? null
|
|
608
|
-
: { cardinality: container.cardinality, baseType: container.baseType, values: remaining };
|
|
607
|
+
return remaining.length === 0 ? null : rpValue(container.cardinality, remaining, container.baseType);
|
|
609
608
|
}
|
|
610
609
|
|
|
611
610
|
case "repeat": {
|
|
@@ -633,7 +632,7 @@ export function evaluateExpression(expression: RpExpressionView, env: EvalEnv):
|
|
|
633
632
|
}
|
|
634
633
|
}
|
|
635
634
|
|
|
636
|
-
return members.length === 0 ? null :
|
|
635
|
+
return members.length === 0 ? null : rpValue("ordered", members, baseType);
|
|
637
636
|
}
|
|
638
637
|
|
|
639
638
|
case "stringMatch":
|
|
@@ -728,7 +727,7 @@ export function evaluateExpression(expression: RpExpressionView, env: EvalEnv):
|
|
|
728
727
|
return null;
|
|
729
728
|
}
|
|
730
729
|
|
|
731
|
-
return
|
|
730
|
+
return rpValue(expression.kind, members, baseType);
|
|
732
731
|
}
|
|
733
732
|
|
|
734
733
|
case "randomInteger": {
|
|
@@ -798,7 +797,7 @@ export function evaluateExpression(expression: RpExpressionView, env: EvalEnv):
|
|
|
798
797
|
|
|
799
798
|
const pick = container.values[Math.floor(env.random() * container.values.length)]!;
|
|
800
799
|
|
|
801
|
-
return
|
|
800
|
+
return rpValue("single", [pick], container.baseType);
|
|
802
801
|
}
|
|
803
802
|
|
|
804
803
|
default:
|
package/src/rp/interpreter.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import type { CapabilityIssue } from "../capability";
|
|
9
9
|
import type { ResponseDeclarationView } from "../types";
|
|
10
|
-
|
|
11
10
|
import {
|
|
12
11
|
RpUnsupportedError,
|
|
13
12
|
collectExpressionIssues,
|
|
@@ -30,6 +29,7 @@ import {
|
|
|
30
29
|
fromFlatValue,
|
|
31
30
|
fromResponse,
|
|
32
31
|
isNumericBaseType,
|
|
32
|
+
rpValue,
|
|
33
33
|
singleBoolean,
|
|
34
34
|
toOutcomeValue,
|
|
35
35
|
type MaybeRpValue,
|
|
@@ -50,11 +50,14 @@ function defaultOutcomes(declarations: readonly OutcomeDeclarationView[]): Map<s
|
|
|
50
50
|
|
|
51
51
|
for (const declaration of declarations) {
|
|
52
52
|
if (declaration.defaultValue) {
|
|
53
|
-
outcomes.set(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
outcomes.set(
|
|
54
|
+
declaration.identifier,
|
|
55
|
+
rpValue(
|
|
56
|
+
declaration.cardinality,
|
|
57
|
+
declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)),
|
|
58
|
+
declaration.baseType,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
58
61
|
continue;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
import type { CapabilityIssue } from "../capability";
|
|
10
10
|
import type { CorrectResponseView, ResponseDeclarationView } from "../types";
|
|
11
|
-
|
|
12
11
|
import {
|
|
13
12
|
RpUnsupportedError,
|
|
14
13
|
collectExpressionIssues,
|
|
@@ -18,7 +17,7 @@ import {
|
|
|
18
17
|
type EvalEnv,
|
|
19
18
|
} from "./evaluate";
|
|
20
19
|
import type { CustomOperatorImplementation, OutcomeValue, RpExpressionView, TemplateDeclarationView } from "./types";
|
|
21
|
-
import { coerceScalar, singleBoolean, toOutcomeValue, type MaybeRpValue } from "./values";
|
|
20
|
+
import { coerceScalar, rpValue, singleBoolean, toOutcomeValue, type MaybeRpValue } from "./values";
|
|
22
21
|
|
|
23
22
|
export interface TemplateConditionBranch {
|
|
24
23
|
readonly expression: RpExpressionView;
|
|
@@ -43,7 +42,7 @@ export interface TemplateProcessingContext {
|
|
|
43
42
|
readonly responseDeclarations: readonly ResponseDeclarationView[];
|
|
44
43
|
readonly seed: number;
|
|
45
44
|
/** Registered vendor `customOperator` implementations by class (opt-in). */
|
|
46
|
-
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation
|
|
45
|
+
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export interface TemplateProcessingResult {
|
|
@@ -101,11 +100,11 @@ export function executeTemplateProcessing(
|
|
|
101
100
|
values.set(
|
|
102
101
|
declaration.identifier,
|
|
103
102
|
declaration.defaultValue
|
|
104
|
-
?
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
? rpValue(
|
|
104
|
+
declaration.cardinality,
|
|
105
|
+
declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)),
|
|
106
|
+
declaration.baseType,
|
|
107
|
+
)
|
|
109
108
|
: null,
|
|
110
109
|
);
|
|
111
110
|
}
|
|
@@ -153,11 +152,7 @@ export function executeTemplateProcessing(
|
|
|
153
152
|
rule.identifier,
|
|
154
153
|
value === null || !declaration
|
|
155
154
|
? value
|
|
156
|
-
:
|
|
157
|
-
cardinality: declaration.cardinality,
|
|
158
|
-
baseType: declaration.baseType ?? value.baseType,
|
|
159
|
-
values: value.values,
|
|
160
|
-
},
|
|
155
|
+
: rpValue(declaration.cardinality, value.values, declaration.baseType ?? value.baseType),
|
|
161
156
|
);
|
|
162
157
|
}
|
|
163
158
|
continue;
|
package/src/rp/types.ts
CHANGED
|
@@ -134,24 +134,30 @@ export interface TemplateDeclarationView {
|
|
|
134
134
|
readonly defaultValue?: { readonly values: ReadonlyArray<{ readonly value: RpScalar }> };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Consumer-supplied input. Optional members deliberately admit explicit
|
|
139
|
+
* `undefined` ("undefined means not provided"): callers pass maybe-undefined
|
|
140
|
+
* values straight through, and the interpreter reads every member field-wise
|
|
141
|
+
* (`??`/`?.`) — option bags are never spread-merged over defaults.
|
|
142
|
+
*/
|
|
137
143
|
export interface ResponseProcessingContext {
|
|
138
144
|
readonly responseDeclarations: readonly ResponseDeclarationView[];
|
|
139
145
|
readonly outcomeDeclarations: readonly OutcomeDeclarationView[];
|
|
140
146
|
readonly responses: Readonly<Record<string, ResponseValue>>;
|
|
141
|
-
readonly normalization?: ResponseNormalization;
|
|
147
|
+
readonly normalization?: ResponseNormalization | undefined;
|
|
142
148
|
/** Template variables for this clone (read by `variable` lookups). */
|
|
143
|
-
readonly templateDeclarations?: readonly TemplateDeclarationView[];
|
|
144
|
-
readonly templateValues?: Readonly<Record<string, OutcomeValue
|
|
149
|
+
readonly templateDeclarations?: readonly TemplateDeclarationView[] | undefined;
|
|
150
|
+
readonly templateValues?: Readonly<Record<string, OutcomeValue>> | undefined;
|
|
145
151
|
/** Adaptive carry-over: outcome values from earlier attempts replace declared defaults. */
|
|
146
|
-
readonly priorOutcomes?: Readonly<Record<string, OutcomeValue
|
|
152
|
+
readonly priorOutcomes?: Readonly<Record<string, OutcomeValue>> | undefined;
|
|
147
153
|
/**
|
|
148
154
|
* Random source for `random`/`randomInteger`/`randomFloat` in RP (adaptive items,
|
|
149
155
|
* e.g. Monty Hall door reveals). Seed it from the attempt seed: the seed plus the
|
|
150
156
|
* submission sequence then replays the exact same outcomes (ADR-0004 determinism).
|
|
151
157
|
*/
|
|
152
|
-
readonly random?: () => number;
|
|
158
|
+
readonly random?: (() => number) | undefined;
|
|
153
159
|
/** Registered vendor `customOperator` implementations by class (opt-in). */
|
|
154
|
-
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation
|
|
160
|
+
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
export interface ResponseProcessingResult {
|
package/src/rp/values.ts
CHANGED
|
@@ -61,11 +61,11 @@ export function fromResponse(declaration: ResponseDeclarationView, response: Res
|
|
|
61
61
|
return null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
return rpValue(
|
|
65
|
+
declaration.cardinality,
|
|
66
|
+
raw.map((value) => coerceScalar(value, declaration.baseType)),
|
|
67
|
+
declaration.baseType,
|
|
68
|
+
);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export function singleNumber(value: MaybeRpValue): number | null {
|
|
@@ -88,6 +88,15 @@ export function singleBoolean(value: MaybeRpValue): boolean | null {
|
|
|
88
88
|
return typeof member === "boolean" ? member : null;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** Construct a typed value; an unknown baseType is omitted, never stored as undefined. */
|
|
92
|
+
export function rpValue(cardinality: Cardinality, values: readonly RpScalar[], baseType?: string): RpValue {
|
|
93
|
+
return {
|
|
94
|
+
cardinality,
|
|
95
|
+
values,
|
|
96
|
+
...(baseType !== undefined ? { baseType } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
export function booleanValue(value: boolean): RpValue {
|
|
92
101
|
return { cardinality: "single", baseType: "boolean", values: [value] };
|
|
93
102
|
}
|
|
@@ -182,7 +191,11 @@ export function fromFlatValue(value: OutcomeValue, cardinality: Cardinality, bas
|
|
|
182
191
|
return null;
|
|
183
192
|
}
|
|
184
193
|
|
|
185
|
-
return
|
|
194
|
+
return rpValue(
|
|
195
|
+
cardinality,
|
|
196
|
+
values.map((member) => coerceScalar(member, baseType)),
|
|
197
|
+
baseType,
|
|
198
|
+
);
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
export function toOutcomeValue(value: MaybeRpValue): OutcomeValue {
|
package/src/runtime.ts
CHANGED
|
@@ -175,9 +175,9 @@ export interface ItemRendererProps {
|
|
|
175
175
|
* per-mount store; passing one enables review/replay modes (rehydrate a stored,
|
|
176
176
|
* already-submitted attempt) and server-side rendering of submitted states.
|
|
177
177
|
*/
|
|
178
|
-
store?: AttemptStore;
|
|
178
|
+
store?: AttemptStore | undefined;
|
|
179
179
|
/** Clone seed for template processing; store it to replay the same clone. */
|
|
180
|
-
seed?: number;
|
|
180
|
+
seed?: number | undefined;
|
|
181
181
|
// Rendered inside the same runtime context as the item body, after it. Lets a consumer
|
|
182
182
|
// drop controls (a Submit bar, a score panel) that call `useAttempt()` for this item —
|
|
183
183
|
// the attempt store is per-item and scoped to this subtree.
|
|
@@ -185,9 +185,9 @@ export interface ItemRendererProps {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
export interface ContentRendererProps {
|
|
188
|
-
nodes?: readonly BodyNode[];
|
|
188
|
+
nodes?: readonly BodyNode[] | undefined;
|
|
189
189
|
/** Values for printedVariable (and showHide-gated feedback) inside the content. */
|
|
190
|
-
outcomes?: Readonly<Record<string, OutcomeValue
|
|
190
|
+
outcomes?: Readonly<Record<string, OutcomeValue>> | undefined;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
export interface QtiRuntime {
|
|
@@ -437,7 +437,7 @@ export function createQtiRuntime(config: QtiRuntimeConfig): QtiRuntime {
|
|
|
437
437
|
}: {
|
|
438
438
|
feedback: FeedbackView;
|
|
439
439
|
element: "span" | "div";
|
|
440
|
-
overrides?: NodeOverrides;
|
|
440
|
+
overrides?: NodeOverrides | undefined;
|
|
441
441
|
}): ReactNode {
|
|
442
442
|
const { store } = useRuntimeContext();
|
|
443
443
|
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
@@ -461,7 +461,7 @@ export function createQtiRuntime(config: QtiRuntimeConfig): QtiRuntime {
|
|
|
461
461
|
}: {
|
|
462
462
|
view: TemplateContentView;
|
|
463
463
|
element: "span" | "div";
|
|
464
|
-
overrides?: NodeOverrides;
|
|
464
|
+
overrides?: NodeOverrides | undefined;
|
|
465
465
|
}): ReactNode {
|
|
466
466
|
const { store } = useRuntimeContext();
|
|
467
467
|
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
@@ -693,11 +693,13 @@ export function createQtiRuntime(config: QtiRuntimeConfig): QtiRuntime {
|
|
|
693
693
|
const parsed = descriptor.schema.safeParse(node);
|
|
694
694
|
|
|
695
695
|
if (!parsed.success) {
|
|
696
|
+
const detail = parsed.error.issues[0]?.message;
|
|
697
|
+
|
|
696
698
|
report({
|
|
697
699
|
type: "invalid-interaction",
|
|
698
700
|
name: node.kind,
|
|
699
701
|
responseIdentifier: node.responseIdentifier,
|
|
700
|
-
detail:
|
|
702
|
+
...(detail !== undefined ? { detail } : {}),
|
|
701
703
|
});
|
|
702
704
|
}
|
|
703
705
|
|
package/src/store.ts
CHANGED
|
@@ -38,19 +38,25 @@ export interface AttemptSnapshot {
|
|
|
38
38
|
readonly attemptCount: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Consumer-supplied input. Optional members deliberately admit explicit
|
|
43
|
+
* `undefined` ("undefined means not provided"), so callers can pass
|
|
44
|
+
* maybe-undefined values straight through; the store reads every member
|
|
45
|
+
* field-wise (`??`/`?.`) and never spread-merges options over defaults.
|
|
46
|
+
*/
|
|
41
47
|
export interface AttemptStoreOptions {
|
|
42
|
-
readonly outcomeDeclarations?: readonly OutcomeDeclarationView[];
|
|
43
|
-
readonly responseProcessing?: ResponseProcessingView;
|
|
48
|
+
readonly outcomeDeclarations?: readonly OutcomeDeclarationView[] | undefined;
|
|
49
|
+
readonly responseProcessing?: ResponseProcessingView | undefined;
|
|
44
50
|
/** The Response Normalization hook (ADR-0004); applies to scores and outcomes alike. */
|
|
45
|
-
readonly normalization?: ResponseNormalization;
|
|
46
|
-
readonly templateDeclarations?: readonly TemplateDeclarationView[];
|
|
47
|
-
readonly templateProcessing?: TemplateProcessingView;
|
|
51
|
+
readonly normalization?: ResponseNormalization | undefined;
|
|
52
|
+
readonly templateDeclarations?: readonly TemplateDeclarationView[] | undefined;
|
|
53
|
+
readonly templateProcessing?: TemplateProcessingView | undefined;
|
|
48
54
|
/** Clone seed for template processing; store it to replay the same clone. */
|
|
49
|
-
readonly seed?: number;
|
|
55
|
+
readonly seed?: number | undefined;
|
|
50
56
|
/** QTI adaptive item: multiple attempts, outcome carry-over, completionStatus lock. */
|
|
51
|
-
readonly adaptive?: boolean;
|
|
57
|
+
readonly adaptive?: boolean | undefined;
|
|
52
58
|
/** Registered vendor `customOperator` implementations by class (opt-in). */
|
|
53
|
-
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation
|
|
59
|
+
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export interface AttemptStore {
|
package/src/test/controller.ts
CHANGED
|
@@ -20,12 +20,12 @@ import {
|
|
|
20
20
|
floatValue,
|
|
21
21
|
fromFlatValue,
|
|
22
22
|
isNumericBaseType,
|
|
23
|
+
rpValue,
|
|
23
24
|
singleBoolean,
|
|
24
25
|
toOutcomeValue,
|
|
25
26
|
type MaybeRpValue,
|
|
26
27
|
type RpValue,
|
|
27
28
|
} from "../rp/values";
|
|
28
|
-
|
|
29
29
|
import type {
|
|
30
30
|
AssessmentItemRefView,
|
|
31
31
|
AssessmentSectionView,
|
|
@@ -262,11 +262,14 @@ export function createTestController(view: AssessmentTestView, options: TestCont
|
|
|
262
262
|
|
|
263
263
|
for (const declaration of view.outcomeDeclarations ?? []) {
|
|
264
264
|
if (declaration.defaultValue) {
|
|
265
|
-
outcomes.set(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
265
|
+
outcomes.set(
|
|
266
|
+
declaration.identifier,
|
|
267
|
+
rpValue(
|
|
268
|
+
declaration.cardinality,
|
|
269
|
+
declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)),
|
|
270
|
+
declaration.baseType,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
270
273
|
continue;
|
|
271
274
|
}
|
|
272
275
|
|
|
@@ -331,7 +334,7 @@ export function createTestController(view: AssessmentTestView, options: TestCont
|
|
|
331
334
|
members.push(...lifted.values);
|
|
332
335
|
}
|
|
333
336
|
|
|
334
|
-
return members.length === 0 ? null :
|
|
337
|
+
return members.length === 0 ? null : rpValue("multiple", members, baseType);
|
|
335
338
|
},
|
|
336
339
|
testAggregate: (expression) => {
|
|
337
340
|
const subset = subsetItems(expression);
|
|
@@ -12,7 +12,6 @@ import type { AssessmentItemView } from "../runtime";
|
|
|
12
12
|
import { createAttemptStore, type AttemptSnapshot, type AttemptStore } from "../store";
|
|
13
13
|
import { isResponseRecord } from "../types";
|
|
14
14
|
import type { ResponseValue } from "../types";
|
|
15
|
-
|
|
16
15
|
import type { AssessmentItemRefView, TestController, TestFeedbackView, TestPlanItem, TestSessionState } from "./types";
|
|
17
16
|
|
|
18
17
|
export interface TestSessionStoreOptions {
|