@longsightgroup/qti3-core 0.8.1 → 0.8.2
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/asset-url.d.ts +2 -0
- package/dist/asset-url.d.ts.map +1 -0
- package/dist/asset-url.js +8 -0
- package/dist/asset-url.js.map +1 -0
- package/dist/companion-materials-resolution.d.ts +30 -0
- package/dist/companion-materials-resolution.d.ts.map +1 -0
- package/dist/companion-materials-resolution.js +53 -0
- package/dist/companion-materials-resolution.js.map +1 -0
- package/dist/companion-materials.d.ts +6 -0
- package/dist/companion-materials.d.ts.map +1 -0
- package/dist/companion-materials.js +16 -0
- package/dist/companion-materials.js.map +1 -0
- 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/parse-diagnostics.d.ts +2 -0
- package/dist/parse-diagnostics.d.ts.map +1 -0
- package/dist/parse-diagnostics.js +28 -0
- package/dist/parse-diagnostics.js.map +1 -0
- package/dist/parser-item-metadata.d.ts.map +1 -1
- package/dist/parser-item-metadata.js +37 -10
- package/dist/parser-item-metadata.js.map +1 -1
- package/dist/support.js +5 -5
- package/dist/support.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation-companion-materials.d.ts +3 -0
- package/dist/validation-companion-materials.d.ts.map +1 -0
- package/dist/validation-companion-materials.js +40 -0
- package/dist/validation-companion-materials.js.map +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -27
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/asset-url.ts +9 -0
- package/src/companion-materials-resolution.ts +99 -0
- package/src/companion-materials.ts +30 -0
- package/src/index.ts +11 -0
- package/src/parse-diagnostics.ts +28 -0
- package/src/parser-item-metadata.ts +79 -10
- package/src/support.ts +5 -5
- package/src/types.ts +8 -0
- package/src/validation-companion-materials.ts +48 -0
- package/src/validation.ts +1 -31
package/src/index.ts
CHANGED
|
@@ -5,6 +5,15 @@ export {
|
|
|
5
5
|
type QtiResolvedCatalogReference,
|
|
6
6
|
type QtiResolvedCatalogSupport,
|
|
7
7
|
} from "./catalog.js";
|
|
8
|
+
export {
|
|
9
|
+
createCompanionMaterialsResolution,
|
|
10
|
+
type QtiCompanionMaterialsResolution,
|
|
11
|
+
type QtiCompanionMaterialsResolutionOptions,
|
|
12
|
+
type QtiResolvedCompanionMaterialUnparsedChild,
|
|
13
|
+
type QtiResolvedDigitalCompanionMaterial,
|
|
14
|
+
type QtiResolvedPhysicalCompanionMaterial,
|
|
15
|
+
} from "./companion-materials-resolution.js";
|
|
16
|
+
export { isResolvableAssetUrl } from "./asset-url.js";
|
|
8
17
|
export {
|
|
9
18
|
analyzeQtiDeliverySecurity,
|
|
10
19
|
buildQtiDeliverySafeXml,
|
|
@@ -20,6 +29,7 @@ export {
|
|
|
20
29
|
type QtiPackageXmlTree,
|
|
21
30
|
} from "./package-xml.js";
|
|
22
31
|
export { parseQtiXml } from "./parser.js";
|
|
32
|
+
export { isConformanceParseDiagnostic } from "./parse-diagnostics.js";
|
|
23
33
|
export {
|
|
24
34
|
scoreQtiItemServerSide,
|
|
25
35
|
type QtiServerScoringInput,
|
|
@@ -179,6 +189,7 @@ export type {
|
|
|
179
189
|
QtiCompanionMaterialsUnparsedChild,
|
|
180
190
|
QtiContentNode,
|
|
181
191
|
QtiCustomInteractionDefinition,
|
|
192
|
+
QtiDigitalMaterial,
|
|
182
193
|
QtiInteractionMarkupDefinition,
|
|
183
194
|
QtiDiagnostic,
|
|
184
195
|
QtiDocument,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic codes emitted while parsing QTI XML, before validation runs.
|
|
3
|
+
* Conformance fixtures use this classifier to separate parse-time diagnostics
|
|
4
|
+
* from validation diagnostics in `parseQtiXml` results.
|
|
5
|
+
*/
|
|
6
|
+
const CONFORMANCE_PARSE_DIAGNOSTIC_CODES = new Set([
|
|
7
|
+
"xml.parse",
|
|
8
|
+
"xml.empty",
|
|
9
|
+
"qti.root",
|
|
10
|
+
"processing.unsupported",
|
|
11
|
+
"processing.response.forbidden",
|
|
12
|
+
"interaction.unsupported",
|
|
13
|
+
"interaction.deprecated",
|
|
14
|
+
"interaction.custom.markupInteraction",
|
|
15
|
+
"interaction.portableCustom.markupInteraction",
|
|
16
|
+
"interaction.portableCustom.child.duplicate",
|
|
17
|
+
"companionMaterials.child.unsupported",
|
|
18
|
+
"companionMaterials.digitalMaterial.fileHref.duplicate",
|
|
19
|
+
"companionMaterials.digitalMaterial.fileHref.missing",
|
|
20
|
+
"companionMaterials.digitalMaterial.fileHref.empty",
|
|
21
|
+
"companionMaterials.digitalMaterial.resourceIcon.duplicate",
|
|
22
|
+
"companionMaterials.physicalMaterial.empty",
|
|
23
|
+
"item.child.duplicate",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export function isConformanceParseDiagnostic(code: string): boolean {
|
|
27
|
+
return CONFORMANCE_PARSE_DIAGNOSTIC_CODES.has(code);
|
|
28
|
+
}
|
|
@@ -9,10 +9,16 @@ import type {
|
|
|
9
9
|
QtiCompanionMaterialsUnparsedChild,
|
|
10
10
|
QtiContentNode,
|
|
11
11
|
QtiDiagnostic,
|
|
12
|
+
QtiDigitalMaterial,
|
|
12
13
|
QtiModalFeedback,
|
|
13
14
|
QtiPhysicalMaterial,
|
|
14
15
|
QtiStylesheet,
|
|
15
16
|
} from "./types.js";
|
|
17
|
+
import {
|
|
18
|
+
PARSED_COMPANION_MATERIAL_CHILD_NAMES,
|
|
19
|
+
type ParsedCompanionMaterialChildQtiName,
|
|
20
|
+
pushCompanionMaterialParseWarning,
|
|
21
|
+
} from "./companion-materials.js";
|
|
16
22
|
import { childElements, descendants, textContent, type XmlNode } from "./xml.js";
|
|
17
23
|
|
|
18
24
|
export function firstChildElement(
|
|
@@ -146,12 +152,23 @@ export function parseCompanionMaterialsInfo(
|
|
|
146
152
|
if (!node) return undefined;
|
|
147
153
|
|
|
148
154
|
const physicalMaterials: QtiPhysicalMaterial[] = [];
|
|
155
|
+
const digitalMaterials: QtiDigitalMaterial[] = [];
|
|
149
156
|
const unparsedChildren: QtiCompanionMaterialsUnparsedChild[] = [];
|
|
150
157
|
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
const companionMaterialHandlers = {
|
|
159
|
+
"qti-physical-material": (child) => {
|
|
153
160
|
const material = parsePhysicalMaterial(child, diagnostics);
|
|
154
161
|
if (material) physicalMaterials.push(material);
|
|
162
|
+
},
|
|
163
|
+
"qti-digital-material": (child) => {
|
|
164
|
+
const material = parseDigitalMaterial(child, diagnostics);
|
|
165
|
+
if (material) digitalMaterials.push(material);
|
|
166
|
+
},
|
|
167
|
+
} satisfies Record<ParsedCompanionMaterialChildQtiName, (child: XmlNode) => void>;
|
|
168
|
+
|
|
169
|
+
for (const child of childElements(node)) {
|
|
170
|
+
if (isParsedCompanionMaterialChildQtiName(child.localName)) {
|
|
171
|
+
companionMaterialHandlers[child.localName](child);
|
|
155
172
|
continue;
|
|
156
173
|
}
|
|
157
174
|
|
|
@@ -167,6 +184,7 @@ export function parseCompanionMaterialsInfo(
|
|
|
167
184
|
|
|
168
185
|
return {
|
|
169
186
|
physicalMaterials,
|
|
187
|
+
digitalMaterials,
|
|
170
188
|
unparsedChildren,
|
|
171
189
|
source: node.source,
|
|
172
190
|
};
|
|
@@ -178,17 +196,68 @@ function parsePhysicalMaterial(
|
|
|
178
196
|
): QtiPhysicalMaterial | undefined {
|
|
179
197
|
const text = textContent(node).trim();
|
|
180
198
|
if (text.length === 0) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return undefined;
|
|
199
|
+
return pushCompanionMaterialParseWarning(
|
|
200
|
+
diagnostics,
|
|
201
|
+
"companionMaterials.physicalMaterial.empty",
|
|
202
|
+
"qti-physical-material requires non-empty text content.",
|
|
203
|
+
node.source?.path,
|
|
204
|
+
node.source,
|
|
205
|
+
);
|
|
189
206
|
}
|
|
190
207
|
return {
|
|
191
208
|
text,
|
|
192
209
|
source: node.source,
|
|
193
210
|
};
|
|
194
211
|
}
|
|
212
|
+
|
|
213
|
+
function parseDigitalMaterial(
|
|
214
|
+
node: XmlNode,
|
|
215
|
+
diagnostics: QtiDiagnostic[],
|
|
216
|
+
): QtiDigitalMaterial | undefined {
|
|
217
|
+
const fileHrefNode = firstChildElement(
|
|
218
|
+
node,
|
|
219
|
+
"qti-file-href",
|
|
220
|
+
diagnostics,
|
|
221
|
+
"companionMaterials.digitalMaterial.fileHref.duplicate",
|
|
222
|
+
);
|
|
223
|
+
if (!fileHrefNode) {
|
|
224
|
+
return pushCompanionMaterialParseWarning(
|
|
225
|
+
diagnostics,
|
|
226
|
+
"companionMaterials.digitalMaterial.fileHref.missing",
|
|
227
|
+
"qti-digital-material requires a qti-file-href child.",
|
|
228
|
+
node.source?.path,
|
|
229
|
+
node.source,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const fileHref = textContent(fileHrefNode).trim();
|
|
234
|
+
if (fileHref.length === 0) {
|
|
235
|
+
return pushCompanionMaterialParseWarning(
|
|
236
|
+
diagnostics,
|
|
237
|
+
"companionMaterials.digitalMaterial.fileHref.empty",
|
|
238
|
+
"qti-digital-material qti-file-href requires non-empty text content.",
|
|
239
|
+
fileHrefNode.source?.path,
|
|
240
|
+
fileHrefNode.source,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const resourceIconNode = firstChildElement(
|
|
245
|
+
node,
|
|
246
|
+
"qti-resource-icon",
|
|
247
|
+
diagnostics,
|
|
248
|
+
"companionMaterials.digitalMaterial.resourceIcon.duplicate",
|
|
249
|
+
);
|
|
250
|
+
const resourceIcon = resourceIconNode ? textContent(resourceIconNode).trim() : "";
|
|
251
|
+
return {
|
|
252
|
+
fileHref,
|
|
253
|
+
resourceIcon: resourceIcon.length > 0 ? resourceIcon : undefined,
|
|
254
|
+
attributes: node.attributes,
|
|
255
|
+
source: node.source,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isParsedCompanionMaterialChildQtiName(
|
|
260
|
+
localName: string,
|
|
261
|
+
): localName is ParsedCompanionMaterialChildQtiName {
|
|
262
|
+
return PARSED_COMPANION_MATERIAL_CHILD_NAMES.has(localName);
|
|
263
|
+
}
|
package/src/support.ts
CHANGED
|
@@ -255,7 +255,7 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
|
|
|
255
255
|
"packages/core/src/parser-item-metadata.test.ts",
|
|
256
256
|
],
|
|
257
257
|
notes:
|
|
258
|
-
"Parses qti-physical-material text
|
|
258
|
+
"Parses qti-physical-material text and qti-digital-material file references. Digital materials require non-empty qti-file-href text and may include label, mime-type, and qti-resource-icon metadata. Hosts read resolved materials through createCompanionMaterialsResolution() or player.getCompanionMaterialsResolution().",
|
|
259
259
|
},
|
|
260
260
|
{
|
|
261
261
|
qtiName: "qti-physical-material",
|
|
@@ -277,10 +277,10 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
|
|
|
277
277
|
{
|
|
278
278
|
qtiName: "qti-digital-material",
|
|
279
279
|
category: "itemMetadata",
|
|
280
|
-
support: "
|
|
280
|
+
support: "parsed",
|
|
281
281
|
specReference: "QTI 3.0.1 ASI",
|
|
282
|
-
parse:
|
|
283
|
-
validate:
|
|
282
|
+
parse: true,
|
|
283
|
+
validate: true,
|
|
284
284
|
render: false,
|
|
285
285
|
process: false,
|
|
286
286
|
fixtures: [
|
|
@@ -291,7 +291,7 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
|
|
|
291
291
|
"packages/core/src/parser-item-metadata.test.ts",
|
|
292
292
|
],
|
|
293
293
|
notes:
|
|
294
|
-
"
|
|
294
|
+
"Child of qti-companion-materials-info. Parsed from qti-file-href with optional label, mime-type, and qti-resource-icon metadata. Element attributes are preserved on the parsed model. Missing or empty qti-file-href emits companionMaterials.digitalMaterial.fileHref.* diagnostics.",
|
|
295
295
|
},
|
|
296
296
|
];
|
|
297
297
|
|
package/src/types.ts
CHANGED
|
@@ -456,6 +456,7 @@ export interface QtiAssessmentItem {
|
|
|
456
456
|
|
|
457
457
|
export interface QtiCompanionMaterialsInfo {
|
|
458
458
|
physicalMaterials: QtiPhysicalMaterial[];
|
|
459
|
+
digitalMaterials: QtiDigitalMaterial[];
|
|
459
460
|
unparsedChildren: QtiCompanionMaterialsUnparsedChild[];
|
|
460
461
|
source?: QtiSourceLocation | undefined;
|
|
461
462
|
}
|
|
@@ -470,6 +471,13 @@ export interface QtiPhysicalMaterial {
|
|
|
470
471
|
source?: QtiSourceLocation | undefined;
|
|
471
472
|
}
|
|
472
473
|
|
|
474
|
+
export interface QtiDigitalMaterial {
|
|
475
|
+
fileHref: string;
|
|
476
|
+
resourceIcon?: string | undefined;
|
|
477
|
+
attributes: Record<string, string>;
|
|
478
|
+
source?: QtiSourceLocation | undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
473
481
|
export interface QtiModalFeedback {
|
|
474
482
|
identifier: string;
|
|
475
483
|
outcomeIdentifier: string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { QtiAssessmentItem, QtiDiagnostic } from "./types.js";
|
|
2
|
+
import { PARSED_COMPANION_MATERIAL_CHILD_NAMES } from "./companion-materials.js";
|
|
3
|
+
|
|
4
|
+
export function validateCompanionMaterials(
|
|
5
|
+
item: QtiAssessmentItem,
|
|
6
|
+
diagnostics: QtiDiagnostic[],
|
|
7
|
+
): void {
|
|
8
|
+
const companionMaterials = item.companionMaterials;
|
|
9
|
+
if (!companionMaterials) return;
|
|
10
|
+
|
|
11
|
+
for (const child of companionMaterials.unparsedChildren) {
|
|
12
|
+
if (!PARSED_COMPANION_MATERIAL_CHILD_NAMES.has(child.qtiName)) continue;
|
|
13
|
+
|
|
14
|
+
diagnostics.push({
|
|
15
|
+
code: "companionMaterials.model.inconsistent",
|
|
16
|
+
severity: "error",
|
|
17
|
+
message: `${child.qtiName} must be represented in the parsed companion materials model, not as an unparsed child.`,
|
|
18
|
+
path: child.source?.path,
|
|
19
|
+
source: child.source,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const material of companionMaterials.physicalMaterials) {
|
|
24
|
+
if (material.text.trim().length === 0) {
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
code: "companionMaterials.physicalMaterial.empty.model",
|
|
27
|
+
severity: "error",
|
|
28
|
+
message:
|
|
29
|
+
"qti-physical-material requires non-empty text content in the parsed companion materials model.",
|
|
30
|
+
path: material.source?.path,
|
|
31
|
+
source: material.source,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const material of companionMaterials.digitalMaterials) {
|
|
37
|
+
if (material.fileHref.trim().length === 0) {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
code: "companionMaterials.digitalMaterial.fileHref.empty.model",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message:
|
|
42
|
+
"qti-digital-material requires non-empty qti-file-href text content in the parsed companion materials model.",
|
|
43
|
+
path: material.source?.path,
|
|
44
|
+
source: material.source,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/validation.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { validateInteractionSharedVocabulary } from "./shared-vocabulary-interac
|
|
|
25
25
|
import { isValidQtiPatternMask } from "./pattern-mask.js";
|
|
26
26
|
import { validateQtiDataSsmlMetadata } from "./tts.js";
|
|
27
27
|
import { qtiValueToStringList } from "./value-format.js";
|
|
28
|
+
import { validateCompanionMaterials } from "./validation-companion-materials.js";
|
|
28
29
|
|
|
29
30
|
const BUILT_IN_COMPLETION_STATUS = "completionStatus";
|
|
30
31
|
|
|
@@ -713,37 +714,6 @@ function validateOutcomeLookupTables(item: QtiAssessmentItem, diagnostics: QtiDi
|
|
|
713
714
|
}
|
|
714
715
|
}
|
|
715
716
|
|
|
716
|
-
function validateCompanionMaterials(item: QtiAssessmentItem, diagnostics: QtiDiagnostic[]): void {
|
|
717
|
-
const companionMaterials = item.companionMaterials;
|
|
718
|
-
if (!companionMaterials) return;
|
|
719
|
-
|
|
720
|
-
for (const child of companionMaterials.unparsedChildren) {
|
|
721
|
-
if (child.qtiName === "qti-physical-material") {
|
|
722
|
-
diagnostics.push({
|
|
723
|
-
code: "companionMaterials.model.inconsistent",
|
|
724
|
-
severity: "error",
|
|
725
|
-
message:
|
|
726
|
-
"Empty qti-physical-material must be represented as a parse warning, not an unparsed child.",
|
|
727
|
-
path: child.source?.path,
|
|
728
|
-
source: child.source,
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
for (const material of companionMaterials.physicalMaterials) {
|
|
734
|
-
if (material.text.trim().length === 0) {
|
|
735
|
-
diagnostics.push({
|
|
736
|
-
code: "companionMaterials.physicalMaterial.empty.model",
|
|
737
|
-
severity: "error",
|
|
738
|
-
message:
|
|
739
|
-
"qti-physical-material requires non-empty text content in the parsed companion materials model.",
|
|
740
|
-
path: material.source?.path,
|
|
741
|
-
source: material.source,
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
717
|
function validateStylesheets(item: QtiAssessmentItem, diagnostics: QtiDiagnostic[]): void {
|
|
748
718
|
for (const stylesheet of item.stylesheets) {
|
|
749
719
|
if (stylesheet.href.trim().length > 0) continue;
|