@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.
Files changed (65) hide show
  1. package/dist/index.js +4566 -212
  2. package/package.json +3 -1
  3. package/src/capability.ts +24 -0
  4. package/src/content-model.ts +104 -5
  5. package/src/graphic.ts +103 -0
  6. package/src/index.ts +139 -3
  7. package/src/interactions/associate.ts +22 -0
  8. package/src/interactions/drawing.ts +24 -0
  9. package/src/interactions/end-attempt.ts +19 -0
  10. package/src/interactions/extended-text.ts +21 -0
  11. package/src/interactions/gap-match.ts +22 -0
  12. package/src/interactions/graphic.ts +104 -0
  13. package/src/interactions/hottext.ts +21 -0
  14. package/src/interactions/index.ts +57 -2
  15. package/src/interactions/match.ts +27 -0
  16. package/src/interactions/media.ts +24 -0
  17. package/src/interactions/order.ts +21 -0
  18. package/src/interactions/slider.ts +24 -0
  19. package/src/interactions/upload.ts +19 -0
  20. package/src/normalized-item.ts +561 -0
  21. package/src/pci/index.ts +22 -0
  22. package/src/pci/interaction.ts +42 -0
  23. package/src/pci/markup.ts +102 -0
  24. package/src/pci/mount.ts +135 -0
  25. package/src/pci/registry.ts +240 -0
  26. package/src/pci/response.ts +138 -0
  27. package/src/pci/skin.ts +87 -0
  28. package/src/reference-skin/associate.ts +98 -0
  29. package/src/reference-skin/choice.ts +44 -0
  30. package/src/reference-skin/content.ts +30 -0
  31. package/src/reference-skin/drawing.ts +150 -0
  32. package/src/reference-skin/end-attempt.ts +27 -0
  33. package/src/reference-skin/extended-text.ts +35 -0
  34. package/src/reference-skin/gap-match.ts +69 -0
  35. package/src/reference-skin/graphic-associate.ts +123 -0
  36. package/src/reference-skin/graphic-base.ts +142 -0
  37. package/src/reference-skin/graphic-gap-match.ts +143 -0
  38. package/src/reference-skin/graphic-order.ts +76 -0
  39. package/src/reference-skin/hotspot.ts +43 -0
  40. package/src/reference-skin/hottext.ts +42 -0
  41. package/src/reference-skin/index.ts +75 -0
  42. package/src/reference-skin/inline-choice.ts +42 -0
  43. package/src/reference-skin/match.ts +80 -0
  44. package/src/reference-skin/media.ts +74 -0
  45. package/src/reference-skin/order.ts +79 -0
  46. package/src/reference-skin/position-object.ts +84 -0
  47. package/src/reference-skin/select-point.ts +87 -0
  48. package/src/reference-skin/slider.ts +41 -0
  49. package/src/reference-skin/text-entry.ts +31 -0
  50. package/src/reference-skin/upload.ts +46 -0
  51. package/src/response-processing.ts +178 -29
  52. package/src/rp/evaluate.ts +828 -0
  53. package/src/rp/index.ts +30 -0
  54. package/src/rp/interpreter.ts +251 -0
  55. package/src/rp/template-processing.ts +295 -0
  56. package/src/rp/templates.ts +190 -0
  57. package/src/rp/types.ts +161 -0
  58. package/src/rp/values.ts +198 -0
  59. package/src/runtime.ts +474 -28
  60. package/src/store.ts +155 -5
  61. package/src/test/controller.ts +806 -0
  62. package/src/test/index.ts +25 -0
  63. package/src/test/session-store.ts +244 -0
  64. package/src/test/types.ts +203 -0
  65. package/src/types.ts +27 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conform-ed/qti-react",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "files": [
5
5
  "src",
6
6
  "dist"
@@ -21,8 +21,10 @@
21
21
  "test": "bun test"
22
22
  },
