@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.
- package/README.md +170 -0
- package/dist/actions/index.d.ts +27 -0
- package/dist/actions/index.js +1893 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/components/index.d.ts +113 -0
- package/dist/components/index.js +2495 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +3640 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-DxNEXGoq.d.ts +508 -0
- package/dist/types-MOyn9ktl.d.ts +102 -0
- package/package.json +61 -0
|
@@ -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, """)}"`;
|
|
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
|
+
"&": "&",
|
|
480
|
+
"<": "<",
|
|
481
|
+
">": ">",
|
|
482
|
+
'"': """,
|
|
483
|
+
"'": "'"
|
|
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, """)}"`;
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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
|