@longsightgroup/qti3-core 0.9.9 → 0.9.10

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.
Files changed (49) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/parser-processing.d.ts.map +1 -1
  6. package/dist/parser-processing.js +4 -0
  7. package/dist/parser-processing.js.map +1 -1
  8. package/dist/processing-expression-collection.d.ts +4 -0
  9. package/dist/processing-expression-collection.d.ts.map +1 -0
  10. package/dist/processing-expression-collection.js +44 -0
  11. package/dist/processing-expression-collection.js.map +1 -0
  12. package/dist/qti-package-assets.d.ts +3 -1
  13. package/dist/qti-package-assets.d.ts.map +1 -1
  14. package/dist/qti-package-assets.js +22 -0
  15. package/dist/qti-package-assets.js.map +1 -1
  16. package/dist/qti-package-items.d.ts +2 -1
  17. package/dist/qti-package-items.d.ts.map +1 -1
  18. package/dist/qti-package-items.js +96 -18
  19. package/dist/qti-package-items.js.map +1 -1
  20. package/dist/qti-package-manifest.d.ts.map +1 -1
  21. package/dist/qti-package-manifest.js +2 -0
  22. package/dist/qti-package-manifest.js.map +1 -1
  23. package/dist/qti-package-metadata.d.ts +5 -2
  24. package/dist/qti-package-metadata.d.ts.map +1 -1
  25. package/dist/qti-package-metadata.js +116 -8
  26. package/dist/qti-package-metadata.js.map +1 -1
  27. package/dist/qti-package-types.d.ts +62 -0
  28. package/dist/qti-package-types.d.ts.map +1 -1
  29. package/dist/qti-package-xml.d.ts.map +1 -1
  30. package/dist/qti-package-xml.js +1 -1
  31. package/dist/qti-package-xml.js.map +1 -1
  32. package/dist/qti-package.d.ts +2 -1
  33. package/dist/qti-package.d.ts.map +1 -1
  34. package/dist/qti-package.js +2 -0
  35. package/dist/qti-package.js.map +1 -1
  36. package/dist/types.d.ts +2 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/index.ts +9 -0
  40. package/src/parser-processing.ts +4 -0
  41. package/src/processing-expression-collection.ts +57 -0
  42. package/src/qti-package-assets.ts +30 -0
  43. package/src/qti-package-items.ts +190 -29
  44. package/src/qti-package-manifest.ts +2 -0
  45. package/src/qti-package-metadata.ts +174 -7
  46. package/src/qti-package-types.ts +69 -0
  47. package/src/qti-package-xml.ts +2 -1
  48. package/src/qti-package.ts +9 -0
  49. package/src/types.ts +2 -0
@@ -1,12 +1,16 @@
1
1
  import type { QtiPackageXmlNode } from "./package-xml.js";
2
2
  import { parseXmlBoolean } from "./parser-values.js";
3
- import { packageDescendants } from "./qti-package-xml.js";
3
+ import { pushPackageDiagnostic } from "./qti-package-paths.js";
4
+ import { childPackageElements, packageDescendants } from "./qti-package-xml.js";
4
5
  import type {
5
6
  QtiAssessmentTestPackageModel,
6
7
  QtiPackageItem,
8
+ QtiItemSessionControl,
7
9
  QtiStandardAlignment,
10
+ QtiTimeLimits,
8
11
  QtiTimingMetadata,
9
12
  } from "./qti-package-types.js";
13
+ import type { QtiDiagnostic } from "./types.js";
10
14
 
11
15
  /** Known QTI / IMS CP alignment element names plus attribute-driven manifest metadata. */