23
23
  "devDependencies": {
24
+ "@conform-ed/qti-xml": "0.0.13",
24
25
  "@types/react": "^19.2.17",
25
26
  "@types/react-dom": "^19",
27
+ "happy-dom": "^20.10.2",
26
28
  "react": "^19.2.7",
27
29
  "react-dom": "^19"
28
30
  },
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Capability Report types (ADR-0003): the runtime's answer to "can this content be
3
+ * delivered, and if not, why". In their own module so the RP interpreter can report
4
+ * issues without importing the React runtime.
5
+ */
6
+
7
+ export type CapabilityIssueType =
8
+ | "unsupported-interaction"
9
+ | "invalid-interaction"
10
+ | "unsupported-element"
11
+ | "unsupported-rp";
12
+
13
+ export interface CapabilityIssue {
14
+ readonly type: CapabilityIssueType;
15
+ /** The interaction kind, element name, or RP rule/operator/template at issue. */
16
+ readonly name: string;
17
+ readonly responseIdentifier?: string;
18
+ readonly detail?: string;
19
+ }
20
+
21
+ export interface CapabilityReport {
22
+ readonly deliverable: boolean;
23
+ readonly issues: readonly CapabilityIssue[];
24
+ }
@@ -13,8 +13,29 @@
13
13
  * HTML5 at once".
14
14
  */
15
15
 
16
- /** Interaction node kinds the v0 runtime can render. */
17
- export const v0InteractionKinds = ["choiceInteraction", "textEntryInteraction", "inlineChoiceInteraction"] as const;
16
+ /** Interaction node kinds conform-ed ships descriptors and Reference Skins for. */
17
+ export const v0InteractionKinds = [
18
+ "associateInteraction",
19
+ "choiceInteraction",
20
+ "drawingInteraction",
21
+ "endAttemptInteraction",
22
+ "extendedTextInteraction",
23
+ "gapMatchInteraction",
24
+ "graphicAssociateInteraction",
25
+ "graphicGapMatchInteraction",
26
+ "graphicOrderInteraction",
27
+ "hotspotInteraction",
28
+ "hottextInteraction",
29
+ "inlineChoiceInteraction",
30
+ "matchInteraction",
31
+ "mediaInteraction",
32
+ "orderInteraction",
33
+ "positionObjectStage",
34
+ "selectPointInteraction",
35
+ "sliderInteraction",
36
+ "textEntryInteraction",
37
+ "uploadInteraction",
38
+ ] as const;
18
39
 
19
40
  export type V0InteractionKind = (typeof v0InteractionKinds)[number];
20
41
 
@@ -26,16 +47,63 @@ const v0FlowElements = new Set<string>([
26
47
  "em",
27
48
  "b",
28
49
  "i",
50
+ "sub",
51
+ "sup",
29
52
  "br",
30
53
  "ul",
31
54
  "ol",
32
55
  "li",
33
56
  // language-critical
34
57
  "ruby",
58
+ "rb",
35
59
  "rt",
36
60
  "rp",
61
+ // media (the first media-milestone growth; src/poster route through the Asset Resolver)
62
+ "img",
63
+ "audio",
64
+ "video",
65
+ "source",
66
+ "track",
67
+ "picture",
68
+ "figure",
69
+ "figcaption",
70
+ // embedded media the corpus uses for stages and standalone assets
71
+ "object",
72
+ // structural vocabulary the official corpus uses (fixture-driven growth, ADR-0002)
73
+ "div",
74
+ "section",
75
+ "h1",
76
+ "h2",
77
+ "h3",
78
+ "h4",
79
+ "h5",
80
+ "h6",
81
+ "blockquote",
82
+ "hr",
83
+ // tables (gradebook-style content; conservative semantics)
84
+ "table",
85
+ "caption",
86
+ "thead",
87
+ "tbody",
88
+ "tfoot",
89
+ "tr",
90
+ "th",
91
+ "td",
37
92
  ]);
38
93
 
94
+ /** Element-specific attribute allowlists, additive to the global set. */
95
+ const v0ElementAttributes: ReadonlyMap<string, ReadonlySet<string>> = new Map([
96
+ ["img", new Set(["src", "alt", "width", "height"])],
97
+ ["audio", new Set(["src", "controls", "loop", "muted", "preload"])],
98
+ ["video", new Set(["src", "controls", "loop", "muted", "preload", "poster", "width", "height"])],
99
+ ["source", new Set(["src", "type"])],
100
+ ["track", new Set(["src", "kind", "srclang", "label", "default"])],
101
+ ["object", new Set(["data", "type", "width", "height"])],
102
+ ]);
103
+
104
+ /** Attribute names treated as packaged-asset references (rewritten by the Asset Resolver). */
105
+ const v0UrlAttributes = new Set<string>(["src", "poster", "data"]);
106
+
39
107
  /**
40
108
  * The MathML root. Its subtree is rendered structurally (presentation MathML) with the
41
109
  * same attribute hardening, but element names inside are not individually allowlisted
@@ -51,6 +119,10 @@ export interface ContentModel {
51
119
  readonly flowElements: ReadonlySet<string>;
52
120
  readonly mathRoot: string;
53
121
  readonly globalAttributes: ReadonlySet<string>;
122
+ /** Per-element attribute allowlists, additive to `globalAttributes`. */
123
+ readonly elementAttributes: ReadonlyMap<string, ReadonlySet<string>>;
124
+ /** Attributes whose values are asset references, routed through the Asset Resolver. */
125
+ readonly urlAttributes: ReadonlySet<string>;
54
126
  }
