@longsightgroup/qti3-core 0.7.3 → 0.8.1
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 +41 -10
- package/dist/content-text.d.ts +7 -1
- package/dist/content-text.d.ts.map +1 -1
- package/dist/content-text.js +14 -0
- package/dist/content-text.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/package-xml.d.ts +12 -0
- package/dist/package-xml.d.ts.map +1 -0
- package/dist/package-xml.js +17 -0
- package/dist/package-xml.js.map +1 -0
- package/dist/parser-custom-interactions.d.ts +8 -0
- package/dist/parser-custom-interactions.d.ts.map +1 -0
- package/dist/parser-custom-interactions.js +178 -0
- package/dist/parser-custom-interactions.js.map +1 -0
- package/dist/parser-declarations.d.ts +6 -0
- package/dist/parser-declarations.d.ts.map +1 -0
- package/dist/parser-declarations.js +136 -0
- package/dist/parser-declarations.js.map +1 -0
- package/dist/parser-item-metadata.d.ts +9 -0
- package/dist/parser-item-metadata.d.ts.map +1 -0
- package/dist/parser-item-metadata.js +158 -0
- package/dist/parser-item-metadata.js.map +1 -0
- package/dist/parser-processing.d.ts +5 -0
- package/dist/parser-processing.d.ts.map +1 -0
- package/dist/parser-processing.js +642 -0
- package/dist/parser-processing.js.map +1 -0
- package/dist/parser-values.d.ts +8 -0
- package/dist/parser-values.d.ts.map +1 -0
- package/dist/parser-values.js +49 -0
- package/dist/parser-values.js.map +1 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +15 -1083
- package/dist/parser.js.map +1 -1
- package/dist/processing-expression-tags.d.ts +56 -0
- package/dist/processing-expression-tags.d.ts.map +1 -0
- package/dist/processing-expression-tags.js +52 -0
- package/dist/processing-expression-tags.js.map +1 -0
- package/dist/processing-rules.d.ts +3 -0
- package/dist/processing-rules.d.ts.map +1 -0
- package/dist/processing-rules.js +10 -0
- package/dist/processing-rules.js.map +1 -0
- package/dist/serializer-processing-expressions.d.ts +8 -0
- package/dist/serializer-processing-expressions.d.ts.map +1 -0
- package/dist/serializer-processing-expressions.js +198 -0
- package/dist/serializer-processing-expressions.js.map +1 -0
- package/dist/serializer-processing-xml.d.ts +15 -0
- package/dist/serializer-processing-xml.d.ts.map +1 -0
- package/dist/serializer-processing-xml.js +46 -0
- package/dist/serializer-processing-xml.js.map +1 -0
- package/dist/serializer-processing.d.ts +8 -0
- package/dist/serializer-processing.d.ts.map +1 -0
- package/dist/serializer-processing.js +106 -0
- package/dist/serializer-processing.js.map +1 -0
- package/dist/shared-vocabulary-authoring.d.ts +52 -0
- package/dist/shared-vocabulary-authoring.d.ts.map +1 -0
- package/dist/shared-vocabulary-authoring.js +327 -0
- package/dist/shared-vocabulary-authoring.js.map +1 -0
- package/dist/shared-vocabulary-interaction-sets.d.ts +6 -0
- package/dist/shared-vocabulary-interaction-sets.d.ts.map +1 -0
- package/dist/shared-vocabulary-interaction-sets.js +23 -0
- package/dist/shared-vocabulary-interaction-sets.js.map +1 -0
- package/dist/shared-vocabulary-interaction-validation.d.ts +3 -0
- package/dist/shared-vocabulary-interaction-validation.d.ts.map +1 -0
- package/dist/shared-vocabulary-interaction-validation.js +99 -0
- package/dist/shared-vocabulary-interaction-validation.js.map +1 -0
- package/dist/shared-vocabulary-registry-validation.d.ts +5 -0
- package/dist/shared-vocabulary-registry-validation.d.ts.map +1 -0
- package/dist/shared-vocabulary-registry-validation.js +147 -0
- package/dist/shared-vocabulary-registry-validation.js.map +1 -0
- package/dist/shared-vocabulary-support.d.ts.map +1 -1
- package/dist/shared-vocabulary-support.js +160 -67
- package/dist/shared-vocabulary-support.js.map +1 -1
- package/dist/shared-vocabulary.d.ts +6 -0
- package/dist/shared-vocabulary.d.ts.map +1 -1
- package/dist/shared-vocabulary.js +12 -0
- package/dist/shared-vocabulary.js.map +1 -1
- package/dist/support.d.ts +4 -1
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +130 -0
- package/dist/support.js.map +1 -1
- package/dist/types.d.ts +32 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +29 -250
- package/dist/validation.js.map +1 -1
- package/dist/xml.d.ts +10 -0
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +321 -272
- package/dist/xml.js.map +1 -1
- package/package.json +1 -4
- package/src/content-text.ts +20 -1
- package/src/index.ts +68 -0
- package/src/interaction-test-fixtures.ts +44 -0
- package/src/package-xml.ts +30 -0
- package/src/parser-custom-interactions.ts +309 -0
- package/src/parser-declarations.ts +174 -0
- package/src/parser-item-metadata.ts +194 -0
- package/src/parser-processing.ts +701 -0
- package/src/parser-values.ts +61 -0
- package/src/parser.ts +36 -1227
- package/src/processing-expression-tags.ts +65 -0
- package/src/processing-rules.ts +11 -0
- package/src/serializer-processing-expressions.ts +456 -0
- package/src/serializer-processing-xml.ts +67 -0
- package/src/serializer-processing.fixtures.ts +469 -0
- package/src/serializer-processing.ts +196 -0
- package/src/shared-vocabulary-authoring.ts +438 -0
- package/src/shared-vocabulary-interaction-sets.ts +28 -0
- package/src/shared-vocabulary-interaction-validation.ts +143 -0
- package/src/shared-vocabulary-registry-validation.ts +210 -0
- package/src/shared-vocabulary-support.ts +200 -127
- package/src/shared-vocabulary.ts +18 -0
- package/src/support.ts +144 -0
- package/src/types.ts +42 -7
- package/src/validation.ts +33 -341
- package/src/xml.ts +376 -276
package/src/support.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
QtiDiagnostic,
|
|
2
3
|
QtiElementSupport,
|
|
3
4
|
QtiInteractionElementSupport,
|
|
5
|
+
QtiInteractionRegistryStatus,
|
|
4
6
|
QtiInteractionType,
|
|
7
|
+
QtiItemMetadataElementSupport,
|
|
5
8
|
QtiProcessingElementSupport,
|
|
9
|
+
QtiSourceLocation,
|
|
6
10
|
} from "./types.js";
|
|
7
11
|
|
|
8
12
|
export const interactionSupport: QtiInteractionElementSupport[] = [
|
|
@@ -188,10 +192,114 @@ export const processingSupport: QtiProcessingElementSupport[] = [
|
|
|
188
192
|
processingEntry("qti-custom-operator", "packages/core/src/core.test.ts"),
|
|
189
193
|
];
|
|
190
194
|
|
|
195
|
+
export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
|
|
196
|
+
{
|
|
197
|
+
qtiName: "qti-catalog-info",
|
|
198
|
+
category: "itemMetadata",
|
|
199
|
+
support: "parsed",
|
|
200
|
+
specReference: "QTI 3.0.1 ASI",
|
|
201
|
+
parse: true,
|
|
202
|
+
validate: true,
|
|
203
|
+
render: false,
|
|
204
|
+
process: false,
|
|
205
|
+
fixtures: [
|
|
206
|
+
"packages/fixtures/packages/basic-item-player/valid-item-only/items/tolerance-extra-features.xml",
|
|
207
|
+
],
|
|
208
|
+
tests: ["packages/core/src/core.test.ts", "packages/core/src/parser-item-metadata.test.ts"],
|
|
209
|
+
notes:
|
|
210
|
+
"Parsed by parser-item-metadata.ts and validated by validateCatalogInfo. Duplicate containers emit item.child.duplicate.",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
qtiName: "qti-stylesheet",
|
|
214
|
+
category: "itemMetadata",
|
|
215
|
+
support: "parsed",
|
|
216
|
+
specReference: "QTI 3.0.1 ASI",
|
|
217
|
+
parse: true,
|
|
218
|
+
validate: true,
|
|
219
|
+
render: false,
|
|
220
|
+
process: false,
|
|
221
|
+
fixtures: [
|
|
222
|
+
"packages/fixtures/packages/basic-item-player/valid-item-only/items/tolerance-extra-features.xml",
|
|
223
|
+
],
|
|
224
|
+
tests: ["packages/core/src/core.test.ts", "packages/core/src/parser-item-metadata.test.ts"],
|
|
225
|
+
notes: "Parsed by parser-item-metadata.ts and validated by validateStylesheets.",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
qtiName: "qti-modal-feedback",
|
|
229
|
+
category: "itemMetadata",
|
|
230
|
+
support: "parsed",
|
|
231
|
+
specReference: "QTI 3.0.1 ASI",
|
|
232
|
+
parse: true,
|
|
233
|
+
validate: true,
|
|
234
|
+
render: false,
|
|
235
|
+
process: false,
|
|
236
|
+
fixtures: ["packages/fixtures/src/index.ts"],
|
|
237
|
+
tests: ["packages/core/src/core.test.ts", "packages/core/src/parser-item-metadata.test.ts"],
|
|
238
|
+
notes:
|
|
239
|
+
"Parsed by parser-item-metadata.ts and validated by validateModalFeedback. Player rendering remains out of scope for this metadata slice.",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
qtiName: "qti-companion-materials-info",
|
|
243
|
+
category: "itemMetadata",
|
|
244
|
+
support: "parsed",
|
|
245
|
+
specReference: "QTI 3.0.1 ASI",
|
|
246
|
+
parse: true,
|
|
247
|
+
validate: true,
|
|
248
|
+
render: false,
|
|
249
|
+
process: false,
|
|
250
|
+
fixtures: [
|
|
251
|
+
"packages/fixtures/packages/basic-item-player/valid-item-only/items/tolerance-extra-features.xml",
|
|
252
|
+
],
|
|
253
|
+
tests: [
|
|
254
|
+
"packages/core/src/parser-companion-materials.test.ts",
|
|
255
|
+
"packages/core/src/parser-item-metadata.test.ts",
|
|
256
|
+
],
|
|
257
|
+
notes:
|
|
258
|
+
"Parses qti-physical-material text only. qti-digital-material is preserved in unparsedChildren and emits companionMaterials.child.unsupported at parse time.",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
qtiName: "qti-physical-material",
|
|
262
|
+
category: "itemMetadata",
|
|
263
|
+
support: "parsed",
|
|
264
|
+
specReference: "QTI 3.0.1 ASI",
|
|
265
|
+
parse: true,
|
|
266
|
+
validate: true,
|
|
267
|
+
render: false,
|
|
268
|
+
process: false,
|
|
269
|
+
fixtures: ["packages/core/src/parser-companion-materials.test.ts"],
|
|
270
|
+
tests: [
|
|
271
|
+
"packages/core/src/parser-companion-materials.test.ts",
|
|
272
|
+
"packages/core/src/parser-item-metadata.test.ts",
|
|
273
|
+
],
|
|
274
|
+
notes:
|
|
275
|
+
"Child of qti-companion-materials-info. Empty XML text emits companionMaterials.physicalMaterial.empty at parse time; invalid model entries emit companionMaterials.physicalMaterial.empty.model at validation time.",
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
qtiName: "qti-digital-material",
|
|
279
|
+
category: "itemMetadata",
|
|
280
|
+
support: "unsupported",
|
|
281
|
+
specReference: "QTI 3.0.1 ASI",
|
|
282
|
+
parse: false,
|
|
283
|
+
validate: false,
|
|
284
|
+
render: false,
|
|
285
|
+
process: false,
|
|
286
|
+
fixtures: [
|
|
287
|
+
"packages/fixtures/packages/basic-item-player/valid-item-only/items/tolerance-extra-features.xml",
|
|
288
|
+
],
|
|
289
|
+
tests: [
|
|
290
|
+
"packages/core/src/parser-companion-materials.test.ts",
|
|
291
|
+
"packages/core/src/parser-item-metadata.test.ts",
|
|
292
|
+
],
|
|
293
|
+
notes:
|
|
294
|
+
"Recognized as an unparsed child of qti-companion-materials-info. Full digital material parsing is not implemented yet.",
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
191
298
|
export const elementSupport: QtiElementSupport[] = [
|
|
192
299
|
...interactionSupport,
|
|
193
300
|
...deprecatedInteractionSupport,
|
|
194
301
|
...processingSupport,
|
|
302
|
+
...itemMetadataSupport,
|
|
195
303
|
];
|
|
196
304
|
|
|
197
305
|
const allInteractionSupport = [...interactionSupport, ...deprecatedInteractionSupport];
|
|
@@ -204,6 +312,42 @@ export function getInteractionSupport(qtiName: string): QtiElementSupport | unde
|
|
|
204
312
|
return allInteractionSupport.find((item) => item.qtiName === qtiName);
|
|
205
313
|
}
|
|
206
314
|
|
|
315
|
+
export function interactionRegistryStatus(qtiName: string): QtiInteractionRegistryStatus {
|
|
316
|
+
const support = getInteractionSupport(qtiName);
|
|
317
|
+
if (!support) return "unsupported";
|
|
318
|
+
return support.support === "deprecated" ? "deprecated" : "supported";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function interactionRegistryDiagnostics(
|
|
322
|
+
qtiName: string,
|
|
323
|
+
source: QtiSourceLocation,
|
|
324
|
+
): QtiDiagnostic[] {
|
|
325
|
+
const support = getInteractionSupport(qtiName);
|
|
326
|
+
if (!support) {
|
|
327
|
+
return [
|
|
328
|
+
{
|
|
329
|
+
code: "interaction.unsupported",
|
|
330
|
+
severity: "warning",
|
|
331
|
+
message: `${qtiName} is not currently in the support registry.`,
|
|
332
|
+
path: source.path,
|
|
333
|
+
source,
|
|
334
|
+
},
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
if (support.support === "deprecated") {
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
code: "interaction.deprecated",
|
|
341
|
+
severity: "warning",
|
|
342
|
+
message: `${qtiName} is deprecated. ${support.notes ?? ""}`.trim(),
|
|
343
|
+
path: source.path,
|
|
344
|
+
source,
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
|
|
207
351
|
function entry(qtiName: string, interactionType: QtiInteractionType): QtiInteractionElementSupport {
|
|
208
352
|
return {
|
|
209
353
|
qtiName,
|
package/src/types.ts
CHANGED
|
@@ -189,8 +189,13 @@ export type QtiInteractionType =
|
|
|
189
189
|
| "textEntry"
|
|
190
190
|
| "upload";
|
|
191
191
|
|
|
192
|
+
/** Parse-time status from the core interaction support registry. */
|
|
193
|
+
export type QtiInteractionRegistryStatus = "supported" | "deprecated" | "unsupported";
|
|
194
|
+
|
|
192
195
|
export interface QtiInteraction {
|
|
193
196
|
type: QtiInteractionType;
|
|
197
|
+
/** Parse-time status from the core interaction support registry. */
|
|
198
|
+
registryStatus: QtiInteractionRegistryStatus;
|
|
194
199
|
qtiName: string;
|
|
195
200
|
responseIdentifier?: string | undefined;
|
|
196
201
|
responseCardinality?: QtiCardinality | undefined;
|
|
@@ -204,6 +209,7 @@ export interface QtiInteraction {
|
|
|
204
209
|
contextText?: string | undefined;
|
|
205
210
|
object?: QtiObjectAsset | undefined;
|
|
206
211
|
positionObjectStage?: QtiObjectAsset | undefined;
|
|
212
|
+
customInteraction?: QtiCustomInteractionDefinition | undefined;
|
|
207
213
|
portableCustom?: QtiPortableCustomDefinition | undefined;
|
|
208
214
|
choices: QtiChoice[];
|
|
209
215
|
hottextSegments?: QtiHottextSegment[] | undefined;
|
|
@@ -272,20 +278,25 @@ export interface QtiMediaTrack {
|
|
|
272
278
|
source?: QtiSourceLocation | undefined;
|
|
273
279
|
}
|
|
274
280
|
|
|
275
|
-
export interface
|
|
281
|
+
export interface QtiInteractionMarkupDefinition {
|
|
276
282
|
responseIdentifier?: string | undefined;
|
|
283
|
+
interactionMarkup: QtiContentNode[];
|
|
284
|
+
interactionMarkupRaw?: string | undefined;
|
|
285
|
+
dataAttributes: Record<string, string>;
|
|
286
|
+
attributes: Record<string, string>;
|
|
287
|
+
source?: QtiSourceLocation | undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export type QtiCustomInteractionDefinition = QtiInteractionMarkupDefinition;
|
|
291
|
+
|
|
292
|
+
export interface QtiPortableCustomDefinition extends QtiInteractionMarkupDefinition {
|
|
277
293
|
customInteractionTypeIdentifier?: string | undefined;
|
|
278
294
|
module?: string | undefined;
|
|
279
295
|
interactionModules?: QtiPortableCustomInteractionModules | undefined;
|
|
280
|
-
interactionMarkup: QtiContentNode[];
|
|
281
|
-
interactionMarkupRaw?: string | undefined;
|
|
282
296
|
templateVariables: QtiPortableCustomVariableBinding[];
|
|
283
297
|
contextVariables: QtiPortableCustomVariableBinding[];
|
|
284
298
|
stylesheets: QtiStylesheet[];
|
|
285
299
|
catalogInfo?: QtiCatalogInfo | undefined;
|
|
286
|
-
dataAttributes: Record<string, string>;
|
|
287
|
-
attributes: Record<string, string>;
|
|
288
|
-
source?: QtiSourceLocation | undefined;
|
|
289
300
|
}
|
|
290
301
|
|
|
291
302
|
export interface QtiPortableCustomInteractionModules {
|
|
@@ -435,6 +446,7 @@ export interface QtiAssessmentItem {
|
|
|
435
446
|
interactions: QtiInteraction[];
|
|
436
447
|
modalFeedback: QtiModalFeedback[];
|
|
437
448
|
catalogInfo?: QtiCatalogInfo | undefined;
|
|
449
|
+
companionMaterials?: QtiCompanionMaterialsInfo | undefined;
|
|
438
450
|
catalogReferences: QtiCatalogReference[];
|
|
439
451
|
stylesheets: QtiStylesheet[];
|
|
440
452
|
body: QtiContentNode[];
|
|
@@ -442,6 +454,22 @@ export interface QtiAssessmentItem {
|
|
|
442
454
|
source?: QtiSourceLocation | undefined;
|
|
443
455
|
}
|
|
444
456
|
|
|
457
|
+
export interface QtiCompanionMaterialsInfo {
|
|
458
|
+
physicalMaterials: QtiPhysicalMaterial[];
|
|
459
|
+
unparsedChildren: QtiCompanionMaterialsUnparsedChild[];
|
|
460
|
+
source?: QtiSourceLocation | undefined;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export interface QtiCompanionMaterialsUnparsedChild {
|
|
464
|
+
qtiName: string;
|
|
465
|
+
source?: QtiSourceLocation | undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export interface QtiPhysicalMaterial {
|
|
469
|
+
text: string;
|
|
470
|
+
source?: QtiSourceLocation | undefined;
|
|
471
|
+
}
|
|
472
|
+
|
|
445
473
|
export interface QtiModalFeedback {
|
|
446
474
|
identifier: string;
|
|
447
475
|
outcomeIdentifier: string;
|
|
@@ -688,7 +716,10 @@ export interface QtiScoreResult {
|
|
|
688
716
|
state: QtiAttemptStateV1;
|
|
689
717
|
}
|
|
690
718
|
|
|
691
|
-
export type QtiElementSupport =
|
|
719
|
+
export type QtiElementSupport =
|
|
720
|
+
| QtiInteractionElementSupport
|
|
721
|
+
| QtiProcessingElementSupport
|
|
722
|
+
| QtiItemMetadataElementSupport;
|
|
692
723
|
|
|
693
724
|
interface QtiElementSupportBase {
|
|
694
725
|
qtiName: string;
|
|
@@ -712,6 +743,10 @@ export interface QtiProcessingElementSupport extends QtiElementSupportBase {
|
|
|
712
743
|
category: "processing";
|
|
713
744
|
}
|
|
714
745
|
|
|
746
|
+
export interface QtiItemMetadataElementSupport extends QtiElementSupportBase {
|
|
747
|
+
category: "itemMetadata";
|
|
748
|
+
}
|
|
749
|
+
|
|
715
750
|
export interface SharedVocabularyClassSupport {
|
|
716
751
|
className: string;
|
|
717
752
|
scope: "interaction" | "content" | "gap";
|
package/src/validation.ts
CHANGED
|
@@ -21,11 +21,7 @@ import type {
|
|
|
21
21
|
QtiValue,
|
|
22
22
|
QtiValidationResult,
|
|
23
23
|
} from "./types.js";
|
|
24
|
-
import {
|
|
25
|
-
validateSharedVocabularyExtendedText,
|
|
26
|
-
validateSharedVocabularyInputWidth,
|
|
27
|
-
validateSharedVocabularyMediaPlayerControls,
|
|
28
|
-
} from "./shared-vocabulary-validation.js";
|
|
24
|
+
import { validateInteractionSharedVocabulary } from "./shared-vocabulary-interaction-validation.js";
|
|
29
25
|
import { isValidQtiPatternMask } from "./pattern-mask.js";
|
|
30
26
|
import { validateQtiDataSsmlMetadata } from "./tts.js";
|
|
31
27
|
import { qtiValueToStringList } from "./value-format.js";
|
|
@@ -45,6 +41,7 @@ export function validateAssessmentItem(document: QtiDocument): QtiValidationResu
|
|
|
45
41
|
validateInteractions(item, diagnostics);
|
|
46
42
|
validateModalFeedback(item, diagnostics);
|
|
47
43
|
validateCatalogInfo(item, diagnostics);
|
|
44
|
+
validateCompanionMaterials(item, diagnostics);
|
|
48
45
|
validateStylesheets(item, diagnostics);
|
|
49
46
|
diagnostics.push(...validateQtiDataSsmlMetadata(item));
|
|
50
47
|
validateResponseProcessingTemplate(item, diagnostics);
|
|
@@ -716,6 +713,37 @@ function validateOutcomeLookupTables(item: QtiAssessmentItem, diagnostics: QtiDi
|
|
|
716
713
|
}
|
|
717
714
|
}
|
|
718
715
|
|
|
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
|
+
|
|
719
747
|
function validateStylesheets(item: QtiAssessmentItem, diagnostics: QtiDiagnostic[]): void {
|
|
720
748
|
for (const stylesheet of item.stylesheets) {
|
|
721
749
|
if (stylesheet.href.trim().length > 0) continue;
|
|
@@ -1692,342 +1720,6 @@ function validateInteractions(item: QtiAssessmentItem, diagnostics: QtiDiagnosti
|
|
|
1692
1720
|
}
|
|
1693
1721
|
}
|
|
1694
1722
|
|
|
1695
|
-
function validateInteractionSharedVocabulary(
|
|
1696
|
-
interaction: QtiInteraction,
|
|
1697
|
-
diagnostics: QtiDiagnostic[],
|
|
1698
|
-
): void {
|
|
1699
|
-
if (
|
|
1700
|
-
interaction.type !== "choice" &&
|
|
1701
|
-
interaction.type !== "match" &&
|
|
1702
|
-
interaction.type !== "gapMatch" &&
|
|
1703
|
-
interaction.type !== "graphicGapMatch" &&
|
|
1704
|
-
interaction.type !== "inlineChoice" &&
|
|
1705
|
-
interaction.type !== "textEntry" &&
|
|
1706
|
-
interaction.type !== "extendedText" &&
|
|
1707
|
-
interaction.type !== "media" &&
|
|
1708
|
-
interaction.type !== "order"
|
|
1709
|
-
) {
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
const classNames = sharedClassNames(interaction.attributes);
|
|
1713
|
-
const classNameSet = new Set(classNames);
|
|
1714
|
-
if (interaction.type === "media") {
|
|
1715
|
-
validateMediaInteractionSharedVocabulary(interaction, diagnostics);
|
|
1716
|
-
return;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
if (interaction.type === "inlineChoice" || interaction.type === "textEntry") {
|
|
1720
|
-
validateInteractionInputWidthSharedVocabulary(interaction, classNames, diagnostics);
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
if (interaction.type === "extendedText") {
|
|
1725
|
-
diagnostics.push(
|
|
1726
|
-
...validateSharedVocabularyExtendedText({
|
|
1727
|
-
classNames,
|
|
1728
|
-
subjectQtiName: interaction.qtiName,
|
|
1729
|
-
expectedLength: interaction.attributes["expected-length"],
|
|
1730
|
-
path: interaction.source?.path,
|
|
1731
|
-
source: interaction.source,
|
|
1732
|
-
}),
|
|
1733
|
-
);
|
|
1734
|
-
return;
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
if (interaction.type === "choice" || interaction.type === "order") {
|
|
1738
|
-
validateSharedVocabularyLabelClasses(interaction, classNames, diagnostics);
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
if (
|
|
1742
|
-
interaction.type === "match" ||
|
|
1743
|
-
interaction.type === "gapMatch" ||
|
|
1744
|
-
interaction.type === "graphicGapMatch"
|
|
1745
|
-
) {
|
|
1746
|
-
if (interaction.type === "match") {
|
|
1747
|
-
validateMatchInteractionSharedVocabulary(interaction, classNames, diagnostics);
|
|
1748
|
-
}
|
|
1749
|
-
validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics);
|
|
1750
|
-
validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics);
|
|
1751
|
-
if (interaction.type === "gapMatch") {
|
|
1752
|
-
validateGapInputWidthSharedVocabulary(interaction, diagnostics);
|
|
1753
|
-
}
|
|
1754
|
-
return;
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
if (interaction.type === "order") {
|
|
1758
|
-
validateOrderInteractionSharedVocabulary(interaction, classNames, classNameSet, diagnostics);
|
|
1759
|
-
return;
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics);
|
|
1763
|
-
|
|
1764
|
-
const validStackingClasses = new Set<string>();
|
|
1765
|
-
const invalidStackingClasses = new Set<string>();
|
|
1766
|
-
for (const className of classNames) {
|
|
1767
|
-
const stacking = /^qti-choices-stacking-(\d+)$/.exec(className)?.[1];
|
|
1768
|
-
if (stacking === undefined) continue;
|
|
1769
|
-
const count = Number(stacking);
|
|
1770
|
-
if (count >= 1 && count <= 5) validStackingClasses.add(className);
|
|
1771
|
-
else invalidStackingClasses.add(className);
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
if (validStackingClasses.size > 1) {
|
|
1775
|
-
diagnostics.push({
|
|
1776
|
-
code: "interaction.sharedVocabulary.stackingConflict",
|
|
1777
|
-
severity: "warning",
|
|
1778
|
-
message: `qti-choice-interaction should not include multiple qti-choices-stacking-* classes: ${[...validStackingClasses].join(", ")}. The first valid stacking class in class attribute order takes precedence at runtime.`,
|
|
1779
|
-
path: interaction.source?.path,
|
|
1780
|
-
source: interaction.source,
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
for (const className of invalidStackingClasses) {
|
|
1785
|
-
diagnostics.push({
|
|
1786
|
-
code: "interaction.sharedVocabulary.stackingInvalid",
|
|
1787
|
-
severity: "warning",
|
|
1788
|
-
message: `qti-choice-interaction shared vocabulary class ${className} is not supported; expected qti-choices-stacking-1 through qti-choices-stacking-5.`,
|
|
1789
|
-
path: interaction.source?.path,
|
|
1790
|
-
source: interaction.source,
|
|
1791
|
-
});
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
function validateSharedVocabularyLabelClasses(
|
|
1796
|
-
interaction: QtiInteraction,
|
|
1797
|
-
classNames: string[],
|
|
1798
|
-
diagnostics: QtiDiagnostic[],
|
|
1799
|
-
): void {
|
|
1800
|
-
const labelClasses = classNames.filter((className) =>
|
|
1801
|
-
[
|
|
1802
|
-
"qti-labels-decimal",
|
|
1803
|
-
"qti-labels-cjk-ideographic",
|
|
1804
|
-
"qti-labels-lower-alpha",
|
|
1805
|
-
"qti-labels-upper-alpha",
|
|
1806
|
-
].includes(className),
|
|
1807
|
-
);
|
|
1808
|
-
if (new Set(labelClasses).size > 1) {
|
|
1809
|
-
diagnostics.push({
|
|
1810
|
-
code: "interaction.sharedVocabulary.labelsConflict",
|
|
1811
|
-
severity: "warning",
|
|
1812
|
-
message: `${interaction.qtiName} should not include multiple qti-labels-* style classes: ${[...new Set(labelClasses)].join(", ")}. qti-labels-decimal takes precedence over qti-labels-cjk-ideographic, then qti-labels-lower-alpha, then qti-labels-upper-alpha at runtime.`,
|
|
1813
|
-
path: interaction.source?.path,
|
|
1814
|
-
source: interaction.source,
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
const suffixClasses = classNames.filter((className) =>
|
|
1819
|
-
[
|
|
1820
|
-
"qti-labels-suffix-none",
|
|
1821
|
-
"qti-labels-suffix-period",
|
|
1822
|
-
"qti-labels-suffix-parenthesis",
|
|
1823
|
-
].includes(className),
|
|
1824
|
-
);
|
|
1825
|
-
if (new Set(suffixClasses).size <= 1) return;
|
|
1826
|
-
diagnostics.push({
|
|
1827
|
-
code: "interaction.sharedVocabulary.labelSuffixConflict",
|
|
1828
|
-
severity: "warning",
|
|
1829
|
-
message: `${interaction.qtiName} should not include multiple qti-labels-suffix-* classes: ${[...new Set(suffixClasses)].join(", ")}. qti-labels-suffix-none takes precedence over qti-labels-suffix-period, then qti-labels-suffix-parenthesis at runtime.`,
|
|
1830
|
-
path: interaction.source?.path,
|
|
1831
|
-
source: interaction.source,
|
|
1832
|
-
});
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
function validateOrientationSharedVocabulary(
|
|
1836
|
-
interaction: QtiInteraction,
|
|
1837
|
-
classNameSet: Set<string>,
|
|
1838
|
-
diagnostics: QtiDiagnostic[],
|
|
1839
|
-
): void {
|
|
1840
|
-
if (
|
|
1841
|
-
!classNameSet.has("qti-orientation-horizontal") ||
|
|
1842
|
-
!classNameSet.has("qti-orientation-vertical")
|
|
1843
|
-
) {
|
|
1844
|
-
return;
|
|
1845
|
-
}
|
|
1846
|
-
diagnostics.push({
|
|
1847
|
-
code: "interaction.sharedVocabulary.orientationConflict",
|
|
1848
|
-
severity: "warning",
|
|
1849
|
-
message: `${interaction.qtiName} should not include both qti-orientation-horizontal and qti-orientation-vertical; qti-orientation-horizontal takes precedence at runtime.`,
|
|
1850
|
-
path: interaction.source?.path,
|
|
1851
|
-
source: interaction.source,
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
function validateOrderInteractionSharedVocabulary(
|
|
1856
|
-
interaction: QtiInteraction,
|
|
1857
|
-
classNames: string[],
|
|
1858
|
-
classNameSet: Set<string>,
|
|
1859
|
-
diagnostics: QtiDiagnostic[],
|
|
1860
|
-
): void {
|
|
1861
|
-
validateOrientationSharedVocabulary(interaction, classNameSet, diagnostics);
|
|
1862
|
-
validateChoicesPositionSharedVocabulary(interaction, classNames, diagnostics);
|
|
1863
|
-
validateChoicesContainerWidthSharedVocabulary(interaction, diagnostics);
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
const SHARED_VOCABULARY_CHOICES_POSITION_CLASSES = [
|
|
1867
|
-
"qti-choices-top",
|
|
1868
|
-
"qti-choices-bottom",
|
|
1869
|
-
"qti-choices-left",
|
|
1870
|
-
"qti-choices-right",
|
|
1871
|
-
] as const;
|
|
1872
|
-
|
|
1873
|
-
function validateMatchInteractionSharedVocabulary(
|
|
1874
|
-
interaction: QtiInteraction,
|
|
1875
|
-
classNames: string[],
|
|
1876
|
-
diagnostics: QtiDiagnostic[],
|
|
1877
|
-
): void {
|
|
1878
|
-
const hasTabular = classNames.includes("qti-match-tabular");
|
|
1879
|
-
const hasHeaderHidden = classNames.includes("qti-header-hidden");
|
|
1880
|
-
const firstColumnHeader = interaction.attributes["data-first-column-header"];
|
|
1881
|
-
if (!hasTabular && (hasHeaderHidden || firstColumnHeader !== undefined)) {
|
|
1882
|
-
diagnostics.push({
|
|
1883
|
-
code: "interaction.sharedVocabulary.matchTabularContext",
|
|
1884
|
-
severity: "warning",
|
|
1885
|
-
message:
|
|
1886
|
-
"qti-match-interaction shared vocabulary class qti-header-hidden and data-first-column-header are only relevant when qti-match-tabular is specified; they are ignored at runtime.",
|
|
1887
|
-
path: interaction.source?.path,
|
|
1888
|
-
source: interaction.source,
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
if (hasTabular && hasHeaderHidden && firstColumnHeader !== undefined) {
|
|
1892
|
-
diagnostics.push({
|
|
1893
|
-
code: "interaction.sharedVocabulary.matchTabularHeaderHidden",
|
|
1894
|
-
severity: "warning",
|
|
1895
|
-
message:
|
|
1896
|
-
"qti-match-interaction data-first-column-header is ignored when qti-header-hidden suppresses the tabular header row.",
|
|
1897
|
-
path: interaction.source?.path,
|
|
1898
|
-
source: interaction.source,
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
if (!hasTabular) return;
|
|
1902
|
-
|
|
1903
|
-
const choicesPositionClasses = classNames.filter((className) =>
|
|
1904
|
-
SHARED_VOCABULARY_CHOICES_POSITION_CLASSES.includes(
|
|
1905
|
-
className as (typeof SHARED_VOCABULARY_CHOICES_POSITION_CLASSES)[number],
|
|
1906
|
-
),
|
|
1907
|
-
);
|
|
1908
|
-
if (
|
|
1909
|
-
choicesPositionClasses.length > 0 ||
|
|
1910
|
-
interaction.attributes["data-choices-container-width"] !== undefined
|
|
1911
|
-
) {
|
|
1912
|
-
diagnostics.push({
|
|
1913
|
-
code: "interaction.sharedVocabulary.matchTabularChoicesConflict",
|
|
1914
|
-
severity: "warning",
|
|
1915
|
-
message:
|
|
1916
|
-
"qti-match-interaction qti-match-tabular uses a matrix layout; qti-choices-* position classes and data-choices-container-width are ignored at runtime.",
|
|
1917
|
-
path: interaction.source?.path,
|
|
1918
|
-
source: interaction.source,
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
if (!hasHeaderHidden && (firstColumnHeader === undefined || firstColumnHeader === "")) {
|
|
1922
|
-
diagnostics.push({
|
|
1923
|
-
code: "interaction.sharedVocabulary.matchTabularFirstColumnHeader",
|
|
1924
|
-
severity: "warning",
|
|
1925
|
-
message:
|
|
1926
|
-
"qti-match-interaction with qti-match-tabular should specify data-first-column-header for the top-left table header when the tabular header row is shown.",
|
|
1927
|
-
path: interaction.source?.path,
|
|
1928
|
-
source: interaction.source,
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
function validateChoicesPositionSharedVocabulary(
|
|
1934
|
-
interaction: QtiInteraction,
|
|
1935
|
-
classNames: string[],
|
|
1936
|
-
diagnostics: QtiDiagnostic[],
|
|
1937
|
-
): void {
|
|
1938
|
-
const choicesPositionClasses = classNames.filter((className) =>
|
|
1939
|
-
SHARED_VOCABULARY_CHOICES_POSITION_CLASSES.includes(
|
|
1940
|
-
className as (typeof SHARED_VOCABULARY_CHOICES_POSITION_CLASSES)[number],
|
|
1941
|
-
),
|
|
1942
|
-
);
|
|
1943
|
-
if (new Set(choicesPositionClasses).size > 1) {
|
|
1944
|
-
diagnostics.push({
|
|
1945
|
-
code: "interaction.sharedVocabulary.orderChoicesPositionConflict",
|
|
1946
|
-
severity: "warning",
|
|
1947
|
-
message: `${interaction.qtiName} should not include multiple qti-choices-* position classes: ${[...new Set(choicesPositionClasses)].join(", ")}. The first position class in class attribute order takes precedence at runtime.`,
|
|
1948
|
-
path: interaction.source?.path,
|
|
1949
|
-
source: interaction.source,
|
|
1950
|
-
});
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
function validateChoicesContainerWidthSharedVocabulary(
|
|
1955
|
-
interaction: QtiInteraction,
|
|
1956
|
-
diagnostics: QtiDiagnostic[],
|
|
1957
|
-
): void {
|
|
1958
|
-
const width = interaction.attributes["data-choices-container-width"];
|
|
1959
|
-
if (width === undefined) return;
|
|
1960
|
-
const parsed = Number(width);
|
|
1961
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1962
|
-
diagnostics.push({
|
|
1963
|
-
code: "interaction.sharedVocabulary.orderChoicesContainerWidth",
|
|
1964
|
-
severity: "warning",
|
|
1965
|
-
message: `${interaction.qtiName} data-choices-container-width must be a positive pixel value; the invalid value is ignored at runtime.`,
|
|
1966
|
-
path: interaction.source?.path,
|
|
1967
|
-
source: interaction.source,
|
|
1968
|
-
});
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
function validateGapInputWidthSharedVocabulary(
|
|
1973
|
-
interaction: QtiInteraction,
|
|
1974
|
-
diagnostics: QtiDiagnostic[],
|
|
1975
|
-
): void {
|
|
1976
|
-
for (const gap of interaction.choices.filter((choice) => choice.qtiName === "qti-gap")) {
|
|
1977
|
-
diagnostics.push(
|
|
1978
|
-
...validateSharedVocabularyInputWidth({
|
|
1979
|
-
classNames: sharedClassNames(gap.attributes),
|
|
1980
|
-
subjectQtiName: "qti-gap",
|
|
1981
|
-
path: gap.source?.path ?? interaction.source?.path,
|
|
1982
|
-
source: gap.source ?? interaction.source,
|
|
1983
|
-
conflictCode: "interaction.sharedVocabulary.gapInputWidthConflict",
|
|
1984
|
-
invalidCode: "interaction.sharedVocabulary.gapInputWidthInvalid",
|
|
1985
|
-
}),
|
|
1986
|
-
);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
function validateInteractionInputWidthSharedVocabulary(
|
|
1991
|
-
interaction: QtiInteraction,
|
|
1992
|
-
classNames: string[],
|
|
1993
|
-
diagnostics: QtiDiagnostic[],
|
|
1994
|
-
): void {
|
|
1995
|
-
diagnostics.push(
|
|
1996
|
-
...validateSharedVocabularyInputWidth({
|
|
1997
|
-
classNames,
|
|
1998
|
-
subjectQtiName: interaction.qtiName,
|
|
1999
|
-
path: interaction.source?.path,
|
|
2000
|
-
source: interaction.source,
|
|
2001
|
-
conflictCode: "interaction.sharedVocabulary.inputWidthConflict",
|
|
2002
|
-
invalidCode: "interaction.sharedVocabulary.inputWidthInvalid",
|
|
2003
|
-
}),
|
|
2004
|
-
);
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
function validateMediaInteractionSharedVocabulary(
|
|
2008
|
-
interaction: QtiInteraction,
|
|
2009
|
-
diagnostics: QtiDiagnostic[],
|
|
2010
|
-
): void {
|
|
2011
|
-
diagnostics.push(
|
|
2012
|
-
...validateSharedVocabularyMediaPlayerControls({
|
|
2013
|
-
value: interaction.attributes["data-qti-media-player-controls"],
|
|
2014
|
-
subjectQtiName: interaction.qtiName,
|
|
2015
|
-
path: interaction.source?.path,
|
|
2016
|
-
source: interaction.source,
|
|
2017
|
-
}),
|
|
2018
|
-
);
|
|
2019
|
-
|
|
2020
|
-
if (!interaction.object) return;
|
|
2021
|
-
diagnostics.push(
|
|
2022
|
-
...validateSharedVocabularyMediaPlayerControls({
|
|
2023
|
-
value: interaction.object.attributes["data-qti-media-player-controls"],
|
|
2024
|
-
subjectQtiName: `${interaction.qtiName} media object`,
|
|
2025
|
-
path: interaction.object.source?.path ?? interaction.source?.path,
|
|
2026
|
-
source: interaction.object.source ?? interaction.source,
|
|
2027
|
-
}),
|
|
2028
|
-
);
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
1723
|
function sharedClassNames(attributes: Record<string, string>): string[] {
|
|
2032
1724
|
return (attributes.class ?? "").split(/\s+/).filter(Boolean);
|
|
2033
1725
|
}
|