@longsightgroup/qti3-core 0.10.6 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -0
- package/dist/adaptive-turn-materializer.js +4 -5
- package/dist/adaptive-turn-materializer.js.map +1 -1
- package/dist/adaptive-turn.d.ts.map +1 -1
- package/dist/adaptive-turn.js +1 -0
- package/dist/adaptive-turn.js.map +1 -1
- package/dist/attempt-state.d.ts.map +1 -1
- package/dist/attempt-state.js +41 -1
- package/dist/attempt-state.js.map +1 -1
- package/dist/binary-choice.d.ts +9 -0
- package/dist/binary-choice.d.ts.map +1 -0
- package/dist/binary-choice.js +81 -0
- package/dist/binary-choice.js.map +1 -0
- package/dist/diagnostics.d.ts +13 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +28 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/item-submission-materialization.d.ts.map +1 -1
- package/dist/item-submission-materialization.js +2 -0
- package/dist/item-submission-materialization.js.map +1 -1
- package/dist/parser-processing.d.ts.map +1 -1
- package/dist/parser-processing.js +8 -18
- package/dist/parser-processing.js.map +1 -1
- package/dist/parser-values.d.ts.map +1 -1
- package/dist/parser-values.js +1 -3
- package/dist/parser-values.js.map +1 -1
- package/dist/parser.js +32 -0
- package/dist/parser.js.map +1 -1
- package/dist/processing-equality.d.ts +10 -0
- package/dist/processing-equality.d.ts.map +1 -0
- package/dist/processing-equality.js +78 -0
- package/dist/processing-equality.js.map +1 -0
- package/dist/processing-evaluator-comparison.d.ts.map +1 -1
- package/dist/processing-evaluator-comparison.js +14 -9
- package/dist/processing-evaluator-comparison.js.map +1 -1
- package/dist/processing-evaluator-numeric.d.ts.map +1 -1
- package/dist/processing-evaluator-numeric.js +2 -1
- package/dist/processing-evaluator-numeric.js.map +1 -1
- package/dist/processing-evaluator-string.d.ts.map +1 -1
- package/dist/processing-evaluator-string.js +26 -10
- package/dist/processing-evaluator-string.js.map +1 -1
- package/dist/processing-evaluator-variable.d.ts.map +1 -1
- package/dist/processing-evaluator-variable.js +9 -3
- package/dist/processing-evaluator-variable.js.map +1 -1
- package/dist/processing-evaluator.d.ts +5 -1
- package/dist/processing-evaluator.d.ts.map +1 -1
- package/dist/processing-evaluator.js +13 -4
- package/dist/processing-evaluator.js.map +1 -1
- package/dist/processing-expression-children.d.ts.map +1 -1
- package/dist/processing-expression-children.js +2 -3
- package/dist/processing-expression-children.js.map +1 -1
- package/dist/processing-expression-tags.d.ts +2 -1
- package/dist/processing-expression-tags.d.ts.map +1 -1
- package/dist/processing-expression-tags.js +2 -1
- package/dist/processing-expression-tags.js.map +1 -1
- package/dist/processing-mapping.d.ts.map +1 -1
- package/dist/processing-mapping.js +26 -16
- package/dist/processing-mapping.js.map +1 -1
- package/dist/processing-state.d.ts +2 -2
- package/dist/processing-state.d.ts.map +1 -1
- package/dist/processing-state.js +5 -2
- package/dist/processing-state.js.map +1 -1
- package/dist/processing-values.d.ts +1 -2
- package/dist/processing-values.d.ts.map +1 -1
- package/dist/processing-values.js +1 -3
- package/dist/processing-values.js.map +1 -1
- package/dist/processing-variables.d.ts.map +1 -1
- package/dist/processing-variables.js +10 -5
- package/dist/processing-variables.js.map +1 -1
- package/dist/qti-identifier.d.ts +3 -0
- package/dist/qti-identifier.d.ts.map +1 -0
- package/dist/qti-identifier.js +5 -0
- package/dist/qti-identifier.js.map +1 -0
- package/dist/qti-package-assets.d.ts.map +1 -1
- package/dist/qti-package-assets.js +16 -10
- package/dist/qti-package-assets.js.map +1 -1
- package/dist/qti-package-manifest.d.ts +1 -0
- package/dist/qti-package-manifest.d.ts.map +1 -1
- package/dist/qti-package-manifest.js +1 -0
- package/dist/qti-package-manifest.js.map +1 -1
- package/dist/qti-package-metadata.d.ts.map +1 -1
- package/dist/qti-package-metadata.js +11 -0
- package/dist/qti-package-metadata.js.map +1 -1
- package/dist/qti-package-paths.d.ts +2 -0
- package/dist/qti-package-paths.d.ts.map +1 -1
- package/dist/qti-package-paths.js +6 -0
- package/dist/qti-package-paths.js.map +1 -1
- package/dist/qti-package-plan.d.ts +1 -1
- package/dist/qti-package-plan.d.ts.map +1 -1
- package/dist/qti-package-types.d.ts +9 -0
- package/dist/qti-package-types.d.ts.map +1 -1
- package/dist/qti-package-xml.d.ts +6 -1
- package/dist/qti-package-xml.d.ts.map +1 -1
- package/dist/qti-package-xml.js +11 -6
- package/dist/qti-package-xml.js.map +1 -1
- package/dist/qti-package-zip.d.ts +13 -1
- package/dist/qti-package-zip.d.ts.map +1 -1
- package/dist/qti-package-zip.js +185 -54
- package/dist/qti-package-zip.js.map +1 -1
- package/dist/qti-package.d.ts +12 -5
- package/dist/qti-package.d.ts.map +1 -1
- package/dist/qti-package.js +63 -8
- package/dist/qti-package.js.map +1 -1
- package/dist/response-validation-policy.d.ts.map +1 -1
- package/dist/response-validation-policy.js +3 -1
- package/dist/response-validation-policy.js.map +1 -1
- package/dist/response-validation.d.ts.map +1 -1
- package/dist/response-validation.js +2 -1
- package/dist/response-validation.js.map +1 -1
- package/dist/serializer-processing-expressions.d.ts.map +1 -1
- package/dist/serializer-processing-expressions.js +2 -10
- package/dist/serializer-processing-expressions.js.map +1 -1
- package/dist/server-scoring.d.ts.map +1 -1
- package/dist/server-scoring.js +1 -0
- package/dist/server-scoring.js.map +1 -1
- package/dist/session-builtins.d.ts +16 -0
- package/dist/session-builtins.d.ts.map +1 -0
- package/dist/session-builtins.js +58 -0
- package/dist/session-builtins.js.map +1 -0
- package/dist/session.d.ts +5 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +63 -11
- package/dist/session.js.map +1 -1
- package/dist/support-evidence.d.ts.map +1 -1
- package/dist/support-evidence.js +2 -0
- package/dist/support-evidence.js.map +1 -1
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +66 -23
- package/dist/support.js.map +1 -1
- package/dist/test-expression-parser.d.ts +6 -0
- package/dist/test-expression-parser.d.ts.map +1 -0
- package/dist/test-expression-parser.js +76 -0
- package/dist/test-expression-parser.js.map +1 -0
- package/dist/test-expression-serializer.d.ts +5 -0
- package/dist/test-expression-serializer.d.ts.map +1 -0
- package/dist/test-expression-serializer.js +54 -0
- package/dist/test-expression-serializer.js.map +1 -0
- package/dist/test-expression-validation.d.ts +14 -0
- package/dist/test-expression-validation.d.ts.map +1 -0
- package/dist/test-expression-validation.js +101 -0
- package/dist/test-expression-validation.js.map +1 -0
- package/dist/test-expression.d.ts +99 -0
- package/dist/test-expression.d.ts.map +1 -0
- package/dist/test-expression.js +28 -0
- package/dist/test-expression.js.map +1 -0
- package/dist/test-model.d.ts +62 -0
- package/dist/test-model.d.ts.map +1 -0
- package/dist/test-model.js +5 -0
- package/dist/test-model.js.map +1 -0
- package/dist/test-parser.d.ts +13 -0
- package/dist/test-parser.d.ts.map +1 -0
- package/dist/test-parser.js +144 -0
- package/dist/test-parser.js.map +1 -0
- package/dist/test-processing.d.ts +22 -0
- package/dist/test-processing.d.ts.map +1 -0
- package/dist/test-processing.js +84 -0
- package/dist/test-processing.js.map +1 -0
- package/dist/test-session.d.ts +31 -0
- package/dist/test-session.d.ts.map +1 -0
- package/dist/test-session.js +102 -0
- package/dist/test-session.js.map +1 -0
- package/dist/test-support.d.ts +4 -0
- package/dist/test-support.d.ts.map +1 -0
- package/dist/test-support.js +30 -0
- package/dist/test-support.js.map +1 -0
- package/dist/test-validation.d.ts +4 -0
- package/dist/test-validation.d.ts.map +1 -0
- package/dist/test-validation.js +133 -0
- package/dist/test-validation.js.map +1 -0
- package/dist/test-xml.d.ts +7 -0
- package/dist/test-xml.d.ts.map +1 -0
- package/dist/test-xml.js +23 -0
- package/dist/test-xml.js.map +1 -0
- package/dist/trusted-item-session.d.ts +4 -1
- package/dist/trusted-item-session.d.ts.map +1 -1
- package/dist/trusted-item-session.js +8 -3
- package/dist/trusted-item-session.js.map +1 -1
- package/dist/types.d.ts +26 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/validation-declaration-entries.d.ts.map +1 -1
- package/dist/validation-declaration-entries.js +11 -1
- package/dist/validation-declaration-entries.js.map +1 -1
- package/dist/validation-item-children.d.ts +4 -0
- package/dist/validation-item-children.d.ts.map +1 -0
- package/dist/validation-item-children.js +45 -0
- package/dist/validation-item-children.js.map +1 -0
- package/dist/validation-processing.d.ts.map +1 -1
- package/dist/validation-processing.js +45 -12
- package/dist/validation-processing.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +12 -0
- package/dist/validation.js.map +1 -1
- package/dist/value-guards.d.ts +4 -0
- package/dist/value-guards.d.ts.map +1 -0
- package/dist/value-guards.js +5 -0
- package/dist/value-guards.js.map +1 -0
- package/dist/xml-schema-regex-data.d.ts +48 -0
- package/dist/xml-schema-regex-data.d.ts.map +1 -0
- package/dist/xml-schema-regex-data.js +403 -0
- package/dist/xml-schema-regex-data.js.map +1 -0
- package/dist/xml-schema-regex-parser.d.ts +31 -0
- package/dist/xml-schema-regex-parser.d.ts.map +1 -0
- package/dist/xml-schema-regex-parser.js +245 -0
- package/dist/xml-schema-regex-parser.js.map +1 -0
- package/dist/xml-schema-regex.d.ts +14 -0
- package/dist/xml-schema-regex.d.ts.map +1 -0
- package/dist/xml-schema-regex.js +114 -0
- package/dist/xml-schema-regex.js.map +1 -0
- package/package.json +1 -1
- package/src/adaptive-turn-materializer.ts +4 -3
- package/src/adaptive-turn.ts +1 -0
- package/src/attempt-state.ts +54 -1
- package/src/binary-choice.ts +96 -0
- package/src/diagnostics.ts +29 -0
- package/src/index.ts +38 -0
- package/src/item-submission-materialization.ts +2 -0
- package/src/parser-processing.ts +15 -18
- package/src/parser-values.ts +1 -5
- package/src/parser.ts +34 -0
- package/src/processing-equality.ts +104 -0
- package/src/processing-evaluator-comparison.ts +19 -14
- package/src/processing-evaluator-numeric.ts +2 -6
- package/src/processing-evaluator-string.ts +25 -18
- package/src/processing-evaluator-variable.ts +8 -3
- package/src/processing-evaluator.ts +20 -7
- package/src/processing-expression-children.ts +2 -3
- package/src/processing-expression-tags.ts +2 -1
- package/src/processing-mapping.ts +31 -25
- package/src/processing-state.ts +6 -1
- package/src/processing-values.ts +2 -11
- package/src/processing-variables.ts +9 -7
- package/src/qti-identifier.ts +4 -0
- package/src/qti-package-assets.ts +20 -13
- package/src/qti-package-manifest.ts +1 -0
- package/src/qti-package-metadata.ts +11 -0
- package/src/qti-package-paths.ts +9 -0
- package/src/qti-package-plan.ts +1 -1
- package/src/qti-package-types.ts +10 -0
- package/src/qti-package-xml.ts +11 -6
- package/src/qti-package-zip.ts +330 -97
- package/src/qti-package.ts +106 -7
- package/src/response-validation-policy.ts +5 -1
- package/src/response-validation.ts +2 -1
- package/src/serializer-processing-expressions.ts +8 -25
- package/src/server-scoring.ts +1 -0
- package/src/session-builtins.ts +75 -0
- package/src/session.ts +64 -8
- package/src/support-evidence.ts +2 -0
- package/src/support.ts +86 -23
- package/src/test-expression-parser.ts +89 -0
- package/src/test-expression-serializer.ts +71 -0
- package/src/test-expression-validation.ts +139 -0
- package/src/test-expression.ts +66 -0
- package/src/test-model.ts +68 -0
- package/src/test-parser.ts +201 -0
- package/src/test-processing.ts +107 -0
- package/src/test-session.ts +160 -0
- package/src/test-support.ts +31 -0
- package/src/test-validation.ts +190 -0
- package/src/test-xml.ts +37 -0
- package/src/trusted-item-session.ts +13 -2
- package/src/types.ts +36 -4
- package/src/validation-declaration-entries.ts +11 -1
- package/src/validation-item-children.ts +48 -0
- package/src/validation-processing.ts +47 -10
- package/src/validation.ts +14 -0
- package/src/value-guards.ts +6 -0
- package/src/xml-schema-regex-data.ts +404 -0
- package/src/xml-schema-regex-parser.ts +234 -0
- package/src/xml-schema-regex.ts +115 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { QtiDocument, QtiResponseDeclaration, QtiValue } from "./types.js";
|
|
2
|
-
import { parseQtiPair } from "./parser-values.js";
|
|
2
|
+
import { parseQtiPair, parseXmlBoolean } from "./parser-values.js";
|
|
3
3
|
import { qtiScalarToString, qtiValueToStringList } from "./value-format.js";
|
|
4
|
-
import {
|
|
4
|
+
import { numericValue, valuesEqual } from "./processing-values.js";
|
|
5
|
+
import { isRecordValue } from "./value-guards.js";
|
|
5
6
|
|
|
6
7
|
export function lookupOutcomeValue(
|
|
7
8
|
document: QtiDocument,
|
|
@@ -21,13 +22,11 @@ export function lookupOutcomeValue(
|
|
|
21
22
|
lookupTable.defaultValue
|
|
22
23
|
);
|
|
23
24
|
}
|
|
24
|
-
const entry =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(candidate)
|
|
28
|
-
|
|
29
|
-
(candidate.includeBoundary !== false && numeric === candidate.sourceValue),
|
|
30
|
-
);
|
|
25
|
+
const entry = lookupTable.entries.find(
|
|
26
|
+
(candidate) =>
|
|
27
|
+
numeric > candidate.sourceValue ||
|
|
28
|
+
(candidate.includeBoundary !== false && numeric === candidate.sourceValue),
|
|
29
|
+
);
|
|
31
30
|
return entry?.targetValue ?? lookupTable.defaultValue;
|
|
32
31
|
}
|
|
33
32
|
|
|
@@ -69,9 +68,12 @@ export function scoreAreaMapping(
|
|
|
69
68
|
for (const [index, entry] of areaMapping.entries.entries()) {
|
|
70
69
|
if (!pointInsideArea(parsed, entry)) continue;
|
|
71
70
|
matchedArea = true;
|
|
72
|
-
if (matchedAreaIndexes.has(index))
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
if (!matchedAreaIndexes.has(index)) {
|
|
72
|
+
matchedAreaIndexes.add(index);
|
|
73
|
+
score += entry.mappedValue;
|
|
74
|
+
}
|
|
75
|
+
// Authored priority applies even when this area was already counted.
|
|
76
|
+
break;
|
|
75
77
|
}
|
|
76
78
|
if (!matchedArea) score += areaMapping.defaultValue;
|
|
77
79
|
}
|
|
@@ -135,23 +137,27 @@ function scoreMapping(
|
|
|
135
137
|
mapping: NonNullable<QtiResponseDeclaration["mapping"]>,
|
|
136
138
|
baseType: QtiResponseDeclaration["baseType"],
|
|
137
139
|
): number {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
const mappedValue = (value: string): number => {
|
|
141
|
+
const key = mappingKey(value, baseType);
|
|
142
|
+
const entry = mapping.entries.find((candidate) => {
|
|
143
|
+
if (candidate.mapKey === undefined) return false;
|
|
144
|
+
const candidateKey = mappingKey(candidate.mapKey, baseType);
|
|
145
|
+
const caseSensitive = parseXmlBoolean(candidate.attributes["case-sensitive"]) ?? false;
|
|
146
|
+
return baseType === "string" && !caseSensitive
|
|
147
|
+
? candidateKey.toLowerCase() === key.toLowerCase()
|
|
148
|
+
: candidateKey === key;
|
|
149
|
+
});
|
|
150
|
+
return entry?.mappedValue ?? mapping.defaultValue;
|
|
151
|
+
};
|
|
143
152
|
if (Array.isArray(response)) {
|
|
144
|
-
const
|
|
145
|
-
(
|
|
146
|
-
|
|
147
|
-
0,
|
|
153
|
+
const distinct = response.filter(
|
|
154
|
+
(value, index) =>
|
|
155
|
+
response.findIndex((candidate) => valuesEqual(candidate, value, false, baseType)) === index,
|
|
148
156
|
);
|
|
157
|
+
const score = distinct.reduce<number>((sum, value) => sum + mappedValue(String(value)), 0);
|
|
149
158
|
return clampMappedScore(score, mapping.attributes);
|
|
150
159
|
}
|
|
151
|
-
const score =
|
|
152
|
-
response === null || isRecordValue(response)
|
|
153
|
-
? 0
|
|
154
|
-
: (values.get(mappingKey(String(response), baseType)) ?? mapping.defaultValue);
|
|
160
|
+
const score = response === null || isRecordValue(response) ? 0 : mappedValue(String(response));
|
|
155
161
|
return clampMappedScore(score, mapping.attributes);
|
|
156
162
|
}
|
|
157
163
|
|
package/src/processing-state.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
QtiAttemptStateV1,
|
|
3
|
+
QtiBuiltInVariables,
|
|
3
4
|
QtiAttemptStatus,
|
|
4
5
|
QtiDiagnostic,
|
|
5
6
|
QtiPortableCustomStateValue,
|
|
6
7
|
QtiValue,
|
|
7
8
|
} from "./types.js";
|
|
8
9
|
import { ATTEMPT_STATE_SCHEMA } from "./attempt-state-constants.js";
|
|
9
|
-
import { isRecordValue } from "./
|
|
10
|
+
import { isRecordValue } from "./value-guards.js";
|
|
10
11
|
|
|
11
12
|
export function serialize(
|
|
12
13
|
itemIdentifier: string,
|
|
@@ -16,9 +17,13 @@ export function serialize(
|
|
|
16
17
|
templateValues: Record<string, QtiValue>,
|
|
17
18
|
interactionStates: Record<string, QtiPortableCustomStateValue>,
|
|
18
19
|
validationMessages: QtiDiagnostic[],
|
|
20
|
+
builtInVariables?: QtiBuiltInVariables,
|
|
19
21
|
): QtiAttemptStateV1 {
|
|
20
22
|
return {
|
|
21
23
|
schema: ATTEMPT_STATE_SCHEMA,
|
|
24
|
+
...(builtInVariables === undefined
|
|
25
|
+
? {}
|
|
26
|
+
: { builtInVariables: { ...builtInVariables, context: { ...builtInVariables.context } } }),
|
|
22
27
|
itemIdentifier,
|
|
23
28
|
status,
|
|
24
29
|
responses: cloneValueRecord(responses),
|
package/src/processing-values.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { parseQtiPair } from "./parser-values.js";
|
|
2
|
-
import type {
|
|
3
|
-
QtiBaseType,
|
|
4
|
-
QtiRecordValue,
|
|
5
|
-
QtiResponseDeclaration,
|
|
6
|
-
QtiScalarValue,
|
|
7
|
-
QtiValue,
|
|
8
|
-
} from "./types.js";
|
|
2
|
+
import type { QtiBaseType, QtiResponseDeclaration, QtiScalarValue, QtiValue } from "./types.js";
|
|
9
3
|
import { qtiValueToString } from "./value-format.js";
|
|
4
|
+
import { isRecordValue } from "./value-guards.js";
|
|
10
5
|
|
|
11
6
|
export function isNullResponse(response: QtiValue): boolean {
|
|
12
7
|
return response === null || response === "" || (Array.isArray(response) && response.length === 0);
|
|
@@ -102,10 +97,6 @@ export function valueContainer(value: QtiValue): QtiScalarValue[] {
|
|
|
102
97
|
return Array.isArray(value) ? value : [value];
|
|
103
98
|
}
|
|
104
99
|
|
|
105
|
-
export function isRecordValue(value: QtiValue): value is QtiRecordValue {
|
|
106
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
100
|
export function containsValues(
|
|
110
101
|
collection: QtiScalarValue[],
|
|
111
102
|
values: QtiScalarValue[],
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
QtiTemplateDeclaration,
|
|
8
8
|
QtiValue,
|
|
9
9
|
} from "./types.js";
|
|
10
|
+
import { builtInVariableBaseType } from "./session-builtins.js";
|
|
10
11
|
import { parseBaseType } from "./parser-values.js";
|
|
11
12
|
|
|
12
13
|
export function getResponseDeclaration(
|
|
@@ -37,6 +38,7 @@ export function resolveOptionalVariableValue(
|
|
|
37
38
|
outcomes: Record<string, QtiValue>,
|
|
38
39
|
templateValues: Record<string, QtiValue>,
|
|
39
40
|
): QtiValue | undefined {
|
|
41
|
+
if (identifier === "completionStatus") return outcomes.completionStatus ?? "not_attempted";
|
|
40
42
|
const declaration = resolveVariableDeclaration(document, identifier);
|
|
41
43
|
if (!declaration) return undefined;
|
|
42
44
|
if (declaration.kind === "response") return responses[identifier] ?? null;
|
|
@@ -52,7 +54,7 @@ export function expressionIsOrdered(
|
|
|
52
54
|
expression: QtiProcessingExpression,
|
|
53
55
|
document: QtiDocument,
|
|
54
56
|
): boolean {
|
|
55
|
-
if (expression.type === "ordered") return true;
|
|
57
|
+
if (expression.type === "ordered" || expression.type === "repeat") return true;
|
|
56
58
|
if (expression.type === "delete") return expressionIsOrdered(expression.collection, document);
|
|
57
59
|
if (
|
|
58
60
|
(expression.type === "variable" ||
|
|
@@ -77,20 +79,20 @@ export function expressionBaseType(
|
|
|
77
79
|
expression.type === "correct" ||
|
|
78
80
|
expression.type === "default"
|
|
79
81
|
) {
|
|
80
|
-
return
|
|
82
|
+
return (
|
|
83
|
+
builtInVariableBaseType(expression.identifier) ??
|
|
84
|
+
resolveVariableDeclaration(document, expression.identifier)?.baseType
|
|
85
|
+
);
|
|
81
86
|
}
|
|
82
87
|
if (expression.type === "index") return expressionBaseType(expression.expression, document);
|
|
83
88
|
if (expression.type === "delete") return expressionBaseType(expression.collection, document);
|
|
89
|
+
if (expression.type === "random") return expressionBaseType(expression.expression, document);
|
|
84
90
|
if (
|
|
85
|
-
expression.type === "random" ||
|
|
86
91
|
expression.type === "multiple" ||
|
|
87
92
|
expression.type === "ordered" ||
|
|
88
93
|
expression.type === "repeat"
|
|
89
94
|
) {
|
|
90
|
-
return commonExpressionBaseType(
|
|
91
|
-
expression.type === "random" ? expression.values : expression.expressions,
|
|
92
|
-
document,
|
|
93
|
-
);
|
|
95
|
+
return commonExpressionBaseType(expression.expressions, document);
|
|
94
96
|
}
|
|
95
97
|
return undefined;
|
|
96
98
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isResolvableAssetUrl } from "./asset-url.js";
|
|
2
|
+
import { normalizePackagePath, pushPackageDiagnostic } from "./qti-package-paths.js";
|
|
2
3
|
import { parseQtiPackageXmlTree } from "./package-xml.js";
|
|
3
4
|
import type {
|
|
4
5
|
QtiAssessmentTestPackageModel,
|
|
@@ -8,12 +9,7 @@ import type {
|
|
|
8
9
|
QtiPackageContentAssetDiscovery,
|
|
9
10
|
QtiPackageItem,
|
|
10
11
|
} from "./qti-package-types.js";
|
|
11
|
-
import {
|
|
12
|
-
isPackageRelativeHref,
|
|
13
|
-
packageDescendants,
|
|
14
|
-
resolvePackageHref,
|
|
15
|
-
type PackageXmlFile,
|
|
16
|
-
} from "./qti-package-xml.js";
|
|
12
|
+
import { packageDescendants, resolvePackageHref, type PackageXmlFile } from "./qti-package-xml.js";
|
|
17
13
|
import type { QtiDiagnostic } from "./types.js";
|
|
18
14
|
|
|
19
15
|
interface PendingAssetReference {
|
|
@@ -196,9 +192,7 @@ function collectPackageRelativeAttributeRefs(
|
|
|
196
192
|
diagnostics: QtiDiagnostic[],
|
|
197
193
|
): void {
|
|
198
194
|
for (const node of packageDescendants(xmlFile.root, localName)) {
|
|
199
|
-
const
|
|
200
|
-
if (!isPackageRelativeHref(href)) continue;
|
|
201
|
-
const resolved = resolvePackageHref(xmlFile.path, href.trim(), diagnostics);
|
|
195
|
+
const resolved = resolveContentAssetHref(xmlFile.path, node.attributes[attribute], diagnostics);
|
|
202
196
|
if (resolved) refs.push(resolved);
|
|
203
197
|
}
|
|
204
198
|
}
|
|
@@ -210,13 +204,26 @@ function collectPackageRelativeTextRefs(
|
|
|
210
204
|
diagnostics: QtiDiagnostic[],
|
|
211
205
|
): void {
|
|
212
206
|
for (const node of packageDescendants(xmlFile.root, localName)) {
|
|
213
|
-
const
|
|
214
|
-
if (!isPackageRelativeHref(href)) continue;
|
|
215
|
-
const resolved = resolvePackageHref(xmlFile.path, href, diagnostics);
|
|
207
|
+
const resolved = resolveContentAssetHref(xmlFile.path, node.text, diagnostics);
|
|
216
208
|
if (resolved) refs.push(resolved);
|
|
217
209
|
}
|
|
218
210
|
}
|
|
219
211
|
|
|
212
|
+
function resolveContentAssetHref(
|
|
213
|
+
sourcePath: string,
|
|
214
|
+
value: string | undefined,
|
|
215
|
+
diagnostics: QtiDiagnostic[],
|
|
216
|
+
): string | undefined {
|
|
217
|
+
const href = value?.trim() ?? "";
|
|
218
|
+
// A server-root path cannot name content inside a portable QTI package.
|
|
219
|
+
// Network-path URLs are external, like URLs with an explicit scheme.
|
|
220
|
+
if (href.startsWith("/") && !href.startsWith("//")) {
|
|
221
|
+
return normalizePackagePath(href, "package asset reference", diagnostics);
|
|
222
|
+
}
|
|
223
|
+
if (!isResolvableAssetUrl(href)) return undefined;
|
|
224
|
+
return resolvePackageHref(sourcePath, href, diagnostics);
|
|
225
|
+
}
|
|
226
|
+
|
|
220
227
|
function materializeAssets(
|
|
221
228
|
pending: readonly PendingAssetReference[],
|
|
222
229
|
entriesByPath: { has(path: string): boolean },
|
|
@@ -10,6 +10,7 @@ export const QTI_PACKAGE_MANIFEST_PATH = "imsmanifest.xml";
|
|
|
10
10
|
|
|
11
11
|
export const QTI_ITEM_RESOURCE_TYPE = "imsqti_item_xmlv3p0";
|
|
12
12
|
|
|
13
|
+
/** Recognize QTI 3 item resource types, including their version suffixes. */
|
|
13
14
|
export function isQtiItemResource(type: string): boolean {
|
|
14
15
|
return type.toLowerCase().startsWith(QTI_ITEM_RESOURCE_TYPE);
|
|
15
16
|
}
|
|
@@ -315,9 +315,20 @@ export function packageTitle(
|
|
|
315
315
|
: undefined;
|
|
316
316
|
return (
|
|
317
317
|
assessmentTest?.title ??
|
|
318
|
+
manifestLomTitle(manifestRoot) ??
|
|
318
319
|
manifestTitle ??
|
|
319
320
|
items.find((item) => item.title !== undefined)?.title ??
|
|
320
321
|
manifestRoot?.attributes.identifier ??
|
|
321
322
|
""
|
|
322
323
|
);
|
|
323
324
|
}
|
|
325
|
+
|
|
326
|
+
function manifestLomTitle(root: QtiPackageXmlNode | undefined): string | undefined {
|
|
327
|
+
if (!root) return undefined;
|
|
328
|
+
const lomNamespace = "http://ltsc.ieee.org/xsd/LOM";
|
|
329
|
+
let nodes = childPackageElements(root, "metadata", QTI_PACKAGE_MANIFEST_NAMESPACE);
|
|
330
|
+
for (const name of ["lom", "general", "title", "string"]) {
|
|
331
|
+
nodes = nodes.flatMap((node) => childPackageElements(node, name, lomNamespace));
|
|
332
|
+
}
|
|
333
|
+
return nodes.map((node) => node.text.trim()).find((text) => text.length > 0);
|
|
334
|
+
}
|
package/src/qti-package-paths.ts
CHANGED
|
@@ -54,3 +54,12 @@ export function normalizePackagePath(
|
|
|
54
54
|
}
|
|
55
55
|
return parts.join("/");
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/** Whether an item href is a normalized package-local path, without URI routing or traversal. */
|
|
59
|
+
export function isQtiPackageItemHref(href: string): boolean {
|
|
60
|
+
return (
|
|
61
|
+
href.length > 0 &&
|
|
62
|
+
!/^[a-z][a-z0-9+.-]*:|[\\?#\s]/i.test(href) &&
|
|
63
|
+
href.split("/").every((part) => part.length > 0 && part !== "." && part !== "..")
|
|
64
|
+
);
|
|
65
|
+
}
|
package/src/qti-package-plan.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { QTI_PACKAGE_MANIFEST_NAMESPACE } from "./qti-namespaces.js";
|
|
|
31
31
|
import type { QtiDiagnostic } from "./types.js";
|
|
32
32
|
|
|
33
33
|
/** Package metadata without extracted bodies or retained item models. */
|
|
34
|
-
export type QtiPackageSummary = Omit<QtiPackageParseResult, "items" | "entries"> & {
|
|
34
|
+
export type QtiPackageSummary = Omit<QtiPackageParseResult, "items" | "entries" | "xmlFiles"> & {
|
|
35
35
|
readonly itemCount: number;
|
|
36
36
|
};
|
|
37
37
|
|
package/src/qti-package-types.ts
CHANGED
|
@@ -170,11 +170,21 @@ export interface QtiPackageContentAssetDiscovery {
|
|
|
170
170
|
readonly diagnostics: readonly QtiDiagnostic[];
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/** Root classification and XML syntax diagnostics for one package XML file, without its tree. */
|
|
174
|
+
export interface QtiPackageXmlFileSummary {
|
|
175
|
+
readonly path: string;
|
|
176
|
+
readonly rootLocalName?: string | undefined;
|
|
177
|
+
readonly rootNamespaceUri?: string | undefined;
|
|
178
|
+
readonly diagnostics: readonly QtiDiagnostic[];
|
|
179
|
+
}
|
|
180
|
+
|
|
173
181
|
/** Parsed QTI package manifest/resource graph and item/test projection. */
|
|
174
182
|
export interface QtiPackageParseResult {
|
|
175
183
|
readonly ok: boolean;
|
|
176
184
|
readonly title: string;
|
|
177
185
|
readonly entries: readonly QtiPackageEntry[];
|
|
186
|
+
/** All .xml entries in inventory order, including files outside the selected resource graph. */
|
|
187
|
+
readonly xmlFiles: readonly QtiPackageXmlFileSummary[];
|
|
178
188
|
readonly packageShape: QtiPackageShape;
|
|
179
189
|
readonly items: readonly QtiPackageItem[];
|
|
180
190
|
readonly assets: readonly QtiPackageAsset[];
|
package/src/qti-package-xml.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isResolvableAssetUrl } from "./asset-url.js";
|
|
1
2
|
import { parseQtiPackageXmlTree, type QtiPackageXmlNode } from "./package-xml.js";
|
|
2
3
|
import { normalizePackagePath, pushPackageDiagnostic } from "./qti-package-paths.js";
|
|
3
4
|
import { decodeUtf8, type QtiPackageEntry } from "./qti-package-zip.js";
|
|
@@ -78,6 +79,10 @@ export function resolvePackageHref(
|
|
|
78
79
|
diagnostics: QtiDiagnostic[],
|
|
79
80
|
): string | undefined {
|
|
80
81
|
const strippedHref = stripHrefSuffix(href);
|
|
82
|
+
// Diagnose absolute package references before adding the containing directory.
|
|
83
|
+
if (!isResolvableAssetUrl(strippedHref)) {
|
|
84
|
+
return normalizePackagePath(strippedHref, "package reference", diagnostics);
|
|
85
|
+
}
|
|
81
86
|
const base = from.includes("/") ? from.slice(0, from.lastIndexOf("/") + 1) : "";
|
|
82
87
|
return normalizePackagePath(`${base}${strippedHref}`, "package reference", diagnostics);
|
|
83
88
|
}
|
|
@@ -86,16 +91,16 @@ export function stripHrefSuffix(href: string): string {
|
|
|
86
91
|
return href.split(/[?#]/, 1)[0] ?? "";
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
export function isPackageRelativeHref(href: string | undefined): href is string {
|
|
90
|
-
const trimmed = href?.trim() ?? "";
|
|
91
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("//")) return false;
|
|
92
|
-
return !/^[a-z][a-z0-9+.-]*:/i.test(trimmed);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
94
|
export function joinPackagePath(packagePath: string, localPath: string): string {
|
|
96
95
|
return localPath.startsWith("/") ? `${packagePath}${localPath}` : `${packagePath}/${localPath}`;
|
|
97
96
|
}
|
|
98
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Return a diagnostic with its local location prefixed by an item or archive path.
|
|
100
|
+
* Prefer path over source.path; update both to the same location without mutating the input.
|
|
101
|
+
* Empty locations and locations equal to packagePath resolve to packagePath itself.
|
|
102
|
+
* A leading slash in the local location is a separator, not an absolute filesystem path.
|
|
103
|
+
*/
|
|
99
104
|
export function scopeDiagnosticToPackagePath(
|
|
100
105
|
packagePath: string,
|
|
101
106
|
diagnostic: QtiDiagnostic,
|