12
16
  const STANDARD_ALIGNMENT_ELEMENT_NAMES = new Set([
@@ -27,18 +31,78 @@ const STANDARD_ALIGNMENT_ATTRIBUTE_NAMES = new Set([
27
31
  export function parseTimingMetadata(
28
32
  root: QtiPackageXmlNode,
29
33
  sourcePath: string,
34
+ diagnostics: QtiDiagnostic[],
35
+ parsedTimeLimits?: QtiTimeLimits,
30
36
  ): QtiTimingMetadata | undefined {
31
- const timeLimits = packageDescendants(root, "qti-time-limits")[0];
37
+ const timeLimitsNode = childPackageElements(root, "qti-time-limits")[0];
38
+ const timeLimits = parsedTimeLimits ?? parseTimeLimits(timeLimitsNode, sourcePath, diagnostics);
32
39
  const timeDependent = parseXmlBoolean(root.attributes["time-dependent"]);
33
- if (!timeLimits && timeDependent === undefined) return undefined;
40
+ if (!timeLimitsNode && timeDependent === undefined) return undefined;
34
41
 
35
42
  return {
36
43
  sourcePath,
37
44
  timeDependent,
38
- minTime: timeLimits?.attributes["min-time"],
39
- maxTime: timeLimits?.attributes["max-time"],
40
- allowLateSubmission: parseXmlBoolean(timeLimits?.attributes["allow-late-submission"]),
41
- attributes: timeLimits ? { ...timeLimits.attributes } : {},
45
+ minTime: timeLimitsNode?.attributes["min-time"],
46
+ maxTime: timeLimitsNode?.attributes["max-time"],
47
+ allowLateSubmission: timeLimits?.allowLateSubmission,
48
+ attributes: timeLimitsNode ? { ...timeLimitsNode.attributes } : {},
49
+ };
50
+ }
51
+
52
+ export function parseTimeLimits(
53
+ node: QtiPackageXmlNode | undefined,
54
+ sourcePath: string,
55
+ diagnostics: QtiDiagnostic[],
56
+ ): QtiTimeLimits | undefined {
57
+ if (!node) return undefined;
58
+
59
+ const minTimeSeconds = parseNonNegativeNumberAttribute(node, "min-time", sourcePath, diagnostics);
60
+ const maxTimeSeconds = parseNonNegativeNumberAttribute(node, "max-time", sourcePath, diagnostics);
61
+ const allowLateSubmission = parseBooleanAttribute(
62
+ node,
63
+ "allow-late-submission",
64
+ sourcePath,
65
+ diagnostics,
66
+ );
67
+
68
+ if (
69
+ minTimeSeconds !== undefined &&
70
+ maxTimeSeconds !== undefined &&
71
+ minTimeSeconds > maxTimeSeconds
72
+ ) {
73
+ pushPackageDiagnostic(
74
+ diagnostics,
75
+ "package.timing.range.invalid",
76
+ "error",
77
+ `qti-time-limits min-time ${minTimeSeconds} exceeds max-time ${maxTimeSeconds}.`,
78
+ sourcePath,
79
+ );
80
+ }
81
+
82
+ return {
83
+ minTimeSeconds,
84
+ maxTimeSeconds,
85
+ allowLateSubmission,
86
+ attributes: { ...node.attributes },
87
+ };
88
+ }
89
+
90
+ export function parseItemSessionControl(
91
+ node: QtiPackageXmlNode | undefined,
92
+ sourcePath: string,
93
+ diagnostics: QtiDiagnostic[],
94
+ ): QtiItemSessionControl | undefined {
95
+ if (!node) return undefined;
96
+
97
+ return {
98
+ maxAttempts: parseNonNegativeNumberAttribute(node, "max-attempts", sourcePath, diagnostics),
99
+ showFeedback: parseBooleanAttribute(node, "show-feedback", sourcePath, diagnostics),
100
+ allowReview: parseBooleanAttribute(node, "allow-review", sourcePath, diagnostics),
101
+ showSolution: parseBooleanAttribute(node, "show-solution", sourcePath, diagnostics),
102
+ allowComment: parseBooleanAttribute(node, "allow-comment", sourcePath, diagnostics),
103
+ allowSkipping: parseBooleanAttribute(node, "allow-skipping", sourcePath, diagnostics),
104
+ validateResponses: parseBooleanAttribute(node, "validate-responses", sourcePath, diagnostics),
105
+ attributes: { ...node.attributes },
42
106
  };
43
107
  }
44
108
 
@@ -76,6 +140,52 @@ export function parseStandardAlignments(
76
140
  attributes: { ...node.attributes },
77
141
  });
78
142
  }
143
+ alignments.push(...parseCurriculumStandardsMetadata(root, sourcePath));
144
+ return alignments;
145
+ }
146
+
147
+ function parseCurriculumStandardsMetadata(
148
+ root: QtiPackageXmlNode,
149
+ sourcePath: string,
150
+ ): QtiStandardAlignment[] {
151
+ const alignments: QtiStandardAlignment[] = [];
152
+ for (const metadataSet of packageDescendants(root, "curriculumStandardsMetadataSet")) {
153
+ const resourceLabel = firstAttribute(metadataSet, ["resourceLabel"]);
154
+ const resourcePartIdentifier = firstAttribute(metadataSet, ["resourcePartId"]);
155
+ const weight = optionalFiniteNumber(firstAttribute(metadataSet, ["weight"]));
156
+
157
+ for (const metadata of childPackageElements(metadataSet, "curriculumStandardsMetadata")) {
158
+ const providerIdentifier = firstAttribute(metadata, [
159
+ "providerId",
160
+ "providerID",
161
+ "domainId",
162
+ "domainID",
163
+ ]);
164
+ for (const labelledGuid of packageDescendants(metadata, "labelledGUID")) {
165
+ const identifier = childText(labelledGuid, "GUID");
166
+ const targetName = childText(labelledGuid, "label");
167
+ const targetUrl =
168
+ childText(labelledGuid, "caseItemURI") ??
169
+ childText(labelledGuid, "uri") ??
170
+ childText(labelledGuid, "URI");
171
+ if (!identifier && !targetName && !targetUrl) continue;
172
+
173
+ alignments.push({
174
+ sourcePath,
175
+ qtiName: "curriculumStandardsMetadata",
176
+ identifier,
177
+ targetName,
178
+ targetUrl,
179
+ text: targetName,
180
+ providerIdentifier,
181
+ resourceLabel,
182
+ resourcePartIdentifier,
183
+ weight,
184
+ attributes: { ...metadataSet.attributes, ...metadata.attributes },
185
+ });
186
+ }
187
+ }
188
+ }
79
189
  return alignments;
80
190
  }
81
191
 
@@ -106,6 +216,10 @@ export function uniqueStandards(
106
216
  standard.targetName ?? "",
107
217
  standard.targetUrl ?? "",
108
218
  standard.text ?? "",
219
+ standard.providerIdentifier ?? "",
220
+ standard.resourceLabel ?? "",
221
+ standard.resourcePartIdentifier ?? "",
222
+ standard.weight?.toString() ?? "",
109
223
  ].join("\u0000");
110
224
  if (seen.has(key)) continue;
111
225
  seen.add(key);
@@ -114,6 +228,59 @@ export function uniqueStandards(
114
228
  return unique;
115
229
  }
116
230
 
231
+ function parseNonNegativeNumberAttribute(
232
+ node: QtiPackageXmlNode,
233
+ name: string,
234
+ sourcePath: string,
235
+ diagnostics: QtiDiagnostic[],
236
+ ): number | undefined {
237
+ const raw = node.attributes[name];
238
+ if (raw === undefined) return undefined;
239
+ const value = Number(raw);
240
+ if (Number.isFinite(value) && value >= 0) return value;
241
+
242
+ pushPackageDiagnostic(
243
+ diagnostics,
244
+ `package.attribute.${name}.number`,
245
+ "error",
246
+ `${node.localName} ${name} must be a non-negative number, found ${raw}.`,
247
+ sourcePath,
248
+ );
249
+ return undefined;
250
+ }
251
+
252
+ function parseBooleanAttribute(
253
+ node: QtiPackageXmlNode,
254
+ name: string,
255
+ sourcePath: string,
256
+ diagnostics: QtiDiagnostic[],
257
+ ): boolean | undefined {
258
+ const raw = node.attributes[name];
259
+ if (raw === undefined) return undefined;
260
+ const value = parseXmlBoolean(raw);
261
+ if (value !== undefined) return value;
262
+
263
+ pushPackageDiagnostic(
264
+ diagnostics,
265
+ `package.attribute.${name}.boolean`,
266
+ "error",
267
+ `${node.localName} ${name} must be an XML boolean, found ${raw}.`,
268
+ sourcePath,
269
+ );
270
+ return undefined;
271
+ }
272
+
273
+ function childText(node: QtiPackageXmlNode, localName: string): string | undefined {
274
+ const text = childPackageElements(node, localName)[0]?.text.trim();
275
+ return text ? text : undefined;
276
+ }
277
+
278
+ function optionalFiniteNumber(raw: string | undefined): number | undefined {
279
+ if (raw === undefined) return undefined;
280
+ const value = Number(raw);
281
+ return Number.isFinite(value) ? value : undefined;
282
+ }
283
+
117
284
  export function primaryTiming(
118
285
  assessmentTest: QtiAssessmentTestPackageModel | undefined,
119
286
  items: readonly QtiPackageItem[],
@@ -1,4 +1,5 @@
1
1
  import type { QtiDiagnostic, QtiDocument } from "./types.js";
2
+ import type { QtiPackageEntry } from "./qti-package-zip.js";
2
3
 
3
4
  export type {
4
5
  QtiPackageInflateContext,
@@ -31,6 +32,7 @@ export interface QtiManifestResource {
31
32
  readonly href?: string | undefined;
32
33
  readonly files: readonly QtiManifestFile[];
33
34
  readonly dependencies: readonly string[];
35
+ readonly standards: readonly QtiStandardAlignment[];
34
36
  readonly attributes: Record<string, string>;
35
37
  }
36
38
 
@@ -44,6 +46,32 @@ export interface QtiTimingMetadata {
44
46
  readonly attributes: Record<string, string>;
45
47
  }
46
48
 
49
+ /** Parsed qti-time-limits attributes for one assessment-test structure node. */
50
+ export interface QtiTimeLimits {
51
+ readonly minTimeSeconds?: number | undefined;
52
+ readonly maxTimeSeconds?: number | undefined;
53
+ readonly allowLateSubmission?: boolean | undefined;
54
+ readonly attributes: Record<string, string>;
55
+ }
56
+
57
+ /** Parsed qti-item-session-control attributes for an assessment item reference. */
58
+ export interface QtiItemSessionControl {
59
+ readonly maxAttempts?: number | undefined;
60
+ readonly showFeedback?: boolean | undefined;
61
+ readonly allowReview?: boolean | undefined;
62
+ readonly showSolution?: boolean | undefined;
63
+ readonly allowComment?: boolean | undefined;
64
+ readonly allowSkipping?: boolean | undefined;
65
+ readonly validateResponses?: boolean | undefined;
66
+ readonly attributes: Record<string, string>;
67
+ }
68
+
69
+ /** Navigation mode declared by a QTI assessment-test part. */
70
+ export type QtiTestPartNavigationMode = "linear" | "nonlinear";
71
+
72
+ /** Submission mode declared by a QTI assessment-test part. */
73
+ export type QtiTestPartSubmissionMode = "individual" | "simultaneous";
74
+
47
75
  /** Neutral standard-alignment metadata discovered in package XML. */
48
76
  export interface QtiStandardAlignment {
49
77
  readonly sourcePath: string;
@@ -53,6 +81,10 @@ export interface QtiStandardAlignment {
53
81
  readonly targetName?: string | undefined;
54
82
  readonly targetUrl?: string | undefined;
55
83
  readonly text?: string | undefined;
84
+ readonly providerIdentifier?: string | undefined;
85
+ readonly resourceLabel?: string | undefined;
86
+ readonly resourcePartIdentifier?: string | undefined;
87
+ readonly weight?: number | undefined;
56
88
  readonly attributes: Record<string, string>;
57
89
  }
58
90
 
@@ -60,6 +92,33 @@ export interface QtiStandardAlignment {
60
92
  export interface QtiAssessmentTestItemRef {
61
93
  readonly identifier?: string | undefined;
62
94
  readonly href: string;
95
+ readonly testPartIdentifier?: string | undefined;
96
+ readonly sectionIdentifier?: string | undefined;
97
+ readonly timeLimits?: QtiTimeLimits | undefined;
98
+ readonly itemSessionControl?: QtiItemSessionControl | undefined;
99
+ readonly attributes: Record<string, string>;
100
+ }
101
+
102
+ /** Parsed assessment section, including nested sections and directly owned item references. */
103
+ export interface QtiAssessmentSectionPackageModel {
104
+ readonly identifier: string;
105
+ readonly title?: string | undefined;
106
+ readonly visible?: boolean | undefined;
107
+ readonly testPartIdentifier: string;
108
+ readonly parentSectionIdentifier?: string | undefined;
109
+ readonly timeLimits?: QtiTimeLimits | undefined;
110
+ readonly itemRefs: readonly QtiAssessmentTestItemRef[];
111
+ readonly sections: readonly QtiAssessmentSectionPackageModel[];
112
+ readonly attributes: Record<string, string>;
113
+ }
114
+
115
+ /** Parsed QTI assessment-test part and its section hierarchy. */
116
+ export interface QtiTestPartPackageModel {
117
+ readonly identifier: string;
118
+ readonly navigationMode?: QtiTestPartNavigationMode | undefined;
119
+ readonly submissionMode?: QtiTestPartSubmissionMode | undefined;
120
+ readonly timeLimits?: QtiTimeLimits | undefined;
121
+ readonly sections: readonly QtiAssessmentSectionPackageModel[];
63
122
  readonly attributes: Record<string, string>;
64
123
  }
65
124
 
@@ -70,7 +129,9 @@ export interface QtiAssessmentTestPackageModel {
70
129
  readonly title?: string | undefined;
71
130
  readonly manifestResourceIdentifier?: string | undefined;
72
131
  readonly itemRefs: readonly QtiAssessmentTestItemRef[];
132
+ readonly testParts: readonly QtiTestPartPackageModel[];
73
133
  readonly timing?: QtiTimingMetadata | undefined;
134
+ readonly timeLimits?: QtiTimeLimits | undefined;
74
135
  readonly standards: readonly QtiStandardAlignment[];
75
136
  readonly assetHrefs: readonly string[];
76
137
  readonly diagnostics: readonly QtiDiagnostic[];
@@ -88,6 +149,7 @@ export interface QtiPackageItem {
88
149
  readonly title?: string | undefined;
89
150
  readonly document?: QtiDocument | undefined;
90
151
  readonly timing?: QtiTimingMetadata | undefined;
152
+ readonly timeLimits?: QtiTimeLimits | undefined;
91
153
  readonly standards: readonly QtiStandardAlignment[];
92
154
  readonly assetHrefs: readonly string[];
93
155
  readonly diagnostics: readonly QtiDiagnostic[];
@@ -102,10 +164,17 @@ export interface QtiPackageAsset {
102
164
  readonly referencedBy: readonly string[];
103
165
  }
104
166
 
167
+ /** Package-local asset references discovered in one XML document. */
168
+ export interface QtiPackageContentAssetDiscovery {
169
+ readonly hrefs: readonly string[];
170
+ readonly diagnostics: readonly QtiDiagnostic[];
171
+ }
172
+
105
173
  /** Parsed QTI package manifest/resource graph and item/test projection. */
106
174
  export interface QtiPackageParseResult {
107
175
  readonly ok: boolean;
108
176
  readonly title: string;
177
+ readonly entries: readonly QtiPackageEntry[];
109
178
  readonly packageShape: QtiPackageShape;
110
179
  readonly items: readonly QtiPackageItem[];
111
180
  readonly assets: readonly QtiPackageAsset[];
@@ -88,7 +88,8 @@ export function scopeDiagnosticToPackagePath(
88
88
  diagnostic: QtiDiagnostic,
89
89
  ): QtiDiagnostic {
90
90
  const localPath = diagnostic.path ?? diagnostic.source?.path;
91
- const path = localPath ? joinPackagePath(packagePath, localPath) : packagePath;
91
+ const path =
92
+ !localPath || localPath === packagePath ? packagePath : joinPackagePath(packagePath, localPath);
92
93
  return {
93
94
  ...diagnostic,
94
95
  path,
@@ -36,22 +36,30 @@ import {
36
36
  import type { QtiDiagnostic } from "./types.js";
37
37
 
38
38
  export { decodeUtf8, readQtiPackageZipEntries };
39
+ export { discoverQtiPackageContentAssets } from "./qti-package-assets.js";
39
40
  export { normalizePackagePath } from "./qti-package-paths.js";
40
41
  export { QTI_ITEM_RESOURCE_TYPE, QTI_PACKAGE_MANIFEST_PATH } from "./qti-package-manifest.js";
41
42
  export type { QtiPackageEntry };
42
43
 
43
44
  export type {
45
+ QtiAssessmentSectionPackageModel,
44
46
  QtiAssessmentTestItemRef,
45
47
  QtiAssessmentTestPackageModel,
46
48
  QtiManifestFile,
47
49
  QtiManifestResource,
48
50
  QtiPackageAsset,
51
+ QtiPackageContentAssetDiscovery,
49
52
  QtiPackageAssetSource,
50
53
  QtiPackageItem,
51
54
  QtiPackageItemSource,
52
55
  QtiPackageParseResult,
53
56
  QtiPackageShape,
57
+ QtiItemSessionControl,
54
58
  QtiStandardAlignment,
59
+ QtiTestPartNavigationMode,
60
+ QtiTestPartPackageModel,
61
+ QtiTestPartSubmissionMode,
62
+ QtiTimeLimits,
55
63
  QtiTimingMetadata,
56
64
  } from "./qti-package-types.js";
57
65
 
@@ -159,6 +167,7 @@ function parseQtiPackageEntries(
159
167
  return {
160
168
  ok: diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
161
169
  title,
170
+ entries,
162
171
  packageShape,
163
172
  items,
164
173
  assets,
package/src/types.ts CHANGED
@@ -494,6 +494,8 @@ export interface QtiResponseProcessing {
494
494
  template?: string | undefined;
495
495
  rules: QtiResponseRule[];
496
496
  conditions: QtiResponseCondition[];
497
+ /** Direct processing expressions retained even when they are not wrapped in a response rule. */
498
+ expressions?: QtiProcessingExpression[] | undefined;
497
499
  }
498
500
 
499
501
  export interface QtiResponseCondition {