@longsightgroup/qti3-core 0.9.3 → 0.9.4
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 +88 -8
- package/dist/adaptive-turn-materializer.d.ts +2 -0
- package/dist/adaptive-turn-materializer.d.ts.map +1 -1
- package/dist/adaptive-turn-materializer.js +25 -19
- package/dist/adaptive-turn-materializer.js.map +1 -1
- package/dist/adaptive-turn.d.ts +8 -1
- package/dist/adaptive-turn.d.ts.map +1 -1
- package/dist/adaptive-turn.js +15 -5
- package/dist/adaptive-turn.js.map +1 -1
- package/dist/delivery-preparation.d.ts +20 -0
- package/dist/delivery-preparation.d.ts.map +1 -0
- package/dist/delivery-preparation.js +71 -0
- package/dist/delivery-preparation.js.map +1 -0
- package/dist/delivery-redaction.d.ts +5 -0
- package/dist/delivery-redaction.d.ts.map +1 -1
- package/dist/delivery-redaction.js +26 -5
- package/dist/delivery-redaction.js.map +1 -1
- package/dist/delivery-security.js +2 -2
- package/dist/delivery-security.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/item-scoring-expectation.d.ts +4 -0
- package/dist/item-scoring-expectation.d.ts.map +1 -0
- package/dist/item-scoring-expectation.js +6 -0
- package/dist/item-scoring-expectation.js.map +1 -0
- package/dist/item-submission-materialization.d.ts +25 -0
- package/dist/item-submission-materialization.d.ts.map +1 -0
- package/dist/item-submission-materialization.js +68 -0
- package/dist/item-submission-materialization.js.map +1 -0
- package/dist/qti-package-assets.d.ts +8 -0
- package/dist/qti-package-assets.d.ts.map +1 -0
- package/dist/qti-package-assets.js +173 -0
- package/dist/qti-package-assets.js.map +1 -0
- package/dist/qti-package-items.d.ts +14 -0
- package/dist/qti-package-items.d.ts.map +1 -0
- package/dist/qti-package-items.js +138 -0
- package/dist/qti-package-items.js.map +1 -0
- package/dist/qti-package-manifest.d.ts +13 -0
- package/dist/qti-package-manifest.d.ts.map +1 -0
- package/dist/qti-package-manifest.js +109 -0
- package/dist/qti-package-manifest.js.map +1 -0
- package/dist/qti-package-metadata.d.ts +8 -0
- package/dist/qti-package-metadata.d.ts.map +1 -0
- package/dist/qti-package-metadata.js +115 -0
- package/dist/qti-package-metadata.js.map +1 -0
- package/dist/qti-package-paths.d.ts +6 -0
- package/dist/qti-package-paths.d.ts.map +1 -0
- package/dist/qti-package-paths.js +32 -0
- package/dist/qti-package-paths.js.map +1 -0
- package/dist/qti-package-types.d.ts +98 -0
- package/dist/qti-package-types.d.ts.map +1 -0
- package/dist/qti-package-types.js +2 -0
- package/dist/qti-package-types.js.map +1 -0
- package/dist/qti-package-xml.d.ts +21 -0
- package/dist/qti-package-xml.d.ts.map +1 -0
- package/dist/qti-package-xml.js +68 -0
- package/dist/qti-package-xml.js.map +1 -0
- package/dist/qti-package-zip.d.ts +24 -0
- package/dist/qti-package-zip.d.ts.map +1 -0
- package/dist/qti-package-zip.js +185 -0
- package/dist/qti-package-zip.js.map +1 -0
- package/dist/qti-package.d.ts +7 -0
- package/dist/qti-package.d.ts.map +1 -0
- package/dist/qti-package.js +76 -0
- package/dist/qti-package.js.map +1 -0
- package/dist/response-input.d.ts +9 -0
- package/dist/response-input.d.ts.map +1 -0
- package/dist/response-input.js +9 -0
- package/dist/response-input.js.map +1 -0
- package/dist/response-validation-policy.d.ts +31 -0
- package/dist/response-validation-policy.d.ts.map +1 -0
- package/dist/response-validation-policy.js +158 -0
- package/dist/response-validation-policy.js.map +1 -0
- package/dist/response-validation.d.ts +23 -0
- package/dist/response-validation.d.ts.map +1 -0
- package/dist/response-validation.js +157 -0
- package/dist/response-validation.js.map +1 -0
- package/dist/scoring-disposition-policy.d.ts +8 -0
- package/dist/scoring-disposition-policy.d.ts.map +1 -0
- package/dist/scoring-disposition-policy.js +24 -0
- package/dist/scoring-disposition-policy.js.map +1 -0
- package/dist/template-presentation-materializer.d.ts +14 -0
- package/dist/template-presentation-materializer.d.ts.map +1 -0
- package/dist/template-presentation-materializer.js +143 -0
- package/dist/template-presentation-materializer.js.map +1 -0
- package/dist/trusted-item-session.d.ts +25 -8
- package/dist/trusted-item-session.d.ts.map +1 -1
- package/dist/trusted-item-session.js +51 -46
- package/dist/trusted-item-session.js.map +1 -1
- package/dist/trusted-session-materialization.d.ts +33 -0
- package/dist/trusted-session-materialization.d.ts.map +1 -0
- package/dist/trusted-session-materialization.js +26 -0
- package/dist/trusted-session-materialization.js.map +1 -0
- package/dist/validation-primitives.d.ts +1 -0
- package/dist/validation-primitives.d.ts.map +1 -1
- package/dist/validation-primitives.js +5 -0
- package/dist/validation-primitives.js.map +1 -1
- package/dist/variable-snapshot.d.ts +9 -0
- package/dist/variable-snapshot.d.ts.map +1 -0
- package/dist/variable-snapshot.js +9 -0
- package/dist/variable-snapshot.js.map +1 -0
- package/package.json +1 -1
- package/src/adaptive-turn-materializer.ts +29 -16
- package/src/adaptive-turn.ts +29 -6
- package/src/delivery-preparation.ts +121 -0
- package/src/delivery-redaction.ts +36 -5
- package/src/delivery-security.ts +2 -2
- package/src/index.ts +65 -0
- package/src/item-scoring-expectation.ts +9 -0
- package/src/item-submission-materialization.ts +120 -0
- package/src/qti-package-assets.ts +256 -0
- package/src/qti-package-items.ts +240 -0
- package/src/qti-package-manifest.ts +210 -0
- package/src/qti-package-metadata.ts +143 -0
- package/src/qti-package-paths.ts +56 -0
- package/src/qti-package-types.ts +117 -0
- package/src/qti-package-xml.ts +97 -0
- package/src/qti-package-zip.ts +325 -0
- package/src/qti-package.ts +176 -0
- package/src/response-input.ts +19 -0
- package/src/response-validation-policy.ts +218 -0
- package/src/response-validation.ts +282 -0
- package/src/scoring-disposition-policy.ts +37 -0
- package/src/template-presentation-materializer.ts +219 -0
- package/src/trusted-item-session.ts +80 -74
- package/src/trusted-session-materialization.ts +67 -0
- package/src/validation-primitives.ts +5 -0
- package/src/variable-snapshot.ts +20 -0
package/src/index.ts
CHANGED
|
@@ -21,6 +21,20 @@ export {
|
|
|
21
21
|
type QtiResolvedPhysicalCompanionMaterial,
|
|
22
22
|
} from "./companion-materials-resolution.js";
|
|
23
23
|
export { isResolvableAssetUrl } from "./asset-url.js";
|
|
24
|
+
export {
|
|
25
|
+
materializeQtiItemSubmission,
|
|
26
|
+
type QtiItemSubmissionMaterializationInput,
|
|
27
|
+
type QtiItemSubmissionMaterializationResult,
|
|
28
|
+
type QtiItemSubmissionResponseInput,
|
|
29
|
+
type QtiItemSubmissionScoringDisposition,
|
|
30
|
+
type QtiVariableSnapshot,
|
|
31
|
+
} from "./item-submission-materialization.js";
|
|
32
|
+
export {
|
|
33
|
+
prepareQtiDeliveryXml,
|
|
34
|
+
type QtiDeliveryPreparationMode,
|
|
35
|
+
type QtiDeliveryPreparationOptions,
|
|
36
|
+
type QtiDeliveryPreparationResult,
|
|
37
|
+
} from "./delivery-preparation.js";
|
|
24
38
|
export {
|
|
25
39
|
analyzeQtiDeliverySecurity,
|
|
26
40
|
buildQtiDeliverySafeXml,
|
|
@@ -35,6 +49,25 @@ export {
|
|
|
35
49
|
type QtiPackageXmlNode,
|
|
36
50
|
type QtiPackageXmlTree,
|
|
37
51
|
} from "./package-xml.js";
|
|
52
|
+
export {
|
|
53
|
+
parseQtiPackage,
|
|
54
|
+
type QtiAssessmentTestItemRef,
|
|
55
|
+
type QtiAssessmentTestPackageModel,
|
|
56
|
+
type QtiManifestFile,
|
|
57
|
+
type QtiManifestResource,
|
|
58
|
+
type QtiPackageAsset,
|
|
59
|
+
type QtiPackageAssetSource,
|
|
60
|
+
type QtiPackageInflateContext,
|
|
61
|
+
type QtiPackageInflateRaw,
|
|
62
|
+
type QtiPackageItem,
|
|
63
|
+
type QtiPackageItemSource,
|
|
64
|
+
type QtiPackageParseOptions,
|
|
65
|
+
type QtiPackageParseResult,
|
|
66
|
+
type QtiPackageShape,
|
|
67
|
+
type QtiStandardAlignment,
|
|
68
|
+
type QtiTimingMetadata,
|
|
69
|
+
} from "./qti-package.js";
|
|
70
|
+
export { detectPackageMediaType } from "./qti-package-assets.js";
|
|
38
71
|
export { parseQtiXml } from "./parser.js";
|
|
39
72
|
export { numericTuple3, numericTuple4 } from "./parser-values.js";
|
|
40
73
|
export { isConformanceParseDiagnostic } from "./parse-diagnostics.js";
|
|
@@ -44,6 +77,38 @@ export {
|
|
|
44
77
|
type QtiServerScoringResponseInput,
|
|
45
78
|
type QtiServerScoringResult,
|
|
46
79
|
} from "./server-scoring.js";
|
|
80
|
+
export {
|
|
81
|
+
associationMaximumResponses,
|
|
82
|
+
choiceMatchLimitExceeded,
|
|
83
|
+
choiceMatchMaximum,
|
|
84
|
+
directedPairChoiceUseCount,
|
|
85
|
+
matchMaxDiagnostics,
|
|
86
|
+
maximumAllowedResponses,
|
|
87
|
+
maximumMediaPlays,
|
|
88
|
+
maximumResponseDiagnostic,
|
|
89
|
+
mediaPlayCount,
|
|
90
|
+
minimumMediaPlays,
|
|
91
|
+
minimumRequiredResponses,
|
|
92
|
+
orderSubsetLimitsActive,
|
|
93
|
+
parseUnlimitedMaximum,
|
|
94
|
+
requiredResponseDiagnostic,
|
|
95
|
+
responseCount,
|
|
96
|
+
responseIsEmpty,
|
|
97
|
+
responseLimitAttribute,
|
|
98
|
+
responseValidationPolicy,
|
|
99
|
+
wouldExceedChoiceMatchMaximum,
|
|
100
|
+
type DirectedPairUseSide,
|
|
101
|
+
type QtiResponseValidationPolicy,
|
|
102
|
+
} from "./response-validation-policy.js";
|
|
103
|
+
export {
|
|
104
|
+
validateQtiResponseVariables,
|
|
105
|
+
type QtiResponseValidationDiagnostic,
|
|
106
|
+
type QtiResponseValidationDiagnosticCode,
|
|
107
|
+
type QtiResponseValidationInput,
|
|
108
|
+
type QtiResponseValidationResult,
|
|
109
|
+
type QtiResponseVariableInput,
|
|
110
|
+
type QtiResponseVariablesInput,
|
|
111
|
+
} from "./response-validation.js";
|
|
47
112
|
export {
|
|
48
113
|
serializeResponseProcessing,
|
|
49
114
|
type QtiSerializeResponseProcessingResult,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { QtiAssessmentItem } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Items that declare automated scoring via response processing or a SCORE outcome. */
|
|
4
|
+
export function itemExpectsAutomatedScore(item: QtiAssessmentItem): boolean {
|
|
5
|
+
return (
|
|
6
|
+
Boolean(item.responseProcessing) ||
|
|
7
|
+
item.outcomeDeclarations.some((declaration) => declaration.identifier === "SCORE")
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { runTrustedAdaptiveItemTurn } from "./adaptive-turn.js";
|
|
2
|
+
import type { QtiItemSubmissionScoringDisposition } from "./scoring-disposition-policy.js";
|
|
3
|
+
import {
|
|
4
|
+
buildItemSubmissionMaterializationFailure,
|
|
5
|
+
buildItemSubmissionMaterializationSuccess,
|
|
6
|
+
} from "./trusted-session-materialization.js";
|
|
7
|
+
import {
|
|
8
|
+
parseTrustedItemDocument,
|
|
9
|
+
readPriorAttemptState,
|
|
10
|
+
runTrustedItemSession,
|
|
11
|
+
type QtiTrustedItemDocument,
|
|
12
|
+
type QtiTrustedItemParseOptions,
|
|
13
|
+
type QtiTrustedResponseInput,
|
|
14
|
+
type QtiTrustedResponseApplication,
|
|
15
|
+
} from "./trusted-item-session.js";
|
|
16
|
+
import type { QtiAttemptStateV1, QtiDiagnostic } from "./types.js";
|
|
17
|
+
import type { QtiVariableSnapshot } from "./variable-snapshot.js";
|
|
18
|
+
|
|
19
|
+
export type QtiItemSubmissionResponseInput = QtiTrustedResponseInput;
|
|
20
|
+
export type { QtiItemSubmissionScoringDisposition } from "./scoring-disposition-policy.js";
|
|
21
|
+
export type { QtiVariableSnapshot } from "./variable-snapshot.js";
|
|
22
|
+
|
|
23
|
+
export interface QtiItemSubmissionMaterializationInput
|
|
24
|
+
extends QtiTrustedItemParseOptions, QtiTrustedResponseApplication {
|
|
25
|
+
itemXml: string;
|
|
26
|
+
existingState?: unknown;
|
|
27
|
+
allowIncompleteResponses?: boolean | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface QtiItemSubmissionMaterializationResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
diagnostics: QtiDiagnostic[];
|
|
33
|
+
state?: QtiAttemptStateV1 | undefined;
|
|
34
|
+
responseVariables: QtiVariableSnapshot[];
|
|
35
|
+
outcomeVariables: QtiVariableSnapshot[];
|
|
36
|
+
score: number | null;
|
|
37
|
+
scoringDisposition: QtiItemSubmissionScoringDisposition;
|
|
38
|
+
candidateSafeXml?: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Materialize trusted submitted responses into QTI attempt state and variable snapshots. */
|
|
42
|
+
export function materializeQtiItemSubmission(
|
|
43
|
+
input: QtiItemSubmissionMaterializationInput,
|
|
44
|
+
): QtiItemSubmissionMaterializationResult {
|
|
45
|
+
const parsedResult = parseTrustedItemDocument(
|
|
46
|
+
input.itemXml,
|
|
47
|
+
input.allowedUndeclaredResponseIdentifiers,
|
|
48
|
+
);
|
|
49
|
+
if (!parsedResult.ok) {
|
|
50
|
+
return buildItemSubmissionMaterializationFailure(parsedResult.diagnostics);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const item = parsedResult.parsed.document.item;
|
|
54
|
+
if (item.adaptive) {
|
|
55
|
+
return materializeAdaptiveSubmission(input, parsedResult.parsed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const priorState = readPriorAttemptState(input.existingState, "itemSubmission");
|
|
59
|
+
if (priorState.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
60
|
+
return buildItemSubmissionMaterializationFailure(priorState.diagnostics);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = runTrustedItemSession({
|
|
64
|
+
itemXml: input.itemXml,
|
|
65
|
+
parsedItem: parsedResult.parsed,
|
|
66
|
+
diagnosticPrefix: "itemSubmission",
|
|
67
|
+
allowedUndeclaredResponseIdentifiers: input.allowedUndeclaredResponseIdentifiers,
|
|
68
|
+
priorState: priorState.state,
|
|
69
|
+
submission: {
|
|
70
|
+
trustedResponses: input.trustedResponses,
|
|
71
|
+
trustedInteractionStates: input.trustedInteractionStates,
|
|
72
|
+
},
|
|
73
|
+
scoring: "always",
|
|
74
|
+
requireNumericScore: false,
|
|
75
|
+
submissionValidation: "strict",
|
|
76
|
+
allowIncompleteResponses: input.allowIncompleteResponses,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
return buildItemSubmissionMaterializationFailure(result.diagnostics, result.state ?? undefined);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return buildItemSubmissionMaterializationSuccess(item, {
|
|
84
|
+
diagnostics: result.diagnostics,
|
|
85
|
+
state: result.state,
|
|
86
|
+
responses: result.responses,
|
|
87
|
+
outcomes: result.outcomes,
|
|
88
|
+
score: result.score,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function materializeAdaptiveSubmission(
|
|
93
|
+
input: QtiItemSubmissionMaterializationInput,
|
|
94
|
+
parsedItem: QtiTrustedItemDocument,
|
|
95
|
+
): QtiItemSubmissionMaterializationResult {
|
|
96
|
+
const result = runTrustedAdaptiveItemTurn({
|
|
97
|
+
itemXml: input.itemXml,
|
|
98
|
+
parsedItem,
|
|
99
|
+
diagnosticPrefix: "itemSubmission",
|
|
100
|
+
allowedUndeclaredResponseIdentifiers: input.allowedUndeclaredResponseIdentifiers,
|
|
101
|
+
priorState: input.existingState,
|
|
102
|
+
trustedResponses: input.trustedResponses,
|
|
103
|
+
trustedInteractionStates: input.trustedInteractionStates,
|
|
104
|
+
submissionValidation: "strict",
|
|
105
|
+
allowIncompleteResponses: input.allowIncompleteResponses,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!result.ok || !result.state) {
|
|
109
|
+
return buildItemSubmissionMaterializationFailure(result.diagnostics, result.state ?? undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return buildItemSubmissionMaterializationSuccess(parsedItem.document.item, {
|
|
113
|
+
diagnostics: result.diagnostics,
|
|
114
|
+
state: result.state,
|
|
115
|
+
responses: result.responses,
|
|
116
|
+
outcomes: result.outcomes,
|
|
117
|
+
score: result.score,
|
|
118
|
+
candidateSafeXml: result.candidateSafeXml,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { pushPackageDiagnostic } from "./qti-package-paths.js";
|
|
2
|
+
import type {
|
|
3
|
+
QtiAssessmentTestPackageModel,
|
|
4
|
+
QtiManifestResource,
|
|
5
|
+
QtiPackageAsset,
|
|
6
|
+
QtiPackageAssetSource,
|
|
7
|
+
QtiPackageItem,
|
|
8
|
+
} from "./qti-package-types.js";
|
|
9
|
+
import {
|
|
10
|
+
isPackageRelativeHref,
|
|
11
|
+
packageDescendants,
|
|
12
|
+
resolvePackageHref,
|
|
13
|
+
type PackageXmlFile,
|
|
14
|
+
} from "./qti-package-xml.js";
|
|
15
|
+
import type { QtiPackageEntry } from "./qti-package-zip.js";
|
|
16
|
+
import type { QtiDiagnostic } from "./types.js";
|
|
17
|
+
|
|
18
|
+
interface PendingAssetReference {
|
|
19
|
+
readonly href: string;
|
|
20
|
+
readonly source: QtiPackageAssetSource;
|
|
21
|
+
readonly referencedBy: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type PackageAssetRefSpec =
|
|
25
|
+
| { readonly kind: "attribute"; readonly localName: string; readonly attribute: string }
|
|
26
|
+
| { readonly kind: "text"; readonly localName: string };
|
|
27
|
+
|
|
28
|
+
const PACKAGE_ASSET_REF_SPECS: readonly PackageAssetRefSpec[] = [
|
|
29
|
+
{ kind: "attribute", localName: "qti-stylesheet", attribute: "href" },
|
|
30
|
+
{ kind: "attribute", localName: "qti-assessment-stimulus-ref", attribute: "href" },
|
|
31
|
+
{ kind: "attribute", localName: "img", attribute: "src" },
|
|
32
|
+
{ kind: "attribute", localName: "object", attribute: "data" },
|
|
33
|
+
{ kind: "attribute", localName: "audio", attribute: "src" },
|
|
34
|
+
{ kind: "attribute", localName: "video", attribute: "src" },
|
|
35
|
+
{ kind: "attribute", localName: "source", attribute: "src" },
|
|
36
|
+
{ kind: "attribute", localName: "track", attribute: "src" },
|
|
37
|
+
{ kind: "text", localName: "qti-file-href" },
|
|
38
|
+
{ kind: "text", localName: "qti-resource-icon" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function discoverContentAssetHrefs(
|
|
42
|
+
xmlFile: PackageXmlFile,
|
|
43
|
+
diagnostics: QtiDiagnostic[],
|
|
44
|
+
): string[] {
|
|
45
|
+
if (!xmlFile.root) return [];
|
|
46
|
+
|
|
47
|
+
const refs: string[] = [];
|
|
48
|
+
for (const spec of PACKAGE_ASSET_REF_SPECS) {
|
|
49
|
+
if (spec.kind === "attribute") {
|
|
50
|
+
collectPackageRelativeAttributeRefs(
|
|
51
|
+
refs,
|
|
52
|
+
xmlFile,
|
|
53
|
+
spec.localName,
|
|
54
|
+
spec.attribute,
|
|
55
|
+
diagnostics,
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
collectPackageRelativeTextRefs(refs, xmlFile, spec.localName, diagnostics);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...new Set(refs)];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function collectPackageAssets(
|
|
65
|
+
resourcesByIdentifier: ReadonlyMap<string, QtiManifestResource>,
|
|
66
|
+
assessmentTest: QtiAssessmentTestPackageModel | undefined,
|
|
67
|
+
items: readonly QtiPackageItem[],
|
|
68
|
+
entriesByPath: ReadonlyMap<string, QtiPackageEntry>,
|
|
69
|
+
diagnostics: QtiDiagnostic[],
|
|
70
|
+
): QtiPackageAsset[] {
|
|
71
|
+
const pending: PendingAssetReference[] = [];
|
|
72
|
+
const selectedResourceIds = new Set<string>();
|
|
73
|
+
if (assessmentTest?.manifestResourceIdentifier !== undefined) {
|
|
74
|
+
selectedResourceIds.add(assessmentTest.manifestResourceIdentifier);
|
|
75
|
+
}
|
|
76
|
+
for (const item of items) {
|
|
77
|
+
if (item.manifestResourceIdentifier !== undefined) {
|
|
78
|
+
selectedResourceIds.add(item.manifestResourceIdentifier);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const resourceId of selectedResourceIds) {
|
|
83
|
+
for (const resource of resourceClosure(resourceId, resourcesByIdentifier)) {
|
|
84
|
+
for (const file of resource.files) {
|
|
85
|
+
if (isPrimaryPackageXml(file.href, assessmentTest, items)) continue;
|
|
86
|
+
pending.push({
|
|
87
|
+
href: file.href,
|
|
88
|
+
source: "manifest-resource",
|
|
89
|
+
referencedBy: resource.identifier,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (assessmentTest) {
|
|
96
|
+
for (const itemRef of assessmentTest.itemRefs) {
|
|
97
|
+
if (!entriesByPath.has(itemRef.href)) {
|
|
98
|
+
pushPackageDiagnostic(
|
|
99
|
+
diagnostics,
|
|
100
|
+
"package.reference.missing",
|
|
101
|
+
"error",
|
|
102
|
+
`Assessment test item reference ${itemRef.href} was not found in the package.`,
|
|
103
|
+
itemRef.href,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const href of assessmentTest.assetHrefs) {
|
|
108
|
+
pending.push({ href, source: "assessment-test-content", referencedBy: assessmentTest.href });
|
|
109
|
+
if (!entriesByPath.has(href)) {
|
|
110
|
+
pushPackageDiagnostic(
|
|
111
|
+
diagnostics,
|
|
112
|
+
"package.asset.missing",
|
|
113
|
+
"error",
|
|
114
|
+
`Package asset ${href} referenced from ${assessmentTest.href} was not found.`,
|
|
115
|
+
href,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const item of items) {
|
|
122
|
+
for (const href of item.assetHrefs) {
|
|
123
|
+
pending.push({ href, source: "item-content", referencedBy: item.href });
|
|
124
|
+
if (!entriesByPath.has(href)) {
|
|
125
|
+
pushPackageDiagnostic(
|
|
126
|
+
diagnostics,
|
|
127
|
+
"package.asset.missing",
|
|
128
|
+
"error",
|
|
129
|
+
`Package asset ${href} referenced from ${item.href} was not found.`,
|
|
130
|
+
href,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return materializeAssets(pending, entriesByPath);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resourceClosure(
|
|
140
|
+
rootIdentifier: string,
|
|
141
|
+
resourcesByIdentifier: ReadonlyMap<string, QtiManifestResource>,
|
|
142
|
+
): QtiManifestResource[] {
|
|
143
|
+
const closed: QtiManifestResource[] = [];
|
|
144
|
+
const visited = new Set<string>();
|
|
145
|
+
const pending = [rootIdentifier];
|
|
146
|
+
|
|
147
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
148
|
+
const identifier = pending[index];
|
|
149
|
+
if (!identifier || visited.has(identifier)) continue;
|
|
150
|
+
visited.add(identifier);
|
|
151
|
+
const resource = resourcesByIdentifier.get(identifier);
|
|
152
|
+
if (!resource) continue;
|
|
153
|
+
closed.push(resource);
|
|
154
|
+
pending.push(...resource.dependencies);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return closed;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isPrimaryPackageXml(
|
|
161
|
+
href: string,
|
|
162
|
+
assessmentTest: QtiAssessmentTestPackageModel | undefined,
|
|
163
|
+
items: readonly { readonly href: string }[],
|
|
164
|
+
): boolean {
|
|
165
|
+
if (assessmentTest?.href === href) return true;
|
|
166
|
+
return items.some((item) => item.href === href);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function collectPackageRelativeAttributeRefs(
|
|
170
|
+
refs: string[],
|
|
171
|
+
xmlFile: PackageXmlFile,
|
|
172
|
+
localName: string,
|
|
173
|
+
attribute: string,
|
|
174
|
+
diagnostics: QtiDiagnostic[],
|
|
175
|
+
): void {
|
|
176
|
+
for (const node of packageDescendants(xmlFile.root, localName)) {
|
|
177
|
+
const href = node.attributes[attribute];
|
|
178
|
+
if (!isPackageRelativeHref(href)) continue;
|
|
179
|
+
const resolved = resolvePackageHref(xmlFile.path, href.trim(), diagnostics);
|
|
180
|
+
if (resolved) refs.push(resolved);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function collectPackageRelativeTextRefs(
|
|
185
|
+
refs: string[],
|
|
186
|
+
xmlFile: PackageXmlFile,
|
|
187
|
+
localName: string,
|
|
188
|
+
diagnostics: QtiDiagnostic[],
|
|
189
|
+
): void {
|
|
190
|
+
for (const node of packageDescendants(xmlFile.root, localName)) {
|
|
191
|
+
const href = node.text.trim();
|
|
192
|
+
if (!isPackageRelativeHref(href)) continue;
|
|
193
|
+
const resolved = resolvePackageHref(xmlFile.path, href, diagnostics);
|
|
194
|
+
if (resolved) refs.push(resolved);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function materializeAssets(
|
|
199
|
+
pending: readonly PendingAssetReference[],
|
|
200
|
+
entriesByPath: ReadonlyMap<string, QtiPackageEntry>,
|
|
201
|
+
): QtiPackageAsset[] {
|
|
202
|
+
const assetsByHref = new Map<string, PendingAssetReference[]>();
|
|
203
|
+
for (const reference of pending) {
|
|
204
|
+
if (!entriesByPath.has(reference.href)) continue;
|
|
205
|
+
const references = assetsByHref.get(reference.href) ?? [];
|
|
206
|
+
references.push(reference);
|
|
207
|
+
assetsByHref.set(reference.href, references);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return [...assetsByHref.entries()]
|
|
211
|
+
.map(([href, references]): QtiPackageAsset => {
|
|
212
|
+
const firstReference = references[0];
|
|
213
|
+
return {
|
|
214
|
+
href,
|
|
215
|
+
mediaType: detectPackageMediaType(href),
|
|
216
|
+
source: firstReference?.source ?? "item-content",
|
|
217
|
+
referencedBy: [...new Set(references.map((reference) => reference.referencedBy))],
|
|
218
|
+
};
|
|
219
|
+
})
|
|
220
|
+
.toSorted((left, right) => left.href.localeCompare(right.href));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function detectPackageMediaType(href: string): string | undefined {
|
|
224
|
+
const extension = href.slice(href.lastIndexOf(".") + 1).toLowerCase();
|
|
225
|
+
switch (extension) {
|
|
226
|
+
case "css":
|
|
227
|
+
return "text/css";
|
|
228
|
+
case "gif":
|
|
229
|
+
return "image/gif";
|
|
230
|
+
case "jpg":
|
|
231
|
+
case "jpeg":
|
|
232
|
+
return "image/jpeg";
|
|
233
|
+
case "mp3":
|
|
234
|
+
return "audio/mpeg";
|
|
235
|
+
case "mp4":
|
|
236
|
+
return "video/mp4";
|
|
237
|
+
case "ogg":
|
|
238
|
+
return "audio/ogg";
|
|
239
|
+
case "png":
|
|
240
|
+
return "image/png";
|
|
241
|
+
case "svg":
|
|
242
|
+
return "image/svg+xml";
|
|
243
|
+
case "vtt":
|
|
244
|
+
return "text/vtt";
|
|
245
|
+
case "wav":
|
|
246
|
+
return "audio/wav";
|
|
247
|
+
case "webm":
|
|
248
|
+
return "video/webm";
|
|
249
|
+
case "webp":
|
|
250
|
+
return "image/webp";
|
|
251
|
+
case "xml":
|
|
252
|
+
return "application/xml";
|
|
253
|
+
default:
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
}
|