55
127
 
56
128
  export const v0ContentModel: ContentModel = {
@@ -58,6 +130,8 @@ export const v0ContentModel: ContentModel = {
58
130
  flowElements: v0FlowElements,
59
131
  mathRoot: v0MathRoot,
60
132
  globalAttributes: v0GlobalAttributes,
133
+ elementAttributes: v0ElementAttributes,
134
+ urlAttributes: v0UrlAttributes,
61
135
  };
62
136
 
63
137
  export function isAllowedFlowElement(model: ContentModel, name: string): boolean {
@@ -84,11 +158,13 @@ function isUnsafeAttribute(name: string, value: unknown): boolean {
84
158
  }
85
159
 
86
160
  /**
87
- * Reduce a raw attribute bag to the safe, allowlisted subset. Used by the body walk so
88
- * a node that validates against QTI structure still cannot carry script or handlers.
161
+ * Reduce a raw attribute bag to the safe, allowlisted subset for one element. Used by
162
+ * the body walk so a node that validates against QTI structure still cannot carry
163
+ * script or handlers. The allowlist is the global set plus the element's own entries.
89
164
  */
90
165
  export function sanitizeAttributes(
91
166
  model: ContentModel,
167
+ elementName: string,
92
168
  attributes: Record<string, unknown> | undefined,
93
169
  ): Record<string, string> {
94
170
  const safe: Record<string, string> = {};
@@ -97,12 +173,14 @@ export function sanitizeAttributes(
97
173
  return safe;
98
174
  }
99
175
 
176
+ const elementAllowed = model.elementAttributes.get(elementName);
177
+
100
178
  for (const [name, value] of Object.entries(attributes)) {
101
179
  if (isUnsafeAttribute(name, value)) {
102
180
  continue;
103
181
  }
104
182
 
105
- if (!model.globalAttributes.has(name)) {
183
+ if (!model.globalAttributes.has(name) && !elementAllowed?.has(name)) {
106
184
  continue;
107
185
  }
108
186
 
@@ -113,3 +191,24 @@ export function sanitizeAttributes(
113
191
 
114
192
  return safe;
115
193
  }
194
+
195
+ /**
196
+ * Attribute hardening for MathML subtrees: presentation attributes (mathvariant,
197
+ * linethickness, …) are not individually allowlisted — MathML has no scripting surface
198
+ * once event handlers and javascript: URLs are stripped.
199
+ */
200
+ export function sanitizeMathAttributes(attributes: Record<string, unknown> | undefined): Record<string, string> {
201
+ const safe: Record<string, string> = {};
202
+
203
+ if (!attributes) {
204
+ return safe;
205
+ }
206
+
207
+ for (const [name, value] of Object.entries(attributes)) {
208
+ if (!isUnsafeAttribute(name, value) && typeof value === "string") {
209
+ safe[name] = value;
210
+ }
211
+ }
212
+
213
+ return safe;
214
+ }
package/src/graphic.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Graphic primitives shared by the graphic interaction family and areaMapping scoring:
3
+ * QTI shape/coords parsing and point-in-shape hit testing. Pure logic, no React.
4
+ *
5
+ * Shapes follow the QTI (HTML image-map) conventions:
6
+ * - `circle`: center-x, center-y, radius
7
+ * - `rect`: left-x, top-y, right-x, bottom-y
8
+ * - `poly`: x1, y1, ..., xn, yn
9
+ * - `ellipse`: center-x, center-y, radius-x, radius-y
10
+ * - `default`: the entire image
11
+ */
12
+
13
+ export type QtiShape = "circle" | "rect" | "poly" | "ellipse" | "default";
14
+
15
+ export interface Point {
16
+ readonly x: number;
17
+ readonly y: number;
18
+ }
19
+
20
+ /** Parse a QTI coords attribute ("10,20,30") into numbers. */
21
+ export function parseCoords(coords: string): number[] {
22
+ return coords
23
+ .split(",")
24
+ .map((entry) => Number(entry.trim()))
25
+ .filter((value) => !Number.isNaN(value));
26
+ }
27
+
28
+ /** Parse a QTI point value ("x y") or null when malformed. */
29
+ export function parsePoint(value: string): Point | null {
30
+ const [x, y, ...rest] = value.trim().split(/\s+/u).map(Number);
31
+
32
+ if (x === undefined || y === undefined || rest.length > 0 || Number.isNaN(x) || Number.isNaN(y)) {
33
+ return null;
34
+ }
35
+
36
+ return { x, y };
37
+ }
38
+
39
+ export function formatPoint(point: Point): string {
40
+ return `${point.x} ${point.y}`;
41
+ }
42
+
43
+ function pointInPolygon(coords: readonly number[], point: Point): boolean {
44
+ let inside = false;
45
+
46
+ for (let i = 0, j = coords.length - 2; i < coords.length; j = i, i += 2) {
47
+ const xi = coords[i]!;
48
+ const yi = coords[i + 1]!;
49
+ const xj = coords[j]!;
50
+ const yj = coords[j + 1]!;
51
+ const intersects = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
52
+
53
+ if (intersects) {
54
+ inside = !inside;
55
+ }
56
+ }
57
+
58
+ return inside;
59
+ }
60
+
61
+ /** QTI hit test: is `point` inside the area described by (shape, coords)? */
62
+ export function pointInShape(shape: string, coords: readonly number[], point: Point): boolean {
63
+ switch (shape) {
64
+ case "default":
65
+ return true;
66
+
67
+ case "circle": {
68
+ const [cx, cy, r] = coords;
69
+
70
+ if (cx === undefined || cy === undefined || r === undefined) {
71
+ return false;
72
+ }
73
+
74
+ return (point.x - cx) ** 2 + (point.y - cy) ** 2 <= r ** 2;
75
+ }
76
+
77
+ case "rect": {
78
+ const [left, top, right, bottom] = coords;
79
+
80
+ if (left === undefined || top === undefined || right === undefined || bottom === undefined) {
81
+ return false;
82
+ }
83
+
84
+ return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
85
+ }
86
+
87
+ case "ellipse": {
88
+ const [cx, cy, rx, ry] = coords;
89
+
90
+ if (cx === undefined || cy === undefined || rx === undefined || ry === undefined || rx === 0 || ry === 0) {
91
+ return false;
92
+ }
93
+
94
+ return (point.x - cx) ** 2 / rx ** 2 + (point.y - cy) ** 2 / ry ** 2 <= 1;
95
+ }
96
+
97
+ case "poly":
98
+ return coords.length >= 6 && pointInPolygon(coords, point);
99
+
100
+ default:
101
+ return false;
102
+ }
103
+ }
package/src/index.ts CHANGED
@@ -12,9 +12,70 @@ export {
12
12
  type V0InteractionKind,
13
13
  } from "./content-model";
14
14
 
15
- export { foldString, mapResponse, matchCorrect, scoreResponse } from "./response-processing";
15
+ export { foldString, mapResponse, matchCorrect, mapResponsePoint, scoreResponse } from "./response-processing";
16
16
 
17
- export { createAttemptStore, type AttemptSnapshot, type AttemptStore } from "./store";
17
+ export { assessmentItemViewFromNormalized, assessmentTestViewFromNormalized } from "./normalized-item";
18
+
19
+ export { formatPoint, parseCoords, parsePoint, pointInShape, type Point, type QtiShape } from "./graphic";
20
+
21
+ export {
22
+ applyCorrectResponseOverrides,
23
+ collectRpIssues,
24
+ collectTemplateIssues,
25
+ executeResponseProcessing,
26
+ executeTemplateProcessing,
27
+ mulberry32,
28
+ resolveTemplate,
29
+ } from "./rp";
30
+
31
+ export type {
32
+ CustomOperatorImplementation,
33
+ MaybeRpValue,
34
+ OutcomeDeclarationView,
35
+ OutcomeValue,
36
+ ResponseNormalization,
37
+ ResponseProcessingContext,
38
+ ResponseProcessingResult,
39
+ ResponseProcessingView,
40
+ RpConditionBranch,
41
+ RpExpressionView,
42
+ RpRecordField,
43
+ RpRuleView,
44
+ RpScalar,
45
+ RpValue,
46
+ TemplateConditionBranch,
47
+ TemplateDeclarationView,
48
+ TemplateProcessingContext,
49
+ TemplateProcessingResult,
50
+ TemplateProcessingView,
51
+ TemplateRuleView,
52
+ } from "./rp";
53
+
54
+ export { createAttemptStore, type AttemptSnapshot, type AttemptStore, type AttemptStoreOptions } from "./store";
55
+
56
+ export {
57
+ createTestController,
58
+ createTestSessionStore,
59
+ type TestSessionSnapshot,
60
+ type TestSessionStore,
61
+ type TestSessionStoreOptions,
62
+ type AssessmentItemRefView,
63
+ type AssessmentSectionView,
64
+ type AssessmentTestView,
65
+ type BranchRuleView,
66
+ type ItemSessionControlView,
67
+ type OutcomeConditionBranch,
68
+ type OutcomeRuleView,
69
+ type TestController,
70
+ type TestFeedbackView,
71
+ type TestItemResult,
72
+ type TestPartView,
73
+ type TestPlan,
74
+ type TestPlanItem,
75
+ type TestPlanPart,
76
+ type TestSessionState,
77
+ type TimeLimitsView,
78
+ } from "./test";
18
79
 
19
80
  export {
20
81
  createQtiRuntime,
@@ -22,12 +83,18 @@ export {
22
83
  type AssessmentItemView,
23
84
  type AttemptController,
24
85
  type BodyNode,
86
+ type CapabilityIssue,
87
+ type CapabilityIssueType,
88
+ type CapabilityReport,
89
+ type ContentRendererProps,
90
+ type FeedbackView,
25
91
  type InteractionDescriptor,
26
92
  type InteractionNode,
27
93
  type InteractionRenderProps,
28
94
  type InteractionSkin,
29
95
  type InteractionStatus,
30
96
  type ItemRendererProps,
97
+ type NodeOverrides,
31
98
  type OptionProps,
32
99
  type OptionStatus,
33
100
  type QtiRuntime,
@@ -36,9 +103,78 @@ export {
36
103
  type XmlContentNode,
37
104
  } from "./runtime";
38
105
 
39
- export { choiceInteraction, inlineChoiceInteraction, qtiCoreInteractions, textEntryInteraction } from "./interactions";
106
+ export {
107
+ createPciModuleRegistry,
108
+ createPciSkin,
109
+ mountPci,
110
+ pciResponseToValue,
111
+ portableCustomInteraction,
112
+ serializePciMarkup,
113
+ valueToPciResponse,
114
+ type PciConfiguration,
115
+ type PciInstance,
116
+ type PciInteractionNode,
117
+ type PciModule,
118
+ type PciModuleRegistry,
119
+ type PciModuleRegistryOptions,
120
+ type PciMountHandle,
121
+ type PciMountOptions,
122
+ type PciSkinOptions,
123
+ } from "./pci";
124
+
125
+ export {
126
+ associateInteraction,
127
+ choiceInteraction,
128
+ drawingInteraction,
129
+ endAttemptInteraction,
130
+ extendedTextInteraction,
131
+ gapMatchInteraction,
132
+ graphicAssociateInteraction,
133
+ graphicGapMatchInteraction,
134
+ graphicOrderInteraction,
135
+ hotspotInteraction,
136
+ hottextInteraction,
137
+ inlineChoiceInteraction,
138
+ matchInteraction,
139
+ mediaInteraction,
140
+ orderInteraction,
141
+ positionObjectStage,
142
+ qtiCoreInteractions,
143
+ selectPointInteraction,
144
+ sliderInteraction,
145
+ textEntryInteraction,
146
+ uploadInteraction,
147
+ } from "./interactions";
148
+
149
+ export {
150
+ AssociateReferenceSkin,
151
+ ChoiceReferenceSkin,
152
+ DrawingReferenceSkin,
153
+ EndAttemptReferenceSkin,
154
+ ExtendedTextReferenceSkin,
155
+ GapMatchReferenceSkin,
156
+ GraphicAssociateReferenceSkin,
157
+ GraphicGapMatchReferenceSkin,
158
+ GraphicOrderReferenceSkin,
159
+ GraphicStage,
160
+ HotspotReferenceSkin,
161
+ HottextReferenceSkin,
162
+ InlineChoiceReferenceSkin,
163
+ MatchReferenceSkin,
164
+ MediaReferenceSkin,
165
+ OrderReferenceSkin,
166
+ PositionObjectReferenceSkin,
167
+ SelectPointReferenceSkin,
168
+ SliderReferenceSkin,
169
+ TextEntryReferenceSkin,
170
+ UploadReferenceSkin,
171
+ referenceSkin,
172
+ textOf,
173
+ } from "./reference-skin";
40
174
 
41
175
  export type {
176
+ AreaMapEntryView,
177
+ AreaMappingView,
42
178
  Cardinality,
43
179
  CorrectResponseView,
44
180
  MapEntryView,
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+
3
+ import { defineInteraction } from "../runtime";
4
+ import type { ResponseValue } from "../types";
5
+
6
+ const associateInteractionNodeSchema = z.object({
7
+ kind: z.literal("associateInteraction"),
8
+ responseIdentifier: z.string().min(1),
9
+ simpleAssociableChoices: z
10
+ .array(z.looseObject({ identifier: z.string().min(1), matchMax: z.number().int().optional() }))
11
+ .min(2),
12
+ maxAssociations: z.number().int().optional(),
13
+ });
14
+
15
+ export const associateInteraction = defineInteraction({
16
+ kind: "associateInteraction",
17
+ schema: associateInteractionNodeSchema,
18
+ scoring: "qti-standard",
19
+ initialResponse(): ResponseValue {
20
+ return null;
21
+ },
22
+ });
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+
3
+ import { defineInteraction } from "../runtime";
4
+ import type { ResponseValue } from "../types";
5
+
6
+ const drawingInteractionNodeSchema = z.object({
7
+ kind: z.literal("drawingInteraction"),
8
+ responseIdentifier: z.string().min(1),
9
+ object: z.object({
10
+ data: z.string().min(1),
11
+ width: z.number().optional(),
12
+ height: z.number().optional(),
13
+ type: z.string().optional(),
14
+ }),
15
+ });
16
+
17
+ export const drawingInteraction = defineInteraction({
18
+ kind: "drawingInteraction",
19
+ schema: drawingInteractionNodeSchema,
20
+ scoring: "qti-standard",
21
+ initialResponse(): ResponseValue {
22
+ return null;
23
+ },
24
+ });
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+
3
+ import { defineInteraction } from "../runtime";
4
+ import type { ResponseValue } from "../types";
5
+
6
+ const endAttemptInteractionNodeSchema = z.object({
7
+ kind: z.literal("endAttemptInteraction"),
8
+ responseIdentifier: z.string().min(1),
9
+ title: z.string().min(1),
10
+ });
11
+
12
+ export const endAttemptInteraction = defineInteraction({
13
+ kind: "endAttemptInteraction",
14
+ schema: endAttemptInteractionNodeSchema,
15
+ scoring: "qti-standard",
16
+ initialResponse(): ResponseValue {
17
+ return null;
18
+ },
19
+ });
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+
3
+ import { defineInteraction } from "../runtime";
4
+ import type { ResponseValue } from "../types";
5
+
6
+ const extendedTextInteractionNodeSchema = z.object({
7
+ kind: z.literal("extendedTextInteraction"),
8
+ responseIdentifier: z.string().min(1),
9
+ expectedLength: z.number().int().optional(),
10
+ expectedLines: z.number().int().optional(),
11
+ placeholderText: z.string().optional(),
12
+ });
13
+
14
+ export const extendedTextInteraction = defineInteraction({
15
+ kind: "extendedTextInteraction",
16
+ schema: extendedTextInteractionNodeSchema,
17
+ scoring: "qti-standard",
18
+ initialResponse(): ResponseValue {
19
+ return null;
20
+ },
21
+ });
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+
3
+ import { defineInteraction } from "../runtime";
4
+ import type { ResponseValue } from "../types";
5
+
6
+ const gapMatchInteractionNodeSchema = z.object({
7
+ kind: z.literal("gapMatchInteraction"),
8
+ responseIdentifier: z.string().min(1),
9
+ gapTexts: z.array(z.looseObject({ identifier: z.string().min(1), matchMax: z.number().int().optional() })).min(1),
10
+ // Flow content with `kind: "gap"` nodes nested anywhere inside it. Responses are
11
+ // directedPairs gapText→gap.
12
+ content: z.array(z.looseObject({ kind: z.string().min(1) })).min(1),
13
+ });
14
+
15
+ export const gapMatchInteraction = defineInteraction({
16
+ kind: "gapMatchInteraction",
17
+ schema: gapMatchInteractionNodeSchema,
18
+ scoring: "qti-standard",
19
+ initialResponse(): ResponseValue {
20
+ return null;
21
+ },
22
+ });