@longsightgroup/qti3-core 0.2.0 → 0.3.0
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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +18 -3
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +191 -80
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +136 -3
- package/dist/validation.js.map +1 -1
- package/dist/value-format.d.ts +7 -0
- package/dist/value-format.d.ts.map +1 -0
- package/dist/value-format.js +46 -0
- package/dist/value-format.js.map +1 -0
- package/dist/xml.d.ts +2 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +2 -0
- package/dist/xml.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +7 -0
- package/src/parser.ts +19 -3
- package/src/session.ts +329 -270
- package/src/types.ts +3 -0
- package/src/validation.ts +151 -2
- package/src/value-format.ts +39 -0
- package/src/xml.ts +4 -0
package/src/types.ts
CHANGED
|
@@ -416,7 +416,10 @@ export interface QtiAssessmentItem {
|
|
|
416
416
|
title?: string | undefined;
|
|
417
417
|
language?: string | undefined;
|
|
418
418
|
adaptive: boolean;
|
|
419
|
+
timeDependent?: boolean | undefined;
|
|
420
|
+
attributes: Record<string, string>;
|
|
419
421
|
prompt?: string | undefined;
|
|
422
|
+
itemBodySource?: QtiSourceLocation | undefined;
|
|
420
423
|
responseDeclarations: QtiResponseDeclaration[];
|
|
421
424
|
outcomeDeclarations: QtiOutcomeDeclaration[];
|
|
422
425
|
templateDeclarations: QtiTemplateDeclaration[];
|
package/src/validation.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
QtiValidationResult,
|
|
22
22
|
} from "./types.js";
|
|
23
23
|
import { validateQtiDataSsmlMetadata } from "./tts.js";
|
|
24
|
+
import { qtiValueToStringList } from "./value-format.js";
|
|
24
25
|
|
|
25
26
|
const BUILT_IN_COMPLETION_STATUS = "completionStatus";
|
|
26
27
|
|
|
@@ -29,6 +30,8 @@ export function validateAssessmentItem(document: QtiDocument): QtiValidationResu
|
|
|
29
30
|
const item = document.item;
|
|
30
31
|
|
|
31
32
|
requireIdentifier("qti-assessment-item", item.identifier, diagnostics, item.source);
|
|
33
|
+
validateAssessmentItemRoot(item, diagnostics);
|
|
34
|
+
validateItemBody(item, diagnostics);
|
|
32
35
|
validateDeclarationIdentifiers(item, diagnostics);
|
|
33
36
|
validateOutcomeLookupTables(item, diagnostics);
|
|
34
37
|
validateInteractions(item, diagnostics);
|
|
@@ -36,6 +39,7 @@ export function validateAssessmentItem(document: QtiDocument): QtiValidationResu
|
|
|
36
39
|
validateCatalogInfo(item, diagnostics);
|
|
37
40
|
validateStylesheets(item, diagnostics);
|
|
38
41
|
diagnostics.push(...validateQtiDataSsmlMetadata(item));
|
|
42
|
+
validateResponseProcessingTemplate(item, diagnostics);
|
|
39
43
|
validateProcessingReferences(item, diagnostics);
|
|
40
44
|
|
|
41
45
|
return {
|
|
@@ -44,6 +48,152 @@ export function validateAssessmentItem(document: QtiDocument): QtiValidationResu
|
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
function validateResponseProcessingTemplate(
|
|
52
|
+
item: QtiAssessmentItem,
|
|
53
|
+
diagnostics: QtiDiagnostic[],
|
|
54
|
+
): void {
|
|
55
|
+
const template = item.responseProcessing?.template;
|
|
56
|
+
if (!template) return;
|
|
57
|
+
const templateKind = responseProcessingTemplateKind(template);
|
|
58
|
+
if (templateKind) {
|
|
59
|
+
validateBuiltInResponseProcessingTemplate(item, templateKind, diagnostics);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
diagnostics.push({
|
|
63
|
+
code: "processing.template.unsupported",
|
|
64
|
+
severity: "error",
|
|
65
|
+
message: `qti-response-processing template ${template} is not currently supported.`,
|
|
66
|
+
path: item.source?.path,
|
|
67
|
+
source: item.source,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function responseProcessingTemplateKind(
|
|
72
|
+
template: string,
|
|
73
|
+
): "matchCorrect" | "mapResponse" | "mapResponsePoint" | undefined {
|
|
74
|
+
const path = template.split(/[?#]/, 1)[0] ?? "";
|
|
75
|
+
const name = path.slice(path.lastIndexOf("/") + 1).replace(/\.xml$/i, "");
|
|
76
|
+
if (name === "match_correct") return "matchCorrect";
|
|
77
|
+
if (name === "map_response") return "mapResponse";
|
|
78
|
+
if (name === "map_response_point") return "mapResponsePoint";
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateAssessmentItemRoot(item: QtiAssessmentItem, diagnostics: QtiDiagnostic[]): void {
|
|
83
|
+
if (!item.attributes.title?.trim()) {
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
code: "assessmentItem.title.required",
|
|
86
|
+
severity: "error",
|
|
87
|
+
message: "qti-assessment-item requires a non-empty title attribute.",
|
|
88
|
+
path: item.source?.path,
|
|
89
|
+
source: item.source,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const timeDependent = item.attributes["time-dependent"];
|
|
93
|
+
if (timeDependent === undefined || timeDependent.trim() === "") {
|
|
94
|
+
diagnostics.push({
|
|
95
|
+
code: "assessmentItem.timeDependent.required",
|
|
96
|
+
severity: "error",
|
|
97
|
+
message: "qti-assessment-item requires a time-dependent attribute.",
|
|
98
|
+
path: item.source?.path,
|
|
99
|
+
source: item.source,
|
|
100
|
+
});
|
|
101
|
+
} else if (!isXmlBoolean(timeDependent)) {
|
|
102
|
+
diagnostics.push({
|
|
103
|
+
code: "assessmentItem.timeDependent.boolean",
|
|
104
|
+
severity: "error",
|
|
105
|
+
message: `qti-assessment-item time-dependent must be an XML boolean, found ${timeDependent}.`,
|
|
106
|
+
path: item.source?.path,
|
|
107
|
+
source: item.source,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateBuiltInResponseProcessingTemplate(
|
|
113
|
+
item: QtiAssessmentItem,
|
|
114
|
+
templateKind: "matchCorrect" | "mapResponse" | "mapResponsePoint",
|
|
115
|
+
diagnostics: QtiDiagnostic[],
|
|
116
|
+
): void {
|
|
117
|
+
const response = item.responseDeclarations.find(
|
|
118
|
+
(declaration) => declaration.identifier === "RESPONSE",
|
|
119
|
+
);
|
|
120
|
+
const score = item.outcomeDeclarations.find((declaration) => declaration.identifier === "SCORE");
|
|
121
|
+
if (!response) {
|
|
122
|
+
diagnostics.push({
|
|
123
|
+
code: "processing.template.responseIdentifier",
|
|
124
|
+
severity: "error",
|
|
125
|
+
message:
|
|
126
|
+
"Built-in response-processing templates require a response declaration named RESPONSE.",
|
|
127
|
+
path: item.source?.path,
|
|
128
|
+
source: item.source,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (!score) {
|
|
132
|
+
diagnostics.push({
|
|
133
|
+
code: "processing.template.scoreIdentifier",
|
|
134
|
+
severity: "error",
|
|
135
|
+
message: "Built-in response-processing templates require an outcome declaration named SCORE.",
|
|
136
|
+
path: item.source?.path,
|
|
137
|
+
source: item.source,
|
|
138
|
+
});
|
|
139
|
+
} else if (score.cardinality !== "single" || score.baseType !== "float") {
|
|
140
|
+
diagnostics.push({
|
|
141
|
+
code: "processing.template.scoreDeclaration",
|
|
142
|
+
severity: "error",
|
|
143
|
+
message:
|
|
144
|
+
"Built-in response-processing templates require SCORE to be single cardinality with base-type float.",
|
|
145
|
+
path: score.source?.path,
|
|
146
|
+
source: score.source,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const responseInteractions = item.interactions.filter(
|
|
150
|
+
(interaction) => interaction.responseIdentifier === "RESPONSE",
|
|
151
|
+
);
|
|
152
|
+
if (responseInteractions.length !== 1 || item.interactions.length !== 1) {
|
|
153
|
+
diagnostics.push({
|
|
154
|
+
code: "processing.template.singleInteraction",
|
|
155
|
+
severity: "error",
|
|
156
|
+
message:
|
|
157
|
+
"Built-in response-processing templates require a single interaction bound to RESPONSE.",
|
|
158
|
+
path: item.source?.path,
|
|
159
|
+
source: item.source,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (templateKind === "mapResponse" && response && !response.mapping) {
|
|
163
|
+
diagnostics.push({
|
|
164
|
+
code: "processing.template.mapping",
|
|
165
|
+
severity: "error",
|
|
166
|
+
message: "The map_response template requires RESPONSE to define qti-mapping.",
|
|
167
|
+
path: response.source?.path,
|
|
168
|
+
source: response.source,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (templateKind === "mapResponsePoint" && response && !response.areaMapping) {
|
|
172
|
+
diagnostics.push({
|
|
173
|
+
code: "processing.template.areaMapping",
|
|
174
|
+
severity: "error",
|
|
175
|
+
message: "The map_response_point template requires RESPONSE to define qti-area-mapping.",
|
|
176
|
+
path: response.source?.path,
|
|
177
|
+
source: response.source,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isXmlBoolean(value: string): boolean {
|
|
183
|
+
return value === "true" || value === "false" || value === "1" || value === "0";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateItemBody(item: QtiAssessmentItem, diagnostics: QtiDiagnostic[]): void {
|
|
187
|
+
if (item.itemBodySource) return;
|
|
188
|
+
diagnostics.push({
|
|
189
|
+
code: "itemBody.required",
|
|
190
|
+
severity: "error",
|
|
191
|
+
message: "qti-assessment-item requires a qti-item-body.",
|
|
192
|
+
path: item.source?.path,
|
|
193
|
+
source: item.source,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
47
197
|
function requireIdentifier(
|
|
48
198
|
elementName: string,
|
|
49
199
|
identifier: string | undefined,
|
|
@@ -1599,8 +1749,7 @@ function validateCorrectResponseReferences(
|
|
|
1599
1749
|
}
|
|
1600
1750
|
|
|
1601
1751
|
function responseValues(value: QtiResponseDeclaration["correctResponse"]): string[] {
|
|
1602
|
-
|
|
1603
|
-
return [String(value)];
|
|
1752
|
+
return qtiValueToStringList(value);
|
|
1604
1753
|
}
|
|
1605
1754
|
|
|
1606
1755
|
function invalidCorrectResponseReference(
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { QtiScalarValue, QtiValue } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function qtiScalarToString(value: QtiScalarValue): string {
|
|
4
|
+
return String(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function qtiValueToString(value: QtiValue): string {
|
|
8
|
+
if (value === null) return "";
|
|
9
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
10
|
+
return qtiScalarToString(value);
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(value)) return value.map(qtiScalarToString).join(" ");
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function qtiValueToStringList(value: QtiValue): string[] {
|
|
17
|
+
if (value === null) return [""];
|
|
18
|
+
if (Array.isArray(value)) return value.map(qtiScalarToString);
|
|
19
|
+
if (typeof value === "object") return [JSON.stringify(value)];
|
|
20
|
+
return [qtiScalarToString(value)];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function qtiValueToIdentifierList(value: QtiValue): string[] {
|
|
24
|
+
if (value === null) return [];
|
|
25
|
+
if (Array.isArray(value)) return value.map(qtiScalarToString);
|
|
26
|
+
if (typeof value === "object") return [JSON.stringify(value)];
|
|
27
|
+
return [qtiScalarToString(value)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function unknownToDisplayString(value: unknown): string {
|
|
31
|
+
if (value === undefined || value === null) return "";
|
|
32
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "bigint" || typeof value === "symbol") return String(value);
|
|
36
|
+
if (Array.isArray(value)) return value.map(unknownToDisplayString).join(", ");
|
|
37
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
38
|
+
return JSON.stringify(value);
|
|
39
|
+
}
|
package/src/xml.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { StaxXmlParserSync, XmlEventType } from "stax-xml";
|
|
|
3
3
|
export interface XmlNode {
|
|
4
4
|
name: string;
|
|
5
5
|
localName: string;
|
|
6
|
+
prefix?: string | undefined;
|
|
7
|
+
uri?: string | undefined;
|
|
6
8
|
attributes: Record<string, string>;
|
|
7
9
|
children: XmlNode[];
|
|
8
10
|
content: Array<string | XmlNode>;
|
|
@@ -40,6 +42,8 @@ export function parseXmlTree(xml: string): { root: XmlNode | undefined; errors:
|
|
|
40
42
|
const node: XmlNode = {
|
|
41
43
|
name: event.name,
|
|
42
44
|
localName: event.localName ?? event.name,
|
|
45
|
+
prefix: event.prefix,
|
|
46
|
+
uri: event.uri,
|
|
43
47
|
attributes: event.attributes,
|
|
44
48
|
children: [],
|
|
45
49
|
content: [],
|