@superbuilders/incept-renderer 0.1.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.
@@ -0,0 +1,1893 @@
1
+ import { createHash } from 'crypto';
2
+ import 'react';
3
+ import 'clsx';
4
+ import 'tailwind-merge';
5
+ import 'react-dom';
6
+ import 'react/jsx-runtime';
7
+ import '@radix-ui/react-radio-group';
8
+ import '@radix-ui/react-checkbox';
9
+ import 'class-variance-authority';
10
+ import '@radix-ui/react-label';
11
+ import '@radix-ui/react-separator';
12
+ import 'lucide-react';
13
+ import '@dnd-kit/core';
14
+ import '@radix-ui/react-select';
15
+ import '@dnd-kit/sortable';
16
+ import '@dnd-kit/utilities';
17
+ import { XMLParser } from 'fast-xml-parser';
18
+ import { z as z$1 } from 'zod';
19
+
20
+ // ../../../../../node_modules/server-only/index.js
21
+ throw new Error(
22
+ "This module cannot be imported from a Client Component module. It should only be used from a Server Component."
23
+ );
24
+
25
+ // ../../node_modules/@superbuilders/errors/dist/index.js
26
+ function z() {
27
+ let k2 = [], j2 = this;
28
+ while (j2 != null) if (k2.push(j2.message), j2.cause instanceof Error) j2 = j2.cause;
29
+ else break;
30
+ return k2.join(": ");
31
+ }
32
+ function A(k2) {
33
+ let j2 = new Error(k2);
34
+ if (Error.captureStackTrace) Error.captureStackTrace(j2, A);
35
+ return j2.toString = z, Object.freeze(j2);
36
+ }
37
+ function B(k2, j2) {
38
+ let x = new Error(j2, { cause: k2 });
39
+ if (Error.captureStackTrace) Error.captureStackTrace(x, B);
40
+ return x.toString = z, Object.freeze(x);
41
+ }
42
+ function I(k2) {
43
+ try {
44
+ return { data: k2(), error: void 0 };
45
+ } catch (j2) {
46
+ return { data: void 0, error: j2 instanceof Error ? j2 : new Error(String(j2)) };
47
+ }
48
+ }
49
+
50
+ // ../../node_modules/@superbuilders/slog/dist/index.js
51
+ var b = new ArrayBuffer(8192);
52
+ var K = new Uint8Array(b);
53
+ var Q = new TextEncoder();
54
+ var C = new ArrayBuffer(64);
55
+ var L = new Uint8Array(C);
56
+ var J = { DEBUG: Q.encode(" DEBUG "), INFO: Q.encode(" INFO "), WARN: Q.encode(" WARN "), ERROR: Q.encode(" ERROR "), NEWLINE: Q.encode(`
57
+ `), SPACE: Q.encode(" "), EQUALS: Q.encode("="), SLASH: Q.encode("/"), COLON: Q.encode(":"), NULL: Q.encode("null"), UNDEFINED: Q.encode("undefined"), TRUE: Q.encode("true"), FALSE: Q.encode("false"), QUOTE: Q.encode('"'), BRACKET_OPEN: Q.encode("["), BRACKET_CLOSE: Q.encode("]"), BRACE_OPEN: Q.encode("{"), BRACE_CLOSE: Q.encode("}"), COMMA: Q.encode(","), ZERO: Q.encode("0"), MINUS: Q.encode("-"), DOT: Q.encode(".") };
58
+ var U = new Uint8Array(19);
59
+ var z2 = new Uint8Array(2);
60
+ var V = 0;
61
+ var F = 0;
62
+ var W = 0;
63
+ function j() {
64
+ let x = Date.now();
65
+ if (x - V >= 1e3) {
66
+ let q = new Date(x), D = 0, R = q.getFullYear();
67
+ U[D++] = 48 + Math.floor(R / 1e3) % 10, U[D++] = 48 + Math.floor(R / 100) % 10, U[D++] = 48 + Math.floor(R / 10) % 10, U[D++] = 48 + R % 10, U[D++] = 47;
68
+ let N = q.getMonth() + 1;
69
+ U[D++] = 48 + Math.floor(N / 10), U[D++] = 48 + N % 10, U[D++] = 47;
70
+ let P = q.getDate();
71
+ U[D++] = 48 + Math.floor(P / 10), U[D++] = 48 + P % 10, U[D++] = 32;
72
+ let M = q.getHours();
73
+ U[D++] = 48 + Math.floor(M / 10), U[D++] = 48 + M % 10, U[D++] = 58;
74
+ let Z = q.getMinutes();
75
+ U[D++] = 48 + Math.floor(Z / 10), U[D++] = 48 + Z % 10, U[D++] = 58, F = D;
76
+ let X = q.getSeconds();
77
+ z2[0] = 48 + Math.floor(X / 10), z2[1] = 48 + X % 10, W = 2, V = x;
78
+ }
79
+ }
80
+ var k = 0;
81
+ function H(x, G, q) {
82
+ let D = 8192 - G;
83
+ if (D <= 0) return G;
84
+ let R = Math.min(q.length, D);
85
+ return x.set(q.subarray(0, R), G), G + R;
86
+ }
87
+ function $(x, G, q) {
88
+ let D = 8192 - G;
89
+ if (D <= 0) return G;
90
+ let R = true, N = q.length;
91
+ for (let M = 0; M < N; M++) if (q.charCodeAt(M) > 127) {
92
+ R = false;
93
+ break;
94
+ }
95
+ if (R) {
96
+ let M = Math.min(N, D);
97
+ for (let Z = 0; Z < M; Z++) x[G + Z] = q.charCodeAt(Z);
98
+ return G + M;
99
+ }
100
+ let P = Q.encodeInto(q, x.subarray(G));
101
+ return G + (P.written ?? 0);
102
+ }
103
+ function A2(x, G, q) {
104
+ if (Number.isNaN(q)) return $(x, G, "NaN");
105
+ if (q === Number.POSITIVE_INFINITY) return $(x, G, "Infinity");
106
+ if (q === Number.NEGATIVE_INFINITY) return $(x, G, "-Infinity");
107
+ let D = G, R = q;
108
+ if (R < 0) D = H(x, D, J.MINUS), R = -R;
109
+ if (R === 0) return H(x, D, J.ZERO);
110
+ if (Number.isInteger(R) && R < Number.MAX_SAFE_INTEGER) {
111
+ let M = Math.floor(R), Z = 0, X = M;
112
+ while (X > 0) Z++, X = Math.floor(X / 10);
113
+ if (D + Z > x.length) return D;
114
+ let Y = D + Z - 1;
115
+ while (M > 0) x[Y--] = 48 + M % 10, M = Math.floor(M / 10);
116
+ return D + Z;
117
+ }
118
+ let N = Q.encodeInto(R.toString(), L), P = Math.min(N.written ?? 0, x.length - G);
119
+ if (P > 0) x.set(L.subarray(0, P), G);
120
+ return G + P;
121
+ }
122
+ function _(x, G, q) {
123
+ if (q === null) return H(x, G, J.NULL);
124
+ if (q === void 0) return H(x, G, J.UNDEFINED);
125
+ switch (typeof q) {
126
+ case "string":
127
+ return $(x, G, q);
128
+ case "number":
129
+ return A2(x, G, q);
130
+ case "boolean":
131
+ return H(x, G, q ? J.TRUE : J.FALSE);
132
+ case "bigint":
133
+ return $(x, G, `${q}`);
134
+ case "symbol":
135
+ return $(x, G, String(q));
136
+ case "function":
137
+ return $(x, G, `${q}`);
138
+ case "object": {
139
+ if (Array.isArray(q)) {
140
+ let X = H(x, G, J.BRACKET_OPEN), Y = q.length;
141
+ for (let O = 0; O < Y; O++) {
142
+ if (O > 0) X = H(x, X, J.COMMA);
143
+ X = _(x, X, q[O]);
144
+ }
145
+ return H(x, X, J.BRACKET_CLOSE);
146
+ }
147
+ let N = q.toString;
148
+ if (typeof N === "function" && N !== Object.prototype.toString) return $(x, G, N.call(q));
149
+ let P = q, M = H(x, G, J.BRACE_OPEN), Z = true;
150
+ for (let X in P) if (Object.hasOwn(P, X)) {
151
+ if (Z === false) M = H(x, M, J.COMMA);
152
+ M = H(x, M, J.QUOTE), M = $(x, M, X), M = H(x, M, J.QUOTE), M = H(x, M, J.COLON), M = _(x, M, P[X]), Z = false;
153
+ }
154
+ return H(x, M, J.BRACE_CLOSE);
155
+ }
156
+ default:
157
+ return $(x, G, `${q}`);
158
+ }
159
+ }
160
+ function E(x, G, q) {
161
+ if (!q) return G;
162
+ let D = G, R = true;
163
+ for (let N in q) {
164
+ if (!R) D = H(x, D, J.SPACE);
165
+ D = $(x, D, N), D = H(x, D, J.EQUALS), D = _(x, D, q[N]), R = false;
166
+ }
167
+ return D;
168
+ }
169
+ function I2(x) {
170
+ let G = K.subarray(0, x);
171
+ process.stderr.write(G);
172
+ }
173
+ function y(x, G) {
174
+ if (-4 < k) return;
175
+ j();
176
+ let q = 0;
177
+ if (q = H(K, q, U.subarray(0, F)), q = H(K, q, z2.subarray(0, W)), q = H(K, q, J.DEBUG), q = $(K, q, x), G) q = H(K, q, J.SPACE), q = E(K, q, G);
178
+ if (q < 8192) q = H(K, q, J.NEWLINE);
179
+ else {
180
+ let D = J.NEWLINE[0];
181
+ if (D !== void 0) K[8191] = D;
182
+ q = 8192;
183
+ }
184
+ I2(q);
185
+ }
186
+ function l(x, G) {
187
+ if (8 < k) return;
188
+ j();
189
+ let q = 0;
190
+ if (q = H(K, q, U.subarray(0, F)), q = H(K, q, z2.subarray(0, W)), q = H(K, q, J.ERROR), q = $(K, q, x), G) q = H(K, q, J.SPACE), q = E(K, q, G);
191
+ if (q < 8192) q = H(K, q, J.NEWLINE);
192
+ else {
193
+ let D = J.NEWLINE[0];
194
+ if (D !== void 0) K[8191] = D;
195
+ q = 8192;
196
+ }
197
+ I2(q);
198
+ }
199
+
200
+ // src/html/sanitize.ts
201
+ var DEFAULT_CONFIG = {
202
+ // HTML content tags
203
+ allowedTags: /* @__PURE__ */ new Set([
204
+ // Text content
205
+ "p",
206
+ "span",
207
+ "div",
208
+ "br",
209
+ "hr",
210
+ // Formatting
211
+ "b",
212
+ "i",
213
+ "u",
214
+ "strong",
215
+ "em",
216
+ "mark",
217
+ "small",
218
+ "sub",
219
+ "sup",
220
+ "code",
221
+ "pre",
222
+ "kbd",
223
+ // Lists
224
+ "ul",
225
+ "ol",
226
+ "li",
227
+ "dl",
228
+ "dt",
229
+ "dd",
230
+ // Tables
231
+ "table",
232
+ "thead",
233
+ "tbody",
234
+ "tfoot",
235
+ "tr",
236
+ "th",
237
+ "td",
238
+ "caption",
239
+ "colgroup",
240
+ "col",
241
+ // Media
242
+ "img",
243
+ "audio",
244
+ "video",
245
+ "source",
246
+ "track",
247
+ // Semantic
248
+ "article",
249
+ "section",
250
+ "nav",
251
+ "aside",
252
+ "header",
253
+ "footer",
254
+ "main",
255
+ "figure",
256
+ "figcaption",
257
+ "blockquote",
258
+ "cite",
259
+ // Links
260
+ "a",
261
+ // Forms (for future interactive elements)
262
+ "label",
263
+ "button"
264
+ ]),
265
+ allowedAttributes: {
266
+ // Global attributes
267
+ "*": /* @__PURE__ */ new Set(["class", "id", "lang", "dir", "title", "style"]),
268
+ // Specific attributes
269
+ img: /* @__PURE__ */ new Set(["src", "alt", "width", "height", "style"]),
270
+ a: /* @__PURE__ */ new Set(["href", "target", "rel"]),
271
+ audio: /* @__PURE__ */ new Set(["src", "controls", "loop", "muted"]),
272
+ video: /* @__PURE__ */ new Set(["src", "controls", "loop", "muted", "width", "height", "poster"]),
273
+ source: /* @__PURE__ */ new Set(["src", "type"]),
274
+ track: /* @__PURE__ */ new Set(["src", "kind", "srclang", "label"])
275
+ },
276
+ allowDataAttributes: false,
277
+ allowMathML: true
278
+ };
279
+ var MATHML_TAGS = /* @__PURE__ */ new Set([
280
+ // Root
281
+ "math",
282
+ // Token elements
283
+ "mi",
284
+ "mn",
285
+ "mo",
286
+ "mtext",
287
+ "mspace",
288
+ "ms",
289
+ // Layout
290
+ "mrow",
291
+ "mfrac",
292
+ "msqrt",
293
+ "mroot",
294
+ "mstyle",
295
+ "merror",
296
+ "mpadded",
297
+ "mphantom",
298
+ "mfenced",
299
+ "menclose",
300
+ // Scripts and limits
301
+ "msub",
302
+ "msup",
303
+ "msubsup",
304
+ "munder",
305
+ "mover",
306
+ "munderover",
307
+ "mmultiscripts",
308
+ "mprescripts",
309
+ "none",
310
+ // Tables
311
+ "mtable",
312
+ "mtr",
313
+ "mtd",
314
+ "maligngroup",
315
+ "malignmark",
316
+ // Elementary math
317
+ "mstack",
318
+ "mlongdiv",
319
+ "msgroup",
320
+ "msrow",
321
+ "mscarries",
322
+ "mscarry",
323
+ "msline",
324
+ // Semantic
325
+ "semantics",
326
+ "annotation",
327
+ "annotation-xml"
328
+ ]);
329
+ function sanitizeHtml(html, config = {}) {
330
+ const cfg = { ...DEFAULT_CONFIG, ...config };
331
+ const dangerousPatterns = [
332
+ /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
333
+ /javascript:/gi,
334
+ /on\w+\s*=/gi,
335
+ // Event handlers
336
+ /<iframe\b/gi,
337
+ /<embed\b/gi,
338
+ /<object\b/gi,
339
+ /data:text\/html/gi
340
+ ];
341
+ let sanitized = html;
342
+ for (const pattern of dangerousPatterns) {
343
+ sanitized = sanitized.replace(pattern, "");
344
+ }
345
+ const cleaned = cleanHtml(sanitized, cfg);
346
+ return cleaned;
347
+ }
348
+ function cleanHtml(html, config) {
349
+ let cleaned = html;
350
+ const tagPattern = /<\/?([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>/g;
351
+ cleaned = cleaned.replace(tagPattern, (match, tagName, _attrs) => {
352
+ const tag = tagName.toLowerCase();
353
+ const isMathML = tag === "math" || cleaned.includes("<math");
354
+ if (isMathML && config.allowMathML && MATHML_TAGS.has(tag)) {
355
+ return match;
356
+ }
357
+ if (config.allowedTags.has(tag)) {
358
+ return cleanAttributesString(match, tag, config);
359
+ }
360
+ return "";
361
+ });
362
+ return cleaned;
363
+ }
364
+ function cleanAttributesString(tagString, tagName, config) {
365
+ const attrPattern = /\s+([a-zA-Z][a-zA-Z0-9-:]*)(?:="([^"]*)"|'([^']*)'|=([^\s>]+)|(?=\s|>))/g;
366
+ let cleanedTag = `<${tagString.startsWith("</") ? "/" : ""}${tagName}`;
367
+ let match = attrPattern.exec(tagString);
368
+ while (match !== null) {
369
+ const attrName = (match[1] || "").toLowerCase();
370
+ const attrValue = match[2] || match[3] || match[4] || "";
371
+ const globalAttrs = config.allowedAttributes["*"] || /* @__PURE__ */ new Set();
372
+ const tagAttrs = config.allowedAttributes[tagName] || /* @__PURE__ */ new Set();
373
+ let isAllowed = globalAttrs.has(attrName) || tagAttrs.has(attrName);
374
+ if (tagName === "img" && (attrName === "width" || attrName === "height" || attrName === "style")) {
375
+ isAllowed = false;
376
+ }
377
+ if (!isAllowed && config.allowDataAttributes && attrName.startsWith("data-")) {
378
+ isAllowed = true;
379
+ }
380
+ if (isAllowed && !isDangerousAttributeValue(attrName, attrValue)) {
381
+ cleanedTag += ` ${attrName}="${attrValue.replace(/"/g, "&quot;")}"`;
382
+ }
383
+ match = attrPattern.exec(tagString);
384
+ }
385
+ cleanedTag += ">";
386
+ return cleanedTag;
387
+ }
388
+ function isDangerousAttributeValue(name, value) {
389
+ const valueLower = value.toLowerCase().trim();
390
+ if (name === "href" || name === "src") {
391
+ if (valueLower.startsWith("javascript:") || valueLower.startsWith("data:text/html")) {
392
+ return true;
393
+ }
394
+ }
395
+ if (valueLower.includes("javascript:") || valueLower.includes("onerror=")) {
396
+ return true;
397
+ }
398
+ return false;
399
+ }
400
+
401
+ // src/html/serialize.ts
402
+ function serializeNodes(nodes) {
403
+ return nodes.map((node) => {
404
+ if (typeof node === "string") {
405
+ return escapeHtml(node);
406
+ }
407
+ return serializeNode(node);
408
+ }).join("");
409
+ }
410
+ function serializeInner(node) {
411
+ return node.children.map((child) => {
412
+ if (typeof child === "string") {
413
+ return escapeHtml(child);
414
+ }
415
+ return serializeNode(child);
416
+ }).join("");
417
+ }
418
+ function serializeNode(node) {
419
+ const selfClosing = ["br", "hr", "img", "input", "meta", "link"];
420
+ const tagName = stripQtiPrefix(node.tagName);
421
+ if (tagName === "math" || isMathMLElement(tagName)) {
422
+ return serializeVerbatim(node);
423
+ }
424
+ const attrs = serializeAttributes(node.attrs);
425
+ const attrString = attrs ? ` ${attrs}` : "";
426
+ if (selfClosing.includes(tagName) && node.children.length === 0) {
427
+ return `<${tagName}${attrString} />`;
428
+ }
429
+ const inner = serializeInner(node);
430
+ return `<${tagName}${attrString}>${inner}</${tagName}>`;
431
+ }
432
+ function stripQtiPrefix(tagName) {
433
+ const qtiToHtml = {
434
+ "qti-item-body": "div",
435
+ "qti-choice-interaction": "div",
436
+ "qti-simple-choice": "div",
437
+ "qti-feedback-block": "div",
438
+ "qti-content-body": "div",
439
+ "qti-prompt": "div",
440
+ "qti-value": "span",
441
+ "qti-variable": "span",
442
+ "qti-correct-response": "div"
443
+ };
444
+ if (qtiToHtml[tagName]) {
445
+ return qtiToHtml[tagName];
446
+ }
447
+ if (tagName.startsWith("qti-")) {
448
+ return tagName.slice(4);
449
+ }
450
+ return tagName;
451
+ }
452
+ function serializeAttributes(attrs) {
453
+ const excludeAttrs = [
454
+ "identifier",
455
+ "response-identifier",
456
+ "shuffle",
457
+ "max-choices",
458
+ "min-choices",
459
+ "cardinality",
460
+ "base-type",
461
+ "outcome-identifier",
462
+ "show-hide",
463
+ "time-dependent",
464
+ "adaptive",
465
+ "xml-lang"
466
+ ];
467
+ const parts = [];
468
+ for (const [key, value] of Object.entries(attrs)) {
469
+ if (excludeAttrs.includes(key)) continue;
470
+ if (value == null) continue;
471
+ const htmlKey = key === "xml:lang" ? "lang" : key;
472
+ const escapedValue = escapeHtml(String(value));
473
+ parts.push(`${htmlKey}="${escapedValue}"`);
474
+ }
475
+ return parts.join(" ");
476
+ }
477
+ function escapeHtml(text) {
478
+ const escapeMap = {
479
+ "&": "&amp;",
480
+ "<": "&lt;",
481
+ ">": "&gt;",
482
+ '"': "&quot;",
483
+ "'": "&#39;"
484
+ };
485
+ return text.replace(/[&<>"']/g, (char) => escapeMap[char] || char);
486
+ }
487
+ function isMathMLElement(tagName) {
488
+ const mathMLTags = [
489
+ "mi",
490
+ "mn",
491
+ "mo",
492
+ "mtext",
493
+ "mspace",
494
+ "ms",
495
+ "mrow",
496
+ "mfrac",
497
+ "msqrt",
498
+ "mroot",
499
+ "mstyle",
500
+ "msub",
501
+ "msup",
502
+ "msubsup",
503
+ "munder",
504
+ "mover",
505
+ "munderover",
506
+ "mtable",
507
+ "mtr",
508
+ "mtd"
509
+ ];
510
+ return mathMLTags.includes(tagName);
511
+ }
512
+ function serializeVerbatim(node) {
513
+ const attrs = Object.entries(node.attrs).map(([key, value]) => {
514
+ if (value == null) return "";
515
+ return ` ${key}="${String(value).replace(/"/g, "&quot;")}"`;
516
+ }).join("");
517
+ if (node.children.length === 0) {
518
+ return `<${node.tagName}${attrs} />`;
519
+ }
520
+ const inner = node.children.map((child) => {
521
+ if (typeof child === "string") {
522
+ return escapeHtml(child);
523
+ }
524
+ return serializeVerbatim(child);
525
+ }).join("");
526
+ return `<${node.tagName}${attrs}>${inner}</${node.tagName}>`;
527
+ }
528
+ var QtiCardinalitySchema = z$1.enum(["single", "multiple", "ordered"]);
529
+ var QtiBaseTypeSchema = z$1.enum(["identifier", "string", "float", "integer", "boolean", "directedPair", "pair"]);
530
+ var SimpleChoiceSchema = z$1.object({
531
+ identifier: z$1.string().min(1),
532
+ contentHtml: z$1.string(),
533
+ // Allows "" for empty or image-only content
534
+ inlineFeedbackHtml: z$1.string().optional()
535
+ // Optional per-choice feedback (qti-feedback-inline)
536
+ });
537
+ var InlineChoiceSchema = z$1.object({
538
+ identifier: z$1.string().min(1),
539
+ contentHtml: z$1.string()
540
+ });
541
+ var ChoiceInteractionCoreSchema = z$1.object({
542
+ responseIdentifier: z$1.string().min(1),
543
+ shuffle: z$1.boolean(),
544
+ minChoices: z$1.number().int().min(0),
545
+ maxChoices: z$1.number().int().min(1),
546
+ promptHtml: z$1.string(),
547
+ choices: z$1.array(SimpleChoiceSchema).min(1)
548
+ });
549
+ var ChoiceInteractionSchema = ChoiceInteractionCoreSchema.extend({ type: z$1.literal("choiceInteraction") });
550
+ var InlineChoiceInteractionCoreSchema = z$1.object({
551
+ responseIdentifier: z$1.string().min(1),
552
+ shuffle: z$1.boolean(),
553
+ choices: z$1.array(InlineChoiceSchema).min(1)
554
+ });
555
+ var InlineChoiceInteractionSchema = InlineChoiceInteractionCoreSchema.extend({
556
+ type: z$1.literal("inlineChoiceInteraction")
557
+ });
558
+ var TextEntryInteractionCoreSchema = z$1.object({
559
+ responseIdentifier: z$1.string().min(1),
560
+ expectedLength: z$1.number().int().min(0).optional(),
561
+ placeholderText: z$1.string().optional(),
562
+ patternMask: z$1.string().optional()
563
+ });
564
+ var TextEntryInteractionSchema = TextEntryInteractionCoreSchema.extend({
565
+ type: z$1.literal("textEntryInteraction")
566
+ });
567
+ var OrderInteractionCoreSchema = z$1.object({
568
+ responseIdentifier: z$1.string().min(1),
569
+ shuffle: z$1.boolean(),
570
+ minChoices: z$1.number().int().min(0),
571
+ maxChoices: z$1.number().int().min(0).optional(),
572
+ // 0 or undefined usually means "all"
573
+ orientation: z$1.enum(["vertical", "horizontal"]),
574
+ promptHtml: z$1.string(),
575
+ choices: z$1.array(SimpleChoiceSchema).min(1)
576
+ });
577
+ var OrderInteractionSchema = OrderInteractionCoreSchema.extend({
578
+ type: z$1.literal("orderInteraction")
579
+ });
580
+ var GapTextSchema = z$1.object({
581
+ identifier: z$1.string().min(1),
582
+ contentHtml: z$1.string(),
583
+ matchMax: z$1.number().int().min(0)
584
+ // 0 = unlimited
585
+ });
586
+ var GapSchema = z$1.object({
587
+ identifier: z$1.string().min(1)
588
+ });
589
+ var GapMatchInteractionCoreSchema = z$1.object({
590
+ responseIdentifier: z$1.string().min(1),
591
+ shuffle: z$1.boolean(),
592
+ gapTexts: z$1.array(GapTextSchema).min(1),
593
+ // Draggable source tokens
594
+ gaps: z$1.array(GapSchema).min(1),
595
+ // Drop target placeholders
596
+ contentHtml: z$1.string()
597
+ // HTML content with gap placeholders
598
+ });
599
+ var GapMatchInteractionSchema = GapMatchInteractionCoreSchema.extend({
600
+ type: z$1.literal("gapMatchInteraction")
601
+ });
602
+ var AssociableChoiceSchema = z$1.object({
603
+ identifier: z$1.string().min(1),
604
+ matchMax: z$1.number().int().min(0),
605
+ // 0 = unlimited uses
606
+ contentHtml: z$1.string()
607
+ });
608
+ var MatchInteractionCoreSchema = z$1.object({
609
+ responseIdentifier: z$1.string().min(1),
610
+ shuffle: z$1.boolean(),
611
+ maxAssociations: z$1.number().int().min(0),
612
+ // 0 = unlimited total associations
613
+ sourceChoices: z$1.array(AssociableChoiceSchema).min(1),
614
+ // First <qti-simple-match-set>
615
+ targetChoices: z$1.array(AssociableChoiceSchema).min(1),
616
+ // Second <qti-simple-match-set>
617
+ promptHtml: z$1.string()
618
+ });
619
+ var MatchInteractionSchema = MatchInteractionCoreSchema.extend({
620
+ type: z$1.literal("matchInteraction")
621
+ });
622
+ var AnyInteractionSchema = z$1.discriminatedUnion("type", [
623
+ ChoiceInteractionSchema,
624
+ InlineChoiceInteractionSchema,
625
+ TextEntryInteractionSchema,
626
+ OrderInteractionSchema,
627
+ GapMatchInteractionSchema,
628
+ MatchInteractionSchema
629
+ ]);
630
+ var CorrectResponseSchema = z$1.object({
631
+ values: z$1.array(z$1.string().min(1)).min(1)
632
+ });
633
+ var ResponseDeclarationSchema = z$1.object({
634
+ identifier: z$1.string().min(1),
635
+ cardinality: QtiCardinalitySchema,
636
+ baseType: QtiBaseTypeSchema,
637
+ correctResponse: CorrectResponseSchema,
638
+ // Optional response mapping for map-response processing (used for summed feedback/score)
639
+ mapping: z$1.object({
640
+ defaultValue: z$1.number().default(0),
641
+ lowerBound: z$1.number().optional(),
642
+ upperBound: z$1.number().optional(),
643
+ entries: z$1.array(
644
+ z$1.object({
645
+ key: z$1.string().min(1),
646
+ value: z$1.number()
647
+ })
648
+ )
649
+ }).optional()
650
+ });
651
+ var OutcomeDefaultValueSchema = z$1.object({
652
+ value: z$1.string()
653
+ });
654
+ var OutcomeDeclarationSchema = z$1.object({
655
+ identifier: z$1.string().min(1),
656
+ cardinality: QtiCardinalitySchema,
657
+ baseType: QtiBaseTypeSchema,
658
+ defaultValue: OutcomeDefaultValueSchema.optional()
659
+ });
660
+ var FeedbackBlockSchema = z$1.object({
661
+ outcomeIdentifier: z$1.string().min(1),
662
+ identifier: z$1.string().min(1),
663
+ showHide: z$1.enum(["show", "hide"]).default("show"),
664
+ contentHtml: z$1.string()
665
+ });
666
+ var StimulusBlockSchema = z$1.object({
667
+ type: z$1.literal("stimulus"),
668
+ html: z$1.string()
669
+ });
670
+ var RichStimulusBlockSchema = z$1.object({
671
+ type: z$1.literal("richStimulus"),
672
+ html: z$1.string(),
673
+ inlineEmbeds: z$1.record(z$1.string(), InlineChoiceInteractionSchema),
674
+ textEmbeds: z$1.record(z$1.string(), TextEntryInteractionSchema)
675
+ });
676
+ var InteractionBlockSchema = z$1.object({
677
+ type: z$1.literal("interaction"),
678
+ interaction: AnyInteractionSchema
679
+ });
680
+ var ItemBodySchema = z$1.object({
681
+ contentBlocks: z$1.array(z$1.union([StimulusBlockSchema, RichStimulusBlockSchema, InteractionBlockSchema])).min(1),
682
+ feedbackBlocks: z$1.array(FeedbackBlockSchema)
683
+ });
684
+ var BaseRuleSchema = z$1.object({
685
+ type: z$1.literal("setOutcomeValue"),
686
+ identifier: z$1.string(),
687
+ value: z$1.string()
688
+ });
689
+ var MatchConditionSchema = z$1.object({
690
+ type: z$1.literal("match"),
691
+ variable: z$1.string(),
692
+ correct: z$1.literal(true)
693
+ // Match against correct response
694
+ });
695
+ var MatchValueConditionSchema = z$1.object({
696
+ type: z$1.literal("matchValue"),
697
+ variable: z$1.string(),
698
+ // The response identifier from <qti-variable>
699
+ value: z$1.string()
700
+ // The target value from <qti-base-value>
701
+ });
702
+ var StringMatchConditionSchema = z$1.object({
703
+ type: z$1.literal("stringMatch"),
704
+ variable: z$1.string(),
705
+ value: z$1.string(),
706
+ caseSensitive: z$1.boolean()
707
+ });
708
+ var MemberConditionSchema = z$1.object({
709
+ type: z$1.literal("member"),
710
+ variable: z$1.string(),
711
+ value: z$1.string()
712
+ });
713
+ var EqualMapResponseConditionSchema = z$1.object({
714
+ type: z$1.literal("equalMapResponse"),
715
+ variable: z$1.string(),
716
+ // The response identifier from <qti-map-response>
717
+ value: z$1.string()
718
+ // The target sum value from <qti-base-value>
719
+ });
720
+ var AndConditionSchema = z$1.object({
721
+ type: z$1.literal("and"),
722
+ conditions: z$1.array(z$1.lazy(() => AnyConditionSchema))
723
+ });
724
+ var OrConditionSchema = z$1.object({
725
+ type: z$1.literal("or"),
726
+ conditions: z$1.array(z$1.lazy(() => AnyConditionSchema))
727
+ });
728
+ var NotConditionSchema = z$1.object({
729
+ type: z$1.literal("not"),
730
+ condition: z$1.lazy(() => AnyConditionSchema)
731
+ });
732
+ var AnyConditionSchema = z$1.union([
733
+ MatchConditionSchema,
734
+ MatchValueConditionSchema,
735
+ StringMatchConditionSchema,
736
+ MemberConditionSchema,
737
+ z$1.lazy(() => AndConditionSchema),
738
+ z$1.lazy(() => OrConditionSchema),
739
+ z$1.lazy(() => NotConditionSchema),
740
+ EqualMapResponseConditionSchema
741
+ ]);
742
+ var ConditionBranchSchema = z$1.object({
743
+ condition: AnyConditionSchema.optional(),
744
+ actions: z$1.array(BaseRuleSchema),
745
+ nestedRules: z$1.array(z$1.lazy(() => ResponseRuleSchema)).optional()
746
+ });
747
+ var ResponseRuleSchema = z$1.union([
748
+ z$1.object({
749
+ type: z$1.literal("condition"),
750
+ branches: z$1.array(ConditionBranchSchema)
751
+ }),
752
+ z$1.object({
753
+ type: z$1.literal("action"),
754
+ action: BaseRuleSchema
755
+ })
756
+ ]);
757
+ var ScoringRuleSchema = z$1.object({
758
+ responseIdentifier: z$1.string().min(1),
759
+ correctScore: z$1.number(),
760
+ incorrectScore: z$1.number()
761
+ });
762
+ var ResponseProcessingSchema = z$1.object({
763
+ rules: z$1.array(ResponseRuleSchema),
764
+ scoring: ScoringRuleSchema
765
+ });
766
+ var AssessmentItemSchema = z$1.object({
767
+ identifier: z$1.string().min(1),
768
+ title: z$1.string().min(1),
769
+ timeDependent: z$1.boolean(),
770
+ xmlLang: z$1.string().min(1),
771
+ responseDeclarations: z$1.array(ResponseDeclarationSchema).min(1),
772
+ outcomeDeclarations: z$1.array(OutcomeDeclarationSchema).min(1),
773
+ itemBody: ItemBodySchema,
774
+ responseProcessing: ResponseProcessingSchema
775
+ });
776
+
777
+ // src/parser.ts
778
+ function createXmlParser() {
779
+ return new XMLParser({
780
+ ignoreAttributes: false,
781
+ attributeNamePrefix: "",
782
+ allowBooleanAttributes: true,
783
+ parseAttributeValue: true,
784
+ parseTagValue: false,
785
+ // CRITICAL: Preserve "000" as string, not convert to number 0
786
+ trimValues: false,
787
+ preserveOrder: true
788
+ });
789
+ }
790
+ function coerceString(value) {
791
+ if (value == null) return "";
792
+ return String(value);
793
+ }
794
+ function isRecord(value) {
795
+ return typeof value === "object" && value !== null;
796
+ }
797
+ function normalizeNode(rawNode) {
798
+ if (rawNode == null) return "";
799
+ if (!isRecord(rawNode)) return coerceString(rawNode);
800
+ const textContent = rawNode["#text"];
801
+ if (textContent != null) {
802
+ return coerceString(textContent);
803
+ }
804
+ const tagName = Object.keys(rawNode).find((k2) => k2 !== ":@");
805
+ if (!tagName) return "";
806
+ const attrsValue = rawNode[":@"];
807
+ const attrs = isRecord(attrsValue) ? attrsValue : {};
808
+ const rawChildren = Array.isArray(rawNode[tagName]) ? rawNode[tagName] : [rawNode[tagName]];
809
+ return {
810
+ tagName,
811
+ attrs,
812
+ children: rawChildren.map(normalizeNode).filter((n) => n !== "")
813
+ };
814
+ }
815
+ function nodeToXml(node) {
816
+ if (typeof node === "string") return node;
817
+ return serializeNode(node);
818
+ }
819
+ function getInnerHtml(node) {
820
+ return serializeInner(node);
821
+ }
822
+ function findChild(node, tagName) {
823
+ const child = node.children.find((c) => typeof c !== "string" && c.tagName === tagName);
824
+ return typeof child === "string" ? void 0 : child;
825
+ }
826
+ function findChildren(node, tagName) {
827
+ return node.children.filter((c) => typeof c !== "string" && c.tagName === tagName);
828
+ }
829
+ function getTextContent(node) {
830
+ if (!node) return "";
831
+ const firstChild = node.children[0];
832
+ if (typeof firstChild === "string") return firstChild.trim();
833
+ return "";
834
+ }
835
+ function findInteractionNodes(node, out = []) {
836
+ if (node.tagName.includes("-interaction")) {
837
+ out.push(node);
838
+ }
839
+ for (const child of node.children) {
840
+ if (typeof child !== "string") {
841
+ findInteractionNodes(child, out);
842
+ }
843
+ }
844
+ return out;
845
+ }
846
+ function escapeAttr(value) {
847
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
848
+ }
849
+ function attrsToString(attrs) {
850
+ const parts = [];
851
+ for (const [key, val] of Object.entries(attrs)) {
852
+ const str = coerceString(val);
853
+ if (str === "") continue;
854
+ parts.push(`${key}="${escapeAttr(str)}"`);
855
+ }
856
+ return parts.length ? ` ${parts.join(" ")}` : "";
857
+ }
858
+ function serializeWithInlinePlaceholders(node, inlineEmbeds, textEmbeds) {
859
+ if (typeof node === "string") return node;
860
+ if (node.tagName === "qti-inline-choice-interaction") {
861
+ const interaction = extractInlineChoiceInteraction(node);
862
+ inlineEmbeds[interaction.responseIdentifier] = interaction;
863
+ return `<span data-qti-inline="${interaction.responseIdentifier}"></span>`;
864
+ }
865
+ if (node.tagName === "qti-text-entry-interaction") {
866
+ const interaction = extractTextEntryInteraction(node);
867
+ textEmbeds[interaction.responseIdentifier] = interaction;
868
+ return `<span data-qti-text-entry="${interaction.responseIdentifier}"></span>`;
869
+ }
870
+ const open = `<${node.tagName}${attrsToString(node.attrs)}>`;
871
+ const childrenHtml = node.children.map((c) => serializeWithInlinePlaceholders(c, inlineEmbeds, textEmbeds)).join("");
872
+ const close = `</${node.tagName}>`;
873
+ return `${open}${childrenHtml}${close}`;
874
+ }
875
+ function extractChoiceInteraction(node) {
876
+ const promptNode = findChild(node, "qti-prompt");
877
+ const promptHtml = promptNode ? getInnerHtml(promptNode) : "";
878
+ const choiceNodes = findChildren(node, "qti-simple-choice");
879
+ const choices = choiceNodes.map((choice) => {
880
+ const inlineFeedbackNode = findChild(choice, "qti-feedback-inline");
881
+ const inlineFeedbackHtml = inlineFeedbackNode ? getInnerHtml(inlineFeedbackNode) : void 0;
882
+ const contentChildren = choice.children.filter(
883
+ (c) => !(typeof c !== "string" && c.tagName === "qti-feedback-inline")
884
+ );
885
+ const contentHtml = serializeNodes(contentChildren);
886
+ return {
887
+ identifier: coerceString(choice.attrs.identifier),
888
+ contentHtml,
889
+ inlineFeedbackHtml
890
+ };
891
+ });
892
+ return {
893
+ type: "choiceInteraction",
894
+ responseIdentifier: coerceString(node.attrs["response-identifier"]),
895
+ shuffle: Boolean(node.attrs.shuffle),
896
+ minChoices: Number(node.attrs["min-choices"] ?? 0),
897
+ maxChoices: Number(node.attrs["max-choices"] ?? 1),
898
+ promptHtml,
899
+ choices
900
+ };
901
+ }
902
+ function extractInlineChoiceInteraction(node) {
903
+ const choiceNodes = findChildren(node, "qti-inline-choice");
904
+ const choices = choiceNodes.map((choice) => {
905
+ const contentHtml = serializeInner(choice);
906
+ return {
907
+ identifier: coerceString(choice.attrs.identifier),
908
+ contentHtml
909
+ };
910
+ });
911
+ return {
912
+ type: "inlineChoiceInteraction",
913
+ responseIdentifier: coerceString(node.attrs["response-identifier"]),
914
+ shuffle: Boolean(node.attrs.shuffle),
915
+ choices
916
+ };
917
+ }
918
+ function extractTextEntryInteraction(node) {
919
+ const expectedLengthAttr = node.attrs["expected-length"];
920
+ const expectedLength = expectedLengthAttr ? Number(expectedLengthAttr) : void 0;
921
+ return {
922
+ type: "textEntryInteraction",
923
+ responseIdentifier: coerceString(node.attrs["response-identifier"]),
924
+ expectedLength: expectedLength !== void 0 && !Number.isNaN(expectedLength) ? expectedLength : void 0,
925
+ placeholderText: coerceString(node.attrs["placeholder-text"]) || void 0,
926
+ patternMask: coerceString(node.attrs["pattern-mask"]) || void 0
927
+ };
928
+ }
929
+ function extractOrderInteraction(node) {
930
+ const promptNode = findChild(node, "qti-prompt");
931
+ const promptHtml = promptNode ? getInnerHtml(promptNode) : "";
932
+ const choiceNodes = findChildren(node, "qti-simple-choice");
933
+ const choices = choiceNodes.map((choice) => {
934
+ const contentChildren = choice.children.filter(
935
+ (c) => !(typeof c !== "string" && c.tagName === "qti-feedback-inline")
936
+ );
937
+ const contentHtml = serializeNodes(contentChildren);
938
+ return {
939
+ identifier: coerceString(choice.attrs.identifier),
940
+ contentHtml,
941
+ // Order interaction rarely displays inline feedback per-item
942
+ inlineFeedbackHtml: void 0
943
+ };
944
+ });
945
+ let orientation = "vertical";
946
+ const attrOrientation = coerceString(node.attrs.orientation);
947
+ const classAttr = coerceString(node.attrs.class);
948
+ if (attrOrientation === "horizontal" || classAttr.includes("qti-orientation-horizontal")) {
949
+ orientation = "horizontal";
950
+ }
951
+ return {
952
+ type: "orderInteraction",
953
+ responseIdentifier: coerceString(node.attrs["response-identifier"]),
954
+ shuffle: Boolean(node.attrs.shuffle),
955
+ minChoices: Number(node.attrs["min-choices"] ?? 0),
956
+ maxChoices: Number(node.attrs["max-choices"] ?? 0) || void 0,
957
+ orientation,
958
+ promptHtml,
959
+ choices
960
+ };
961
+ }
962
+ function extractGapMatchInteraction(node) {
963
+ const gapTextNodes = findChildren(node, "qti-gap-text");
964
+ const gapTexts = gapTextNodes.map((gt) => ({
965
+ identifier: coerceString(gt.attrs.identifier),
966
+ contentHtml: serializeInner(gt),
967
+ matchMax: Number(gt.attrs["match-max"] ?? 0)
968
+ // 0 = unlimited
969
+ }));
970
+ const gaps = [];
971
+ function serializeWithGapPlaceholders(child) {
972
+ if (typeof child === "string") return child;
973
+ if (child.tagName === "qti-gap") {
974
+ const gapId = coerceString(child.attrs.identifier);
975
+ gaps.push({ identifier: gapId });
976
+ return `<span data-qti-gap="${gapId}"></span>`;
977
+ }
978
+ if (child.tagName === "qti-gap-text") {
979
+ return "";
980
+ }
981
+ const open = `<${child.tagName}${attrsToString(child.attrs)}>`;
982
+ const childrenHtml = child.children.map((c) => serializeWithGapPlaceholders(c)).join("");
983
+ const close = `</${child.tagName}>`;
984
+ return `${open}${childrenHtml}${close}`;
985
+ }
986
+ const contentHtml = node.children.filter((c) => typeof c === "string" || c.tagName !== "qti-gap-text").map((c) => serializeWithGapPlaceholders(c)).join("");
987
+ return {
988
+ type: "gapMatchInteraction",
989
+ responseIdentifier: coerceString(node.attrs["response-identifier"]),
990
+ shuffle: Boolean(node.attrs.shuffle),
991
+ gapTexts,
992
+ gaps,
993
+ contentHtml
994
+ };
995
+ }
996
+ function extractMatchInteraction(node) {
997
+ const promptNode = findChild(node, "qti-prompt");
998
+ const promptHtml = promptNode ? getInnerHtml(promptNode) : "";
999
+ const matchSets = findChildren(node, "qti-simple-match-set");
1000
+ const extractChoices = (setNode) => {
1001
+ const choiceNodes = findChildren(setNode, "qti-simple-associable-choice");
1002
+ return choiceNodes.map((c) => ({
1003
+ identifier: coerceString(c.attrs.identifier),
1004
+ matchMax: Number(c.attrs["match-max"]) || 1,
1005
+ contentHtml: serializeInner(c)
1006
+ }));
1007
+ };
1008
+ const firstSet = matchSets[0];
1009
+ const secondSet = matchSets[1];
1010
+ const sourceChoices = firstSet ? extractChoices(firstSet) : [];
1011
+ const targetChoices = secondSet ? extractChoices(secondSet) : [];
1012
+ return {
1013
+ type: "matchInteraction",
1014
+ responseIdentifier: coerceString(node.attrs["response-identifier"]),
1015
+ shuffle: node.attrs.shuffle === "true",
1016
+ maxAssociations: Number(node.attrs["max-associations"]) || 0,
1017
+ sourceChoices,
1018
+ targetChoices,
1019
+ promptHtml
1020
+ };
1021
+ }
1022
+ function extractFeedbackBlock(node) {
1023
+ const contentBodyNode = findChild(node, "qti-content-body");
1024
+ const contentHtml = contentBodyNode ? getInnerHtml(contentBodyNode) : "";
1025
+ const showHide = coerceString(node.attrs["show-hide"]) === "hide" ? "hide" : "show";
1026
+ return {
1027
+ outcomeIdentifier: coerceString(node.attrs["outcome-identifier"]),
1028
+ identifier: coerceString(node.attrs.identifier),
1029
+ showHide,
1030
+ contentHtml
1031
+ };
1032
+ }
1033
+ function extractResponseDeclarations(rootChildren) {
1034
+ const declNodes = rootChildren.filter((n) => n.tagName === "qti-response-declaration");
1035
+ return declNodes.map((node) => {
1036
+ const correctResponseNode = findChild(node, "qti-correct-response");
1037
+ const valueNodes = correctResponseNode ? findChildren(correctResponseNode, "qti-value") : [];
1038
+ const values = valueNodes.map(getTextContent);
1039
+ const mappingNode = findChild(node, "qti-mapping");
1040
+ let mapping;
1041
+ if (mappingNode) {
1042
+ const defaultValueRaw = coerceString(mappingNode.attrs["default-value"]);
1043
+ const lowerBoundRaw = coerceString(mappingNode.attrs["lower-bound"]);
1044
+ const upperBoundRaw = coerceString(mappingNode.attrs["upper-bound"]);
1045
+ const entryNodes = findChildren(mappingNode, "qti-map-entry");
1046
+ const entries = entryNodes.map((en) => {
1047
+ return {
1048
+ key: coerceString(en.attrs["map-key"]),
1049
+ value: Number(coerceString(en.attrs["mapped-value"]) || 0)
1050
+ };
1051
+ });
1052
+ mapping = {
1053
+ defaultValue: Number(defaultValueRaw || 0),
1054
+ lowerBound: lowerBoundRaw !== "" ? Number(lowerBoundRaw) : void 0,
1055
+ upperBound: upperBoundRaw !== "" ? Number(upperBoundRaw) : void 0,
1056
+ entries
1057
+ };
1058
+ }
1059
+ const cardinalityRaw = coerceString(node.attrs.cardinality);
1060
+ const cardinalityResult = QtiCardinalitySchema.safeParse(cardinalityRaw);
1061
+ if (!cardinalityResult.success) {
1062
+ throw A(`invalid cardinality '${cardinalityRaw}' in response declaration`);
1063
+ }
1064
+ const baseTypeRaw = coerceString(node.attrs["base-type"]);
1065
+ const baseTypeResult = QtiBaseTypeSchema.safeParse(baseTypeRaw);
1066
+ if (!baseTypeResult.success) {
1067
+ throw A(`invalid base-type '${baseTypeRaw}' in response declaration`);
1068
+ }
1069
+ return {
1070
+ identifier: coerceString(node.attrs.identifier),
1071
+ cardinality: cardinalityResult.data,
1072
+ baseType: baseTypeResult.data,
1073
+ correctResponse: { values },
1074
+ mapping
1075
+ };
1076
+ });
1077
+ }
1078
+ function extractOutcomeDeclarations(rootChildren) {
1079
+ const declNodes = rootChildren.filter((n) => n.tagName === "qti-outcome-declaration");
1080
+ return declNodes.map((node) => {
1081
+ const defaultValueNode = findChild(node, "qti-default-value");
1082
+ const valueNode = defaultValueNode ? findChild(defaultValueNode, "qti-value") : void 0;
1083
+ const defaultValue = valueNode ? { value: getTextContent(valueNode) } : void 0;
1084
+ const cardinalityRaw = coerceString(node.attrs.cardinality);
1085
+ const cardinalityResult = QtiCardinalitySchema.safeParse(cardinalityRaw);
1086
+ if (!cardinalityResult.success) {
1087
+ throw A(`invalid cardinality '${cardinalityRaw}' in outcome declaration`);
1088
+ }
1089
+ const baseTypeRaw = coerceString(node.attrs["base-type"]);
1090
+ const baseTypeResult = QtiBaseTypeSchema.safeParse(baseTypeRaw);
1091
+ if (!baseTypeResult.success) {
1092
+ throw A(`invalid base-type '${baseTypeRaw}' in outcome declaration`);
1093
+ }
1094
+ return {
1095
+ identifier: coerceString(node.attrs.identifier),
1096
+ cardinality: cardinalityResult.data,
1097
+ baseType: baseTypeResult.data,
1098
+ defaultValue
1099
+ };
1100
+ });
1101
+ }
1102
+ function parseCondition(node) {
1103
+ if (node.tagName === "qti-and") {
1104
+ const conditions = [];
1105
+ for (const child of node.children) {
1106
+ if (typeof child !== "string") {
1107
+ const parsed = parseCondition(child);
1108
+ if (parsed) conditions.push(parsed);
1109
+ }
1110
+ }
1111
+ if (conditions.length === 0) return null;
1112
+ return { type: "and", conditions };
1113
+ }
1114
+ if (node.tagName === "qti-or") {
1115
+ const conditions = [];
1116
+ for (const child of node.children) {
1117
+ if (typeof child !== "string") {
1118
+ const parsed = parseCondition(child);
1119
+ if (parsed) conditions.push(parsed);
1120
+ }
1121
+ }
1122
+ if (conditions.length === 0) return null;
1123
+ return { type: "or", conditions };
1124
+ }
1125
+ if (node.tagName === "qti-not") {
1126
+ for (const child of node.children) {
1127
+ if (typeof child !== "string") {
1128
+ const parsed = parseCondition(child);
1129
+ if (parsed) return { type: "not", condition: parsed };
1130
+ }
1131
+ }
1132
+ return null;
1133
+ }
1134
+ const variableNode = findChild(node, "qti-variable");
1135
+ const baseValueNode = findChild(node, "qti-base-value");
1136
+ const identifier = variableNode ? coerceString(variableNode.attrs.identifier) : void 0;
1137
+ const value = baseValueNode ? getTextContent(baseValueNode) : void 0;
1138
+ if (node.tagName === "qti-match") {
1139
+ if (findChild(node, "qti-correct") && identifier) {
1140
+ return { type: "match", variable: identifier, correct: true };
1141
+ }
1142
+ if (identifier && value !== void 0) {
1143
+ return { type: "matchValue", variable: identifier, value };
1144
+ }
1145
+ }
1146
+ if (node.tagName === "qti-string-match") {
1147
+ if (identifier && value !== void 0) {
1148
+ return {
1149
+ type: "stringMatch",
1150
+ variable: identifier,
1151
+ value,
1152
+ // QTI spec: case-sensitive defaults to true
1153
+ caseSensitive: node.attrs["case-sensitive"] !== "false"
1154
+ };
1155
+ }
1156
+ }
1157
+ if (node.tagName === "qti-member") {
1158
+ if (identifier && value !== void 0) {
1159
+ return { type: "member", variable: identifier, value };
1160
+ }
1161
+ }
1162
+ if (node.tagName === "qti-equal") {
1163
+ const mapResponseNode = findChild(node, "qti-map-response");
1164
+ const equalBaseValueNode = findChild(node, "qti-base-value");
1165
+ if (mapResponseNode && equalBaseValueNode) {
1166
+ return {
1167
+ type: "equalMapResponse",
1168
+ variable: coerceString(mapResponseNode.attrs.identifier),
1169
+ value: getTextContent(equalBaseValueNode)
1170
+ };
1171
+ }
1172
+ if (findChild(node, "qti-correct") && identifier) {
1173
+ return { type: "match", variable: identifier, correct: true };
1174
+ }
1175
+ }
1176
+ return null;
1177
+ }
1178
+ function parseActions(node) {
1179
+ const rules = [];
1180
+ const setNodes = findChildren(node, "qti-set-outcome-value");
1181
+ for (const setNode of setNodes) {
1182
+ const valueNode = findChild(setNode, "qti-base-value");
1183
+ if (valueNode) {
1184
+ rules.push({
1185
+ type: "setOutcomeValue",
1186
+ identifier: coerceString(setNode.attrs.identifier),
1187
+ value: getTextContent(valueNode)
1188
+ });
1189
+ }
1190
+ }
1191
+ return rules;
1192
+ }
1193
+ function parseResponseRule(node) {
1194
+ if (node.tagName === "qti-set-outcome-value") {
1195
+ const valueNode = findChild(node, "qti-base-value");
1196
+ if (valueNode) {
1197
+ return {
1198
+ type: "action",
1199
+ action: {
1200
+ type: "setOutcomeValue",
1201
+ identifier: coerceString(node.attrs.identifier),
1202
+ value: getTextContent(valueNode)
1203
+ }
1204
+ };
1205
+ }
1206
+ return null;
1207
+ }
1208
+ if (node.tagName === "qti-response-condition") {
1209
+ const branches = [];
1210
+ const ifNode = findChild(node, "qti-response-if");
1211
+ if (ifNode) {
1212
+ const condition = findConditionInBranch(ifNode);
1213
+ if (condition) {
1214
+ branches.push({
1215
+ condition,
1216
+ actions: parseActions(ifNode),
1217
+ nestedRules: findNestedRules(ifNode)
1218
+ });
1219
+ }
1220
+ }
1221
+ const elseIfNodes = findChildren(node, "qti-response-else-if");
1222
+ for (const elseIfNode of elseIfNodes) {
1223
+ const condition = findConditionInBranch(elseIfNode);
1224
+ if (condition) {
1225
+ branches.push({
1226
+ condition,
1227
+ actions: parseActions(elseIfNode),
1228
+ nestedRules: findNestedRules(elseIfNode)
1229
+ });
1230
+ }
1231
+ }
1232
+ const elseNode = findChild(node, "qti-response-else");
1233
+ if (elseNode) {
1234
+ branches.push({
1235
+ condition: void 0,
1236
+ // Else has no condition
1237
+ actions: parseActions(elseNode),
1238
+ nestedRules: findNestedRules(elseNode)
1239
+ });
1240
+ }
1241
+ if (branches.length > 0) {
1242
+ return {
1243
+ type: "condition",
1244
+ branches
1245
+ };
1246
+ }
1247
+ }
1248
+ return null;
1249
+ }
1250
+ function findConditionInBranch(node) {
1251
+ const conditionTagNames = ["qti-match", "qti-and", "qti-or", "qti-not", "qti-equal", "qti-string-match", "qti-member"];
1252
+ for (const child of node.children) {
1253
+ if (typeof child !== "string" && conditionTagNames.includes(child.tagName)) {
1254
+ return parseCondition(child);
1255
+ }
1256
+ }
1257
+ return null;
1258
+ }
1259
+ function findNestedRules(node) {
1260
+ const nested = [];
1261
+ for (const child of node.children) {
1262
+ if (typeof child !== "string" && child.tagName === "qti-response-condition") {
1263
+ const rule = parseResponseRule(child);
1264
+ if (rule) nested.push(rule);
1265
+ }
1266
+ }
1267
+ return nested;
1268
+ }
1269
+ function extractResponseProcessing(rootChildren) {
1270
+ const processingNode = rootChildren.find((n) => n.tagName === "qti-response-processing");
1271
+ if (!processingNode) {
1272
+ return { rules: [], scoring: { responseIdentifier: "", correctScore: 1, incorrectScore: 0 } };
1273
+ }
1274
+ const rules = [];
1275
+ for (const child of processingNode.children) {
1276
+ if (typeof child !== "string") {
1277
+ const rule = parseResponseRule(child);
1278
+ if (rule) rules.push(rule);
1279
+ }
1280
+ }
1281
+ let scoring = { responseIdentifier: "", correctScore: 1, incorrectScore: 0 };
1282
+ const conditionNodes = findChildren(processingNode, "qti-response-condition");
1283
+ for (const conditionNode of conditionNodes) {
1284
+ const responseIfNode = findChild(conditionNode, "qti-response-if");
1285
+ const responseElseNode = findChild(conditionNode, "qti-response-else");
1286
+ if (responseIfNode && responseElseNode) {
1287
+ const andNode = findChild(responseIfNode, "qti-and");
1288
+ if (andNode) {
1289
+ const matchNode = findChild(andNode, "qti-match");
1290
+ const variableNode = matchNode ? findChild(matchNode, "qti-variable") : void 0;
1291
+ if (variableNode) {
1292
+ const ifSetOutcomeNode = findChild(responseIfNode, "qti-set-outcome-value");
1293
+ const elseSetOutcomeNode = findChild(responseElseNode, "qti-set-outcome-value");
1294
+ const correctValueNode = ifSetOutcomeNode ? findChild(ifSetOutcomeNode, "qti-base-value") : void 0;
1295
+ const incorrectValueNode = elseSetOutcomeNode ? findChild(elseSetOutcomeNode, "qti-base-value") : void 0;
1296
+ scoring = {
1297
+ responseIdentifier: coerceString(variableNode.attrs.identifier),
1298
+ correctScore: Number(getTextContent(correctValueNode) || 1),
1299
+ incorrectScore: Number(getTextContent(incorrectValueNode) || 0)
1300
+ };
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ return { rules, scoring };
1306
+ }
1307
+ function parseAssessmentItemXml(xml) {
1308
+ if (!xml || typeof xml !== "string") {
1309
+ throw A("xml input must be a non-empty string");
1310
+ }
1311
+ const parser = createXmlParser();
1312
+ const parseResult = I(() => {
1313
+ return parser.parse(xml, true);
1314
+ });
1315
+ if (parseResult.error) {
1316
+ throw B(parseResult.error, "xml parse");
1317
+ }
1318
+ const raw = parseResult.data;
1319
+ if (!Array.isArray(raw)) {
1320
+ throw A("expected xml parser to output an array for preserveOrder");
1321
+ }
1322
+ const normalizedTree = raw.map(normalizeNode).filter((n) => typeof n !== "string");
1323
+ const rootNode = normalizedTree.find((n) => n.tagName.endsWith("assessment-item"));
1324
+ if (!rootNode) {
1325
+ throw A("qti assessment item not found in xml document");
1326
+ }
1327
+ const rootChildren = rootNode.children.filter((c) => typeof c !== "string");
1328
+ const itemBodyNode = rootChildren.find((n) => n.tagName === "qti-item-body");
1329
+ const contentBlocks = [];
1330
+ const feedbackBlocks = [];
1331
+ if (itemBodyNode) {
1332
+ for (const child of itemBodyNode.children) {
1333
+ if (typeof child === "string") {
1334
+ if (child.trim()) contentBlocks.push({ type: "stimulus", html: child });
1335
+ continue;
1336
+ }
1337
+ if (child.tagName === "qti-feedback-block") {
1338
+ feedbackBlocks.push(extractFeedbackBlock(child));
1339
+ } else {
1340
+ const interactionNodes = findInteractionNodes(child);
1341
+ const hasInline = interactionNodes.some(
1342
+ (n) => n.tagName === "qti-inline-choice-interaction" || n.tagName === "qti-text-entry-interaction"
1343
+ );
1344
+ if (hasInline) {
1345
+ const inlineEmbeds = {};
1346
+ const textEmbeds = {};
1347
+ const html = serializeWithInlinePlaceholders(child, inlineEmbeds, textEmbeds);
1348
+ contentBlocks.push({ type: "richStimulus", html, inlineEmbeds, textEmbeds });
1349
+ continue;
1350
+ }
1351
+ if (interactionNodes.length > 0) {
1352
+ for (const inode of interactionNodes) {
1353
+ if (inode.tagName === "qti-choice-interaction") {
1354
+ const interaction = extractChoiceInteraction(inode);
1355
+ contentBlocks.push({ type: "interaction", interaction });
1356
+ } else if (inode.tagName === "qti-text-entry-interaction") {
1357
+ const interaction = extractTextEntryInteraction(inode);
1358
+ contentBlocks.push({ type: "interaction", interaction });
1359
+ } else if (inode.tagName === "qti-order-interaction") {
1360
+ const interaction = extractOrderInteraction(inode);
1361
+ contentBlocks.push({ type: "interaction", interaction });
1362
+ } else if (inode.tagName === "qti-gap-match-interaction") {
1363
+ const interaction = extractGapMatchInteraction(inode);
1364
+ contentBlocks.push({ type: "interaction", interaction });
1365
+ } else if (inode.tagName === "qti-match-interaction") {
1366
+ const interaction = extractMatchInteraction(inode);
1367
+ contentBlocks.push({ type: "interaction", interaction });
1368
+ } else {
1369
+ contentBlocks.push({ type: "stimulus", html: nodeToXml(inode) });
1370
+ }
1371
+ }
1372
+ continue;
1373
+ }
1374
+ contentBlocks.push({ type: "stimulus", html: nodeToXml(child) });
1375
+ }
1376
+ }
1377
+ }
1378
+ let normalizedItem = {
1379
+ identifier: coerceString(rootNode.attrs.identifier),
1380
+ title: coerceString(rootNode.attrs.title),
1381
+ timeDependent: Boolean(rootNode.attrs["time-dependent"]),
1382
+ xmlLang: coerceString(rootNode.attrs["xml:lang"]) || coerceString(rootNode.attrs["xml-lang"]) || "en",
1383
+ responseDeclarations: extractResponseDeclarations(rootChildren),
1384
+ outcomeDeclarations: extractOutcomeDeclarations(rootChildren),
1385
+ itemBody: { contentBlocks, feedbackBlocks },
1386
+ responseProcessing: extractResponseProcessing(rootChildren)
1387
+ };
1388
+ if (normalizedItem.responseProcessing.scoring.responseIdentifier === "" && normalizedItem.responseDeclarations.length > 0) {
1389
+ const firstDecl = normalizedItem.responseDeclarations[0];
1390
+ if (firstDecl) {
1391
+ normalizedItem.responseProcessing.scoring.responseIdentifier = firstDecl.identifier;
1392
+ }
1393
+ }
1394
+ const validation = AssessmentItemSchema.safeParse(normalizedItem);
1395
+ if (!validation.success) {
1396
+ const errorDetails = validation.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
1397
+ throw A(`qti item validation: ${errorDetails}`);
1398
+ }
1399
+ return validation.data;
1400
+ }
1401
+
1402
+ // src/evaluator.ts
1403
+ function normalizeString(str, caseSensitive) {
1404
+ const s = (str ?? "").trim();
1405
+ return caseSensitive ? s : s.toLowerCase();
1406
+ }
1407
+ function checkCondition(condition, item, responses, responseResults) {
1408
+ if (condition.type === "and") {
1409
+ const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
1410
+ const allTrue = results.every((r) => r === true);
1411
+ y("qti evaluator: checking AND condition", {
1412
+ numConditions: condition.conditions.length,
1413
+ results,
1414
+ allTrue
1415
+ });
1416
+ return allTrue;
1417
+ }
1418
+ if (condition.type === "or") {
1419
+ const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
1420
+ const anyTrue = results.some((r) => r === true);
1421
+ y("qti evaluator: checking OR condition", {
1422
+ numConditions: condition.conditions.length,
1423
+ results,
1424
+ anyTrue
1425
+ });
1426
+ return anyTrue;
1427
+ }
1428
+ if (condition.type === "not") {
1429
+ const result = checkCondition(condition.condition, item, responses, responseResults);
1430
+ y("qti evaluator: checking NOT condition", { result, negated: !result });
1431
+ return !result;
1432
+ }
1433
+ if (condition.type === "match") {
1434
+ const result = responseResults[condition.variable];
1435
+ y("qti evaluator: checking match condition", {
1436
+ variable: condition.variable,
1437
+ isCorrect: result,
1438
+ matches: result === true
1439
+ });
1440
+ return result === true;
1441
+ }
1442
+ if (condition.type === "matchValue") {
1443
+ const userResponse = responses[condition.variable];
1444
+ let userValues;
1445
+ if (Array.isArray(userResponse)) {
1446
+ userValues = userResponse;
1447
+ } else if (userResponse) {
1448
+ userValues = [userResponse];
1449
+ } else {
1450
+ userValues = [];
1451
+ }
1452
+ const matches = userValues.includes(condition.value);
1453
+ y("qti evaluator: checking matchValue condition", {
1454
+ variable: condition.variable,
1455
+ targetValue: condition.value,
1456
+ userValues,
1457
+ matches
1458
+ });
1459
+ return matches;
1460
+ }
1461
+ if (condition.type === "stringMatch") {
1462
+ const userResponse = responses[condition.variable];
1463
+ const val = Array.isArray(userResponse) ? userResponse[0] : userResponse;
1464
+ if (!val) return false;
1465
+ const u = normalizeString(String(val), condition.caseSensitive);
1466
+ const t = normalizeString(condition.value, condition.caseSensitive);
1467
+ const matches = u === t;
1468
+ y("qti evaluator: checking stringMatch", {
1469
+ variable: condition.variable,
1470
+ caseSensitive: condition.caseSensitive,
1471
+ matches
1472
+ });
1473
+ return matches;
1474
+ }
1475
+ if (condition.type === "member") {
1476
+ const userResponse = responses[condition.variable];
1477
+ let userValues;
1478
+ if (Array.isArray(userResponse)) {
1479
+ userValues = userResponse;
1480
+ } else if (userResponse) {
1481
+ userValues = [userResponse];
1482
+ } else {
1483
+ userValues = [];
1484
+ }
1485
+ const matches = userValues.includes(condition.value);
1486
+ y("qti evaluator: checking member", {
1487
+ variable: condition.variable,
1488
+ target: condition.value,
1489
+ userValues,
1490
+ matches
1491
+ });
1492
+ return matches;
1493
+ }
1494
+ if (condition.type === "equalMapResponse") {
1495
+ const responseId = condition.variable;
1496
+ const userResponse = responses[responseId];
1497
+ const responseDecl = item.responseDeclarations.find((rd) => rd.identifier === responseId);
1498
+ if (!userResponse || !responseDecl?.mapping) return false;
1499
+ const userValues = Array.isArray(userResponse) ? userResponse : [userResponse];
1500
+ const valueMap = new Map(responseDecl.mapping.entries.map((e) => [e.key, e.value]));
1501
+ let sum = 0;
1502
+ for (const val of userValues) {
1503
+ sum += valueMap.get(val) ?? responseDecl.mapping.defaultValue;
1504
+ }
1505
+ const target = Number(condition.value);
1506
+ const diff = Math.abs(sum - target);
1507
+ const matches = diff < 1e-4;
1508
+ y("qti evaluator: checking equalMapResponse", {
1509
+ responseId,
1510
+ userValues,
1511
+ sum,
1512
+ target: condition.value,
1513
+ matches
1514
+ });
1515
+ return matches;
1516
+ }
1517
+ return false;
1518
+ }
1519
+ function evaluateRule(rule, item, responses, responseResults) {
1520
+ const feedbackIds = [];
1521
+ if (rule.type === "action") {
1522
+ const action = rule.action;
1523
+ if (action.type === "setOutcomeValue" && (action.identifier === "FEEDBACK__OVERALL" || action.identifier === "FEEDBACK__PEDAGOGY" || action.identifier === "FEEDBACK")) {
1524
+ y("qti evaluator: executing action", { id: action.identifier, value: action.value });
1525
+ feedbackIds.push(action.value);
1526
+ }
1527
+ return feedbackIds;
1528
+ }
1529
+ if (rule.type === "condition") {
1530
+ for (const branch of rule.branches) {
1531
+ const isMatch = branch.condition ? checkCondition(branch.condition, item, responses, responseResults) : true;
1532
+ y("qti evaluator: evaluating branch", {
1533
+ hasCondition: !!branch.condition,
1534
+ isMatch
1535
+ });
1536
+ if (isMatch) {
1537
+ const feedbackActions = branch.actions.filter(
1538
+ (r) => r.type === "setOutcomeValue" && (r.identifier === "FEEDBACK__OVERALL" || r.identifier === "FEEDBACK__PEDAGOGY" || r.identifier === "FEEDBACK")
1539
+ );
1540
+ for (const action of feedbackActions) {
1541
+ feedbackIds.push(action.value);
1542
+ }
1543
+ if (branch.nestedRules) {
1544
+ for (const nested of branch.nestedRules) {
1545
+ const nestedIds = evaluateRule(nested, item, responses, responseResults);
1546
+ feedbackIds.push(...nestedIds);
1547
+ }
1548
+ }
1549
+ break;
1550
+ }
1551
+ }
1552
+ }
1553
+ return feedbackIds;
1554
+ }
1555
+ function evaluateFeedbackIdentifiers(item, responses, responseResults) {
1556
+ const processing = item.responseProcessing;
1557
+ if (!processing) {
1558
+ y("qti evaluator: no response processing found");
1559
+ return [];
1560
+ }
1561
+ y("qti evaluator: starting evaluation", {
1562
+ numRules: processing.rules.length,
1563
+ responseResults
1564
+ });
1565
+ const allFeedbackIds = [];
1566
+ for (const rule of processing.rules) {
1567
+ const ids = evaluateRule(rule, item, responses, responseResults);
1568
+ allFeedbackIds.push(...ids);
1569
+ }
1570
+ y("qti evaluator: selected feedback identifiers", { feedbackIds: allFeedbackIds });
1571
+ return allFeedbackIds;
1572
+ }
1573
+
1574
+ // src/actions/internal/display.ts
1575
+ function shuffleArray(items) {
1576
+ const arr = items.slice();
1577
+ for (let i = arr.length - 1; i > 0; i--) {
1578
+ const j2 = Math.floor(Math.random() * (i + 1));
1579
+ const vi = arr[i];
1580
+ const vj = arr[j2];
1581
+ if (vi === void 0 || vj === void 0) {
1582
+ continue;
1583
+ }
1584
+ arr[i] = vj;
1585
+ arr[j2] = vi;
1586
+ }
1587
+ return arr;
1588
+ }
1589
+ function buildDisplayModelFromXml(qtiXml) {
1590
+ y("qti build display model", {});
1591
+ const parseResult = I(() => parseAssessmentItemXml(qtiXml));
1592
+ if (parseResult.error) {
1593
+ l("qti parse failed", { error: parseResult.error });
1594
+ throw B(parseResult.error, "qti parse");
1595
+ }
1596
+ const item = parseResult.data;
1597
+ const shape = {};
1598
+ for (const rd of item.responseDeclarations) {
1599
+ if (rd.baseType === "directedPair" || rd.baseType === "pair") {
1600
+ shape[rd.identifier] = "directedPair";
1601
+ } else if (rd.cardinality === "single") {
1602
+ shape[rd.identifier] = "single";
1603
+ } else if (rd.cardinality === "ordered") {
1604
+ shape[rd.identifier] = "ordered";
1605
+ } else {
1606
+ shape[rd.identifier] = "multiple";
1607
+ }
1608
+ }
1609
+ const contentBlocks = item.itemBody.contentBlocks.map((block) => {
1610
+ if (block.type === "stimulus") {
1611
+ return { type: "stimulus", html: sanitizeHtml(block.html) };
1612
+ }
1613
+ if (block.type === "richStimulus") {
1614
+ const html = sanitizeHtml(block.html, { allowDataAttributes: true });
1615
+ const inlineEmbeds = {};
1616
+ for (const [key, embed] of Object.entries(block.inlineEmbeds)) {
1617
+ inlineEmbeds[key] = {
1618
+ responseId: embed.responseIdentifier,
1619
+ shuffle: embed.shuffle,
1620
+ choices: embed.choices.map((c) => ({
1621
+ id: c.identifier,
1622
+ contentHtml: sanitizeHtml(c.contentHtml)
1623
+ }))
1624
+ };
1625
+ }
1626
+ const textEmbeds = {};
1627
+ for (const [key, embed] of Object.entries(block.textEmbeds)) {
1628
+ textEmbeds[key] = {
1629
+ responseId: embed.responseIdentifier,
1630
+ expectedLength: embed.expectedLength,
1631
+ placeholder: embed.placeholderText,
1632
+ patternMask: embed.patternMask
1633
+ };
1634
+ }
1635
+ return { type: "richStimulus", html, inlineEmbeds, textEmbeds };
1636
+ }
1637
+ const interaction = block.interaction;
1638
+ if (interaction.type === "choiceInteraction") {
1639
+ const promptHtml = sanitizeHtml(interaction.promptHtml);
1640
+ let choices = interaction.choices.map((c) => ({
1641
+ id: c.identifier,
1642
+ contentHtml: sanitizeHtml(c.contentHtml),
1643
+ inlineFeedbackHtml: c.inlineFeedbackHtml ? sanitizeHtml(c.inlineFeedbackHtml) : void 0
1644
+ }));
1645
+ if (interaction.shuffle === true) {
1646
+ choices = shuffleArray(choices);
1647
+ }
1648
+ const cardinality = shape[interaction.responseIdentifier] === "multiple" ? "multiple" : "single";
1649
+ return {
1650
+ type: "interaction",
1651
+ interaction: {
1652
+ type: "choice",
1653
+ responseId: interaction.responseIdentifier,
1654
+ cardinality,
1655
+ promptHtml,
1656
+ choices,
1657
+ minChoices: interaction.minChoices,
1658
+ maxChoices: interaction.maxChoices
1659
+ }
1660
+ };
1661
+ }
1662
+ if (interaction.type === "textEntryInteraction") {
1663
+ return {
1664
+ type: "interaction",
1665
+ interaction: {
1666
+ type: "text",
1667
+ responseId: interaction.responseIdentifier,
1668
+ expectedLength: interaction.expectedLength,
1669
+ placeholder: interaction.placeholderText,
1670
+ patternMask: interaction.patternMask
1671
+ }
1672
+ };
1673
+ }
1674
+ if (interaction.type === "orderInteraction") {
1675
+ let choices = interaction.choices.map((c) => ({
1676
+ id: c.identifier,
1677
+ contentHtml: sanitizeHtml(c.contentHtml),
1678
+ inlineFeedbackHtml: void 0
1679
+ }));
1680
+ if (interaction.shuffle === true) {
1681
+ choices = shuffleArray(choices);
1682
+ }
1683
+ return {
1684
+ type: "interaction",
1685
+ interaction: {
1686
+ type: "order",
1687
+ responseId: interaction.responseIdentifier,
1688
+ promptHtml: sanitizeHtml(interaction.promptHtml),
1689
+ orientation: interaction.orientation,
1690
+ choices
1691
+ }
1692
+ };
1693
+ }
1694
+ if (interaction.type === "gapMatchInteraction") {
1695
+ let gapTexts = interaction.gapTexts.map((gt) => ({
1696
+ id: gt.identifier,
1697
+ contentHtml: sanitizeHtml(gt.contentHtml),
1698
+ matchMax: gt.matchMax
1699
+ }));
1700
+ if (interaction.shuffle === true) {
1701
+ gapTexts = shuffleArray(gapTexts);
1702
+ }
1703
+ const gaps = interaction.gaps.map((g) => ({
1704
+ id: g.identifier
1705
+ }));
1706
+ return {
1707
+ type: "interaction",
1708
+ interaction: {
1709
+ type: "gapMatch",
1710
+ responseId: interaction.responseIdentifier,
1711
+ gapTexts,
1712
+ gaps,
1713
+ contentHtml: sanitizeHtml(interaction.contentHtml, { allowDataAttributes: true })
1714
+ }
1715
+ };
1716
+ }
1717
+ if (interaction.type === "matchInteraction") {
1718
+ const mapChoice = (c) => ({
1719
+ id: c.identifier,
1720
+ contentHtml: sanitizeHtml(c.contentHtml),
1721
+ matchMax: c.matchMax
1722
+ });
1723
+ let sourceChoices = interaction.sourceChoices.map(mapChoice);
1724
+ const targetChoices = interaction.targetChoices.map(mapChoice);
1725
+ if (interaction.shuffle === true) {
1726
+ sourceChoices = shuffleArray(sourceChoices);
1727
+ }
1728
+ return {
1729
+ type: "interaction",
1730
+ interaction: {
1731
+ type: "match",
1732
+ responseId: interaction.responseIdentifier,
1733
+ promptHtml: sanitizeHtml(interaction.promptHtml),
1734
+ maxAssociations: interaction.maxAssociations,
1735
+ sourceChoices,
1736
+ targetChoices
1737
+ }
1738
+ };
1739
+ }
1740
+ return { type: "stimulus", html: sanitizeHtml("") };
1741
+ });
1742
+ const displayItem = { title: item.title, contentBlocks };
1743
+ const hash = createHash("sha256").update(qtiXml).digest("hex").slice(0, 16);
1744
+ return { itemKey: hash, item: displayItem, shape };
1745
+ }
1746
+
1747
+ // src/actions/build.ts
1748
+ async function buildDisplayModel(qtiXml) {
1749
+ return buildDisplayModelFromXml(qtiXml);
1750
+ }
1751
+
1752
+ // src/actions/internal/validate.ts
1753
+ function validateResponsesFromXml(qtiXml, responses) {
1754
+ y("qti validate secure", {});
1755
+ const parseResult = I(() => parseAssessmentItemXml(qtiXml));
1756
+ if (parseResult.error) {
1757
+ l("qti parse failed during validate", { error: parseResult.error });
1758
+ throw B(parseResult.error, "qti parse");
1759
+ }
1760
+ const item = parseResult.data;
1761
+ y("qti validate: parsed item", {
1762
+ title: item.title,
1763
+ responseCount: Object.keys(responses).length
1764
+ });
1765
+ const perResponse = {};
1766
+ const selectedChoicesByResponse = {};
1767
+ const responseCorrectness = {};
1768
+ let allCorrect = true;
1769
+ for (const rd of item.responseDeclarations) {
1770
+ const respId = rd.identifier;
1771
+ const user = responses[respId];
1772
+ if (user == null) {
1773
+ allCorrect = false;
1774
+ responseCorrectness[respId] = false;
1775
+ y("qti validate: missing response", { responseId: respId });
1776
+ continue;
1777
+ }
1778
+ const userVals = Array.isArray(user) ? user.filter((v) => typeof v === "string") : [user];
1779
+ const correctVals = rd.correctResponse.values;
1780
+ let isCorrect = false;
1781
+ if (rd.cardinality === "ordered") {
1782
+ isCorrect = userVals.length === correctVals.length && userVals.every((val, index) => val === correctVals[index]);
1783
+ y("qti validate: ordered check", {
1784
+ responseId: respId,
1785
+ userVals,
1786
+ correctVals,
1787
+ isCorrect
1788
+ });
1789
+ } else if (rd.baseType === "directedPair" || rd.baseType === "pair") {
1790
+ const correctSet2 = new Set(correctVals);
1791
+ const userSet = new Set(userVals);
1792
+ isCorrect = userSet.size === correctSet2.size && [...userSet].every((v) => correctSet2.has(v));
1793
+ y("qti validate: directedPair check", {
1794
+ responseId: respId,
1795
+ userVals,
1796
+ correctVals,
1797
+ isCorrect
1798
+ });
1799
+ } else if (rd.baseType === "float" || rd.baseType === "integer") {
1800
+ const userVal = userVals[0];
1801
+ if (userVal != null) {
1802
+ const userNum = Number.parseFloat(userVal);
1803
+ if (!Number.isNaN(userNum)) {
1804
+ const epsilon = rd.baseType === "float" ? 1e-6 : 0;
1805
+ isCorrect = correctVals.some((correctVal) => {
1806
+ const correctNum = Number.parseFloat(correctVal);
1807
+ return !Number.isNaN(correctNum) && Math.abs(userNum - correctNum) <= epsilon;
1808
+ });
1809
+ }
1810
+ }
1811
+ y("qti validate: numeric check", {
1812
+ responseId: respId,
1813
+ baseType: rd.baseType,
1814
+ userVal: userVals[0],
1815
+ correctVals,
1816
+ isCorrect
1817
+ });
1818
+ } else if (rd.cardinality === "single") {
1819
+ const userVal = userVals[0];
1820
+ isCorrect = userVal != null && correctVals.includes(userVal);
1821
+ y("qti validate: single cardinality check", {
1822
+ responseId: respId,
1823
+ userVal,
1824
+ correctVals,
1825
+ isCorrect
1826
+ });
1827
+ } else {
1828
+ const correctSet2 = new Set(correctVals);
1829
+ const userSet = new Set(userVals);
1830
+ isCorrect = userSet.size === correctSet2.size && [...userSet].every((v) => correctSet2.has(v));
1831
+ y("qti validate: multiple cardinality check", {
1832
+ responseId: respId,
1833
+ userVals,
1834
+ correctVals,
1835
+ isCorrect
1836
+ });
1837
+ }
1838
+ if (!isCorrect) allCorrect = false;
1839
+ responseCorrectness[respId] = isCorrect;
1840
+ y("qti validate: evaluated response", {
1841
+ responseId: respId,
1842
+ selected: userVals,
1843
+ isCorrect
1844
+ });
1845
+ const correctSet = new Set(correctVals);
1846
+ selectedChoicesByResponse[respId] = userVals.map((id) => ({ id, isCorrect: correctSet.has(id) }));
1847
+ perResponse[respId] = { isCorrect };
1848
+ }
1849
+ const feedbackIdentifiers = evaluateFeedbackIdentifiers(item, responses, responseCorrectness);
1850
+ let overallFeedback;
1851
+ if (feedbackIdentifiers.length > 0) {
1852
+ const feedbackHtmlParts = [];
1853
+ for (const feedbackId of feedbackIdentifiers) {
1854
+ const feedbackBlock = item.itemBody.feedbackBlocks.find((fb) => fb.identifier === feedbackId);
1855
+ if (feedbackBlock) {
1856
+ feedbackHtmlParts.push(sanitizeHtml(feedbackBlock.contentHtml));
1857
+ }
1858
+ }
1859
+ if (feedbackHtmlParts.length > 0) {
1860
+ overallFeedback = {
1861
+ isCorrect: allCorrect,
1862
+ messageHtml: feedbackHtmlParts.join("")
1863
+ };
1864
+ y("qti validate: overall feedback via response processing rules", {
1865
+ isCorrect: allCorrect,
1866
+ feedbackIds: feedbackIdentifiers,
1867
+ numBlocks: feedbackHtmlParts.length
1868
+ });
1869
+ }
1870
+ } else {
1871
+ const targetId = allCorrect ? "CORRECT" : "INCORRECT";
1872
+ const fallbackBlock = item.itemBody.feedbackBlocks.find(
1873
+ (fb) => fb.identifier.toUpperCase() === targetId && (fb.outcomeIdentifier === "FEEDBACK" || fb.outcomeIdentifier === "FEEDBACK__OVERALL")
1874
+ );
1875
+ if (fallbackBlock) {
1876
+ overallFeedback = { isCorrect: allCorrect, messageHtml: sanitizeHtml(fallbackBlock.contentHtml) };
1877
+ y("qti validate: overall feedback via fallback block", {
1878
+ isCorrect: allCorrect,
1879
+ blockId: fallbackBlock.identifier
1880
+ });
1881
+ }
1882
+ }
1883
+ return { overallCorrect: allCorrect, perResponse, selectedChoicesByResponse, overallFeedback };
1884
+ }
1885
+
1886
+ // src/actions/validate.ts
1887
+ async function validateResponsesSecure(qtiXml, responses) {
1888
+ return validateResponsesFromXml(qtiXml, responses);
1889
+ }
1890
+
1891
+ export { buildDisplayModel, buildDisplayModelFromXml, validateResponsesFromXml, validateResponsesSecure };
1892
+ //# sourceMappingURL=index.js.map
1893
+ //# sourceMappingURL=index.js.map