@conform-ed/qti-xml 0.0.16
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 +25 -0
- package/dist/example-inventory.d.ts +2 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +5414 -0
- package/dist/normalize.d.ts +3 -0
- package/dist/parse-xml.d.ts +15 -0
- package/dist/root-detection.d.ts +2 -0
- package/dist/schema-selection.d.ts +12 -0
- package/dist/serialize-asi.d.ts +30 -0
- package/dist/serialize-document.d.ts +10 -0
- package/dist/serialize-manifest.d.ts +11 -0
- package/dist/serialize-pnp.d.ts +11 -0
- package/dist/serialize-result.d.ts +12 -0
- package/dist/serialize-usage-data.d.ts +8 -0
- package/dist/types.d.ts +53 -0
- package/dist/validate-package.d.ts +30 -0
- package/dist/validate.d.ts +10 -0
- package/dist/xinclude.d.ts +11 -0
- package/dist/xml-writer.d.ts +17 -0
- package/package.json +33 -0
- package/src/example-inventory.ts +194 -0
- package/src/index.ts +14 -0
- package/src/normalize.ts +2814 -0
- package/src/parse-xml.ts +147 -0
- package/src/root-detection.ts +214 -0
- package/src/schema-selection.ts +78 -0
- package/src/serialize-asi.ts +2030 -0
- package/src/serialize-document.ts +89 -0
- package/src/serialize-manifest.ts +215 -0
- package/src/serialize-pnp.ts +385 -0
- package/src/serialize-result.ts +215 -0
- package/src/serialize-usage-data.ts +85 -0
- package/src/types.ts +78 -0
- package/src/validate-package.ts +408 -0
- package/src/validate.ts +118 -0
- package/src/xinclude.ts +92 -0
- package/src/xml-writer.ts +68 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QTI Results Reporting XML serialization — conform-ed's first XML writer. Takes the
|
|
3
|
+
* normalized/contracts document shape and emits an instance against the official
|
|
4
|
+
* binding (namespace imsqti_result_v3p0), in the XSD's element order. The export
|
|
5
|
+
* conformance gate: "The system MUST create an instance with all of the REQUIRED
|
|
6
|
+
* properties and values; … The XML instance MUST be valid with respect to the
|
|
7
|
+
* official XSD" — guarded in tests by round-tripping the output through our own
|
|
8
|
+
* parser, normalizer, and strict contracts schema.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
QtiAssessmentResultDocument,
|
|
13
|
+
QtiResultContextVariable,
|
|
14
|
+
QtiResultItemResult,
|
|
15
|
+
QtiResultOutcomeVariable,
|
|
16
|
+
QtiResultResponseVariable,
|
|
17
|
+
QtiResultSupport,
|
|
18
|
+
QtiResultTemplateVariable,
|
|
19
|
+
QtiResultTestResult,
|
|
20
|
+
QtiResultValue,
|
|
21
|
+
} from "@conform-ed/contracts/qti/v3_0_1";
|
|
22
|
+
|
|
23
|
+
import { XmlWriter, type AttributeValue } from "./xml-writer";
|
|
24
|
+
|
|
25
|
+
const resultNamespace = "http://www.imsglobal.org/xsd/imsqti_result_v3p0";
|
|
26
|
+
const resultSchemaLocation = `${resultNamespace} https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_resultv3p0_v1p0.xsd`;
|
|
27
|
+
|
|
28
|
+
function writeValues(writer: XmlWriter, values: readonly QtiResultValue[] | undefined): void {
|
|
29
|
+
for (const entry of values ?? []) {
|
|
30
|
+
writer.element(
|
|
31
|
+
"value",
|
|
32
|
+
[
|
|
33
|
+
["fieldIdentifier", entry.fieldIdentifier],
|
|
34
|
+
["baseType", entry.baseType],
|
|
35
|
+
],
|
|
36
|
+
entry.value,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeResponseVariable(writer: XmlWriter, variable: QtiResultResponseVariable): void {
|
|
42
|
+
writer.element(
|
|
43
|
+
"responseVariable",
|
|
44
|
+
[
|
|
45
|
+
["identifier", variable.identifier],
|
|
46
|
+
["cardinality", variable.cardinality],
|
|
47
|
+
["baseType", variable.baseType],
|
|
48
|
+
["choiceSequence", variable.choiceSequence?.join(" ")],
|
|
49
|
+
["scoreStatus", variable.scoreStatus],
|
|
50
|
+
["answeredStatus", variable.answeredStatus],
|
|
51
|
+
],
|
|
52
|
+
() => {
|
|
53
|
+
if (variable.correctResponse) {
|
|
54
|
+
writer.element("correctResponse", [["interpretation", variable.correctResponse.interpretation]], () => {
|
|
55
|
+
writeValues(writer, variable.correctResponse?.values);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
writer.element("candidateResponse", [], () => {
|
|
60
|
+
writeValues(writer, variable.candidateResponse.values);
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeOutcomeVariable(writer: XmlWriter, variable: QtiResultOutcomeVariable): void {
|
|
67
|
+
const attributes: ReadonlyArray<readonly [string, AttributeValue]> = [
|
|
68
|
+
["identifier", variable.identifier],
|
|
69
|
+
["cardinality", variable.cardinality],
|
|
70
|
+
["baseType", variable.baseType],
|
|
71
|
+
["view", variable.view?.join(" ")],
|
|
72
|
+
["interpretation", variable.interpretation],
|
|
73
|
+
["longInterpretation", variable.longInterpretation],
|
|
74
|
+
["normalMaximum", variable.normalMaximum],
|
|
75
|
+
["normalMinimum", variable.normalMinimum],
|
|
76
|
+
["masteryValue", variable.masteryValue],
|
|
77
|
+
["external-scored", variable.externalScored],
|
|
78
|
+
["variable-identifier-ref", variable.variableIdentifierRef],
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
if (!variable.values?.length) {
|
|
82
|
+
writer.element("outcomeVariable", attributes);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
writer.element("outcomeVariable", attributes, () => {
|
|
87
|
+
writeValues(writer, variable.values);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeContextTemplateVariable(
|
|
92
|
+
writer: XmlWriter,
|
|
93
|
+
name: "templateVariable" | "contextVariable",
|
|
94
|
+
variable: QtiResultTemplateVariable | QtiResultContextVariable,
|
|
95
|
+
): void {
|
|
96
|
+
const attributes: ReadonlyArray<readonly [string, AttributeValue]> = [
|
|
97
|
+
["identifier", variable.identifier],
|
|
98
|
+
["cardinality", variable.cardinality],
|
|
99
|
+
["baseType", variable.baseType],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
if (!variable.values?.length) {
|
|
103
|
+
writer.element(name, attributes);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
writer.element(name, attributes, () => {
|
|
108
|
+
writeValues(writer, variable.values);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeSupport(writer: XmlWriter, support: QtiResultSupport): void {
|
|
113
|
+
writer.element("support", [
|
|
114
|
+
["name", support.name],
|
|
115
|
+
["assignment", support.assignment],
|
|
116
|
+
["value", support.value],
|
|
117
|
+
["language", support.xmlLang],
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeVariables(writer: XmlWriter, container: QtiResultTestResult | QtiResultItemResult): void {
|
|
122
|
+
for (const variable of container.responseVariables ?? []) {
|
|
123
|
+
writeResponseVariable(writer, variable);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const variable of container.templateVariables ?? []) {
|
|
127
|
+
writeContextTemplateVariable(writer, "templateVariable", variable);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const variable of container.outcomeVariables ?? []) {
|
|
131
|
+
writeOutcomeVariable(writer, variable);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const variable of container.contextVariables ?? []) {
|
|
135
|
+
writeContextTemplateVariable(writer, "contextVariable", variable);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Serialize a results document against the official 3.0 binding. */
|
|
140
|
+
export function serializeQtiAssessmentResult(document: QtiAssessmentResultDocument): string {
|
|
141
|
+
const { assessmentResult } = document;
|
|
142
|
+
const writer = new XmlWriter();
|
|
143
|
+
|
|
144
|
+
writer.line('<?xml version="1.0" encoding="UTF-8"?>');
|
|
145
|
+
writer.element(
|
|
146
|
+
"assessmentResult",
|
|
147
|
+
[
|
|
148
|
+
["xmlns", resultNamespace],
|
|
149
|
+
["xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"],
|
|
150
|
+
["xsi:schemaLocation", resultSchemaLocation],
|
|
151
|
+
],
|
|
152
|
+
() => {
|
|
153
|
+
const { context } = assessmentResult;
|
|
154
|
+
|
|
155
|
+
if (context.sessionIdentifiers?.length) {
|
|
156
|
+
writer.element("context", [["sourcedId", context.sourcedId]], () => {
|
|
157
|
+
for (const session of context.sessionIdentifiers ?? []) {
|
|
158
|
+
// The binding spells the attribute sourceID.
|
|
159
|
+
writer.element("sessionIdentifier", [
|
|
160
|
+
["sourceID", session.sourceId],
|
|
161
|
+
["identifier", session.identifier],
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
writer.element("context", [["sourcedId", context.sourcedId]]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { testResult } = assessmentResult;
|
|
170
|
+
|
|
171
|
+
if (testResult) {
|
|
172
|
+
writer.element(
|
|
173
|
+
"testResult",
|
|
174
|
+
[
|
|
175
|
+
["identifier", testResult.identifier],
|
|
176
|
+
["datestamp", testResult.datestamp],
|
|
177
|
+
],
|
|
178
|
+
() => {
|
|
179
|
+
writeVariables(writer, testResult);
|
|
180
|
+
|
|
181
|
+
for (const support of testResult.supports ?? []) {
|
|
182
|
+
writeSupport(writer, support);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const itemResult of assessmentResult.itemResults ?? []) {
|
|
189
|
+
writer.element(
|
|
190
|
+
"itemResult",
|
|
191
|
+
[
|
|
192
|
+
["identifier", itemResult.identifier],
|
|
193
|
+
["sequenceIndex", itemResult.sequenceIndex],
|
|
194
|
+
["datestamp", itemResult.datestamp],
|
|
195
|
+
["sessionStatus", itemResult.sessionStatus],
|
|
196
|
+
],
|
|
197
|
+
() => {
|
|
198
|
+
writeVariables(writer, itemResult);
|
|
199
|
+
|
|
200
|
+
// The XSD sequence puts candidateComment after the variables.
|
|
201
|
+
if (itemResult.candidateComment !== undefined) {
|
|
202
|
+
writer.element("candidateComment", [], itemResult.candidateComment);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const support of itemResult.supports ?? []) {
|
|
206
|
+
writeSupport(writer, support);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return writer.toString();
|
|
215
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Data & Item Statistics XML serialization (imsqti_usagedata_v3p0) — the
|
|
3
|
+
* EXPORT direction of the Usage Data specification, gated in tests by the round
|
|
4
|
+
* trip through our own parser, normalizer, and strict contracts schema.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { QtiUsageDataDocument, QtiUsageStatistic, QtiUsageTargetObject } from "@conform-ed/contracts/qti/v3_0_1";
|
|
8
|
+
|
|
9
|
+
import { XmlWriter, type AttributeValue } from "./xml-writer";
|
|
10
|
+
|
|
11
|
+
const usageDataNamespace = "http://www.imsglobal.org/xsd/imsqti_usagedata_v3p0";
|
|
12
|
+
const usageDataSchemaLocation = `${usageDataNamespace} https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_usagedatav3p0_v1p0.xsd`;
|
|
13
|
+
|
|
14
|
+
function writeTargetObjects(writer: XmlWriter, targetObjects: readonly QtiUsageTargetObject[]): void {
|
|
15
|
+
for (const target of targetObjects) {
|
|
16
|
+
writer.element("targetObject", [
|
|
17
|
+
["identifier", target.identifier],
|
|
18
|
+
["partIdentifier", target.partIdentifier],
|
|
19
|
+
["objectType", target.objectType],
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function statisticAttributes(statistic: QtiUsageStatistic): ReadonlyArray<readonly [string, AttributeValue]> {
|
|
25
|
+
return [
|
|
26
|
+
["name", statistic.name],
|
|
27
|
+
["glossary", statistic.glossary],
|
|
28
|
+
["context", statistic.context],
|
|
29
|
+
["caseCount", statistic.caseCount],
|
|
30
|
+
["stdError", statistic.stdError],
|
|
31
|
+
["stdDeviation", statistic.stdDeviation],
|
|
32
|
+
["lastUpdated", statistic.lastUpdated],
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Serialize a usage data document against the official 3.0 binding. */
|
|
37
|
+
export function serializeQtiUsageData(document: QtiUsageDataDocument): string {
|
|
38
|
+
const { usageData } = document;
|
|
39
|
+
const writer = new XmlWriter();
|
|
40
|
+
|
|
41
|
+
writer.line('<?xml version="1.0" encoding="UTF-8"?>');
|
|
42
|
+
writer.element(
|
|
43
|
+
"usageData",
|
|
44
|
+
[
|
|
45
|
+
["xmlns", usageDataNamespace],
|
|
46
|
+
["xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"],
|
|
47
|
+
["xsi:schemaLocation", usageDataSchemaLocation],
|
|
48
|
+
["glossary", usageData.glossary],
|
|
49
|
+
],
|
|
50
|
+
() => {
|
|
51
|
+
for (const statistic of usageData.statistics) {
|
|
52
|
+
if (statistic.kind === "ordinaryStatistic") {
|
|
53
|
+
writer.element("ordinaryStatistic", statisticAttributes(statistic), () => {
|
|
54
|
+
writeTargetObjects(writer, statistic.targetObjects);
|
|
55
|
+
writer.element("value", [], statistic.value.value);
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
writer.element("categorizedStatistic", statisticAttributes(statistic), () => {
|
|
61
|
+
writeTargetObjects(writer, statistic.targetObjects);
|
|
62
|
+
writer.element(
|
|
63
|
+
"mapping",
|
|
64
|
+
[
|
|
65
|
+
["lowerBound", statistic.mapping.lowerBound],
|
|
66
|
+
["upperBound", statistic.mapping.upperBound],
|
|
67
|
+
["defaultValue", statistic.mapping.defaultValue],
|
|
68
|
+
],
|
|
69
|
+
() => {
|
|
70
|
+
for (const entry of statistic.mapping.mapEntries) {
|
|
71
|
+
writer.element("mapEntry", [
|
|
72
|
+
["mapKey", entry.mapKey],
|
|
73
|
+
["mappedValue", entry.mappedValue],
|
|
74
|
+
["caseSensitive", entry.caseSensitive],
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return writer.toString();
|
|
85
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type QtiFileKind = "html" | "other" | "xml" | "zip";
|
|
2
|
+
export type QtiXmlStatus = "malformed" | "not-xml" | "well-formed";
|
|
3
|
+
export type QtiSupportStatus =
|
|
4
|
+
| "known-broken-example"
|
|
5
|
+
| "not-xml"
|
|
6
|
+
| "supported"
|
|
7
|
+
| "unsupported-normalization"
|
|
8
|
+
| "unsupported-root"
|
|
9
|
+
| "zip-package";
|
|
10
|
+
export type QtiValidationStatus = "invalid" | "parse-error" | "unsupported" | "valid";
|
|
11
|
+
export type QtiVersion = "2.2" | "3.0.1";
|
|
12
|
+
export type QtiSchemaSelectionKey =
|
|
13
|
+
| "qtiAccessForAllPnpDocument"
|
|
14
|
+
| "qtiAccessForAllPnpRecordsDocument"
|
|
15
|
+
| "qtiAssessmentItemDocument"
|
|
16
|
+
| "qtiAssessmentResultDocument"
|
|
17
|
+
| "qtiOutcomeDeclarationDocument"
|
|
18
|
+
| "qtiOutcomeProcessingDocument"
|
|
19
|
+
| "qtiResponseProcessingDocument"
|
|
20
|
+
| "qtiUsageDataDocument"
|
|
21
|
+
| "qtiAssessmentSectionDocument"
|
|
22
|
+
| "qtiAssessmentStimulusDocument"
|
|
23
|
+
| "qtiAssessmentTestDocument"
|
|
24
|
+
| "qtiManifestDocument"
|
|
25
|
+
| "qtiMetadataDocument";
|
|
26
|
+
|
|
27
|
+
export interface QtiRootDetection {
|
|
28
|
+
rootName: string;
|
|
29
|
+
localName: string;
|
|
30
|
+
prefix?: string;
|
|
31
|
+
namespaceUri?: string;
|
|
32
|
+
inferredVersion?: QtiVersion;
|
|
33
|
+
schemaSelectionKey?: QtiSchemaSelectionKey;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface QtiExampleInventoryEntry {
|
|
37
|
+
absolutePath: string;
|
|
38
|
+
relativePath: string;
|
|
39
|
+
sourceGroup: string;
|
|
40
|
+
fileKind: QtiFileKind;
|
|
41
|
+
xmlStatus: QtiXmlStatus;
|
|
42
|
+
supportStatus: QtiSupportStatus;
|
|
43
|
+
rootName?: string;
|
|
44
|
+
localName?: string;
|
|
45
|
+
namespaceUri?: string;
|
|
46
|
+
inferredVersion?: QtiVersion;
|
|
47
|
+
schemaSelectionKey?: QtiSchemaSelectionKey;
|
|
48
|
+
contentHash: string;
|
|
49
|
+
note?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface QtiExampleInventorySummary {
|
|
53
|
+
totalFiles: number;
|
|
54
|
+
byFileKind: Record<QtiFileKind, number>;
|
|
55
|
+
bySupportStatus: Record<QtiSupportStatus, number>;
|
|
56
|
+
byVersion: Record<string, number>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface QtiExampleInventoryReport {
|
|
60
|
+
rootPath: string;
|
|
61
|
+
generatedAt: string;
|
|
62
|
+
entries: QtiExampleInventoryEntry[];
|
|
63
|
+
summary: QtiExampleInventorySummary;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface QtiValidationIssue {
|
|
67
|
+
path: string;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface QtiValidationResult {
|
|
72
|
+
filePath: string;
|
|
73
|
+
rootDetection?: QtiRootDetection;
|
|
74
|
+
status: QtiValidationStatus;
|
|
75
|
+
schemaSelectionKey?: QtiSchemaSelectionKey;
|
|
76
|
+
normalizedDocument?: unknown;
|
|
77
|
+
issues: QtiValidationIssue[];
|
|
78
|
+
}
|