@conform-ed/qti-react 0.0.17 → 0.0.18

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/src/runtime.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  } from "react";
21
21
  import type { ZodType } from "zod";
22
22
 
23
- import type { CapabilityIssue, CapabilityReport } from "./capability";
23
+ import type { CapabilityReport } from "./capability";
24
24
  import {
25
25
  isAllowedFlowElement,
26
26
  sanitizeAttributes,
@@ -28,9 +28,9 @@ import {
28
28
  v0ContentModel,
29
29
  type ContentModel,
30
30
  } from "./content-model";
31
+ import { reportItemCapability } from "./item-capability";
31
32
  import { resolveCatalogSupports, type CatalogView, type PnpView, type ResolvedCatalogSupport } from "./pnp";
32
33
  import { collectInteractionConstraints } from "./response-validity";
33
- import { collectRpIssues, collectTemplateIssues } from "./rp";
34
34
  import type {
35
35
  CustomOperatorImplementation,
36
36
  OutcomeDeclarationView,
@@ -391,9 +391,6 @@ function templateVisible(value: OutcomeValue, view: TemplateContentView): boolea
391
391
  return matched !== (view.showHide === "hide");
392
392
  }
393
393
 
394
- /** Body node kinds that render without a descriptor, skin, or content-model entry. */
395
- const intrinsicLeafKinds = new Set(["text", "printedVariable"]);
396
-
397
394
  /** A read-only, already-"submitted" store: backs content rendered outside an attempt. */
398
395
  function createStaticStore(outcomes: Readonly<Record<string, OutcomeValue>>): AttemptStore {
399
396
  const snapshot: AttemptSnapshot = {
@@ -1015,125 +1012,22 @@ export function createQtiRuntime(config: QtiRuntimeConfig): QtiRuntime {
1015
1012
  }
1016
1013
 
1017
1014
  function canDeliver(item: AssessmentItemView): CapabilityReport {
1018
- const issues: CapabilityIssue[] = [];
1019
- const seen = new Set<string>();
1020
-
1021
- function report(issue: CapabilityIssue): void {
1022
- const dedupeKey = `${issue.type}:${issue.name}:${issue.responseIdentifier ?? ""}`;
1023
-
1024
- if (!seen.has(dedupeKey)) {
1025
- seen.add(dedupeKey);
1026
- issues.push(issue);
1027
- }
1028
- }
1029
-
1030
- function walk(node: BodyNode): void {
1031
- if (isFeedbackNode(node) || isTemplateContentNode(node) || node.kind === "rubricBlock") {
1032
- for (const child of (node as unknown as { content?: readonly BodyNode[] }).content ?? []) {
1033
- walk(child);
1034
- }
1035
-
1036
- return;
1037
- }
1038
-
1039
- if (isInteractionNode(node)) {
1040
- const descriptor = descriptorsByKind.get(node.kind);
1041
-
1042
- if (!descriptor || !config.skin[node.kind]) {
1043
- report({
1044
- type: "unsupported-interaction",
1045
- name: node.kind,
1046
- responseIdentifier: node.responseIdentifier,
1047
- });
1048
-
1049
- return;
1050
- }
1051
-
1052
- const parsed = descriptor.schema.safeParse(node);
1053
-
1054
- if (!parsed.success) {
1055
- const detail = parsed.error.issues[0]?.message;
1056
-
1057
- report({
1058
- type: "invalid-interaction",
1059
- name: node.kind,
1060
- responseIdentifier: node.responseIdentifier,
1061
- ...(detail !== undefined ? { detail } : {}),
1062
- });
1063
- }
1064
-
1065
- // Interaction-internal content (prompt, choice bodies) is structurally
1066
- // validated by the descriptor schema; its flow elements are walked when the
1067
- // descriptor surfaces them. Generic field-sniffing is deliberately avoided.
1068
- return;
1069
- }
1070
-
1071
- if (node.kind === "xml") {
1072
- const xmlNode = node as XmlContentNode;
1073
-
1074
- if (xmlNode.name === model.mathRoot) {
1075
- return; // MathML renders structurally; its subtree is not flow content
1076
- }
1077
-
1078
- if (!isAllowedFlowElement(model, xmlNode.name)) {
1079
- report({ type: "unsupported-element", name: xmlNode.name });
1080
- }
1081
-
1082
- for (const child of xmlNode.children ?? []) {
1083
- walk(child);
1084
- }
1085
-
1086
- return;
1087
- }
1088
-
1089
- if (intrinsicLeafKinds.has(node.kind)) {
1090
- return;
1091
- }
1092
-
1093
- // Any other kind (include, multi-stage groups, future vocabulary) has no
1094
- // rendering path: report it rather than let the renderer drop it (ADR-0003).
1095
- report({ type: "unsupported-element", name: node.kind });
1096
- }
1097
-
1098
- for (const node of item.itemBody.content ?? []) {
1099
- walk(node);
1100
- }
1101
-
1102
- // Shared stimulus refs must resolve to be deliverable; resolved content passes
1103
- // through the same content-model gate as the body.
1104
- for (const ref of item.assessmentStimulusRefs ?? []) {
1105
- const stimulus = config.resolveStimulus?.(ref) ?? null;
1106
-
1107
- if (stimulus === null) {
1108
- report({ type: "unsupported-element", name: "assessmentStimulusRef", detail: ref.href });
1109
- continue;
1110
- }
1111
-
1112
- for (const node of stimulus.content) {
1113
- walk(node);
1114
- }
1115
- }
1116
-
1117
- for (const feedback of item.modalFeedbacks ?? []) {
1118
- for (const child of feedback.content ?? []) {
1119
- walk(child);
1120
- }
1121
- }
1122
-
1123
- const customOperatorClasses = new Set(Object.keys(config.customOperators ?? {}));
1124
-
1125
- for (const issue of collectRpIssues(item.responseProcessing, {
1126
- customOperatorClasses,
1127
- outcomeDeclarations: item.outcomeDeclarations ?? [],
1128
- })) {
1129
- report(issue);
1130
- }
1131
-
1132
- for (const issue of collectTemplateIssues(item.templateProcessing, { customOperatorClasses })) {
1133
- report(issue);
1134
- }
1015
+ // Reduce this runtime's React config to the headless capability inputs: an interaction
1016
+ // is supported when it has both a descriptor and a skin; descriptor schemas drive the
1017
+ // stricter invalid-interaction check. The walk itself lives in ./item-capability so a
1018
+ // server-side caller reaches the same verdict without importing React.
1019
+ const supportedInteractions = new Set([...descriptorsByKind.keys()].filter((kind) => Boolean(config.skin[kind])));
1020
+ const interactionSchemas = new Map(
1021
+ [...descriptorsByKind].map(([kind, descriptor]) => [kind, descriptor.schema] as const),
1022
+ );
1135
1023
 
1136
- return { deliverable: issues.length === 0, issues };
1024
+ return reportItemCapability(item, {
1025
+ supportedInteractions,
1026
+ interactionSchemas,
1027
+ model,
1028
+ customOperatorClasses: new Set(Object.keys(config.customOperators ?? {})),
1029
+ ...(config.resolveStimulus !== undefined ? { resolveStimulus: config.resolveStimulus } : {}),
1030
+ });
1137
1031
  }
1138
1032
 
1139
1033
  return { ItemRenderer, ContentRenderer, useAttempt, useCatalogSupports, canDeliver };