@conform-ed/qti-react 0.0.13 → 0.0.14

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 (99) hide show
  1. package/dist/capability.d.ts +17 -0
  2. package/dist/content-model.d.ts +42 -0
  3. package/dist/graphic.d.ts +23 -0
  4. package/dist/index.d.ts +14 -0
  5. package/dist/index.js +149 -159
  6. package/dist/interactions/associate.d.ts +2 -0
  7. package/dist/interactions/choice.d.ts +2 -0
  8. package/dist/interactions/drawing.d.ts +2 -0
  9. package/dist/interactions/end-attempt.d.ts +2 -0
  10. package/dist/interactions/extended-text.d.ts +2 -0
  11. package/dist/interactions/gap-match.d.ts +2 -0
  12. package/dist/interactions/graphic.d.ts +13 -0
  13. package/dist/interactions/hottext.d.ts +2 -0
  14. package/dist/interactions/index.d.ts +18 -0
  15. package/dist/interactions/inline-choice.d.ts +2 -0
  16. package/dist/interactions/match.d.ts +2 -0
  17. package/dist/interactions/media.d.ts +2 -0
  18. package/dist/interactions/order.d.ts +2 -0
  19. package/dist/interactions/slider.d.ts +2 -0
  20. package/dist/interactions/text-entry.d.ts +2 -0
  21. package/dist/interactions/upload.d.ts +2 -0
  22. package/dist/normalized-item.d.ts +30 -0
  23. package/dist/pci/index.d.ts +6 -0
  24. package/dist/pci/interaction.d.ts +8 -0
  25. package/dist/pci/markup.d.ts +10 -0
  26. package/dist/pci/mount.d.ts +50 -0
  27. package/dist/pci/registry.d.ts +53 -0
  28. package/dist/pci/response.d.ts +11 -0
  29. package/dist/pci/skin.d.ts +12 -0
  30. package/dist/reference-skin/associate.d.ts +8 -0
  31. package/dist/reference-skin/choice.d.ts +8 -0
  32. package/dist/reference-skin/content.d.ts +6 -0
  33. package/dist/reference-skin/drawing.d.ts +9 -0
  34. package/dist/reference-skin/end-attempt.d.ts +7 -0
  35. package/dist/reference-skin/extended-text.d.ts +6 -0
  36. package/dist/reference-skin/gap-match.d.ts +8 -0
  37. package/dist/reference-skin/graphic-associate.d.ts +8 -0
  38. package/dist/reference-skin/graphic-base.d.ts +39 -0
  39. package/dist/reference-skin/graphic-gap-match.d.ts +8 -0
  40. package/dist/reference-skin/graphic-order.d.ts +8 -0
  41. package/dist/reference-skin/hotspot.d.ts +8 -0
  42. package/dist/reference-skin/hottext.d.ts +8 -0
  43. package/dist/reference-skin/index.d.ts +30 -0
  44. package/dist/reference-skin/inline-choice.d.ts +7 -0
  45. package/dist/reference-skin/match.d.ts +8 -0
  46. package/dist/reference-skin/media.d.ts +9 -0
  47. package/dist/reference-skin/order.d.ts +8 -0
  48. package/dist/reference-skin/position-object.d.ts +9 -0
  49. package/dist/reference-skin/select-point.d.ts +8 -0
  50. package/dist/reference-skin/slider.d.ts +8 -0
  51. package/dist/reference-skin/text-entry.d.ts +6 -0
  52. package/dist/reference-skin/upload.d.ts +8 -0
  53. package/dist/response-processing.d.ts +48 -0
  54. package/dist/rp/evaluate.d.ts +35 -0
  55. package/dist/rp/index.d.ts +4 -0
  56. package/dist/rp/interpreter.d.ts +15 -0
  57. package/dist/rp/template-processing.d.ts +49 -0
  58. package/dist/rp/templates.d.ts +8 -0
  59. package/dist/rp/types.d.ts +158 -0
  60. package/dist/rp/values.d.ts +27 -0
  61. package/dist/runtime.d.ts +164 -0
  62. package/dist/store.d.ts +61 -0
  63. package/dist/test/controller.d.ts +11 -0
  64. package/dist/test/index.d.ts +3 -0
  65. package/dist/test/session-store.d.ts +46 -0
  66. package/dist/test/types.d.ts +194 -0
  67. package/dist/types.d.ts +58 -0
  68. package/package.json +6 -6
  69. package/src/interactions/associate.ts +2 -2
  70. package/src/interactions/choice.ts +2 -2
  71. package/src/interactions/drawing.ts +2 -2
  72. package/src/interactions/end-attempt.ts +2 -2
  73. package/src/interactions/extended-text.ts +2 -2
  74. package/src/interactions/gap-match.ts +2 -2
  75. package/src/interactions/graphic.ts +7 -7
  76. package/src/interactions/hottext.ts +2 -2
  77. package/src/interactions/index.ts +0 -1
  78. package/src/interactions/inline-choice.ts +2 -2
  79. package/src/interactions/match.ts +2 -2
  80. package/src/interactions/media.ts +2 -2
  81. package/src/interactions/order.ts +2 -2
  82. package/src/interactions/slider.ts +2 -2
  83. package/src/interactions/text-entry.ts +2 -2
  84. package/src/interactions/upload.ts +2 -2
  85. package/src/normalized-item.ts +6 -4
  86. package/src/pci/interaction.ts +2 -2
  87. package/src/pci/mount.ts +1 -2
  88. package/src/pci/skin.ts +0 -1
  89. package/src/reference-skin/drawing.ts +25 -15
  90. package/src/reference-skin/index.ts +2 -3
  91. package/src/rp/evaluate.ts +22 -23
  92. package/src/rp/interpreter.ts +9 -6
  93. package/src/rp/template-processing.ts +8 -13
  94. package/src/rp/types.ts +12 -6
  95. package/src/rp/values.ts +19 -6
  96. package/src/runtime.ts +9 -7
  97. package/src/store.ts +14 -8
  98. package/src/test/controller.ts +10 -7
  99. package/src/test/session-store.ts +0 -1
