@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/dist/headless.d.ts +14 -0
- package/dist/headless.js +3062 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +142 -96
- package/dist/item-capability.d.ts +43 -0
- package/package.json +9 -4
- package/src/headless.ts +26 -0
- package/src/index.ts +2 -0
- package/src/item-capability.ts +211 -0
- package/src/runtime.ts +17 -123
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 {
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
|
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 };
|