@longsightgroup/qti3-core 0.3.0 → 0.4.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/dist/xml.js CHANGED
@@ -3,53 +3,88 @@ export function parseXmlTree(xml) {
3
3
  const parser = new StaxXmlParserSync(xml, {
4
4
  autoDecodeEntities: true,
5
5
  });
6
+ const tagTokens = scanXmlTagTokens(xml);
6
7
  const stack = [];
7
8
  const errors = [];
8
9
  let root;
9
- let searchOffset = 0;
10
- for (const event of parser) {
11
- if (event.type === XmlEventType.ERROR) {
12
- errors.push(event.error);
13
- continue;
14
- }
15
- if (event.type === XmlEventType.START_ELEMENT) {
16
- const parent = stack.at(-1);
17
- const offset = findStartElementOffset(xml, event.name, searchOffset);
18
- searchOffset = offset >= 0 ? offset + 1 : searchOffset;
19
- const node = {
20
- name: event.name,
21
- localName: event.localName ?? event.name,
22
- prefix: event.prefix,
23
- uri: event.uri,
24
- attributes: event.attributes,
25
- children: [],
26
- content: [],
27
- text: "",
28
- source: sourceLocation(xml, offset, nodePath(parent, event.localName ?? event.name)),
29
- };
30
- if (parent) {
31
- node.parent = parent;
32
- parent.children.push(node);
33
- parent.content.push(node);
10
+ let tagTokenIndex = 0;
11
+ try {
12
+ for (const event of parser) {
13
+ if (event.type === XmlEventType.ERROR) {
14
+ errors.push(event.error);
15
+ continue;
34
16
  }
35
- else {
36
- root = node;
17
+ if (event.type === XmlEventType.START_ELEMENT) {
18
+ const parent = stack.at(-1);
19
+ const path = nodePath(parent, event.localName ?? event.name);
20
+ const sourceRange = { startOffset: -1, startTagEndOffset: -1 };
21
+ const token = tagTokens[tagTokenIndex];
22
+ if (token?.kind === "start" && token.name === event.name) {
23
+ tagTokenIndex += 1;
24
+ sourceRange.startOffset = token.startOffset;
25
+ sourceRange.startTagEndOffset = token.startTagEndOffset;
26
+ if (token.endOffset !== undefined) {
27
+ sourceRange.endOffset = token.endOffset;
28
+ }
29
+ }
30
+ else {
31
+ errors.push(new Error(`XML source range alignment failed for <${event.name}>.`));
32
+ }
33
+ const node = {
34
+ name: event.name,
35
+ localName: event.localName ?? event.name,
36
+ prefix: event.prefix,
37
+ uri: event.uri,
38
+ attributes: event.attributes,
39
+ children: [],
40
+ content: [],
41
+ text: "",
42
+ source: sourceLocation(xml, sourceRange.startOffset, path),
43
+ sourceRange,
44
+ };
45
+ if (parent) {
46
+ node.parent = parent;
47
+ parent.children.push(node);
48
+ parent.content.push(node);
49
+ }
50
+ else {
51
+ root = node;
52
+ }
53
+ stack.push(node);
54
+ continue;
37
55
  }
38
- stack.push(node);
39
- continue;
40
- }
41
- if (event.type === XmlEventType.END_ELEMENT) {
42
- stack.pop();
43
- continue;
44
- }
45
- if (event.type === XmlEventType.CHARACTERS || event.type === XmlEventType.CDATA) {
46
- const node = stack.at(-1);
47
- if (node) {
48
- node.text += event.value;
49
- node.content.push(event.value);
56
+ if (event.type === XmlEventType.END_ELEMENT) {
57
+ const node = stack.pop();
58
+ if (node) {
59
+ if (node.sourceRange.endOffset === undefined) {
60
+ const token = tagTokens[tagTokenIndex];
61
+ if (token?.kind === "end" && token.name === event.name) {
62
+ tagTokenIndex += 1;
63
+ node.sourceRange.endOffset = token.endOffset;
64
+ node.endSource = sourceLocation(xml, token.startOffset, node.source.path);
65
+ }
66
+ else {
67
+ errors.push(new Error(`XML source range alignment failed for </${event.name}>.`));
68
+ }
69
+ }
70
+ }
71
+ continue;
72
+ }
73
+ if (event.type === XmlEventType.CHARACTERS || event.type === XmlEventType.CDATA) {
74
+ const node = stack.at(-1);
75
+ if (node) {
76
+ node.text += event.value;
77
+ node.content.push(event.value);
78
+ }
50
79
  }
51
80
  }
52
81
  }
82
+ catch (error) {
83
+ errors.push(error instanceof Error ? error : new Error(String(error)));
84
+ }
85
+ for (const node of [...stack].reverse()) {
86
+ errors.push(new Error(`Unexpected end of document. Missing closing tag for <${node.name}>.`));
87
+ }
53
88
  return { root, errors };
54
89
  }
55
90
  export function childElements(node, localName) {
@@ -68,26 +103,139 @@ export function textContent(node) {
68
103
  const parts = node.content.map((entry) => typeof entry === "string" ? entry : textContent(entry));
69
104
  return parts.join(" ").replace(/\s+/g, " ").trim();
70
105
  }
71
- function findStartElementOffset(xml, name, from) {
72
- let offset = from;
106
+ function scanXmlTagTokens(xml) {
107
+ const tokens = [];
108
+ let offset = 0;
73
109
  while (offset < xml.length) {
74
- const start = xml.indexOf("<", offset);
75
- if (start === -1)
76
- return -1;
77
- const next = xml.charAt(start + 1);
78
- if (next === "/" || next === "!" || next === "?") {
79
- offset = start + 1;
110
+ const startOffset = xml.indexOf("<", offset);
111
+ if (startOffset === -1 || startOffset + 1 >= xml.length)
112
+ return tokens;
113
+ if (xml.startsWith("<!--", startOffset)) {
114
+ offset = skipPastSequence(xml, "-->", startOffset + 4);
80
115
  continue;
81
116
  }
82
- const afterName = start + 1 + name.length;
83
- if (xml.slice(start + 1, afterName) === name &&
84
- (afterName >= xml.length || /[\s/>]/.test(xml.charAt(afterName)))) {
85
- return start;
117
+ if (xml.startsWith("<![CDATA[", startOffset)) {
118
+ offset = skipPastSequence(xml, "]]>", startOffset + 9);
119
+ continue;
86
120
  }
87
- offset = start + 1;
121
+ const next = xml.charAt(startOffset + 1);
122
+ if (next === "?") {
123
+ offset = skipPastSequence(xml, "?>", startOffset + 2);
124
+ continue;
125
+ }
126
+ if (next === "!") {
127
+ const declarationEndOffset = findMarkupDeclarationEndOffset(xml, startOffset + 2);
128
+ offset = declarationEndOffset >= 0 ? declarationEndOffset + 1 : xml.length;
129
+ continue;
130
+ }
131
+ if (next === "/") {
132
+ const tagEndOffset = findTagEndOffset(xml, startOffset + 2);
133
+ if (tagEndOffset < 0)
134
+ return tokens;
135
+ const name = readTagName(xml, startOffset + 2, tagEndOffset);
136
+ if (name) {
137
+ tokens.push({
138
+ kind: "end",
139
+ name,
140
+ startOffset,
141
+ startTagEndOffset: tagEndOffset,
142
+ endOffset: tagEndOffset + 1,
143
+ selfClosing: false,
144
+ });
145
+ }
146
+ offset = tagEndOffset + 1;
147
+ continue;
148
+ }
149
+ const tagEndOffset = findTagEndOffset(xml, startOffset + 1);
150
+ if (tagEndOffset < 0)
151
+ return tokens;
152
+ const name = readTagName(xml, startOffset + 1, tagEndOffset);
153
+ if (name) {
154
+ const selfClosing = isSelfClosingStartTag(xml, tagEndOffset);
155
+ tokens.push({
156
+ kind: "start",
157
+ name,
158
+ startOffset,
159
+ startTagEndOffset: tagEndOffset,
160
+ endOffset: selfClosing ? tagEndOffset + 1 : undefined,
161
+ selfClosing,
162
+ });
163
+ }
164
+ offset = tagEndOffset + 1;
165
+ }
166
+ return tokens;
167
+ }
168
+ function skipPastSequence(xml, sequence, from) {
169
+ const endOffset = xml.indexOf(sequence, from);
170
+ return endOffset >= 0 ? endOffset + sequence.length : xml.length;
171
+ }
172
+ function findMarkupDeclarationEndOffset(xml, from) {
173
+ let quote = null;
174
+ let internalSubsetDepth = 0;
175
+ for (let index = from; index < xml.length; index += 1) {
176
+ const char = xml.charAt(index);
177
+ if (quote) {
178
+ if (char === quote)
179
+ quote = null;
180
+ continue;
181
+ }
182
+ if (char === '"' || char === "'") {
183
+ quote = char;
184
+ continue;
185
+ }
186
+ if (char === "[") {
187
+ internalSubsetDepth += 1;
188
+ continue;
189
+ }
190
+ if (char === "]" && internalSubsetDepth > 0) {
191
+ internalSubsetDepth -= 1;
192
+ continue;
193
+ }
194
+ if (char === ">" && internalSubsetDepth === 0)
195
+ return index;
196
+ }
197
+ return -1;
198
+ }
199
+ function readTagName(xml, from, to) {
200
+ let start = from;
201
+ while (start < to && /\s/.test(xml.charAt(start)))
202
+ start += 1;
203
+ let end = start;
204
+ while (end < to) {
205
+ const char = xml.charAt(end);
206
+ if (/\s/.test(char) || char === "/" || char === ">")
207
+ break;
208
+ end += 1;
209
+ }
210
+ return xml.slice(start, end);
211
+ }
212
+ function findTagEndOffset(xml, from) {
213
+ let quote = null;
214
+ for (let index = from; index < xml.length; index += 1) {
215
+ const char = xml.charAt(index);
216
+ if (quote) {
217
+ if (char === quote)
218
+ quote = null;
219
+ continue;
220
+ }
221
+ if (char === '"' || char === "'") {
222
+ quote = char;
223
+ continue;
224
+ }
225
+ if (char === ">")
226
+ return index;
88
227
  }
89
228
  return -1;
90
229
  }
230
+ function isSelfClosingStartTag(xml, tagEndOffset) {
231
+ for (let index = tagEndOffset - 1; index >= 0; index -= 1) {
232
+ const char = xml.charAt(index);
233
+ if (/\s/.test(char))
234
+ continue;
235
+ return char === "/";
236
+ }
237
+ return false;
238
+ }
91
239
  function sourceLocation(xml, offset, path) {
92
240
  if (offset < 0)
93
241
  return { line: 1, column: 1, offset: 0, path };
package/dist/xml.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"xml.js","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xml.js","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@longsightgroup/qti3-core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Framework-neutral TypeScript core for parsing, validating, scoring, and serializing QTI 3 assessment items.",
5
5
  "keywords": [
6
6
  "assessment",
@@ -0,0 +1,283 @@
1
+ import type { QtiDiagnostic, QtiSourceLocation } from "./types.js";
2
+ import { descendants, parseXmlTree, type XmlNode } from "./xml.js";
3
+
4
+ const FORBIDDEN_DELIVERY_ELEMENT_NAMES = new Set([
5
+ "correct-response",
6
+ "mapping",
7
+ "area-mapping",
8
+ "match-table",
9
+ "interpolation-table",
10
+ "response-processing",
11
+ "feedback-inline",
12
+ "feedback-block",
13
+ "modal-feedback",
14
+ ]);
15
+
16
+ const UNSUPPORTED_SECURE_DELIVERY_ELEMENT_NAMES = new Set([
17
+ "template-processing",
18
+ "set-correct-response",
19
+ ]);
20
+
21
+ const DEFAULT_VALUE_DECLARATION_ELEMENT_NAMES = new Set([
22
+ "response-declaration",
23
+ "outcome-declaration",
24
+ "template-declaration",
25
+ ]);
26
+
27
+ export type QtiDeliverySecurityFindingKind =
28
+ | "forbidden-delivery-element"
29
+ | "unsupported-secure-delivery-element"
30
+ | "unsupported-adaptive-response-processing";
31
+
32
+ export interface QtiDeliverySecurityFinding {
33
+ kind: QtiDeliverySecurityFindingKind;
34
+ qtiName: string;
35
+ localName: string;
36
+ message: string;
37
+ source?: QtiSourceLocation | undefined;
38
+ }
39
+
40
+ export interface QtiDeliverySecurityAnalysis {
41
+ diagnostics: QtiDiagnostic[];
42
+ findings: QtiDeliverySecurityFinding[];
43
+ /** True when this exact XML contains no known answer/scoring/feedback delivery leaks. */
44
+ deliverySafe: boolean;
45
+ /** True when secure-delivery redaction can be attempted for this XML. */
46
+ secureDeliverySupported: boolean;
47
+ }
48
+
49
+ export interface QtiDeliverySafeXmlResult {
50
+ ok: boolean;
51
+ diagnostics: QtiDiagnostic[];
52
+ analysis: QtiDeliverySecurityAnalysis;
53
+ xml?: string | undefined;
54
+ }
55
+
56
+ export function analyzeQtiDeliverySecurity(xml: string): QtiDeliverySecurityAnalysis {
57
+ const parsed = parseDeliveryXml(xml);
58
+ return analyzeParsedDeliveryXml(parsed);
59
+ }
60
+
61
+ function analyzeParsedDeliveryXml(parsed: {
62
+ root: XmlNode | undefined;
63
+ diagnostics: QtiDiagnostic[];
64
+ }): QtiDeliverySecurityAnalysis {
65
+ const diagnostics = [...parsed.diagnostics];
66
+ const findings: QtiDeliverySecurityFinding[] = [];
67
+
68
+ if (parsed.root) {
69
+ const nodes = [parsed.root, ...descendants(parsed.root, () => true)];
70
+ for (const node of nodes) {
71
+ const normalizedName = normalizedQtiElementName(node.localName);
72
+ if (isForbiddenDeliveryElement(node, normalizedName)) {
73
+ findings.push({
74
+ kind: "forbidden-delivery-element",
75
+ qtiName: node.name,
76
+ localName: node.localName,
77
+ message: `${node.name} exposes answer keys, scoring, mapping, feedback, or solution information during delivery.`,
78
+ source: node.source,
79
+ });
80
+ }
81
+ if (UNSUPPORTED_SECURE_DELIVERY_ELEMENT_NAMES.has(normalizedName)) {
82
+ findings.push({
83
+ kind: "unsupported-secure-delivery-element",
84
+ qtiName: node.name,
85
+ localName: node.localName,
86
+ message: `${node.name} is not supported by secure delivery redaction v1.`,
87
+ source: node.source,
88
+ });
89
+ }
90
+ }
91
+
92
+ if (
93
+ parseXmlBoolean(parsed.root.attributes.adaptive) === true &&
94
+ nodes.some((node) => normalizedQtiElementName(node.localName) === "response-processing")
95
+ ) {
96
+ findings.push({
97
+ kind: "unsupported-adaptive-response-processing",
98
+ qtiName: parsed.root.name,
99
+ localName: parsed.root.localName,
100
+ message:
101
+ "Adaptive response processing requires server-side item materialization before secure delivery.",
102
+ source: parsed.root.source,
103
+ });
104
+ }
105
+ }
106
+
107
+ diagnostics.push(...findings.map(findingToDiagnostic));
108
+ const parseOk = parsed.diagnostics.every((diagnostic) => diagnostic.severity !== "error");
109
+
110
+ return {
111
+ diagnostics,
112
+ findings,
113
+ deliverySafe:
114
+ parseOk && !findings.some((finding) => finding.kind === "forbidden-delivery-element"),
115
+ secureDeliverySupported:
116
+ parseOk &&
117
+ !findings.some(
118
+ (finding) =>
119
+ finding.kind === "unsupported-secure-delivery-element" ||
120
+ finding.kind === "unsupported-adaptive-response-processing",
121
+ ),
122
+ };
123
+ }
124
+
125
+ export function buildQtiDeliverySafeXml(xml: string): QtiDeliverySafeXmlResult {
126
+ const parsed = parseDeliveryXml(xml);
127
+ const analysis = analyzeParsedDeliveryXml(parsed);
128
+ if (!analysis.secureDeliverySupported) {
129
+ return {
130
+ ok: false,
131
+ diagnostics: analysis.diagnostics,
132
+ analysis,
133
+ };
134
+ }
135
+
136
+ if (!parsed.root) {
137
+ return {
138
+ ok: false,
139
+ diagnostics: parsed.diagnostics,
140
+ analysis,
141
+ };
142
+ }
143
+
144
+ const redactionRanges = readRedactionRanges([
145
+ parsed.root,
146
+ ...descendants(parsed.root, () => true),
147
+ ]);
148
+ const redactedXml = removeSourceRanges(xml, redactionRanges);
149
+ const redactedAnalysis = analyzeQtiDeliverySecurity(redactedXml);
150
+ if (!redactedAnalysis.deliverySafe) {
151
+ return {
152
+ ok: false,
153
+ diagnostics: [...analysis.diagnostics, ...redactedAnalysis.diagnostics],
154
+ analysis: redactedAnalysis,
155
+ };
156
+ }
157
+
158
+ return {
159
+ ok: true,
160
+ xml: redactedXml,
161
+ diagnostics: redactedAnalysis.diagnostics,
162
+ analysis: redactedAnalysis,
163
+ };
164
+ }
165
+
166
+ function parseDeliveryXml(xml: string): {
167
+ root: XmlNode | undefined;
168
+ diagnostics: QtiDiagnostic[];
169
+ } {
170
+ let parsed: ReturnType<typeof parseXmlTree>;
171
+ try {
172
+ parsed = parseXmlTree(xml);
173
+ } catch (error) {
174
+ return {
175
+ root: undefined,
176
+ diagnostics: [
177
+ {
178
+ code: "xml.parse",
179
+ severity: "error",
180
+ message: error instanceof Error ? error.message : String(error),
181
+ },
182
+ ],
183
+ };
184
+ }
185
+
186
+ const diagnostics: QtiDiagnostic[] = parsed.errors.map((error) => ({
187
+ code: "xml.parse",
188
+ severity: "error",
189
+ message: error.message,
190
+ }));
191
+ if (!parsed.root) {
192
+ diagnostics.push({
193
+ code: "xml.empty",
194
+ severity: "error",
195
+ message: "No XML root element was found.",
196
+ });
197
+ }
198
+ return { root: parsed.root, diagnostics };
199
+ }
200
+
201
+ function normalizedQtiElementName(localName: string): string {
202
+ const lower = localName.toLowerCase();
203
+ return lower.startsWith("qti-") ? lower.slice("qti-".length) : lower;
204
+ }
205
+
206
+ function parseXmlBoolean(value: string | undefined): boolean | undefined {
207
+ if (value === undefined) return undefined;
208
+ const normalized = value.trim().toLowerCase();
209
+ if (normalized === "true" || normalized === "1") return true;
210
+ if (normalized === "false" || normalized === "0") return false;
211
+ return undefined;
212
+ }
213
+
214
+ function isForbiddenDeliveryElement(
215
+ node: XmlNode,
216
+ normalizedName = normalizedQtiElementName(node.localName),
217
+ ): boolean {
218
+ if (FORBIDDEN_DELIVERY_ELEMENT_NAMES.has(normalizedName)) return true;
219
+ return (
220
+ normalizedName === "default-value" &&
221
+ DEFAULT_VALUE_DECLARATION_ELEMENT_NAMES.has(
222
+ normalizedQtiElementName(node.parent?.localName ?? ""),
223
+ )
224
+ );
225
+ }
226
+
227
+ function findingToDiagnostic(finding: QtiDeliverySecurityFinding): QtiDiagnostic {
228
+ const code = diagnosticCodeForFinding(finding.kind);
229
+ return {
230
+ code,
231
+ severity: "error",
232
+ message: finding.message,
233
+ path: finding.source?.path,
234
+ source: finding.source,
235
+ };
236
+ }
237
+
238
+ function diagnosticCodeForFinding(kind: QtiDeliverySecurityFindingKind): string {
239
+ if (kind === "forbidden-delivery-element") return "delivery.forbiddenElement";
240
+ if (kind === "unsupported-adaptive-response-processing") {
241
+ return "delivery.unsupportedAdaptiveResponseProcessing";
242
+ }
243
+ return "delivery.unsupportedSecureDelivery";
244
+ }
245
+
246
+ interface RedactionRange {
247
+ startOffset: number;
248
+ endOffset: number;
249
+ }
250
+
251
+ function readRedactionRanges(nodes: XmlNode[]): RedactionRange[] {
252
+ const ranges = nodes.flatMap((node) => {
253
+ if (!isForbiddenDeliveryElement(node)) return [];
254
+ const endOffset = node.sourceRange.endOffset;
255
+ if (node.sourceRange.startOffset < 0 || endOffset === undefined) return [];
256
+ return [{ startOffset: node.sourceRange.startOffset, endOffset }];
257
+ });
258
+ return mergeSourceRanges(ranges);
259
+ }
260
+
261
+ function mergeSourceRanges(ranges: RedactionRange[]): RedactionRange[] {
262
+ const sorted = [...ranges].sort((left, right) => left.startOffset - right.startOffset);
263
+ const merged: RedactionRange[] = [];
264
+ for (const range of sorted) {
265
+ const last = merged.at(-1);
266
+ if (last && range.startOffset <= last.endOffset) {
267
+ last.endOffset = Math.max(last.endOffset, range.endOffset);
268
+ continue;
269
+ }
270
+ merged.push({ ...range });
271
+ }
272
+ return merged;
273
+ }
274
+
275
+ function removeSourceRanges(xml: string, ranges: RedactionRange[]): string {
276
+ let output = "";
277
+ let cursor = 0;
278
+ for (const range of ranges) {
279
+ output += xml.slice(cursor, range.startOffset);
280
+ cursor = range.endOffset;
281
+ }
282
+ return output + xml.slice(cursor);
283
+ }
package/src/index.ts CHANGED
@@ -5,7 +5,21 @@ export {
5
5
  type QtiResolvedCatalogReference,
6
6
  type QtiResolvedCatalogSupport,
7
7
  } from "./catalog.js";
8
+ export {
9
+ analyzeQtiDeliverySecurity,
10
+ buildQtiDeliverySafeXml,
11
+ type QtiDeliverySafeXmlResult,
12
+ type QtiDeliverySecurityAnalysis,
13
+ type QtiDeliverySecurityFinding,
14
+ type QtiDeliverySecurityFindingKind,
15
+ } from "./delivery-security.js";
8
16
  export { parseQtiXml } from "./parser.js";
17
+ export {
18
+ scoreQtiItemServerSide,
19
+ type QtiServerScoringInput,
20
+ type QtiServerScoringResponseInput,
21
+ type QtiServerScoringResult,
22
+ } from "./server-scoring.js";
9
23
  export {
10
24
  createTextToSpeechTraversal,
11
25
  parseQtiDataSsml,
@@ -84,9 +98,13 @@ export type {
84
98
  QtiValue,
85
99
  } from "./types.js";
86
100
  export {
101
+ isQtiPortableCustomStateValue,
102
+ isQtiValue,
87
103
  qtiScalarToString,
88
104
  qtiValueToIdentifierList,
89
105
  qtiValueToString,
90
106
  qtiValueToStringList,
107
+ readQtiPortableCustomStateValue,
108
+ readQtiJsonValue,
91
109
  unknownToDisplayString,
92
110
  } from "./value-format.js";
package/src/parser.ts CHANGED
@@ -70,6 +70,10 @@ export function parseQtiXml(xml: string): QtiParseResult {
70
70
  });
71
71
  }
72
72
 
73
+ if (tree.errors.length > 0) {
74
+ return { ok: false, diagnostics };
75
+ }
76
+
73
77
  if (!tree.root) {
74
78
  diagnostics.push({
75
79
  code: "xml.empty",