@longsightgroup/qti3-core 0.9.1 → 0.9.3
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/README.md +59 -3
- package/dist/adaptive-turn-materializer.d.ts +14 -0
- package/dist/adaptive-turn-materializer.d.ts.map +1 -0
- package/dist/adaptive-turn-materializer.js +56 -0
- package/dist/adaptive-turn-materializer.js.map +1 -0
- package/dist/adaptive-turn.d.ts +23 -0
- package/dist/adaptive-turn.d.ts.map +1 -0
- package/dist/adaptive-turn.js +60 -0
- package/dist/adaptive-turn.js.map +1 -0
- package/dist/assert-never.d.ts +3 -0
- package/dist/assert-never.d.ts.map +1 -0
- package/dist/assert-never.js +5 -0
- package/dist/assert-never.js.map +1 -0
- package/dist/attempt-state-constants.d.ts +6 -0
- package/dist/attempt-state-constants.d.ts.map +1 -0
- package/dist/attempt-state-constants.js +6 -0
- package/dist/attempt-state-constants.js.map +1 -0
- package/dist/attempt-state.d.ts +5 -0
- package/dist/attempt-state.d.ts.map +1 -0
- package/dist/attempt-state.js +190 -0
- package/dist/attempt-state.js.map +1 -0
- package/dist/catalog.js +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/content-text.d.ts.map +1 -1
- package/dist/content-text.js +3 -0
- package/dist/content-text.js.map +1 -1
- package/dist/custom-operators.d.ts +13 -0
- package/dist/custom-operators.d.ts.map +1 -0
- package/dist/custom-operators.js +2 -0
- package/dist/custom-operators.js.map +1 -0
- package/dist/delivery-redaction.d.ts +50 -0
- package/dist/delivery-redaction.d.ts.map +1 -0
- package/dist/delivery-redaction.js +176 -0
- package/dist/delivery-redaction.js.map +1 -0
- package/dist/delivery-security.d.ts +3 -18
- package/dist/delivery-security.d.ts.map +1 -1
- package/dist/delivery-security.js +35 -189
- package/dist/delivery-security.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/parser-custom-interactions.js +1 -1
- package/dist/parser-custom-interactions.js.map +1 -1
- package/dist/parser-declarations.d.ts.map +1 -1
- package/dist/parser-declarations.js +4 -4
- package/dist/parser-declarations.js.map +1 -1
- package/dist/parser-item-metadata.d.ts.map +1 -1
- package/dist/parser-item-metadata.js +8 -7
- package/dist/parser-item-metadata.js.map +1 -1
- package/dist/parser-processing.js +2 -2
- package/dist/parser-processing.js.map +1 -1
- package/dist/parser-values.d.ts +5 -1
- package/dist/parser-values.d.ts.map +1 -1
- package/dist/parser-values.js +33 -0
- package/dist/parser-values.js.map +1 -1
- package/dist/processing-evaluator-boolean.d.ts +8 -0
- package/dist/processing-evaluator-boolean.d.ts.map +1 -0
- package/dist/processing-evaluator-boolean.js +47 -0
- package/dist/processing-evaluator-boolean.js.map +1 -0
- package/dist/processing-evaluator-collection.d.ts +12 -0
- package/dist/processing-evaluator-collection.d.ts.map +1 -0
- package/dist/processing-evaluator-collection.js +35 -0
- package/dist/processing-evaluator-collection.js.map +1 -0
- package/dist/processing-evaluator-comparison.d.ts +8 -0
- package/dist/processing-evaluator-comparison.d.ts.map +1 -0
- package/dist/processing-evaluator-comparison.js +88 -0
- package/dist/processing-evaluator-comparison.js.map +1 -0
- package/dist/processing-evaluator-custom.d.ts +8 -0
- package/dist/processing-evaluator-custom.d.ts.map +1 -0
- package/dist/processing-evaluator-custom.js +14 -0
- package/dist/processing-evaluator-custom.js.map +1 -0
- package/dist/processing-evaluator-geometry.d.ts +8 -0
- package/dist/processing-evaluator-geometry.d.ts.map +1 -0
- package/dist/processing-evaluator-geometry.js +21 -0
- package/dist/processing-evaluator-geometry.js.map +1 -0
- package/dist/processing-evaluator-numeric.d.ts +8 -0
- package/dist/processing-evaluator-numeric.d.ts.map +1 -0
- package/dist/processing-evaluator-numeric.js +112 -0
- package/dist/processing-evaluator-numeric.js.map +1 -0
- package/dist/processing-evaluator-string.d.ts +8 -0
- package/dist/processing-evaluator-string.d.ts.map +1 -0
- package/dist/processing-evaluator-string.js +33 -0
- package/dist/processing-evaluator-string.js.map +1 -0
- package/dist/processing-evaluator-variable.d.ts +8 -0
- package/dist/processing-evaluator-variable.d.ts.map +1 -0
- package/dist/processing-evaluator-variable.js +39 -0
- package/dist/processing-evaluator-variable.js.map +1 -0
- package/dist/processing-evaluator.d.ts +22 -0
- package/dist/processing-evaluator.d.ts.map +1 -0
- package/dist/processing-evaluator.js +137 -0
- package/dist/processing-evaluator.js.map +1 -0
- package/dist/processing-expression-children.d.ts +3 -0
- package/dist/processing-expression-children.d.ts.map +1 -0
- package/dist/processing-expression-children.js +68 -0
- package/dist/processing-expression-children.js.map +1 -0
- package/dist/processing-mapping.d.ts +13 -0
- package/dist/processing-mapping.d.ts.map +1 -0
- package/dist/processing-mapping.js +120 -0
- package/dist/processing-mapping.js.map +1 -0
- package/dist/processing-operators.d.ts +11 -0
- package/dist/processing-operators.d.ts.map +1 -0
- package/dist/processing-operators.js +178 -0
- package/dist/processing-operators.js.map +1 -0
- package/dist/processing-random.d.ts +2 -0
- package/dist/processing-random.d.ts.map +1 -0
- package/dist/processing-random.js +16 -0
- package/dist/processing-random.js.map +1 -0
- package/dist/processing-state.d.ts +8 -0
- package/dist/processing-state.d.ts.map +1 -0
- package/dist/processing-state.js +44 -0
- package/dist/processing-state.js.map +1 -0
- package/dist/processing-templates.d.ts +3 -0
- package/dist/processing-templates.d.ts.map +1 -0
- package/dist/processing-templates.js +12 -0
- package/dist/processing-templates.js.map +1 -0
- package/dist/processing-values.d.ts +15 -0
- package/dist/processing-values.d.ts.map +1 -0
- package/dist/processing-values.js +150 -0
- package/dist/processing-values.js.map +1 -0
- package/dist/processing-variables.d.ts +7 -0
- package/dist/processing-variables.d.ts.map +1 -0
- package/dist/processing-variables.js +40 -0
- package/dist/processing-variables.js.map +1 -0
- package/dist/serializer-processing-expressions.d.ts.map +1 -1
- package/dist/serializer-processing-expressions.js +2 -8
- package/dist/serializer-processing-expressions.js.map +1 -1
- package/dist/serializer-processing-xml.js +2 -2
- package/dist/serializer-processing-xml.js.map +1 -1
- package/dist/serializer-processing.d.ts.map +1 -1
- package/dist/serializer-processing.js +3 -0
- package/dist/serializer-processing.js.map +1 -1
- package/dist/server-scoring.d.ts +4 -14
- package/dist/server-scoring.d.ts.map +1 -1
- package/dist/server-scoring.js +19 -157
- package/dist/server-scoring.js.map +1 -1
- package/dist/session.d.ts +5 -14
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +76 -1158
- package/dist/session.js.map +1 -1
- package/dist/shared-vocabulary-coverage-policy.js +1 -1
- package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
- package/dist/shared-vocabulary-generated-families.js +1 -1
- package/dist/shared-vocabulary-generated-families.js.map +1 -1
- package/dist/shared-vocabulary-support.js +3 -3
- package/dist/shared-vocabulary-support.js.map +1 -1
- package/dist/shared-vocabulary.d.ts.map +1 -1
- package/dist/shared-vocabulary.js +4 -3
- package/dist/shared-vocabulary.js.map +1 -1
- package/dist/support-evidence.d.ts +22 -0
- package/dist/support-evidence.d.ts.map +1 -0
- package/dist/support-evidence.js +93 -0
- package/dist/support-evidence.js.map +1 -0
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +38 -47
- package/dist/support.js.map +1 -1
- package/dist/trusted-item-session.d.ts +49 -0
- package/dist/trusted-item-session.d.ts.map +1 -0
- package/dist/trusted-item-session.js +261 -0
- package/dist/trusted-item-session.js.map +1 -0
- package/dist/tts.d.ts.map +1 -1
- package/dist/tts.js +16 -2
- package/dist/tts.js.map +1 -1
- package/dist/validation-geometry.d.ts +8 -0
- package/dist/validation-geometry.d.ts.map +1 -0
- package/dist/validation-geometry.js +187 -0
- package/dist/validation-geometry.js.map +1 -0
- package/dist/validation-interactions.d.ts +3 -0
- package/dist/validation-interactions.d.ts.map +1 -0
- package/dist/validation-interactions.js +644 -0
- package/dist/validation-interactions.js.map +1 -0
- package/dist/validation-primitives.d.ts +11 -0
- package/dist/validation-primitives.d.ts.map +1 -0
- package/dist/validation-primitives.js +48 -0
- package/dist/validation-primitives.js.map +1 -0
- package/dist/validation-processing.d.ts +4 -0
- package/dist/validation-processing.d.ts.map +1 -0
- package/dist/validation-processing.js +569 -0
- package/dist/validation-processing.js.map +1 -0
- package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
- package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
- package/dist/validation-shared-vocabulary-layout.js +102 -0
- package/dist/validation-shared-vocabulary-layout.js.map +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +27 -1641
- package/dist/validation.js.map +1 -1
- package/dist/xml.js +1 -1
- package/dist/xml.js.map +1 -1
- package/package.json +4 -2
- package/src/adaptive-turn-materializer.ts +85 -0
- package/src/adaptive-turn.ts +109 -0
- package/src/assert-never.ts +4 -0
- package/src/attempt-state-constants.ts +5 -0
- package/src/attempt-state.ts +245 -0
- package/src/catalog.ts +1 -1
- package/src/content-text.ts +3 -0
- package/src/custom-operators.ts +12 -0
- package/src/delivery-redaction.ts +268 -0
- package/src/delivery-security.ts +59 -245
- package/src/index.ts +9 -0
- package/src/parser-custom-interactions.ts +1 -1
- package/src/parser-declarations.ts +4 -3
- package/src/parser-item-metadata.ts +8 -7
- package/src/parser-processing.ts +2 -2
- package/src/parser-values.ts +36 -0
- package/src/processing-evaluator-boolean.ts +48 -0
- package/src/processing-evaluator-collection.ts +50 -0
- package/src/processing-evaluator-comparison.ts +106 -0
- package/src/processing-evaluator-custom.ts +20 -0
- package/src/processing-evaluator-geometry.ts +26 -0
- package/src/processing-evaluator-numeric.ts +155 -0
- package/src/processing-evaluator-string.ts +60 -0
- package/src/processing-evaluator-variable.ts +81 -0
- package/src/processing-evaluator.ts +173 -0
- package/src/processing-expression-children.ts +69 -0
- package/src/processing-mapping.ts +153 -0
- package/src/processing-operators.ts +184 -0
- package/src/processing-random.ts +16 -0
- package/src/processing-state.ts +69 -0
- package/src/processing-templates.ts +12 -0
- package/src/processing-values.ts +162 -0
- package/src/processing-variables.ts +83 -0
- package/src/serializer-processing-expressions.ts +2 -12
- package/src/serializer-processing-xml.ts +2 -2
- package/src/serializer-processing.ts +3 -0
- package/src/server-scoring.ts +30 -226
- package/src/session.ts +128 -2006
- package/src/shared-vocabulary-coverage-policy.ts +1 -1
- package/src/shared-vocabulary-generated-families.ts +1 -1
- package/src/shared-vocabulary-support.ts +3 -3
- package/src/shared-vocabulary.ts +3 -2
- package/src/support-evidence.ts +107 -0
- package/src/support.ts +68 -54
- package/src/trusted-item-session.ts +429 -0
- package/src/tts.ts +29 -15
- package/src/validation-geometry.ts +204 -0
- package/src/validation-interactions.ts +831 -0
- package/src/validation-primitives.ts +63 -0
- package/src/validation-processing.ts +785 -0
- package/src/validation-shared-vocabulary-layout.ts +117 -0
- package/src/validation.ts +44 -2077
- package/src/xml.ts +1 -1
- package/src/interaction-test-fixtures.ts +0 -44
- package/src/serializer-processing.fixtures.ts +0 -469
package/src/delivery-security.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
FORBIDDEN_DELIVERY_SECRET_ELEMENT_NAMES,
|
|
3
|
+
analyzeDeliveryXml,
|
|
4
|
+
isDeclarationDefaultValue,
|
|
5
|
+
normalizedQtiElementName,
|
|
6
|
+
parseDeliveryXml,
|
|
7
|
+
parseXmlBoolean,
|
|
8
|
+
redactDeliveryXml,
|
|
9
|
+
type DeliveryRedactionPolicy,
|
|
10
|
+
type QtiDeliverySecurityAnalysis,
|
|
11
|
+
type QtiDeliverySecurityFindingKind,
|
|
12
|
+
} from "./delivery-redaction.js";
|
|
13
|
+
import type { XmlNode } from "./xml.js";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
QtiDeliverySecurityAnalysis,
|
|
17
|
+
QtiDeliverySecurityFinding,
|
|
18
|
+
QtiDeliverySecurityFindingKind,
|
|
19
|
+
} from "./delivery-redaction.js";
|
|
3
20
|
|
|
4
21
|
const FORBIDDEN_DELIVERY_ELEMENT_NAMES = new Set([
|
|
5
|
-
|
|
6
|
-
"mapping",
|
|
7
|
-
"area-mapping",
|
|
8
|
-
"match-table",
|
|
9
|
-
"interpolation-table",
|
|
10
|
-
"response-processing",
|
|
22
|
+
...FORBIDDEN_DELIVERY_SECRET_ELEMENT_NAMES,
|
|
11
23
|
"feedback-inline",
|
|
12
24
|
"feedback-block",
|
|
13
25
|
"modal-feedback",
|
|
@@ -18,222 +30,63 @@ const UNSUPPORTED_SECURE_DELIVERY_ELEMENT_NAMES = new Set([
|
|
|
18
30
|
"set-correct-response",
|
|
19
31
|
]);
|
|
20
32
|
|
|
21
|
-
const DEFAULT_VALUE_DECLARATION_ELEMENT_NAMES = new Set([
|
|
22
|
-
"response-declaration",
|
|
23
|
-
"outcome-declaration",
|
|
24
|
-
"template-declaration",
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
export type QtiDeliverySecurityFindingKind =
|
|
28
|
-
| "forbidden-delivery-element"
|
|
29
|
-
| "unsupported-secure-delivery-element"
|
|
30
|
-
| "unsupported-adaptive-response-processing";
|
|
31
|
-
|
|
32
|
-
export interface QtiDeliverySecurityFinding {
|
|
33
|
-
kind: QtiDeliverySecurityFindingKind;
|
|
34
|
-
qtiName: string;
|
|
35
|
-
localName: string;
|
|
36
|
-
message: string;
|
|
37
|
-
source?: QtiSourceLocation | undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface QtiDeliverySecurityAnalysis {
|
|
41
|
-
diagnostics: QtiDiagnostic[];
|
|
42
|
-
findings: QtiDeliverySecurityFinding[];
|
|
43
|
-
/** True when this exact XML contains no known answer/scoring/feedback delivery leaks. */
|
|
44
|
-
deliverySafe: boolean;
|
|
45
|
-
/** True when secure-delivery redaction can be attempted for this XML. */
|
|
46
|
-
secureDeliverySupported: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
33
|
export interface QtiDeliverySafeXmlResult {
|
|
50
34
|
ok: boolean;
|
|
51
|
-
diagnostics:
|
|
35
|
+
diagnostics: QtiDeliverySecurityAnalysis["diagnostics"];
|
|
52
36
|
analysis: QtiDeliverySecurityAnalysis;
|
|
53
37
|
xml?: string | undefined;
|
|
54
38
|
}
|
|
55
39
|
|
|
56
40
|
export function analyzeQtiDeliverySecurity(xml: string): QtiDeliverySecurityAnalysis {
|
|
57
|
-
|
|
58
|
-
return analyzeParsedDeliveryXml(parsed);
|
|
41
|
+
return analyzeDeliveryXml(parseDeliveryXml(xml), staticDeliveryPolicy);
|
|
59
42
|
}
|
|
60
43
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
if (UNSUPPORTED_SECURE_DELIVERY_ELEMENT_NAMES.has(normalizedName)) {
|
|
82
|
-
findings.push({
|
|
83
|
-
kind: "unsupported-secure-delivery-element",
|
|
84
|
-
qtiName: node.name,
|
|
85
|
-
localName: node.localName,
|
|
86
|
-
message: `${node.name} is not supported by secure delivery redaction v1.`,
|
|
87
|
-
source: node.source,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
44
|
+
export function buildQtiDeliverySafeXml(xml: string): QtiDeliverySafeXmlResult {
|
|
45
|
+
return redactDeliveryXml(xml, staticDeliveryPolicy);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const staticDeliveryPolicy: DeliveryRedactionPolicy = {
|
|
49
|
+
isForbiddenElement(node, normalizedName) {
|
|
50
|
+
return (
|
|
51
|
+
FORBIDDEN_DELIVERY_ELEMENT_NAMES.has(normalizedName) ||
|
|
52
|
+
isDeclarationDefaultValue(node, normalizedName)
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
unsupportedFinding(node, normalizedName, nodes) {
|
|
56
|
+
if (UNSUPPORTED_SECURE_DELIVERY_ELEMENT_NAMES.has(normalizedName)) {
|
|
57
|
+
return {
|
|
58
|
+
kind: "unsupported-secure-delivery-element",
|
|
59
|
+
qtiName: node.name,
|
|
60
|
+
localName: node.localName,
|
|
61
|
+
message: `${node.name} is not supported by secure delivery redaction v1.`,
|
|
62
|
+
source: node.source,
|
|
63
|
+
};
|
|
90
64
|
}
|
|
91
65
|
|
|
92
66
|
if (
|
|
93
|
-
|
|
94
|
-
|
|
67
|
+
node.parent === undefined &&
|
|
68
|
+
parseXmlBoolean(node.attributes.adaptive) === true &&
|
|
69
|
+
nodes.some(
|
|
70
|
+
(candidate) => normalizedQtiElementName(candidate.localName) === "response-processing",
|
|
71
|
+
)
|
|
95
72
|
) {
|
|
96
|
-
|
|
73
|
+
return {
|
|
97
74
|
kind: "unsupported-adaptive-response-processing",
|
|
98
|
-
qtiName:
|
|
99
|
-
localName:
|
|
75
|
+
qtiName: node.name,
|
|
76
|
+
localName: node.localName,
|
|
100
77
|
message:
|
|
101
78
|
"Adaptive response processing requires server-side item materialization before secure delivery.",
|
|
102
|
-
source:
|
|
103
|
-
}
|
|
79
|
+
source: node.source,
|
|
80
|
+
};
|
|
104
81
|
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
diagnostics.push(...findings.map(findingToDiagnostic));
|
|
108
|
-
const parseOk = parsed.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
|
|
109
82
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
!findings.some(
|
|
118
|
-
(finding) =>
|
|
119
|
-
finding.kind === "unsupported-secure-delivery-element" ||
|
|
120
|
-
finding.kind === "unsupported-adaptive-response-processing",
|
|
121
|
-
),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function buildQtiDeliverySafeXml(xml: string): QtiDeliverySafeXmlResult {
|
|
126
|
-
const parsed = parseDeliveryXml(xml);
|
|
127
|
-
const analysis = analyzeParsedDeliveryXml(parsed);
|
|
128
|
-
if (!analysis.secureDeliverySupported) {
|
|
129
|
-
return {
|
|
130
|
-
ok: false,
|
|
131
|
-
diagnostics: analysis.diagnostics,
|
|
132
|
-
analysis,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!parsed.root) {
|
|
137
|
-
return {
|
|
138
|
-
ok: false,
|
|
139
|
-
diagnostics: parsed.diagnostics,
|
|
140
|
-
analysis,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const redactionRanges = readRedactionRanges([
|
|
145
|
-
parsed.root,
|
|
146
|
-
...descendants(parsed.root, () => true),
|
|
147
|
-
]);
|
|
148
|
-
const redactedXml = removeSourceRanges(xml, redactionRanges);
|
|
149
|
-
const redactedAnalysis = analyzeQtiDeliverySecurity(redactedXml);
|
|
150
|
-
if (!redactedAnalysis.deliverySafe) {
|
|
151
|
-
return {
|
|
152
|
-
ok: false,
|
|
153
|
-
diagnostics: [...analysis.diagnostics, ...redactedAnalysis.diagnostics],
|
|
154
|
-
analysis: redactedAnalysis,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
ok: true,
|
|
160
|
-
xml: redactedXml,
|
|
161
|
-
diagnostics: redactedAnalysis.diagnostics,
|
|
162
|
-
analysis: redactedAnalysis,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function parseDeliveryXml(xml: string): {
|
|
167
|
-
root: XmlNode | undefined;
|
|
168
|
-
diagnostics: QtiDiagnostic[];
|
|
169
|
-
} {
|
|
170
|
-
let parsed: ReturnType<typeof parseXmlTree>;
|
|
171
|
-
try {
|
|
172
|
-
parsed = parseXmlTree(xml);
|
|
173
|
-
} catch (error) {
|
|
174
|
-
return {
|
|
175
|
-
root: undefined,
|
|
176
|
-
diagnostics: [
|
|
177
|
-
{
|
|
178
|
-
code: "xml.parse",
|
|
179
|
-
severity: "error",
|
|
180
|
-
message: error instanceof Error ? error.message : String(error),
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const diagnostics: QtiDiagnostic[] = parsed.errors.map((error) => ({
|
|
187
|
-
code: "xml.parse",
|
|
188
|
-
severity: "error",
|
|
189
|
-
message: error.message,
|
|
190
|
-
}));
|
|
191
|
-
if (!parsed.root) {
|
|
192
|
-
diagnostics.push({
|
|
193
|
-
code: "xml.empty",
|
|
194
|
-
severity: "error",
|
|
195
|
-
message: "No XML root element was found.",
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
return { root: parsed.root, diagnostics };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function normalizedQtiElementName(localName: string): string {
|
|
202
|
-
const lower = localName.toLowerCase();
|
|
203
|
-
return lower.startsWith("qti-") ? lower.slice("qti-".length) : lower;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function parseXmlBoolean(value: string | undefined): boolean | undefined {
|
|
207
|
-
if (value === undefined) return undefined;
|
|
208
|
-
const normalized = value.trim().toLowerCase();
|
|
209
|
-
if (normalized === "true" || normalized === "1") return true;
|
|
210
|
-
if (normalized === "false" || normalized === "0") return false;
|
|
211
|
-
return undefined;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function isForbiddenDeliveryElement(
|
|
215
|
-
node: XmlNode,
|
|
216
|
-
normalizedName = normalizedQtiElementName(node.localName),
|
|
217
|
-
): boolean {
|
|
218
|
-
if (FORBIDDEN_DELIVERY_ELEMENT_NAMES.has(normalizedName)) return true;
|
|
219
|
-
return (
|
|
220
|
-
normalizedName === "default-value" &&
|
|
221
|
-
DEFAULT_VALUE_DECLARATION_ELEMENT_NAMES.has(
|
|
222
|
-
normalizedQtiElementName(node.parent?.localName ?? ""),
|
|
223
|
-
)
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function findingToDiagnostic(finding: QtiDeliverySecurityFinding): QtiDiagnostic {
|
|
228
|
-
const code = diagnosticCodeForFinding(finding.kind);
|
|
229
|
-
return {
|
|
230
|
-
code,
|
|
231
|
-
severity: "error",
|
|
232
|
-
message: finding.message,
|
|
233
|
-
path: finding.source?.path,
|
|
234
|
-
source: finding.source,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
diagnosticCodeForFinding,
|
|
86
|
+
forbiddenElementMessage(node: XmlNode) {
|
|
87
|
+
return `${node.name} exposes answer keys, scoring, mapping, feedback, or solution information during delivery.`;
|
|
88
|
+
},
|
|
89
|
+
};
|
|
237
90
|
|
|
238
91
|
function diagnosticCodeForFinding(kind: QtiDeliverySecurityFindingKind): string {
|
|
239
92
|
if (kind === "forbidden-delivery-element") return "delivery.forbiddenElement";
|
|
@@ -242,42 +95,3 @@ function diagnosticCodeForFinding(kind: QtiDeliverySecurityFindingKind): string
|
|
|
242
95
|
}
|
|
243
96
|
return "delivery.unsupportedSecureDelivery";
|
|
244
97
|
}
|
|
245
|
-
|
|
246
|
-
interface RedactionRange {
|
|
247
|
-
startOffset: number;
|
|
248
|
-
endOffset: number;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function readRedactionRanges(nodes: XmlNode[]): RedactionRange[] {
|
|
252
|
-
const ranges = nodes.flatMap((node) => {
|
|
253
|
-
if (!isForbiddenDeliveryElement(node)) return [];
|
|
254
|
-
const endOffset = node.sourceRange.endOffset;
|
|
255
|
-
if (node.sourceRange.startOffset < 0 || endOffset === undefined) return [];
|
|
256
|
-
return [{ startOffset: node.sourceRange.startOffset, endOffset }];
|
|
257
|
-
});
|
|
258
|
-
return mergeSourceRanges(ranges);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function mergeSourceRanges(ranges: RedactionRange[]): RedactionRange[] {
|
|
262
|
-
const sorted = [...ranges].sort((left, right) => left.startOffset - right.startOffset);
|
|
263
|
-
const merged: RedactionRange[] = [];
|
|
264
|
-
for (const range of sorted) {
|
|
265
|
-
const last = merged.at(-1);
|
|
266
|
-
if (last && range.startOffset <= last.endOffset) {
|
|
267
|
-
last.endOffset = Math.max(last.endOffset, range.endOffset);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
merged.push({ ...range });
|
|
271
|
-
}
|
|
272
|
-
return merged;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function removeSourceRanges(xml: string, ranges: RedactionRange[]): string {
|
|
276
|
-
let output = "";
|
|
277
|
-
let cursor = 0;
|
|
278
|
-
for (const range of ranges) {
|
|
279
|
-
output += xml.slice(cursor, range.startOffset);
|
|
280
|
-
cursor = range.endOffset;
|
|
281
|
-
}
|
|
282
|
-
return output + xml.slice(cursor);
|
|
283
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export {
|
|
2
|
+
processQtiAdaptiveItemTurn,
|
|
3
|
+
type QtiAdaptiveTurnInput,
|
|
4
|
+
type QtiAdaptiveTurnResponseInput,
|
|
5
|
+
type QtiAdaptiveTurnResult,
|
|
6
|
+
} from "./adaptive-turn.js";
|
|
7
|
+
export { assertNever } from "./assert-never.js";
|
|
1
8
|
export {
|
|
2
9
|
createCatalogSupportResolution,
|
|
3
10
|
type QtiCatalogSupportResolution,
|
|
@@ -29,6 +36,7 @@ export {
|
|
|
29
36
|
type QtiPackageXmlTree,
|
|
30
37
|
} from "./package-xml.js";
|
|
31
38
|
export { parseQtiXml } from "./parser.js";
|
|
39
|
+
export { numericTuple3, numericTuple4 } from "./parser-values.js";
|
|
32
40
|
export { isConformanceParseDiagnostic } from "./parse-diagnostics.js";
|
|
33
41
|
export {
|
|
34
42
|
scoreQtiItemServerSide,
|
|
@@ -227,6 +235,7 @@ export type {
|
|
|
227
235
|
QtiResponseProcessing,
|
|
228
236
|
QtiResponseRule,
|
|
229
237
|
QtiScoreResult,
|
|
238
|
+
QtiStylesheet,
|
|
230
239
|
QtiLookupOutcomeValue,
|
|
231
240
|
QtiSetOutcomeValue,
|
|
232
241
|
QtiSupportStatus,
|
|
@@ -212,7 +212,7 @@ function parseCustomMarkupNode(
|
|
|
212
212
|
code: options.nestedInteractionCode,
|
|
213
213
|
severity: "error",
|
|
214
214
|
message: `${options.contextName} must not contain nested QTI interaction ${node.localName}.`,
|
|
215
|
-
path: node.source
|
|
215
|
+
path: node.source.path,
|
|
216
216
|
source: node.source,
|
|
217
217
|
});
|
|
218
218
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
coerceValue,
|
|
3
3
|
normalizeValueForCardinality,
|
|
4
|
+
parseBaseType,
|
|
4
5
|
parseCardinality,
|
|
5
6
|
parseCoords,
|
|
6
7
|
parseShape,
|
|
@@ -18,7 +19,7 @@ import { childElements, textContent, type XmlNode } from "./xml.js";
|
|
|
18
19
|
|
|
19
20
|
export function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration {
|
|
20
21
|
const cardinality = parseCardinality(node.attributes.cardinality);
|
|
21
|
-
const baseType = node.attributes["base-type"]
|
|
22
|
+
const baseType = parseBaseType(node.attributes["base-type"]);
|
|
22
23
|
return {
|
|
23
24
|
kind: "response",
|
|
24
25
|
identifier: node.attributes.identifier ?? "",
|
|
@@ -37,7 +38,7 @@ export function parseResponseDeclaration(node: XmlNode): QtiResponseDeclaration
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export function parseOutcomeDeclaration(node: XmlNode): QtiOutcomeDeclaration {
|
|
40
|
-
const baseType = node.attributes["base-type"]
|
|
41
|
+
const baseType = parseBaseType(node.attributes["base-type"]);
|
|
41
42
|
return {
|
|
42
43
|
kind: "outcome",
|
|
43
44
|
identifier: node.attributes.identifier ?? "",
|
|
@@ -51,7 +52,7 @@ export function parseOutcomeDeclaration(node: XmlNode): QtiOutcomeDeclaration {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export function parseTemplateDeclaration(node: XmlNode): QtiTemplateDeclaration {
|
|
54
|
-
const baseType = node.attributes["base-type"]
|
|
55
|
+
const baseType = parseBaseType(node.attributes["base-type"]);
|
|
55
56
|
return {
|
|
56
57
|
kind: "template",
|
|
57
58
|
identifier: node.attributes.identifier ?? "",
|
|
@@ -28,13 +28,14 @@ export function firstChildElement(
|
|
|
28
28
|
duplicateCode: string,
|
|
29
29
|
): XmlNode | undefined {
|
|
30
30
|
const matches = childElements(parent, childName);
|
|
31
|
-
|
|
31
|
+
const duplicate = matches[1];
|
|
32
|
+
if (duplicate) {
|
|
32
33
|
diagnostics.push({
|
|
33
34
|
code: duplicateCode,
|
|
34
35
|
severity: "error",
|
|
35
36
|
message: `${parent.localName} allows at most one ${childName} child.`,
|
|
36
|
-
path:
|
|
37
|
-
source:
|
|
37
|
+
path: duplicate.source.path,
|
|
38
|
+
source: duplicate.source,
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
return matches[0];
|
|
@@ -177,7 +178,7 @@ export function parseCompanionMaterialsInfo(
|
|
|
177
178
|
code: "companionMaterials.child.unsupported",
|
|
178
179
|
severity: "warning",
|
|
179
180
|
message: `${child.localName} is not currently modeled in companion materials parsing.`,
|
|
180
|
-
path: child.source
|
|
181
|
+
path: child.source.path,
|
|
181
182
|
source: child.source,
|
|
182
183
|
});
|
|
183
184
|
}
|
|
@@ -200,7 +201,7 @@ function parsePhysicalMaterial(
|
|
|
200
201
|
diagnostics,
|
|
201
202
|
"companionMaterials.physicalMaterial.empty",
|
|
202
203
|
"qti-physical-material requires non-empty text content.",
|
|
203
|
-
node.source
|
|
204
|
+
node.source.path,
|
|
204
205
|
node.source,
|
|
205
206
|
);
|
|
206
207
|
}
|
|
@@ -225,7 +226,7 @@ function parseDigitalMaterial(
|
|
|
225
226
|
diagnostics,
|
|
226
227
|
"companionMaterials.digitalMaterial.fileHref.missing",
|
|
227
228
|
"qti-digital-material requires a qti-file-href child.",
|
|
228
|
-
node.source
|
|
229
|
+
node.source.path,
|
|
229
230
|
node.source,
|
|
230
231
|
);
|
|
231
232
|
}
|
|
@@ -236,7 +237,7 @@ function parseDigitalMaterial(
|
|
|
236
237
|
diagnostics,
|
|
237
238
|
"companionMaterials.digitalMaterial.fileHref.empty",
|
|
238
239
|
"qti-digital-material qti-file-href requires non-empty text content.",
|
|
239
|
-
fileHrefNode.source
|
|
240
|
+
fileHrefNode.source.path,
|
|
240
241
|
fileHrefNode.source,
|
|
241
242
|
);
|
|
242
243
|
}
|
package/src/parser-processing.ts
CHANGED
|
@@ -672,8 +672,8 @@ function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
|
|
|
672
672
|
if (variable && correct) {
|
|
673
673
|
return {
|
|
674
674
|
type: "matchCorrect",
|
|
675
|
-
identifier: variable
|
|
676
|
-
correctIdentifier: correct
|
|
675
|
+
identifier: variable.attributes.identifier ?? "",
|
|
676
|
+
correctIdentifier: correct.attributes.identifier ?? "",
|
|
677
677
|
source: node.source,
|
|
678
678
|
};
|
|
679
679
|
}
|
package/src/parser-values.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
QtiBaseType,
|
|
2
3
|
QtiCardinality,
|
|
3
4
|
QtiRecordValue,
|
|
4
5
|
QtiResponseDeclaration,
|
|
@@ -6,6 +7,41 @@ import type {
|
|
|
6
7
|
QtiValue,
|
|
7
8
|
} from "./types.js";
|
|
8
9
|
|
|
10
|
+
const QTI_BASE_TYPES = new Set<string>([
|
|
11
|
+
"identifier",
|
|
12
|
+
"boolean",
|
|
13
|
+
"integer",
|
|
14
|
+
"float",
|
|
15
|
+
"string",
|
|
16
|
+
"point",
|
|
17
|
+
"pair",
|
|
18
|
+
"directedPair",
|
|
19
|
+
"duration",
|
|
20
|
+
"file",
|
|
21
|
+
"uri",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function isQtiBaseType(value: string): value is QtiBaseType {
|
|
25
|
+
return QTI_BASE_TYPES.has(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseBaseType(value: string | undefined): QtiBaseType | undefined {
|
|
29
|
+
if (value === undefined) return undefined;
|
|
30
|
+
return isQtiBaseType(value) ? value : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function numericTuple3(values: number[]): [number, number, number] | undefined {
|
|
34
|
+
const [a, b, c] = values;
|
|
35
|
+
if (a === undefined || b === undefined || c === undefined) return undefined;
|
|
36
|
+
return [a, b, c];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function numericTuple4(values: number[]): [number, number, number, number] | undefined {
|
|
40
|
+
const [a, b, c, d] = values;
|
|
41
|
+
if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
|
|
42
|
+
return [a, b, c, d];
|
|
43
|
+
}
|
|
44
|
+
|
|
9
45
|
export function coerceValue(value: string, baseType: string | undefined): QtiScalarValue {
|
|
10
46
|
if (baseType === "integer") return Number.parseInt(value, 10);
|
|
11
47
|
if (baseType === "float") return Number.parseFloat(value);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { QtiProcessingExpression, QtiValue } from "./types.js";
|
|
2
|
+
import { assertNever } from "./assert-never.js";
|
|
3
|
+
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
4
|
+
import { booleanValueOrNull } from "./processing-values.js";
|
|
5
|
+
|
|
6
|
+
type BooleanExpression = Extract<QtiProcessingExpression, { type: "and" | "anyN" | "or" | "not" }>;
|
|
7
|
+
|
|
8
|
+
export function evaluateBooleanExpression(
|
|
9
|
+
expression: BooleanExpression,
|
|
10
|
+
context: EvaluationContext,
|
|
11
|
+
): QtiValue {
|
|
12
|
+
switch (expression.type) {
|
|
13
|
+
case "and": {
|
|
14
|
+
let sawNull = false;
|
|
15
|
+
for (const item of expression.expressions) {
|
|
16
|
+
const value = booleanValueOrNull(context.evaluate(item));
|
|
17
|
+
if (value === false) return false;
|
|
18
|
+
if (value === null) sawNull = true;
|
|
19
|
+
}
|
|
20
|
+
return sawNull ? null : true;
|
|
21
|
+
}
|
|
22
|
+
case "anyN": {
|
|
23
|
+
const min = context.indexValue(expression.min) ?? 0;
|
|
24
|
+
const max = context.indexValue(expression.max) ?? 0;
|
|
25
|
+
const values = expression.expressions.map((item) => context.evaluate(item));
|
|
26
|
+
const trueCount = values.filter((value) => value === true).length;
|
|
27
|
+
const nullCount = values.filter((value) => value === null).length;
|
|
28
|
+
if (min > max || trueCount > max || trueCount + nullCount < min) return false;
|
|
29
|
+
if (trueCount >= min && trueCount <= max) return true;
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
case "or": {
|
|
33
|
+
let sawNull = false;
|
|
34
|
+
for (const item of expression.expressions) {
|
|
35
|
+
const value = booleanValueOrNull(context.evaluate(item));
|
|
36
|
+
if (value === true) return true;
|
|
37
|
+
if (value === null) sawNull = true;
|
|
38
|
+
}
|
|
39
|
+
return sawNull ? null : false;
|
|
40
|
+
}
|
|
41
|
+
case "not": {
|
|
42
|
+
const value = booleanValueOrNull(context.evaluate(expression.expression));
|
|
43
|
+
return value === null ? null : !value;
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
return assertNever(expression);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { QtiProcessingExpression, QtiScalarValue, QtiValue } from "./types.js";
|
|
2
|
+
import { assertNever } from "./assert-never.js";
|
|
3
|
+
import type { EvaluationContext } from "./processing-evaluator.js";
|
|
4
|
+
import { valueContainer } from "./processing-values.js";
|
|
5
|
+
|
|
6
|
+
type CollectionExpression = Extract<
|
|
7
|
+
QtiProcessingExpression,
|
|
8
|
+
{ type: "multiple" | "ordered" | "index" | "containerSize" }
|
|
9
|
+
>;
|
|
10
|
+
type RepeatExpression = Extract<QtiProcessingExpression, { type: "repeat" }>;
|
|
11
|
+
|
|
12
|
+
export function evaluateCollectionExpression(
|
|
13
|
+
expression: CollectionExpression,
|
|
14
|
+
context: EvaluationContext,
|
|
15
|
+
): QtiValue {
|
|
16
|
+
switch (expression.type) {
|
|
17
|
+
case "multiple":
|
|
18
|
+
case "ordered": {
|
|
19
|
+
const values = expression.expressions.flatMap((item) =>
|
|
20
|
+
valueContainer(context.evaluate(item)),
|
|
21
|
+
);
|
|
22
|
+
return values.length > 0 ? values : null;
|
|
23
|
+
}
|
|
24
|
+
case "index": {
|
|
25
|
+
const values = valueContainer(context.evaluate(expression.expression));
|
|
26
|
+
const n = context.indexValue(expression.n);
|
|
27
|
+
if (n === undefined || n < 1 || n > values.length) return null;
|
|
28
|
+
return values[n - 1] ?? null;
|
|
29
|
+
}
|
|
30
|
+
case "containerSize":
|
|
31
|
+
return valueContainer(context.evaluate(expression.expression)).length;
|
|
32
|
+
default:
|
|
33
|
+
return assertNever(expression);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function evaluateRepeatExpression(
|
|
38
|
+
expression: RepeatExpression,
|
|
39
|
+
context: EvaluationContext,
|
|
40
|
+
): QtiValue {
|
|
41
|
+
const repeats = context.indexValue(expression.numberRepeats);
|
|
42
|
+
if (repeats === undefined || repeats < 1) return null;
|
|
43
|
+
const container: QtiScalarValue[] = [];
|
|
44
|
+
for (let repeat = 0; repeat < repeats; repeat += 1) {
|
|
45
|
+
for (const item of expression.expressions) {
|
|
46
|
+
container.push(...valueContainer(context.evaluate(item)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return container.length > 0 ? container : null;
|
|
50
|
+
}
|