@@ -0,0 +1,17 @@
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
+ export type CapabilityIssueType = "unsupported-interaction" | "invalid-interaction" | "unsupported-element" | "unsupported-rp";
7
+ export interface CapabilityIssue {
8
+ readonly type: CapabilityIssueType;
9
+ /** The interaction kind, element name, or RP rule/operator/template at issue. */
10
+ readonly name: string;
11
+ readonly responseIdentifier?: string;
12
+ readonly detail?: string;
13
+ }
14
+ export interface CapabilityReport {
15
+ readonly deliverable: boolean;
16
+ readonly issues: readonly CapabilityIssue[];
17
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The single source of truth for what may appear inside an item/stimulus body.
3
+ *
4
+ * Both the renderer's allowlist tree-walk and (later) the authoring editor schema
5
+ * derive from this definition. QTI 3 bodies are validated for *structure* by
6
+ * `@conform-ed/contracts`, but their embedded HTML flow content is modelled as a
7
+ * generic node tree — so validation does not sanitize. This allowlist is the
8
+ * sanitizer: the renderer emits React only for elements/attributes named here and
9
+ * drops everything else. It never injects HTML strings.
10
+ *
11
+ * v0 scope: the minimal flow/inline vocabulary plus the language-critical bits
12
+ * (ruby/furigana, MathML). It grows incrementally with the renderer — never "all of
13
+ * HTML5 at once".
14
+ */
15
+ /** Interaction node kinds conform-ed ships descriptors and Reference Skins for. */
16
+ export declare const v0InteractionKinds: readonly ["associateInteraction", "choiceInteraction", "drawingInteraction", "endAttemptInteraction", "extendedTextInteraction", "gapMatchInteraction", "graphicAssociateInteraction", "graphicGapMatchInteraction", "graphicOrderInteraction", "hotspotInteraction", "hottextInteraction", "inlineChoiceInteraction", "matchInteraction", "mediaInteraction", "orderInteraction", "positionObjectStage", "selectPointInteraction", "sliderInteraction", "textEntryInteraction", "uploadInteraction"];
17
+ export type V0InteractionKind = (typeof v0InteractionKinds)[number];
18
+ export interface ContentModel {
19
+ readonly interactionKinds: ReadonlySet<string>;
20
+ readonly flowElements: ReadonlySet<string>;
21
+ readonly mathRoot: string;
22
+ readonly globalAttributes: ReadonlySet<string>;
23
+ /** Per-element attribute allowlists, additive to `globalAttributes`. */
24
+ readonly elementAttributes: ReadonlyMap<string, ReadonlySet<string>>;
25
+ /** Attributes whose values are asset references, routed through the Asset Resolver. */
26
+ readonly urlAttributes: ReadonlySet<string>;
27
+ }
28
+ export declare const v0ContentModel: ContentModel;
29
+ export declare function isAllowedFlowElement(model: ContentModel, name: string): boolean;
30
+ export declare function isInteractionKind(model: ContentModel, kind: string): boolean;
31
+ /**
32
+ * Reduce a raw attribute bag to the safe, allowlisted subset for one element. Used by
33
+ * the body walk so a node that validates against QTI structure still cannot carry
34
+ * script or handlers. The allowlist is the global set plus the element's own entries.
35
+ */
36
+ export declare function sanitizeAttributes(model: ContentModel, elementName: string, attributes: Record<string, unknown> | undefined): Record<string, string>;
37
+ /**
38
+ * Attribute hardening for MathML subtrees: presentation attributes (mathvariant,
39
+ * linethickness, …) are not individually allowlisted — MathML has no scripting surface
40
+ * once event handlers and javascript: URLs are stripped.
41
+ */
42
+ export declare function sanitizeMathAttributes(attributes: Record<string, unknown> | undefined): Record<string, string>;
@@ -0,0 +1,23 @@
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
+ export type QtiShape = "circle" | "rect" | "poly" | "ellipse" | "default";
13
+ export interface Point {
14
+ readonly x: number;
15
+ readonly y: number;
16
+ }
17
+ /** Parse a QTI coords attribute ("10,20,30") into numbers. */
18
+ export declare function parseCoords(coords: string): number[];
19
+ /** Parse a QTI point value ("x y") or null when malformed. */
20
+ export declare function parsePoint(value: string): Point | null;
21
+ export declare function formatPoint(point: Point): string;
22
+ /** QTI hit test: is `point` inside the area described by (shape, coords)? */
23
+ export declare function pointInShape(shape: string, coords: readonly number[], point: Point): boolean;
@@ -0,0 +1,14 @@
1
+ export declare const qtiReactPackageName = "@conform-ed/qti-react";
2
+ export { v0ContentModel, v0InteractionKinds, isAllowedFlowElement, isInteractionKind, sanitizeAttributes, type ContentModel, type V0InteractionKind, } from "./content-model";
3
+ export { foldString, mapResponse, matchCorrect, mapResponsePoint, scoreResponse } from "./response-processing";
4
+ export { assessmentItemViewFromNormalized, assessmentTestViewFromNormalized } from "./normalized-item";
5
+ export { formatPoint, parseCoords, parsePoint, pointInShape, type Point, type QtiShape } from "./graphic";
6
+ export { applyCorrectResponseOverrides, collectRpIssues, collectTemplateIssues, executeResponseProcessing, executeTemplateProcessing, mulberry32, resolveTemplate, } from "./rp";
7
+ export type { CustomOperatorImplementation, MaybeRpValue, OutcomeDeclarationView, OutcomeValue, ResponseNormalization, ResponseProcessingContext, ResponseProcessingResult, ResponseProcessingView, RpConditionBranch, RpExpressionView, RpRecordField, RpRuleView, RpScalar, RpValue, TemplateConditionBranch, TemplateDeclarationView, TemplateProcessingContext, TemplateProcessingResult, TemplateProcessingView, TemplateRuleView, } from "./rp";
8
+ export { createAttemptStore, type AttemptSnapshot, type AttemptStore, type AttemptStoreOptions } from "./store";
9
+ export { createTestController, createTestSessionStore, type TestSessionSnapshot, type TestSessionStore, type TestSessionStoreOptions, type AssessmentItemRefView, type AssessmentSectionView, type AssessmentTestView, type BranchRuleView, type ItemSessionControlView, type OutcomeConditionBranch, type OutcomeRuleView, type TestController, type TestFeedbackView, type TestItemResult, type TestPartView, type TestPlan, type TestPlanItem, type TestPlanPart, type TestSessionState, type TimeLimitsView, } from "./test";
10
+ export { createQtiRuntime, defineInteraction, type AssessmentItemView, type AttemptController, type BodyNode, type CapabilityIssue, type CapabilityIssueType, type CapabilityReport, type ContentRendererProps, type FeedbackView, type InteractionDescriptor, type InteractionNode, type InteractionRenderProps, type InteractionSkin, type InteractionStatus, type ItemRendererProps, type NodeOverrides, type OptionProps, type OptionStatus, type QtiRuntime, type QtiRuntimeConfig, type SkinRegistry, type XmlContentNode, } from "./runtime";
11
+ export { createPciModuleRegistry, createPciSkin, mountPci, pciResponseToValue, portableCustomInteraction, serializePciMarkup, valueToPciResponse, type PciConfiguration, type PciInstance, type PciInteractionNode, type PciModule, type PciModuleRegistry, type PciModuleRegistryOptions, type PciMountHandle, type PciMountOptions, type PciSkinOptions, } from "./pci";
12
+ export { associateInteraction, choiceInteraction, drawingInteraction, endAttemptInteraction, extendedTextInteraction, gapMatchInteraction, graphicAssociateInteraction, graphicGapMatchInteraction, graphicOrderInteraction, hotspotInteraction, hottextInteraction, inlineChoiceInteraction, matchInteraction, mediaInteraction, orderInteraction, positionObjectStage, qtiCoreInteractions, selectPointInteraction, sliderInteraction, textEntryInteraction, uploadInteraction, } from "./interactions";
13
+ export { AssociateReferenceSkin, ChoiceReferenceSkin, DrawingReferenceSkin, EndAttemptReferenceSkin, ExtendedTextReferenceSkin, GapMatchReferenceSkin, GraphicAssociateReferenceSkin, GraphicGapMatchReferenceSkin, GraphicOrderReferenceSkin, GraphicStage, HotspotReferenceSkin, HottextReferenceSkin, InlineChoiceReferenceSkin, MatchReferenceSkin, MediaReferenceSkin, OrderReferenceSkin, PositionObjectReferenceSkin, SelectPointReferenceSkin, SliderReferenceSkin, TextEntryReferenceSkin, UploadReferenceSkin, referenceSkin, textOf, } from "./reference-skin";
14
+ export type { AreaMapEntryView, AreaMappingView, Cardinality, CorrectResponseView, MapEntryView, MappingView, ResponseDeclarationView, ResponseValue, ScoreResult, } from "./types";
package/dist/index.js CHANGED
@@ -743,11 +743,7 @@ function fromResponse(declaration, response) {
743
743
  if (raw.length === 0) {
744
744
  return null;
745
745
  }
746
- return {
747
- cardinality: declaration.cardinality,
748
- baseType: declaration.baseType,
749
- values: raw.map((value) => coerceScalar(value, declaration.baseType))
750
- };
746
+ return rpValue(declaration.cardinality, raw.map((value) => coerceScalar(value, declaration.baseType)), declaration.baseType);
751
747
  }
752
748
  function singleNumber(value) {
753
749
  if (value === null || value.values.length !== 1) {
@@ -763,6 +759,13 @@ function singleBoolean(value) {
763
759
  const member = value.values[0];
764
760
  return typeof member === "boolean" ? member : null;
765
761
  }
762
+ function rpValue(cardinality, values, baseType) {
763
+ return {
764
+ cardinality,
765
+ values,
766
+ ...baseType !== undefined ? { baseType } : {}
767
+ };
768
+ }
766
769
  function booleanValue(value) {
767
770
  return { cardinality: "single", baseType: "boolean", values: [value] };
768
771
  }
@@ -826,7 +829,7 @@ function fromFlatValue(value, cardinality, baseType) {
826
829
  if (values.length === 0) {
827
830
  return null;
828
831
  }
829
- return { cardinality, baseType, values: values.map((member) => coerceScalar(member, baseType)) };
832
+ return rpValue(cardinality, values.map((member) => coerceScalar(member, baseType)), baseType);
830
833
  }
831
834
  function toOutcomeValue(value) {
832
835
  if (value === null || value.values.length === 0) {
@@ -979,7 +982,7 @@ function evaluateExpression(expression, env) {
979
982
  case "baseValue": {
980
983
  const baseType = expression.baseType;
981
984
  const value = expression.value;
982
- return value === undefined ? null : { cardinality: "single", baseType, values: [coerceScalar(value, baseType)] };
985
+ return value === undefined ? null : rpValue("single", [coerceScalar(value, baseType)], baseType);
983
986
  }
984
987
  case "variable":
985
988
  return env.lookupVariable(expression.identifier ?? "");
@@ -988,11 +991,7 @@ function evaluateExpression(expression, env) {
988
991
  if (!declaration?.correctResponse) {
989
992
  return null;
990
993
  }
991
- return {
992
- cardinality: declaration.cardinality,
993
- baseType: declaration.baseType,
994
- values: declaration.correctResponse.values.map((entry) => coerceScalar(entry.value, declaration.baseType))
995
- };
994
+ return rpValue(declaration.cardinality, declaration.correctResponse.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType);
996
995
  }
997
996
  case "mapResponse": {
998
997
  const identifier = expression.identifier ?? "";
@@ -1030,7 +1029,7 @@ function evaluateExpression(expression, env) {
1030
1029
  const operand = expression.expressions?.[0];
1031
1030
  const value = operand === undefined ? null : evaluate(operand);
1032
1031
  const field = value?.fields?.find((entry) => entry.name === expression.fieldIdentifier);
1033
- return field === undefined ? null : { cardinality: "single", ...field.baseType ? { baseType: field.baseType } : {}, values: [field.value] };
1032
+ return field === undefined ? null : rpValue("single", [field.value], field.baseType);
1034
1033
  }
1035
1034
  case "and":
1036
1035
  case "or": {
@@ -1119,7 +1118,7 @@ function evaluateExpression(expression, env) {
1119
1118
  if (member === undefined) {
1120
1119
  return null;
1121
1120
  }
1122
- return { cardinality: "single", baseType: container.baseType, values: [member] };
1121
+ return rpValue("single", [member], container.baseType);
1123
1122
  }
1124
1123
  case "mathConstant": {
1125
1124
  const constant = expression.name === undefined ? undefined : mathConstants[expression.name];
@@ -1270,7 +1269,7 @@ function evaluateExpression(expression, env) {
1270
1269
  }
1271
1270
  const baseType = container.baseType ?? value.baseType;
1272
1271
  const remaining = container.values.filter((member) => !scalarsEqual(member, scalar, baseType, env.normalization));
1273
- return remaining.length === 0 ? null : { cardinality: container.cardinality, baseType: container.baseType, values: remaining };
1272
+ return remaining.length === 0 ? null : rpValue(container.cardinality, remaining, container.baseType);
1274
1273
  }
1275
1274
  case "repeat": {
1276
1275
  if (typeof expression.numberRepeats !== "number") {
@@ -1291,7 +1290,7 @@ function evaluateExpression(expression, env) {
1291
1290
  members.push(...value.values);
1292
1291
  }
1293
1292
  }
1294
- return members.length === 0 ? null : { cardinality: "ordered", baseType, values: members };
1293
+ return members.length === 0 ? null : rpValue("ordered", members, baseType);
1295
1294
  }
1296
1295
  case "stringMatch":
1297
1296
  case "substring": {
@@ -1359,7 +1358,7 @@ function evaluateExpression(expression, env) {
1359
1358
  if (members.length === 0) {
1360
1359
  return null;
1361
1360
  }
1362
- return { cardinality: expression.kind, baseType, values: members };
1361
+ return rpValue(expression.kind, members, baseType);
1363
1362
  }
1364
1363
  case "randomInteger": {
1365
1364
  if (!env.random) {
@@ -1411,7 +1410,7 @@ function evaluateExpression(expression, env) {
1411
1410
  return null;
1412
1411
  }
1413
1412
  const pick = container.values[Math.floor(env.random() * container.values.length)];
1414
- return { cardinality: "single", baseType: container.baseType, values: [pick] };
1413
+ return rpValue("single", [pick], container.baseType);
1415
1414
  }
1416
1415
  default:
1417
1416
  throw new RpUnsupportedError(expression.kind);
@@ -1608,11 +1607,7 @@ function defaultOutcomes(declarations) {
1608
1607
  const outcomes = new Map;
1609
1608
  for (const declaration of declarations) {
1610
1609
  if (declaration.defaultValue) {
1611
- outcomes.set(declaration.identifier, {
1612
- cardinality: declaration.cardinality,
1613
- baseType: declaration.baseType,
1614
- values: declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType))
1615
- });
1610
+ outcomes.set(declaration.identifier, rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType));
1616
1611
  continue;
1617
1612
  }
1618
1613
  outcomes.set(declaration.identifier, isNumericBaseType(declaration.baseType) ? floatValue(0) : null);
@@ -1780,11 +1775,7 @@ function executeTemplateProcessing(view, context) {
1780
1775
  function initialValues() {
1781
1776
  const values = new Map;
1782
1777
  for (const declaration of context.templateDeclarations) {
1783
- values.set(declaration.identifier, declaration.defaultValue ? {
1784
- cardinality: declaration.cardinality,
1785
- baseType: declaration.baseType,
1786
- values: declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType))
1787
- } : null);
1778
+ values.set(declaration.identifier, declaration.defaultValue ? rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType) : null);
1788
1779
  }
1789
1780
  return values;
1790
1781
  }
@@ -1815,11 +1806,7 @@ function executeTemplateProcessing(view, context) {
1815
1806
  if (rule.identifier !== undefined && rule.expression !== undefined) {
1816
1807
  const declaration = declarationsById.get(rule.identifier);
1817
1808
  const value = evaluateExpression(rule.expression, env);
1818
- templateValues.set(rule.identifier, value === null || !declaration ? value : {
1819
- cardinality: declaration.cardinality,
1820
- baseType: declaration.baseType ?? value.baseType,
1821
- values: value.values
1822
- });
1809
+ templateValues.set(rule.identifier, value === null || !declaration ? value : rpValue(declaration.cardinality, value.values, declaration.baseType ?? value.baseType));
1823
1810
  }
1824
1811
  continue;
1825
1812
  }
@@ -2192,11 +2179,7 @@ function createTestController(view, options) {
2192
2179
  const outcomes = new Map;
2193
2180
  for (const declaration of view.outcomeDeclarations ?? []) {
2194
2181
  if (declaration.defaultValue) {
2195
- outcomes.set(declaration.identifier, {
2196
- cardinality: declaration.cardinality,
2197
- baseType: declaration.baseType,
2198
- values: declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType))
2199
- });
2182
+ outcomes.set(declaration.identifier, rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType));
2200
2183
  continue;
2201
2184
  }
2202
2185
  outcomes.set(declaration.identifier, isNumericBaseType(declaration.baseType) ? floatValue(0) : null);
@@ -2244,7 +2227,7 @@ function createTestController(view, options) {
2244
2227
  baseType ??= lifted.baseType;
2245
2228
  members.push(...lifted.values);
2246
2229
  }
2247
- return members.length === 0 ? null : { cardinality: "multiple", baseType, values: members };
2230
+ return members.length === 0 ? null : rpValue("multiple", members, baseType);
2248
2231
  },
2249
2232
  testAggregate: (expression) => {
2250
2233
  const subset = subsetItems(expression);
@@ -3021,11 +3004,12 @@ function createQtiRuntime(config) {
3021
3004
  }
3022
3005
  const parsed = descriptor.schema.safeParse(node);
3023
3006
  if (!parsed.success) {
3007
+ const detail = parsed.error.issues[0]?.message;
3024
3008
  report({
3025
3009
  type: "invalid-interaction",
3026
3010
  name: node.kind,
3027
3011
  responseIdentifier: node.responseIdentifier,
3028
- detail: parsed.error.issues[0]?.message
3012
+ ...detail !== undefined ? { detail } : {}
3029
3013
  });
3030
3014
  }
3031
3015
  return;
@@ -3384,7 +3368,7 @@ async function mountPci(options) {
3384
3368
  ...options.declaration ? { boundTo: { [node.responseIdentifier]: valueToPciResponse(options.boundValue ?? null, options.declaration) } } : {},
3385
3369
  status: "interacting",
3386
3370
  onready: (instance2) => resolveReady(instance2),
3387
- ondone: (instance2, response, state) => options.ondone?.(pciResponseToValue(response), state)
3371
+ ondone: (_instance, response, state) => options.ondone?.(pciResponseToValue(response), state)
3388
3372
  };
3389
3373
  const returned = module.getInstance(container, configuration, options.state);
3390
3374
  if (returned) {
@@ -3919,7 +3903,12 @@ function ChoiceReferenceSkin(props) {
3919
3903
  }
3920
3904
 
3921
3905
  // src/reference-skin/drawing.ts
3922
- import { createElement as createElement5, useEffect as useEffect2, useRef as useRef2 } from "react";
3906
+ import {
3907
+ createElement as createElement5,
3908
+ useCallback,
3909
+ useEffect as useEffect2,
3910
+ useRef as useRef2
3911
+ } from "react";
3923
3912
  var strokeStyle = "#c2410c";
3924
3913
  var strokeWidth = 3;
3925
3914
  function DrawingReferenceSkin(props) {
@@ -3930,7 +3919,8 @@ function DrawingReferenceSkin(props) {
3930
3919
  const drawingRef = useRef2(false);
3931
3920
  const propsRef = useRef2(props);
3932
3921
  propsRef.current = props;
3933
- const paintBackground = (canvas) => {
3922
+ const stageData = node.object.data;
3923
+ const paintBackground = useCallback((canvas) => {
3934
3924
  const context = canvas.getContext("2d");
3935
3925
  if (!context) {
3936
3926
  return;
@@ -3940,13 +3930,13 @@ function DrawingReferenceSkin(props) {
3940
3930
  image.onload = () => {
3941
3931
  context.drawImage(image, 0, 0, canvas.width, canvas.height);
3942
3932
  };
3943
- image.src = propsRef.current.resolveAsset(node.object.data);
3944
- };
3933
+ image.src = propsRef.current.resolveAsset(stageData);
3934
+ }, [stageData]);
3945
3935
  useEffect2(() => {
3946
3936
  if (canvasRef.current) {
3947
3937
  paintBackground(canvasRef.current);
3948
3938
  }
3949
- }, [node.object.data]);
3939
+ }, [paintBackground]);
3950
3940
  const pointerPosition = (event) => {
3951
3941
  const canvas = event.currentTarget;
3952
3942
  const rect = canvas.getBoundingClientRect();
@@ -4359,124 +4349,27 @@ function HotspotReferenceSkin(props) {
4359
4349
  });
4360
4350
  }
4361
4351
 
4362
- // src/reference-skin/position-object.ts
4363
- import { createElement as createElement14 } from "react";
4364
- function PositionObjectReferenceSkin(props) {
4365
- const node = props.node;
4366
- const maxChoices = node.maxChoices ?? 1;
4367
- const points = props.value === null ? [] : typeof props.value === "string" ? [props.value] : Array.isArray(props.value) ? [...props.value] : [];
4368
- if (!node.stageObject || !node.object) {
4369
- return null;
4370
- }
4371
- const movable = node.object;
4372
- function stageClick(point) {
4373
- if (props.disabled) {
4374
- return;
4375
- }
4376
- const formatted = formatPoint(point);
4377
- if (maxChoices === 1) {
4378
- props.setValue(formatted);
4379
- return;
4380
- }
4381
- if (points.length < maxChoices) {
4382
- props.setValue([...points, formatted]);
4383
- }
4384
- }
4385
- return createElement14(GraphicStage, {
4386
- object: node.stageObject,
4387
- resolveAsset: props.resolveAsset,
4388
- interaction: "positionObjectStage",
4389
- status: props.status,
4390
- onStageClick: stageClick,
4391
- prompt: node.prompt ? createElement14("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null,
4392
- overlay: points.map((value, index) => {
4393
- const point = parsePoint(value);
4394
- if (!point) {
4395
- return null;
4396
- }
4397
- return createElement14("image", {
4398
- key: `${value}-${index}`,
4399
- href: props.resolveAsset(movable.data),
4400
- x: point.x - (movable.width ?? 0) / 2,
4401
- y: point.y - (movable.height ?? 0) / 2,
4402
- width: movable.width,
4403
- height: movable.height,
4404
- "data-qti-point": value,
4405
- style: { pointerEvents: "none" }
4406
- });
4407
- })
4408
- });
4409
- }
4410
-
4411
- // src/reference-skin/select-point.ts
4412
- import { Fragment as Fragment5, createElement as createElement15 } from "react";
4413
- function SelectPointReferenceSkin(props) {
4414
- const node = props.node;
4415
- const maxChoices = node.maxChoices ?? 1;
4416
- const points = props.value === null ? [] : typeof props.value === "string" ? [props.value] : Array.isArray(props.value) ? [...props.value] : [];
4417
- if (!node.object) {
4418
- return null;
4419
- }
4420
- function stageClick(point) {
4421
- if (props.disabled) {
4422
- return;
4423
- }
4424
- const formatted = formatPoint(point);
4425
- if (maxChoices === 1) {
4426
- props.setValue(formatted);
4427
- return;
4428
- }
4429
- if (points.length < maxChoices) {
4430
- props.setValue([...points, formatted]);
4431
- }
4432
- }
4433
- return createElement15(GraphicStage, {
4434
- object: node.object,
4435
- resolveAsset: props.resolveAsset,
4436
- interaction: "selectPointInteraction",
4437
- status: props.status,
4438
- onStageClick: stageClick,
4439
- prompt: node.prompt ? createElement15("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null,
4440
- overlay: points.map((value, index) => {
4441
- const [x, y] = value.split(/\s+/u).map(Number);
4442
- return createElement15(Fragment5, { key: `${value}-${index}` }, createElement15("circle", {
4443
- cx: x,
4444
- cy: y,
4445
- r: 5,
4446
- fill: "currentColor",
4447
- "data-qti-point": value,
4448
- style: { pointerEvents: "none" }
4449
- }));
4450
- }),
4451
- after: createElement15("button", {
4452
- type: "button",
4453
- disabled: props.disabled || points.length === 0,
4454
- onClick: () => props.setValue(null)
4455
- }, "Clear points")
4456
- });
4457
- }
4458
-
4459
4352
  // src/reference-skin/hottext.ts
4460
- import { createElement as createElement16 } from "react";
4353
+ import { createElement as createElement14 } from "react";
4461
4354
  function HottextReferenceSkin(props) {
4462
4355
  const node = props.node;
4463
- return createElement16("div", { "data-qti-interaction": "hottextInteraction", "data-status": props.status }, node.prompt ? createElement16("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null, props.renderContent(node.content, {
4356
+ return createElement14("div", { "data-qti-interaction": "hottextInteraction", "data-status": props.status }, node.prompt ? createElement14("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null, props.renderContent(node.content, {
4464
4357
  hottext: (child, key) => {
4465
4358
  const view = child;
4466
4359
  const identifier = view.identifier ?? "";
4467
4360
  const optionProps = props.getOptionProps(identifier);
4468
- return createElement16("button", { key, type: "button", disabled: props.disabled, "data-qti-hottext": identifier, ...optionProps }, props.renderContent(view.content) ?? identifier);
4361
+ return createElement14("button", { key, type: "button", disabled: props.disabled, "data-qti-hottext": identifier, ...optionProps }, props.renderContent(view.content) ?? identifier);
4469
4362
  }
4470
4363
  }));
4471
4364
  }
4472
4365
 
4473
4366
  // src/reference-skin/inline-choice.ts
4474
- import { createElement as createElement17 } from "react";
4367
+ import { createElement as createElement15 } from "react";
4475
4368
  function InlineChoiceReferenceSkin(props) {
4476
4369
  const node = props.node;
4477
4370
  const choices = node.inlineChoices ?? [];
4478
4371
  const value = typeof props.value === "string" ? props.value : "";
4479
- return createElement17("select", {
4372
+ return createElement15("select", {
4480
4373
  value,
4481
4374
  disabled: props.disabled,
4482
4375
  "aria-disabled": props.disabled,
@@ -4485,11 +4378,11 @@ function InlineChoiceReferenceSkin(props) {
4485
4378
  onChange: (event) => {
4486
4379
  props.setValue(event.target.value === "" ? null : event.target.value);
4487
4380
  }
4488
- }, createElement17("option", { key: "", value: "" }, ""), choices.map((choice) => createElement17("option", { key: choice.identifier, value: choice.identifier }, textOf(choice.content))));
4381
+ }, createElement15("option", { key: "", value: "" }, ""), choices.map((choice) => createElement15("option", { key: choice.identifier, value: choice.identifier }, textOf(choice.content))));
4489
4382
  }
4490
4383
 
4491
4384
  // src/reference-skin/match.ts
4492
- import { createElement as createElement18 } from "react";
4385
+ import { createElement as createElement16 } from "react";
4493
4386
  function MatchReferenceSkin(props) {
4494
4387
  const node = props.node;
4495
4388
  const rows = node.simpleMatchSets?.[0]?.simpleAssociableChoices ?? [];
@@ -4498,9 +4391,9 @@ function MatchReferenceSkin(props) {
4498
4391
  function togglePair(pair) {
4499
4392
  props.setValue(pairs.includes(pair) ? pairs.filter((entry) => entry !== pair) : [...pairs, pair]);
4500
4393
  }
4501
- return createElement18("div", { "data-qti-interaction": "matchInteraction", "data-status": props.status }, node.prompt ? createElement18("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null, createElement18("table", null, createElement18("thead", null, createElement18("tr", null, createElement18("td", null), columns.map((column) => createElement18("th", { key: column.identifier, scope: "col" }, textOf(column.content) || column.identifier)))), createElement18("tbody", null, rows.map((row) => createElement18("tr", { key: row.identifier }, createElement18("th", { scope: "row" }, textOf(row.content) || row.identifier), columns.map((column) => {
4394
+ return createElement16("div", { "data-qti-interaction": "matchInteraction", "data-status": props.status }, node.prompt ? createElement16("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null, createElement16("table", null, createElement16("thead", null, createElement16("tr", null, createElement16("td", null), columns.map((column) => createElement16("th", { key: column.identifier, scope: "col" }, textOf(column.content) || column.identifier)))), createElement16("tbody", null, rows.map((row) => createElement16("tr", { key: row.identifier }, createElement16("th", { scope: "row" }, textOf(row.content) || row.identifier), columns.map((column) => {
4502
4395
  const pair = `${row.identifier} ${column.identifier}`;
4503
- return createElement18("td", { key: column.identifier }, createElement18("input", {
4396
+ return createElement16("td", { key: column.identifier }, createElement16("input", {
4504
4397
  type: "checkbox",
4505
4398
  checked: pairs.includes(pair),
4506
4399
  disabled: props.disabled,
@@ -4512,7 +4405,7 @@ function MatchReferenceSkin(props) {
4512
4405
  }
4513
4406
 
4514
4407
  // src/reference-skin/media.ts
4515
- import { createElement as createElement19 } from "react";
4408
+ import { createElement as createElement17 } from "react";
4516
4409
  function findMediaElement(nodes) {
4517
4410
  for (const node of nodes ?? []) {
4518
4411
  if (node.kind === "xml") {
@@ -4534,10 +4427,10 @@ function MediaReferenceSkin(props) {
4534
4427
  const plays = typeof props.value === "string" ? Number(props.value) || 0 : 0;
4535
4428
  const playsExhausted = node.maxPlays !== undefined && node.maxPlays > 0 && plays >= node.maxPlays;
4536
4429
  if (!media) {
4537
- return createElement19("div", { "data-qti-interaction": "mediaInteraction", "data-status": props.status }, "No media element.");
4430
+ return createElement17("div", { "data-qti-interaction": "mediaInteraction", "data-status": props.status }, "No media element.");
4538
4431
  }
4539
4432
  const src = typeof media.attributes?.["src"] === "string" ? props.resolveAsset(media.attributes["src"]) : undefined;
4540
- return createElement19("div", { "data-qti-interaction": "mediaInteraction", "data-status": props.status, "data-qti-plays": plays }, node.prompt ? createElement19("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null, createElement19(media.name, {
4433
+ return createElement17("div", { "data-qti-interaction": "mediaInteraction", "data-status": props.status, "data-qti-plays": plays }, node.prompt ? createElement17("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null, createElement17(media.name, {
4541
4434
  src,
4542
4435
  controls: true,
4543
4436
  loop: node.loop ?? false,
@@ -4548,11 +4441,11 @@ function MediaReferenceSkin(props) {
4548
4441
  }
4549
4442
  props.setValue(String(plays + 1));
4550
4443
  }
4551
- }), playsExhausted ? createElement19("p", { role: "status" }, "No plays remaining.") : null);
4444
+ }), playsExhausted ? createElement17("p", { role: "status" }, "No plays remaining.") : null);
4552
4445
  }
4553
4446
 
4554
4447
  // src/reference-skin/order.ts
4555
- import { createElement as createElement20 } from "react";
4448
+ import { createElement as createElement18 } from "react";
4556
4449
  function OrderReferenceSkin(props) {
4557
4450
  const node = props.node;
4558
4451
  const choices = node.simpleChoices ?? [];
@@ -4570,12 +4463,12 @@ function OrderReferenceSkin(props) {
4570
4463
  next[target] = moved;
4571
4464
  props.setValue(next);
4572
4465
  }
4573
- return createElement20("div", { "data-qti-interaction": "orderInteraction", "data-status": props.status }, node.prompt ? createElement20("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null, createElement20("ol", null, order.map((identifier, index) => createElement20("li", { key: identifier }, props.renderContent(choicesById.get(identifier)?.content) ?? identifier, createElement20("button", {
4466
+ return createElement18("div", { "data-qti-interaction": "orderInteraction", "data-status": props.status }, node.prompt ? createElement18("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null, createElement18("ol", null, order.map((identifier, index) => createElement18("li", { key: identifier }, props.renderContent(choicesById.get(identifier)?.content) ?? identifier, createElement18("button", {
4574
4467
  type: "button",
4575
4468
  "aria-label": `Move ${identifier} up`,
4576
4469
  disabled: props.disabled || index === 0,
4577
4470
  onClick: () => move(index, -1)
4578
- }, "↑"), createElement20("button", {
4471
+ }, "↑"), createElement18("button", {
4579
4472
  type: "button",
4580
4473
  "aria-label": `Move ${identifier} down`,
4581
4474
  disabled: props.disabled || index === order.length - 1,
@@ -4583,6 +4476,103 @@ function OrderReferenceSkin(props) {
4583
4476
  }, "↓")))));
4584
4477
  }
4585
4478
 
4479
+ // src/reference-skin/position-object.ts
4480
+ import { createElement as createElement19 } from "react";
4481
+ function PositionObjectReferenceSkin(props) {
4482
+ const node = props.node;
4483
+ const maxChoices = node.maxChoices ?? 1;
4484
+ const points = props.value === null ? [] : typeof props.value === "string" ? [props.value] : Array.isArray(props.value) ? [...props.value] : [];
4485
+ if (!node.stageObject || !node.object) {
4486
+ return null;
4487
+ }
4488
+ const movable = node.object;
4489
+ function stageClick(point) {
4490
+ if (props.disabled) {
4491
+ return;
4492
+ }
4493
+ const formatted = formatPoint(point);
4494
+ if (maxChoices === 1) {
4495
+ props.setValue(formatted);
4496
+ return;
4497
+ }
4498
+ if (points.length < maxChoices) {
4499
+ props.setValue([...points, formatted]);
4500
+ }
4501
+ }
4502
+ return createElement19(GraphicStage, {
4503
+ object: node.stageObject,
4504
+ resolveAsset: props.resolveAsset,
4505
+ interaction: "positionObjectStage",
4506
+ status: props.status,
4507
+ onStageClick: stageClick,
4508
+ prompt: node.prompt ? createElement19("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null,
4509
+ overlay: points.map((value, index) => {
4510
+ const point = parsePoint(value);
4511
+ if (!point) {
4512
+ return null;
4513
+ }
4514
+ return createElement19("image", {
4515
+ key: `${value}-${index}`,
4516
+ href: props.resolveAsset(movable.data),
4517
+ x: point.x - (movable.width ?? 0) / 2,
4518
+ y: point.y - (movable.height ?? 0) / 2,
4519
+ width: movable.width,
4520
+ height: movable.height,
4521
+ "data-qti-point": value,
4522
+ style: { pointerEvents: "none" }
4523
+ });
4524
+ })
4525
+ });
4526
+ }
4527
+
4528
+ // src/reference-skin/select-point.ts
4529
+ import { Fragment as Fragment5, createElement as createElement20 } from "react";
4530
+ function SelectPointReferenceSkin(props) {
4531
+ const node = props.node;
4532
+ const maxChoices = node.maxChoices ?? 1;
4533
+ const points = props.value === null ? [] : typeof props.value === "string" ? [props.value] : Array.isArray(props.value) ? [...props.value] : [];
4534
+ if (!node.object) {
4535
+ return null;
4536
+ }
4537
+ function stageClick(point) {
4538
+ if (props.disabled) {
4539
+ return;
4540
+ }
4541
+ const formatted = formatPoint(point);
4542
+ if (maxChoices === 1) {
4543
+ props.setValue(formatted);
4544
+ return;
4545
+ }
4546
+ if (points.length < maxChoices) {
4547
+ props.setValue([...points, formatted]);
4548
+ }
4549
+ }
4550
+ return createElement20(GraphicStage, {
4551
+ object: node.object,
4552
+ resolveAsset: props.resolveAsset,
4553
+ interaction: "selectPointInteraction",
4554
+ status: props.status,
4555
+ onStageClick: stageClick,
4556
+ prompt: node.prompt ? createElement20("div", { "data-qti-prompt": true }, props.renderContent(node.prompt.content)) : null,
4557
+ overlay: points.map((value, index) => {
4558
+ const [x, y] = value.split(/\s+/u).map(Number);
4559
+ return createElement20(Fragment5, { key: `${value}-${index}` }, createElement20("circle", {
4560
+ cx: x,
4561
+ cy: y,
4562
+ r: 5,
4563
+ fill: "currentColor",
4564
+ "data-qti-point": value,
4565
+ style: { pointerEvents: "none" }
4566
+ }));
4567
+ }),
4568
+ after: createElement20("button", {
4569
+ type: "button",
4570
+ disabled: props.disabled || points.length === 0,
4571
+ onClick: () => props.setValue(null)
4572
+ }, "Clear points")
4573
+ });
4574
+ }
4575
+
4586
4576
  // src/reference-skin/slider.ts
4587
4577
  import { createElement as createElement21 } from "react";
4588
4578
  function SliderReferenceSkin(props) {
@@ -0,0 +1,2 @@
1
+ import { type InteractionDescriptor } from "../runtime";
2
+ export declare const associateInteraction: InteractionDescriptor<"associateInteraction">;
@@ -0,0 +1,2 @@
1
+ import { type InteractionDescriptor } from "../runtime";
2
+ export declare const choiceInteraction: InteractionDescriptor<"choiceInteraction">;
@@ -0,0 +1,2 @@
1
+ import { type InteractionDescriptor } from "../runtime";
2
+ export declare const drawingInteraction: InteractionDescriptor<"drawingInteraction">;