@conform-ed/qti-react 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/headless.d.ts +25 -0
- package/dist/headless.js +4804 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +160 -96
- package/dist/item-capability.d.ts +43 -0
- package/dist/item-score.d.ts +17 -0
- package/package.json +10 -5
- package/src/headless.ts +58 -0
- package/src/index.ts +4 -0
- package/src/item-capability.ts +211 -0
- package/src/item-score.ts +40 -0
- package/src/runtime.ts +17 -123
package/dist/headless.js
ADDED
|
@@ -0,0 +1,4804 @@
|
|
|
1
|
+
// src/graphic.ts
|
|
2
|
+
function parseCoords(coords) {
|
|
3
|
+
return coords.split(",").map((entry) => Number(entry.trim())).filter((value) => !Number.isNaN(value));
|
|
4
|
+
}
|
|
5
|
+
function parsePoint(value) {
|
|
6
|
+
const [x, y, ...rest] = value.trim().split(/\s+/u).map(Number);
|
|
7
|
+
if (x === undefined || y === undefined || rest.length > 0 || Number.isNaN(x) || Number.isNaN(y)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return { x, y };
|
|
11
|
+
}
|
|
12
|
+
function formatPoint(point) {
|
|
13
|
+
return `${point.x} ${point.y}`;
|
|
14
|
+
}
|
|
15
|
+
function pointInPolygon(coords, point) {
|
|
16
|
+
let inside = false;
|
|
17
|
+
for (let i = 0, j = coords.length - 2;i < coords.length; j = i, i += 2) {
|
|
18
|
+
const xi = coords[i];
|
|
19
|
+
const yi = coords[i + 1];
|
|
20
|
+
const xj = coords[j];
|
|
21
|
+
const yj = coords[j + 1];
|
|
22
|
+
const intersects = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
|
|
23
|
+
if (intersects) {
|
|
24
|
+
inside = !inside;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return inside;
|
|
28
|
+
}
|
|
29
|
+
function pointInShape(shape, coords, point) {
|
|
30
|
+
switch (shape) {
|
|
31
|
+
case "default":
|
|
32
|
+
return true;
|
|
33
|
+
case "circle": {
|
|
34
|
+
const [cx, cy, r] = coords;
|
|
35
|
+
if (cx === undefined || cy === undefined || r === undefined) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return (point.x - cx) ** 2 + (point.y - cy) ** 2 <= r ** 2;
|
|
39
|
+
}
|
|
40
|
+
case "rect": {
|
|
41
|
+
const [left, top, right, bottom] = coords;
|
|
42
|
+
if (left === undefined || top === undefined || right === undefined || bottom === undefined) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
|
|
46
|
+
}
|
|
47
|
+
case "ellipse": {
|
|
48
|
+
const [cx, cy, rx, ry] = coords;
|
|
49
|
+
if (cx === undefined || cy === undefined || rx === undefined || ry === undefined || rx === 0 || ry === 0) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return (point.x - cx) ** 2 / rx ** 2 + (point.y - cy) ** 2 / ry ** 2 <= 1;
|
|
53
|
+
}
|
|
54
|
+
case "poly":
|
|
55
|
+
return coords.length >= 6 && pointInPolygon(coords, point);
|
|
56
|
+
default:
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/normalized-item.ts
|
|
62
|
+
function isRecord(value) {
|
|
63
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
function asRecords(value) {
|
|
66
|
+
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
67
|
+
}
|
|
68
|
+
var kindRenames = {
|
|
69
|
+
hotTextInteraction: "hottextInteraction",
|
|
70
|
+
hotText: "hottext"
|
|
71
|
+
};
|
|
72
|
+
function numberValue(value) {
|
|
73
|
+
if (typeof value === "number") {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "string" && value !== "") {
|
|
77
|
+
const parsed = Number(value);
|
|
78
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
function findImageSource(node) {
|
|
83
|
+
if (!isRecord(node)) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
const attributes = isRecord(node["attributes"]) ? node["attributes"] : {};
|
|
87
|
+
if (typeof attributes["data"] === "string" || typeof attributes["src"] === "string") {
|
|
88
|
+
return attributes;
|
|
89
|
+
}
|
|
90
|
+
for (const child of asRecords(node["children"])) {
|
|
91
|
+
const found = findImageSource(child);
|
|
92
|
+
if (typeof found["data"] === "string" || typeof found["src"] === "string") {
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return attributes;
|
|
97
|
+
}
|
|
98
|
+
function toStageObject(node) {
|
|
99
|
+
const attributes = findImageSource(node);
|
|
100
|
+
const data = attributes["data"] ?? attributes["src"];
|
|
101
|
+
const width = numberValue(attributes["width"]);
|
|
102
|
+
const height = numberValue(attributes["height"]);
|
|
103
|
+
return {
|
|
104
|
+
data: typeof data === "string" ? data : "",
|
|
105
|
+
...width !== undefined ? { width } : {},
|
|
106
|
+
...height !== undefined ? { height } : {},
|
|
107
|
+
...typeof attributes["type"] === "string" ? { type: attributes["type"] } : {}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function flattenText(value) {
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
return value.map(flattenText).join("");
|
|
116
|
+
}
|
|
117
|
+
if (!isRecord(value)) {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
if (value["kind"] === "text") {
|
|
121
|
+
return typeof value["value"] === "string" ? value["value"] : "";
|
|
122
|
+
}
|
|
123
|
+
return flattenText(value["children"]) + flattenText(value["content"]) + flattenText(value["value"]);
|
|
124
|
+
}
|
|
125
|
+
function withNumericCoords(node) {
|
|
126
|
+
return typeof node["coords"] === "string" ? { ...node, coords: parseCoords(node["coords"]) } : node;
|
|
127
|
+
}
|
|
128
|
+
function reshapeContentNode(node) {
|
|
129
|
+
const kind = node["kind"];
|
|
130
|
+
if (typeof kind !== "string") {
|
|
131
|
+
return node;
|
|
132
|
+
}
|
|
133
|
+
const renamed = kindRenames[kind];
|
|
134
|
+
if (renamed !== undefined) {
|
|
135
|
+
return { ...node, kind: renamed };
|
|
136
|
+
}
|
|
137
|
+
switch (kind) {
|
|
138
|
+
case "hotspotChoice":
|
|
139
|
+
case "associableHotspot":
|
|
140
|
+
return withNumericCoords(node);
|
|
141
|
+
case "hotspotInteraction":
|
|
142
|
+
case "graphicOrderInteraction":
|
|
143
|
+
case "graphicAssociateInteraction":
|
|
144
|
+
case "selectPointInteraction": {
|
|
145
|
+
const { image, ...rest } = node;
|
|
146
|
+
return { ...rest, object: toStageObject(image) };
|
|
147
|
+
}
|
|
148
|
+
case "drawingInteraction": {
|
|
149
|
+
const { content, ...rest } = node;
|
|
150
|
+
const media = asRecords(content).find((fragment) => fragment["kind"] === "xml" && typeof fragment["name"] === "string" && ["object", "picture", "img"].includes(fragment["name"]));
|
|
151
|
+
return { ...rest, object: toStageObject(media) };
|
|
152
|
+
}
|
|
153
|
+
case "graphicGapMatchInteraction": {
|
|
154
|
+
const { image, gapChoices, ...rest } = node;
|
|
155
|
+
const gapImgs = asRecords(gapChoices).map(({ media, content, ...choice }) => choice["kind"] === "gapImg" ? { ...choice, object: toStageObject(media) } : { ...choice, label: flattenText(content) });
|
|
156
|
+
return { ...rest, object: toStageObject(image), gapImgs };
|
|
157
|
+
}
|
|
158
|
+
case "gapMatchInteraction": {
|
|
159
|
+
const gapTexts = asRecords(node["gapChoices"]).filter((choice) => choice["kind"] === "gapText");
|
|
160
|
+
return { ...node, gapTexts };
|
|
161
|
+
}
|
|
162
|
+
case "mediaInteraction": {
|
|
163
|
+
const { media, ...rest } = node;
|
|
164
|
+
return { ...rest, content: media === undefined ? [] : [media] };
|
|
165
|
+
}
|
|
166
|
+
case "uploadInteraction": {
|
|
167
|
+
const acceptedTypes = node["acceptedTypes"];
|
|
168
|
+
return Array.isArray(acceptedTypes) ? { ...node, type: acceptedTypes.join(",") } : node;
|
|
169
|
+
}
|
|
170
|
+
case "positionObjectStage": {
|
|
171
|
+
const interactions = asRecords(node["positionObjectInteractions"]);
|
|
172
|
+
const [first] = interactions;
|
|
173
|
+
if (interactions.length === 1 && first) {
|
|
174
|
+
return {
|
|
175
|
+
kind: "positionObjectStage",
|
|
176
|
+
responseIdentifier: first["responseIdentifier"],
|
|
177
|
+
stageObject: toStageObject(node["image"]),
|
|
178
|
+
object: toStageObject(first["image"]),
|
|
179
|
+
...first["maxChoices"] !== undefined ? { maxChoices: first["maxChoices"] } : {},
|
|
180
|
+
...first["minChoices"] !== undefined ? { minChoices: first["minChoices"] } : {}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const responseIdentifier = first?.["responseIdentifier"];
|
|
184
|
+
return { ...node, ...typeof responseIdentifier === "string" ? { responseIdentifier } : {} };
|
|
185
|
+
}
|
|
186
|
+
default:
|
|
187
|
+
return node;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function convertContentEntry(entry) {
|
|
191
|
+
if (typeof entry === "string") {
|
|
192
|
+
return { kind: "text", value: entry };
|
|
193
|
+
}
|
|
194
|
+
return convertContentValue(entry);
|
|
195
|
+
}
|
|
196
|
+
function convertContentValue(value) {
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
return value.map(convertContentValue);
|
|
199
|
+
}
|
|
200
|
+
if (!isRecord(value)) {
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
const converted = Object.fromEntries(Object.entries(value).map(([key, entry]) => {
|
|
204
|
+
if ((key === "children" || key === "content") && Array.isArray(entry)) {
|
|
205
|
+
return [key, entry.map(convertContentEntry)];
|
|
206
|
+
}
|
|
207
|
+
return [key, convertContentValue(entry)];
|
|
208
|
+
}));
|
|
209
|
+
return reshapeContentNode(converted);
|
|
210
|
+
}
|
|
211
|
+
function convertExpression(expression) {
|
|
212
|
+
const record = isRecord(expression) ? expression : {};
|
|
213
|
+
const { children, ...rest } = record;
|
|
214
|
+
return {
|
|
215
|
+
...rest,
|
|
216
|
+
...Array.isArray(children) ? { expressions: children.map(convertExpression) } : {}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function convertBranch(branch, convertRule) {
|
|
220
|
+
const record = isRecord(branch) ? branch : {};
|
|
221
|
+
return {
|
|
222
|
+
expression: convertExpression(record["expression"]),
|
|
223
|
+
rules: (Array.isArray(record["actions"]) ? record["actions"] : []).map(convertRule)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function convertRpRule(rule) {
|
|
227
|
+
const record = isRecord(rule) ? rule : {};
|
|
228
|
+
const kind = typeof record["kind"] === "string" ? record["kind"] : "";
|
|
229
|
+
if (kind === "responseCondition") {
|
|
230
|
+
const elseIfs = Array.isArray(record["responseElseIf"]) ? record["responseElseIf"].map((branch) => convertBranch(branch, convertRpRule)) : [];
|
|
231
|
+
return {
|
|
232
|
+
kind,
|
|
233
|
+
responseIf: convertBranch(record["responseIf"], convertRpRule),
|
|
234
|
+
...elseIfs.length ? { responseElseIfs: elseIfs } : {},
|
|
235
|
+
...isRecord(record["responseElse"]) ? {
|
|
236
|
+
responseElse: {
|
|
237
|
+
rules: (Array.isArray(record["responseElse"]["actions"]) ? record["responseElse"]["actions"] : []).map(convertRpRule)
|
|
238
|
+
}
|
|
239
|
+
} : {}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (kind === "responseProcessingFragment") {
|
|
243
|
+
return { kind, rules: (Array.isArray(record["rules"]) ? record["rules"] : []).map(convertRpRule) };
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
kind,
|
|
247
|
+
...typeof record["identifier"] === "string" ? { identifier: record["identifier"] } : {},
|
|
248
|
+
...record["expression"] !== undefined ? { expression: convertExpression(record["expression"]) } : {}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function convertTemplateRule(rule) {
|
|
252
|
+
const record = isRecord(rule) ? rule : {};
|
|
253
|
+
const kind = typeof record["kind"] === "string" ? record["kind"] : "";
|
|
254
|
+
if (kind === "templateCondition") {
|
|
255
|
+
const elseIfs = Array.isArray(record["templateElseIf"]) ? record["templateElseIf"].map((branch) => convertBranch(branch, convertTemplateRule)) : [];
|
|
256
|
+
return {
|
|
257
|
+
kind,
|
|
258
|
+
templateIf: convertBranch(record["templateIf"], convertTemplateRule),
|
|
259
|
+
...elseIfs.length ? { templateElseIfs: elseIfs } : {},
|
|
260
|
+
...isRecord(record["templateElse"]) ? {
|
|
261
|
+
templateElse: {
|
|
262
|
+
rules: (Array.isArray(record["templateElse"]["actions"]) ? record["templateElse"]["actions"] : []).map(convertTemplateRule)
|
|
263
|
+
}
|
|
264
|
+
} : {}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
kind,
|
|
269
|
+
...typeof record["identifier"] === "string" ? { identifier: record["identifier"] } : {},
|
|
270
|
+
...record["expression"] !== undefined ? { expression: convertExpression(record["expression"]) } : {}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function convertResponseProcessing(value) {
|
|
274
|
+
return {
|
|
275
|
+
...typeof value["template"] === "string" ? { template: value["template"] } : {},
|
|
276
|
+
...Array.isArray(value["rules"]) ? { rules: value["rules"].map(convertRpRule) } : {}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function convertResponseDeclaration(declaration) {
|
|
280
|
+
const areaMapping = declaration["areaMapping"];
|
|
281
|
+
if (!isRecord(areaMapping)) {
|
|
282
|
+
return declaration;
|
|
283
|
+
}
|
|
284
|
+
const areaMapEntries = asRecords(areaMapping["areaMapEntries"]).map((entry) => withNumericCoords(entry));
|
|
285
|
+
return { ...declaration, areaMapping: { ...areaMapping, areaMapEntries } };
|
|
286
|
+
}
|
|
287
|
+
function appendCatalogViews(target, value) {
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
for (const entry of value) {
|
|
290
|
+
appendCatalogViews(target, entry);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!isRecord(value)) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const catalogInfo = value["catalogInfo"];
|
|
298
|
+
if (isRecord(catalogInfo) && Array.isArray(catalogInfo["catalogs"])) {
|
|
299
|
+
target.push(...catalogInfo["catalogs"]);
|
|
300
|
+
}
|
|
301
|
+
appendCatalogViews(target, value["content"]);
|
|
302
|
+
appendCatalogViews(target, value["children"]);
|
|
303
|
+
}
|
|
304
|
+
function documentCatalogViews(root, convertedContent) {
|
|
305
|
+
const catalogs = [];
|
|
306
|
+
appendCatalogViews(catalogs, isRecord(root["catalogInfo"]) ? { catalogInfo: convertContentValue(root["catalogInfo"]) } : undefined);
|
|
307
|
+
appendCatalogViews(catalogs, convertedContent);
|
|
308
|
+
return catalogs;
|
|
309
|
+
}
|
|
310
|
+
function assessmentItemViewFromNormalized(document) {
|
|
311
|
+
if (!isRecord(document) || !isRecord(document["assessmentItem"])) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const item = document["assessmentItem"];
|
|
315
|
+
const itemBody = isRecord(item["itemBody"]) ? item["itemBody"] : {};
|
|
316
|
+
const content = Array.isArray(itemBody["content"]) ? itemBody["content"].map(convertContentEntry) : [];
|
|
317
|
+
const templateRules = isRecord(item["templateProcessing"]) ? item["templateProcessing"]["rules"] : undefined;
|
|
318
|
+
const convertedModalFeedbacks = Array.isArray(item["modalFeedbacks"]) ? item["modalFeedbacks"].map(convertContentValue) : undefined;
|
|
319
|
+
const catalogs = documentCatalogViews(item, [content, convertedModalFeedbacks]);
|
|
320
|
+
return {
|
|
321
|
+
responseDeclarations: asRecords(item["responseDeclarations"]).map(convertResponseDeclaration),
|
|
322
|
+
outcomeDeclarations: item["outcomeDeclarations"] ?? [],
|
|
323
|
+
...isRecord(item["responseProcessing"]) ? { responseProcessing: convertResponseProcessing(item["responseProcessing"]) } : {},
|
|
324
|
+
...Array.isArray(item["templateDeclarations"]) ? { templateDeclarations: item["templateDeclarations"] } : {},
|
|
325
|
+
...Array.isArray(templateRules) ? {
|
|
326
|
+
templateProcessing: {
|
|
327
|
+
rules: templateRules.map(convertTemplateRule)
|
|
328
|
+
}
|
|
329
|
+
} : {},
|
|
330
|
+
...typeof item["adaptive"] === "boolean" ? { adaptive: item["adaptive"] } : {},
|
|
331
|
+
...convertedModalFeedbacks ? { modalFeedbacks: convertedModalFeedbacks } : {},
|
|
332
|
+
...catalogs.length ? { catalogs } : {},
|
|
333
|
+
...isRecord(item["companionMaterialsInfo"]) ? { companionMaterials: item["companionMaterialsInfo"] } : {},
|
|
334
|
+
...Array.isArray(item["assessmentStimulusRefs"]) ? {
|
|
335
|
+
assessmentStimulusRefs: asRecords(item["assessmentStimulusRefs"]).map((ref) => ({
|
|
336
|
+
identifier: typeof ref["identifier"] === "string" ? ref["identifier"] : "",
|
|
337
|
+
href: typeof ref["href"] === "string" ? ref["href"] : "",
|
|
338
|
+
...typeof ref["title"] === "string" ? { title: ref["title"] } : {}
|
|
339
|
+
}))
|
|
340
|
+
} : {},
|
|
341
|
+
itemBody: { content }
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function stimulusContentFromNormalized(document) {
|
|
345
|
+
if (!isRecord(document) || !isRecord(document["assessmentStimulus"])) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const stimulus = document["assessmentStimulus"];
|
|
349
|
+
const body = isRecord(stimulus["stimulusBody"]) ? stimulus["stimulusBody"] : {};
|
|
350
|
+
const content = Array.isArray(body["content"]) ? body["content"].map(convertContentEntry) : [];
|
|
351
|
+
const catalogs = documentCatalogViews(stimulus, content);
|
|
352
|
+
return { content, ...catalogs.length ? { catalogs } : {} };
|
|
353
|
+
}
|
|
354
|
+
function convertPreConditions(value) {
|
|
355
|
+
return asRecords(value).map((wrapper) => convertExpression(wrapper["expression"]));
|
|
356
|
+
}
|
|
357
|
+
function convertBranchRules(value) {
|
|
358
|
+
return asRecords(value).map((rule) => ({
|
|
359
|
+
target: typeof rule["target"] === "string" ? rule["target"] : "",
|
|
360
|
+
expression: convertExpression(rule["expression"])
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
function convertOutcomeRule(rule) {
|
|
364
|
+
const record = isRecord(rule) ? rule : {};
|
|
365
|
+
const kind = typeof record["kind"] === "string" ? record["kind"] : "";
|
|
366
|
+
if (kind === "outcomeCondition") {
|
|
367
|
+
const elseIfs = Array.isArray(record["outcomeElseIf"]) ? record["outcomeElseIf"].map((branch) => convertBranch(branch, convertOutcomeRule)) : [];
|
|
368
|
+
return {
|
|
369
|
+
kind,
|
|
370
|
+
outcomeIf: convertBranch(record["outcomeIf"], convertOutcomeRule),
|
|
371
|
+
...elseIfs.length ? { outcomeElseIfs: elseIfs } : {},
|
|
372
|
+
...isRecord(record["outcomeElse"]) ? {
|
|
373
|
+
outcomeElse: {
|
|
374
|
+
rules: (Array.isArray(record["outcomeElse"]["actions"]) ? record["outcomeElse"]["actions"] : []).map(convertOutcomeRule)
|
|
375
|
+
}
|
|
376
|
+
} : {}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (kind === "outcomeProcessingFragment") {
|
|
380
|
+
return { kind, rules: (Array.isArray(record["rules"]) ? record["rules"] : []).map(convertOutcomeRule) };
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
kind,
|
|
384
|
+
...typeof record["identifier"] === "string" ? { identifier: record["identifier"] } : {},
|
|
385
|
+
...record["expression"] !== undefined ? { expression: convertExpression(record["expression"]) } : {}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function sessionControlAndTimeLimits(record) {
|
|
389
|
+
return {
|
|
390
|
+
...isRecord(record["itemSessionControl"]) ? { itemSessionControl: record["itemSessionControl"] } : {},
|
|
391
|
+
...isRecord(record["timeLimits"]) ? { timeLimits: record["timeLimits"] } : {}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function convertItemRef(ref) {
|
|
395
|
+
return {
|
|
396
|
+
kind: "assessmentItemRef",
|
|
397
|
+
identifier: typeof ref["identifier"] === "string" ? ref["identifier"] : "",
|
|
398
|
+
...typeof ref["href"] === "string" ? { href: ref["href"] } : {},
|
|
399
|
+
...Array.isArray(ref["category"]) ? { categories: ref["category"] } : {},
|
|
400
|
+
...typeof ref["fixed"] === "boolean" ? { fixed: ref["fixed"] } : {},
|
|
401
|
+
...typeof ref["required"] === "boolean" ? { required: ref["required"] } : {},
|
|
402
|
+
...ref["preConditions"] !== undefined ? { preConditions: convertPreConditions(ref["preConditions"]) } : {},
|
|
403
|
+
...ref["branchRules"] !== undefined ? { branchRules: convertBranchRules(ref["branchRules"]) } : {},
|
|
404
|
+
...Array.isArray(ref["weights"]) ? { weights: ref["weights"] } : {},
|
|
405
|
+
...Array.isArray(ref["templateDefaults"]) ? {
|
|
406
|
+
templateDefaults: asRecords(ref["templateDefaults"]).map((entry) => ({
|
|
407
|
+
templateIdentifier: typeof entry["templateIdentifier"] === "string" ? entry["templateIdentifier"] : "",
|
|
408
|
+
expression: convertExpression(entry["expression"])
|
|
409
|
+
}))
|
|
410
|
+
} : {},
|
|
411
|
+
...sessionControlAndTimeLimits(ref)
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function convertSection(section) {
|
|
415
|
+
const children = asRecords(section["children"]).map((child) => child["visible"] !== undefined || child["children"] !== undefined ? convertSection(child) : convertItemRef(child));
|
|
416
|
+
return {
|
|
417
|
+
kind: "assessmentSection",
|
|
418
|
+
identifier: typeof section["identifier"] === "string" ? section["identifier"] : "",
|
|
419
|
+
...typeof section["title"] === "string" ? { title: section["title"] } : {},
|
|
420
|
+
...typeof section["visible"] === "boolean" ? { visible: section["visible"] } : {},
|
|
421
|
+
...typeof section["fixed"] === "boolean" ? { fixed: section["fixed"] } : {},
|
|
422
|
+
...typeof section["required"] === "boolean" ? { required: section["required"] } : {},
|
|
423
|
+
...typeof section["keepTogether"] === "boolean" ? { keepTogether: section["keepTogether"] } : {},
|
|
424
|
+
...isRecord(section["selection"]) ? { selection: section["selection"] } : {},
|
|
425
|
+
...isRecord(section["ordering"]) ? { ordering: section["ordering"] } : {},
|
|
426
|
+
...section["preConditions"] !== undefined ? { preConditions: convertPreConditions(section["preConditions"]) } : {},
|
|
427
|
+
...section["branchRules"] !== undefined ? { branchRules: convertBranchRules(section["branchRules"]) } : {},
|
|
428
|
+
...sessionControlAndTimeLimits(section),
|
|
429
|
+
children
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function convertTestFeedback(feedback) {
|
|
433
|
+
const converted = convertContentValue(feedback);
|
|
434
|
+
return {
|
|
435
|
+
outcomeIdentifier: typeof converted["outcomeIdentifier"] === "string" ? converted["outcomeIdentifier"] : "",
|
|
436
|
+
identifier: typeof converted["identifier"] === "string" ? converted["identifier"] : "",
|
|
437
|
+
...converted["access"] === "atEnd" || converted["access"] === "during" ? { access: converted["access"] } : {},
|
|
438
|
+
...converted["showHide"] === "show" || converted["showHide"] === "hide" ? { showHide: converted["showHide"] } : {},
|
|
439
|
+
...Array.isArray(converted["content"]) ? { content: converted["content"] } : {}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function assessmentTestViewFromNormalized(document) {
|
|
443
|
+
if (!isRecord(document) || !isRecord(document["assessmentTest"])) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const testDocument = document["assessmentTest"];
|
|
447
|
+
const outcomeRules = isRecord(testDocument["outcomeProcessing"]) ? testDocument["outcomeProcessing"]["rules"] : undefined;
|
|
448
|
+
const testParts = asRecords(testDocument["testParts"]).map((part) => ({
|
|
449
|
+
identifier: typeof part["identifier"] === "string" ? part["identifier"] : "",
|
|
450
|
+
navigationMode: part["navigationMode"] === "nonlinear" ? "nonlinear" : "linear",
|
|
451
|
+
submissionMode: part["submissionMode"] === "simultaneous" ? "simultaneous" : "individual",
|
|
452
|
+
...part["preConditions"] !== undefined ? { preConditions: convertPreConditions(part["preConditions"]) } : {},
|
|
453
|
+
...part["branchRules"] !== undefined ? { branchRules: convertBranchRules(part["branchRules"]) } : {},
|
|
454
|
+
...sessionControlAndTimeLimits(part),
|
|
455
|
+
assessmentSections: asRecords(part["children"]).map(convertSection)
|
|
456
|
+
}));
|
|
457
|
+
return {
|
|
458
|
+
identifier: typeof testDocument["identifier"] === "string" ? testDocument["identifier"] : "",
|
|
459
|
+
...typeof testDocument["title"] === "string" ? { title: testDocument["title"] } : {},
|
|
460
|
+
outcomeDeclarations: testDocument["outcomeDeclarations"] ?? [],
|
|
461
|
+
...isRecord(testDocument["timeLimits"]) ? { timeLimits: testDocument["timeLimits"] } : {},
|
|
462
|
+
testParts,
|
|
463
|
+
...Array.isArray(outcomeRules) ? { outcomeProcessing: { rules: outcomeRules.map(convertOutcomeRule) } } : {},
|
|
464
|
+
...Array.isArray(testDocument["testFeedbacks"]) ? { testFeedbacks: asRecords(testDocument["testFeedbacks"]).map(convertTestFeedback) } : {}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
// src/content-model.ts
|
|
468
|
+
var v0InteractionKinds = [
|
|
469
|
+
"associateInteraction",
|
|
470
|
+
"choiceInteraction",
|
|
471
|
+
"drawingInteraction",
|
|
472
|
+
"endAttemptInteraction",
|
|
473
|
+
"extendedTextInteraction",
|
|
474
|
+
"gapMatchInteraction",
|
|
475
|
+
"graphicAssociateInteraction",
|
|
476
|
+
"graphicGapMatchInteraction",
|
|
477
|
+
"graphicOrderInteraction",
|
|
478
|
+
"hotspotInteraction",
|
|
479
|
+
"hottextInteraction",
|
|
480
|
+
"inlineChoiceInteraction",
|
|
481
|
+
"matchInteraction",
|
|
482
|
+
"mediaInteraction",
|
|
483
|
+
"orderInteraction",
|
|
484
|
+
"positionObjectStage",
|
|
485
|
+
"selectPointInteraction",
|
|
486
|
+
"sliderInteraction",
|
|
487
|
+
"textEntryInteraction",
|
|
488
|
+
"uploadInteraction"
|
|
489
|
+
];
|
|
490
|
+
var v0FlowElements = new Set([
|
|
491
|
+
"p",
|
|
492
|
+
"span",
|
|
493
|
+
"strong",
|
|
494
|
+
"em",
|
|
495
|
+
"b",
|
|
496
|
+
"i",
|
|
497
|
+
"sub",
|
|
498
|
+
"sup",
|
|
499
|
+
"br",
|
|
500
|
+
"ul",
|
|
501
|
+
"ol",
|
|
502
|
+
"li",
|
|
503
|
+
"ruby",
|
|
504
|
+
"rb",
|
|
505
|
+
"rt",
|
|
506
|
+
"rp",
|
|
507
|
+
"rtc",
|
|
508
|
+
"bdo",
|
|
509
|
+
"bdi",
|
|
510
|
+
"img",
|
|
511
|
+
"audio",
|
|
512
|
+
"video",
|
|
513
|
+
"source",
|
|
514
|
+
"track",
|
|
515
|
+
"picture",
|
|
516
|
+
"figure",
|
|
517
|
+
"figcaption",
|
|
518
|
+
"object",
|
|
519
|
+
"div",
|
|
520
|
+
"section",
|
|
521
|
+
"h1",
|
|
522
|
+
"h2",
|
|
523
|
+
"h3",
|
|
524
|
+
"h4",
|
|
525
|
+
"h5",
|
|
526
|
+
"h6",
|
|
527
|
+
"blockquote",
|
|
528
|
+
"hr",
|
|
529
|
+
"table",
|
|
530
|
+
"caption",
|
|
531
|
+
"thead",
|
|
532
|
+
"tbody",
|
|
533
|
+
"tfoot",
|
|
534
|
+
"tr",
|
|
535
|
+
"th",
|
|
536
|
+
"td",
|
|
537
|
+
"a",
|
|
538
|
+
"abbr",
|
|
539
|
+
"acronym",
|
|
540
|
+
"address",
|
|
541
|
+
"article",
|
|
542
|
+
"aside",
|
|
543
|
+
"big",
|
|
544
|
+
"code",
|
|
545
|
+
"details",
|
|
546
|
+
"summary",
|
|
547
|
+
"dfn",
|
|
548
|
+
"dl",
|
|
549
|
+
"dt",
|
|
550
|
+
"dd",
|
|
551
|
+
"kbd",
|
|
552
|
+
"label",
|
|
553
|
+
"nav",
|
|
554
|
+
"pre",
|
|
555
|
+
"q",
|
|
556
|
+
"samp",
|
|
557
|
+
"small",
|
|
558
|
+
"tt",
|
|
559
|
+
"var",
|
|
560
|
+
"footer",
|
|
561
|
+
"header"
|
|
562
|
+
]);
|
|
563
|
+
var v0ElementAttributes = new Map([
|
|
564
|
+
["a", new Set(["href", "type"])],
|
|
565
|
+
["img", new Set(["src", "alt", "width", "height"])],
|
|
566
|
+
["audio", new Set(["src", "controls", "loop", "muted", "preload"])],
|
|
567
|
+
["video", new Set(["src", "controls", "loop", "muted", "preload", "poster", "width", "height"])],
|
|
568
|
+
["source", new Set(["src", "type"])],
|
|
569
|
+
["track", new Set(["src", "kind", "srclang", "label", "default"])],
|
|
570
|
+
["object", new Set(["data", "type", "width", "height"])]
|
|
571
|
+
]);
|
|
572
|
+
var v0UrlAttributes = new Set(["src", "poster", "data", "href"]);
|
|
573
|
+
var v0MathRoot = "math";
|
|
574
|
+
var v0GlobalAttributes = new Set(["id", "class", "lang", "xml:lang", "dir"]);
|
|
575
|
+
var v0ContentModel = {
|
|
576
|
+
interactionKinds: new Set(v0InteractionKinds),
|
|
577
|
+
flowElements: v0FlowElements,
|
|
578
|
+
mathRoot: v0MathRoot,
|
|
579
|
+
globalAttributes: v0GlobalAttributes,
|
|
580
|
+
elementAttributes: v0ElementAttributes,
|
|
581
|
+
urlAttributes: v0UrlAttributes
|
|
582
|
+
};
|
|
583
|
+
function isAllowedFlowElement(model, name) {
|
|
584
|
+
return model.flowElements.has(name) || name === model.mathRoot;
|
|
585
|
+
}
|
|
586
|
+
function isInteractionKind(model, kind) {
|
|
587
|
+
return model.interactionKinds.has(kind);
|
|
588
|
+
}
|
|
589
|
+
function isUnsafeAttribute(name, value) {
|
|
590
|
+
const lowerName = name.toLowerCase();
|
|
591
|
+
if (lowerName.startsWith("on")) {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
if (typeof value === "string" && /^\s*javascript:/iu.test(value)) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
function sanitizeAttributes(model, elementName, attributes) {
|
|
600
|
+
const safe = {};
|
|
601
|
+
if (!attributes) {
|
|
602
|
+
return safe;
|
|
603
|
+
}
|
|
604
|
+
const elementAllowed = model.elementAttributes.get(elementName);
|
|
605
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
606
|
+
if (isUnsafeAttribute(name, value)) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const ariaOrData = name === "role" || name.startsWith("aria-") || name.startsWith("data-");
|
|
610
|
+
if (!ariaOrData && !model.globalAttributes.has(name) && !elementAllowed?.has(name)) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (typeof value === "string") {
|
|
614
|
+
safe[name] = value;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return safe;
|
|
618
|
+
}
|
|
619
|
+
function sanitizeMathAttributes(attributes) {
|
|
620
|
+
const safe = {};
|
|
621
|
+
if (!attributes) {
|
|
622
|
+
return safe;
|
|
623
|
+
}
|
|
624
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
625
|
+
if (!isUnsafeAttribute(name, value) && typeof value === "string") {
|
|
626
|
+
safe[name] = value;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return safe;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ../../node_modules/.bun/whynot@5.0.0/node_modules/whynot/dist/whynot.esm.js
|
|
633
|
+
function t(t2, s, r, i) {
|
|
634
|
+
const n = { op: s, func: r, data: i };
|
|
635
|
+
return t2.push(n), n;
|
|
636
|
+
}
|
|
637
|
+
function s(t2, s2) {
|
|
638
|
+
return t2;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
class r {
|
|
642
|
+
constructor() {
|
|
643
|
+
this.program = [];
|
|
644
|
+
}
|
|
645
|
+
test(s2, r2) {
|
|
646
|
+
return t(this.program, 5, s2, r2 === undefined ? null : r2);
|
|
647
|
+
}
|
|
648
|
+
jump(s2) {
|
|
649
|
+
return t(this.program, 3, null, s2);
|
|
650
|
+
}
|
|
651
|
+
record(r2, i) {
|
|
652
|
+
return t(this.program, 4, i === undefined ? s : i, r2);
|
|
653
|
+
}
|
|
654
|
+
bad(s2 = 1) {
|
|
655
|
+
return t(this.program, 1, null, s2);
|
|
656
|
+
}
|
|
657
|
+
accept() {
|
|
658
|
+
return t(this.program, 0, null, null);
|
|
659
|
+
}
|
|
660
|
+
fail(s2) {
|
|
661
|
+
return t(this.program, 2, s2 || null, null);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
class i {
|
|
666
|
+
constructor(t2, s2, r2) {
|
|
667
|
+
this.programLength = t2, this.maxFromByPc = s2, this.maxSurvivorFromByPc = r2;
|
|
668
|
+
}
|
|
669
|
+
static fromProgram(t2) {
|
|
670
|
+
const s2 = t2.length, r2 = [], n = [];
|
|
671
|
+
return t2.forEach((t3) => {
|
|
672
|
+
r2.push(0), n.push(0);
|
|
673
|
+
}), t2.forEach((t3, i2) => {
|
|
674
|
+
switch (t3.op) {
|
|
675
|
+
case 2:
|
|
676
|
+
if (t3.func === null)
|
|
677
|
+
return;
|
|
678
|
+
if (i2 + 1 >= s2)
|
|
679
|
+
throw new Error("Invalid program: program could run past end");
|
|
680
|
+
r2[i2 + 1] += 1;
|
|
681
|
+
break;
|
|
682
|
+
case 1:
|
|
683
|
+
case 4:
|
|
684
|
+
if (i2 + 1 >= s2)
|
|
685
|
+
throw new Error("Invalid program: program could run past end");
|
|
686
|
+
r2[i2 + 1] += 1;
|
|
687
|
+
break;
|
|
688
|
+
case 3:
|
|
689
|
+
t3.data.forEach((t4) => {
|
|
690
|
+
if (t4 < 0 || t4 >= s2)
|
|
691
|
+
throw new Error("Invalid program: program could run past end");
|
|
692
|
+
r2[t4] += 1;
|
|
693
|
+
});
|
|
694
|
+
break;
|
|
695
|
+
case 5:
|
|
696
|
+
if (i2 + 1 >= s2)
|
|
697
|
+
throw new Error("Invalid program: program could run past end");
|
|
698
|
+
n[i2 + 1] += 1;
|
|
699
|
+
break;
|
|
700
|
+
case 0:
|
|
701
|
+
n[i2] += 1;
|
|
702
|
+
}
|
|
703
|
+
}), new i(s2, r2, n);
|
|
704
|
+
}
|
|
705
|
+
static createStub(t2) {
|
|
706
|
+
const s2 = [], r2 = [];
|
|
707
|
+
for (let i2 = 0;i2 < t2; ++i2)
|
|
708
|
+
s2.push(t2), r2.push(t2);
|
|
709
|
+
return new i(t2, s2, r2);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
class n {
|
|
714
|
+
constructor(t2) {
|
|
715
|
+
this.acceptingTraces = t2, this.success = t2.length > 0;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
class h {
|
|
720
|
+
constructor(t2) {
|
|
721
|
+
this.t = 0, this.i = 0, this.h = new Uint16Array(t2), this.l = new Uint8Array(t2);
|
|
722
|
+
}
|
|
723
|
+
getBadness(t2) {
|
|
724
|
+
return this.l[t2];
|
|
725
|
+
}
|
|
726
|
+
add(t2, s2) {
|
|
727
|
+
this.l[t2] = s2 > 255 ? 255 : s2;
|
|
728
|
+
const r2 = function(t3, s3, r3, i2, n2) {
|
|
729
|
+
let h2 = i2, e = n2;
|
|
730
|
+
for (;h2 < e; ) {
|
|
731
|
+
const i3 = h2 + e >>> 1;
|
|
732
|
+
r3 < s3[t3[i3]] ? e = i3 : h2 = i3 + 1;
|
|
733
|
+
}
|
|
734
|
+
return h2;
|
|
735
|
+
}(this.h, this.l, s2, this.i, this.t);
|
|
736
|
+
this.h.copyWithin(r2 + 1, r2, this.t), this.h[r2] = t2, this.t += 1;
|
|
737
|
+
}
|
|
738
|
+
reschedule(t2, s2) {
|
|
739
|
+
const r2 = Math.max(this.l[t2], s2 > 255 ? 255 : s2);
|
|
740
|
+
if (this.l[t2] !== r2) {
|
|
741
|
+
const s3 = this.h.indexOf(t2, this.i);
|
|
742
|
+
if (s3 < 0 || s3 >= this.t)
|
|
743
|
+
return void (this.l[t2] = r2);
|
|
744
|
+
this.h.copyWithin(s3, s3 + 1, this.t), this.t -= 1, this.add(t2, r2);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
getNextPc() {
|
|
748
|
+
return this.i >= this.t ? null : this.h[this.i++];
|
|
749
|
+
}
|
|
750
|
+
reset() {
|
|
751
|
+
this.t = 0, this.i = 0, this.l.fill(0);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
class e {
|
|
756
|
+
constructor(t2) {
|
|
757
|
+
this.o = [];
|
|
758
|
+
let s2 = t2.length;
|
|
759
|
+
t2.forEach((t3) => {
|
|
760
|
+
this.o.push(t3 > 0 ? s2 : -1), s2 += t3;
|
|
761
|
+
}), this.u = new Uint16Array(s2);
|
|
762
|
+
}
|
|
763
|
+
clear() {
|
|
764
|
+
this.u.fill(0, 0, this.o.length);
|
|
765
|
+
}
|
|
766
|
+
add(t2, s2) {
|
|
767
|
+
const r2 = this.u[s2], i2 = this.o[s2];
|
|
768
|
+
this.u[s2] += 1, this.u[i2 + r2] = t2;
|
|
769
|
+
}
|
|
770
|
+
has(t2) {
|
|
771
|
+
return this.u[t2] > 0;
|
|
772
|
+
}
|
|
773
|
+
forEach(t2, s2) {
|
|
774
|
+
const r2 = this.u[t2], i2 = this.o[t2];
|
|
775
|
+
for (let t3 = i2;t3 < i2 + r2; ++t3)
|
|
776
|
+
s2(this.u[t3]);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function l(t2, s2, r2 = false) {
|
|
780
|
+
return t2 === null ? s2 : Array.isArray(t2) ? (t2.indexOf(s2) === -1 && (r2 && (t2 = t2.slice()), t2.push(s2)), t2) : t2 === s2 ? t2 : [t2, s2];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
class c {
|
|
784
|
+
constructor(t2, s2) {
|
|
785
|
+
this.prefixes = t2, this.record = s2;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function o(t2, s2) {
|
|
789
|
+
let r2;
|
|
790
|
+
if (s2 === null) {
|
|
791
|
+
if (!Array.isArray(t2))
|
|
792
|
+
return t2;
|
|
793
|
+
r2 = t2;
|
|
794
|
+
} else
|
|
795
|
+
r2 = t2 === c.EMPTY ? [] : Array.isArray(t2) ? t2 : [t2];
|
|
796
|
+
return new c(r2, s2);
|
|
797
|
+
}
|
|
798
|
+
c.EMPTY = new c([], null);
|
|
799
|
+
|
|
800
|
+
class u {
|
|
801
|
+
constructor(t2) {
|
|
802
|
+
this.p = [], this.v = [];
|
|
803
|
+
for (let s2 = 0;s2 < t2; ++s2)
|
|
804
|
+
this.p.push(0), this.v.push(null);
|
|
805
|
+
}
|
|
806
|
+
mergeTraces(t2, s2, r2, i2, n2, h2) {
|
|
807
|
+
let e2 = false;
|
|
808
|
+
return r2.forEach(s2, (s3) => {
|
|
809
|
+
const r3 = this.trace(s3, i2, n2, h2);
|
|
810
|
+
var c2, o2, u2;
|
|
811
|
+
o2 = r3, u2 = e2, t2 = (c2 = t2) === null ? o2 : o2 === null ? c2 : Array.isArray(o2) ? o2.reduce((t3, s4) => l(t3, s4, t3 === o2), c2) : l(c2, o2, u2), e2 = t2 === r3;
|
|
812
|
+
}), t2;
|
|
813
|
+
}
|
|
814
|
+
trace(t2, s2, r2, i2) {
|
|
815
|
+
switch (this.p[t2]) {
|
|
816
|
+
case 2:
|
|
817
|
+
return this.v[t2];
|
|
818
|
+
case 1:
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
this.p[t2] = 1;
|
|
822
|
+
let n2 = null;
|
|
823
|
+
const h2 = s2[t2];
|
|
824
|
+
if (h2 !== null)
|
|
825
|
+
n2 = h2;
|
|
826
|
+
else if (!r2.has(t2))
|
|
827
|
+
throw new Error("Trace without source at pc " + t2);
|
|
828
|
+
if (n2 = this.mergeTraces(n2, t2, r2, s2, r2, i2), n2 !== null) {
|
|
829
|
+
const s3 = i2[t2];
|
|
830
|
+
s3 !== null && (n2 = o(n2, s3));
|
|
831
|
+
}
|
|
832
|
+
return this.v[t2] = n2, this.p[t2] = 2, n2;
|
|
833
|
+
}
|
|
834
|
+
buildSurvivorTraces(t2, s2, r2, i2, n2) {
|
|
835
|
+
for (let h2 = 0, e2 = t2.length;h2 < e2; ++h2) {
|
|
836
|
+
if (!r2.has(h2)) {
|
|
837
|
+
s2[h2] = null;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
this.v.fill(null), this.p.fill(0);
|
|
841
|
+
const e3 = this.mergeTraces(null, h2, r2, t2, i2, n2);
|
|
842
|
+
if (e3 === null)
|
|
843
|
+
throw new Error("No non-cyclic paths found to survivor " + h2);
|
|
844
|
+
s2[h2] = o(e3, null);
|
|
845
|
+
}
|
|
846
|
+
this.v.fill(null);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
class a {
|
|
851
|
+
constructor(t2) {
|
|
852
|
+
this.g = [], this.k = [], this.m = [], this.A = new e(t2.maxFromByPc), this.T = new e(t2.maxSurvivorFromByPc), this.S = new u(t2.programLength);
|
|
853
|
+
for (let s2 = 0;s2 < t2.programLength; ++s2)
|
|
854
|
+
this.g.push(null), this.k.push(null), this.m.push(null);
|
|
855
|
+
this.k[0] = c.EMPTY;
|
|
856
|
+
}
|
|
857
|
+
reset(t2) {
|
|
858
|
+
this.A.clear(), this.T.clear(), this.g.fill(null), t2 && (this.k.fill(null), this.m.fill(null), this.k[0] = c.EMPTY);
|
|
859
|
+
}
|
|
860
|
+
record(t2, s2) {
|
|
861
|
+
this.g[t2] = s2;
|
|
862
|
+
}
|
|
863
|
+
has(t2) {
|
|
864
|
+
return this.A.has(t2) || this.k[t2] !== null;
|
|
865
|
+
}
|
|
866
|
+
add(t2, s2) {
|
|
867
|
+
this.A.add(t2, s2);
|
|
868
|
+
}
|
|
869
|
+
hasSurvivor(t2) {
|
|
870
|
+
return this.T.has(t2);
|
|
871
|
+
}
|
|
872
|
+
addSurvivor(t2, s2) {
|
|
873
|
+
this.T.add(t2, s2);
|
|
874
|
+
}
|
|
875
|
+
buildSurvivorTraces() {
|
|
876
|
+
const t2 = this.k;
|
|
877
|
+
this.S.buildSurvivorTraces(t2, this.m, this.T, this.A, this.g), this.k = this.m, this.m = t2;
|
|
878
|
+
}
|
|
879
|
+
getTraces(t2) {
|
|
880
|
+
const s2 = t2.reduce((t3, s3) => l(t3, this.k[s3]), null);
|
|
881
|
+
return s2 === null ? [] : Array.isArray(s2) ? s2 : [s2];
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
class f {
|
|
886
|
+
constructor(t2) {
|
|
887
|
+
this.I = [], this.N = new h(t2.programLength), this.M = new h(t2.programLength), this.P = new a(t2);
|
|
888
|
+
}
|
|
889
|
+
reset() {
|
|
890
|
+
this.N.reset(), this.N.add(0, 0), this.I.length = 0, this.P.reset(true);
|
|
891
|
+
}
|
|
892
|
+
getNextThreadPc() {
|
|
893
|
+
return this.N.getNextPc();
|
|
894
|
+
}
|
|
895
|
+
step(t2, s2, r2) {
|
|
896
|
+
const i2 = this.P.has(s2);
|
|
897
|
+
this.P.add(t2, s2);
|
|
898
|
+
const n2 = this.N.getBadness(t2) + r2;
|
|
899
|
+
i2 ? this.N.reschedule(s2, n2) : this.N.add(s2, n2);
|
|
900
|
+
}
|
|
901
|
+
stepToNextGeneration(t2, s2) {
|
|
902
|
+
const r2 = this.P.hasSurvivor(s2);
|
|
903
|
+
this.P.addSurvivor(t2, s2);
|
|
904
|
+
const i2 = this.N.getBadness(t2);
|
|
905
|
+
r2 ? this.M.reschedule(s2, i2) : this.M.add(s2, i2);
|
|
906
|
+
}
|
|
907
|
+
accept(t2) {
|
|
908
|
+
this.I.push(t2), this.P.addSurvivor(t2, t2);
|
|
909
|
+
}
|
|
910
|
+
fail(t2) {}
|
|
911
|
+
record(t2, s2) {
|
|
912
|
+
this.P.record(t2, s2);
|
|
913
|
+
}
|
|
914
|
+
nextGeneration() {
|
|
915
|
+
this.P.buildSurvivorTraces(), this.P.reset(false);
|
|
916
|
+
const t2 = this.N;
|
|
917
|
+
t2.reset(), this.N = this.M, this.M = t2;
|
|
918
|
+
}
|
|
919
|
+
getAcceptingTraces() {
|
|
920
|
+
return this.P.getTraces(this.I);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
class d {
|
|
925
|
+
constructor(t2) {
|
|
926
|
+
this.U = [], this.G = t2, this.V = i.fromProgram(t2), this.U.push(new f(this.V));
|
|
927
|
+
}
|
|
928
|
+
execute(t2, s2) {
|
|
929
|
+
const r2 = this.U.pop() || new f(this.V);
|
|
930
|
+
r2.reset();
|
|
931
|
+
const i2 = t2.length;
|
|
932
|
+
let h2, e2 = -1;
|
|
933
|
+
do {
|
|
934
|
+
let n2 = r2.getNextThreadPc();
|
|
935
|
+
if (n2 === null)
|
|
936
|
+
break;
|
|
937
|
+
for (++e2, h2 = e2 >= i2 ? null : t2[e2];n2 !== null; ) {
|
|
938
|
+
const t3 = this.G[n2];
|
|
939
|
+
switch (t3.op) {
|
|
940
|
+
case 0:
|
|
941
|
+
h2 === null ? r2.accept(n2) : r2.fail(n2);
|
|
942
|
+
break;
|
|
943
|
+
case 2: {
|
|
944
|
+
const i3 = t3.func;
|
|
945
|
+
if (i3 === null || i3(s2)) {
|
|
946
|
+
r2.fail(n2);
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
r2.step(n2, n2 + 1, 0);
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
case 1:
|
|
953
|
+
r2.step(n2, n2 + 1, t3.data);
|
|
954
|
+
break;
|
|
955
|
+
case 5:
|
|
956
|
+
if (h2 === null) {
|
|
957
|
+
r2.fail(n2);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
if (!(0, t3.func)(h2, t3.data, s2)) {
|
|
961
|
+
r2.fail(n2);
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
r2.stepToNextGeneration(n2, n2 + 1);
|
|
965
|
+
break;
|
|
966
|
+
case 3: {
|
|
967
|
+
const s3 = t3.data, i3 = s3.length;
|
|
968
|
+
if (i3 === 0) {
|
|
969
|
+
r2.fail(n2);
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
for (let t4 = 0;t4 < i3; ++t4)
|
|
973
|
+
r2.step(n2, s3[t4], 0);
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
case 4: {
|
|
977
|
+
const i3 = (0, t3.func)(t3.data, e2, s2);
|
|
978
|
+
i3 != null && r2.record(n2, i3), r2.step(n2, n2 + 1, 0);
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
n2 = r2.getNextThreadPc();
|
|
983
|
+
}
|
|
984
|
+
r2.nextGeneration();
|
|
985
|
+
} while (h2 !== null);
|
|
986
|
+
const l2 = new n(r2.getAcceptingTraces());
|
|
987
|
+
return r2.reset(), this.U.push(r2), l2;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function w(t2) {
|
|
991
|
+
const s2 = new r;
|
|
992
|
+
return t2(s2), new d(s2.program);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ../../node_modules/.bun/xspattern@3.1.0/node_modules/xspattern/dist/xspattern.esm.js
|
|
996
|
+
function B(A) {
|
|
997
|
+
return (B2) => B2 === A;
|
|
998
|
+
}
|
|
999
|
+
function a2(A, B2) {
|
|
1000
|
+
if (A === null || B2 === null)
|
|
1001
|
+
throw new Error("unescaped hyphen may not be used as a range endpoint");
|
|
1002
|
+
if (B2 < A)
|
|
1003
|
+
throw new Error("character range is in the wrong order");
|
|
1004
|
+
return (a3) => A <= a3 && a3 <= B2;
|
|
1005
|
+
}
|
|
1006
|
+
function n2(A) {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
function e2() {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
function t2(A, B2) {
|
|
1013
|
+
return (a3) => A(a3) || B2(a3);
|
|
1014
|
+
}
|
|
1015
|
+
function G(A, B2) {
|
|
1016
|
+
switch (B2.kind) {
|
|
1017
|
+
case "predicate":
|
|
1018
|
+
return void A.test(B2.value);
|
|
1019
|
+
case "regexp":
|
|
1020
|
+
return void r2(A, B2.value, false);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function i2(A, B2) {
|
|
1024
|
+
B2.forEach((B3) => {
|
|
1025
|
+
(function(A2, B4) {
|
|
1026
|
+
const [a3, { min: n3, max: e3 }] = B4;
|
|
1027
|
+
if (e3 !== null) {
|
|
1028
|
+
for (let B5 = 0;B5 < n3; ++B5)
|
|
1029
|
+
G(A2, a3);
|
|
1030
|
+
for (let B5 = n3;B5 < e3; ++B5) {
|
|
1031
|
+
const B6 = A2.jump([]);
|
|
1032
|
+
B6.data.push(A2.program.length), G(A2, a3), B6.data.push(A2.program.length);
|
|
1033
|
+
}
|
|
1034
|
+
} else if (n3 > 0) {
|
|
1035
|
+
for (let B6 = 0;B6 < n3 - 1; ++B6)
|
|
1036
|
+
G(A2, a3);
|
|
1037
|
+
const B5 = A2.program.length;
|
|
1038
|
+
G(A2, a3), A2.jump([B5]).data.push(A2.program.length);
|
|
1039
|
+
} else {
|
|
1040
|
+
const B5 = A2.program.length, n4 = A2.jump([]);
|
|
1041
|
+
n4.data.push(A2.program.length), G(A2, a3), A2.jump([B5]), n4.data.push(A2.program.length);
|
|
1042
|
+
}
|
|
1043
|
+
})(A, B3);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
function r2(A, B2, a3) {
|
|
1047
|
+
const n3 = A.program.length, e3 = A.jump([]);
|
|
1048
|
+
a3 && (e3.data.push(A.program.length), A.test(() => true), A.jump([n3]));
|
|
1049
|
+
const t3 = [];
|
|
1050
|
+
if (B2.forEach((B3) => {
|
|
1051
|
+
e3.data.push(A.program.length), i2(A, B3), t3.push(A.jump([]));
|
|
1052
|
+
}), t3.forEach((B3) => {
|
|
1053
|
+
B3.data.push(A.program.length);
|
|
1054
|
+
}), a3) {
|
|
1055
|
+
const B3 = A.program.length, a4 = A.jump([]);
|
|
1056
|
+
a4.data.push(A.program.length), A.test(() => true), A.jump([B3]), a4.data.push(A.program.length);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function o2(A, B2) {
|
|
1060
|
+
return { success: true, offset: A, value: B2 };
|
|
1061
|
+
}
|
|
1062
|
+
function l2(A) {
|
|
1063
|
+
return o2(A, undefined);
|
|
1064
|
+
}
|
|
1065
|
+
function H(A, B2, a3 = false) {
|
|
1066
|
+
return { success: false, offset: A, expected: B2, fatal: a3 };
|
|
1067
|
+
}
|
|
1068
|
+
function C(A) {
|
|
1069
|
+
return (B2, a3) => {
|
|
1070
|
+
const n3 = a3 + A.length;
|
|
1071
|
+
return B2.slice(a3, n3) === A ? o2(n3, A) : H(a3, [A]);
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function u2(A, B2) {
|
|
1075
|
+
return (a3, n3) => {
|
|
1076
|
+
const e3 = A(a3, n3);
|
|
1077
|
+
return e3.success ? o2(e3.offset, B2(e3.value)) : e3;
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function s2(A, B2, a3, n3) {
|
|
1081
|
+
return (e3, t3) => {
|
|
1082
|
+
const G2 = A(e3, t3);
|
|
1083
|
+
return G2.success ? B2(G2.value) ? G2 : H(t3, a3, n3) : G2;
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
function c2(A, B2) {
|
|
1087
|
+
return (a3, n3) => {
|
|
1088
|
+
let e3 = null;
|
|
1089
|
+
for (const t3 of A) {
|
|
1090
|
+
const A2 = t3(a3, n3);
|
|
1091
|
+
if (A2.success)
|
|
1092
|
+
return A2;
|
|
1093
|
+
if (e3 === null || A2.offset > e3.offset ? e3 = A2 : A2.offset === e3.offset && B2 === undefined && (e3.expected = e3.expected.concat(A2.expected)), A2.fatal)
|
|
1094
|
+
return A2;
|
|
1095
|
+
}
|
|
1096
|
+
return B2 = B2 || (e3 == null ? undefined : e3.expected) || [], e3 && (e3.expected = B2), e3 || H(n3, B2);
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
function D(A) {
|
|
1100
|
+
return (B2, a3) => {
|
|
1101
|
+
const n3 = A(B2, a3);
|
|
1102
|
+
return n3.success || n3.fatal ? n3 : o2(a3, null);
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function m(A) {
|
|
1106
|
+
return (B2, a3) => {
|
|
1107
|
+
let n3 = [], e3 = a3;
|
|
1108
|
+
for (;; ) {
|
|
1109
|
+
const a4 = A(B2, e3);
|
|
1110
|
+
if (!a4.success) {
|
|
1111
|
+
if (a4.fatal)
|
|
1112
|
+
return a4;
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
if (n3.push(a4.value), a4.offset === e3)
|
|
1116
|
+
break;
|
|
1117
|
+
e3 = a4.offset;
|
|
1118
|
+
}
|
|
1119
|
+
return o2(e3, n3);
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function I(A, B2, a3) {
|
|
1123
|
+
return (n3, e3) => {
|
|
1124
|
+
const t3 = A(n3, e3);
|
|
1125
|
+
if (!t3.success)
|
|
1126
|
+
return t3;
|
|
1127
|
+
const G2 = B2(n3, t3.offset);
|
|
1128
|
+
return G2.success ? o2(G2.offset, a3(t3.value, G2.value)) : G2;
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function d2(A) {
|
|
1132
|
+
return I(A, m(A), (A2, B2) => [A2].concat(B2));
|
|
1133
|
+
}
|
|
1134
|
+
function h2(A, B2) {
|
|
1135
|
+
return A;
|
|
1136
|
+
}
|
|
1137
|
+
function p(A, B2) {
|
|
1138
|
+
return B2;
|
|
1139
|
+
}
|
|
1140
|
+
function T(A, B2) {
|
|
1141
|
+
return I(A, B2, p);
|
|
1142
|
+
}
|
|
1143
|
+
function F(A, B2) {
|
|
1144
|
+
return I(A, B2, h2);
|
|
1145
|
+
}
|
|
1146
|
+
function E(A, B2, a3, n3 = false) {
|
|
1147
|
+
return T(A, n3 ? f2(F(B2, a3)) : F(B2, a3));
|
|
1148
|
+
}
|
|
1149
|
+
function g(A, B2) {
|
|
1150
|
+
return (a3, n3) => A(a3, n3).success ? H(n3, B2) : l2(n3);
|
|
1151
|
+
}
|
|
1152
|
+
function f2(A) {
|
|
1153
|
+
return (B2, a3) => {
|
|
1154
|
+
const n3 = A(B2, a3);
|
|
1155
|
+
return n3.success ? n3 : H(n3.offset, n3.expected, true);
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
var P = (A, B2) => A.length === B2 ? l2(B2) : H(B2, ["end of input"]);
|
|
1159
|
+
var M = ["Lu", "Ll", "Lt", "Lm", "Lo", "Mn", "Mc", "Me", "Nd", "Nl", "No", "Pc", "Pd", "Ps", "Pe", "Pi", "Pf", "Po", "Zs", "Zl", "Zp", "Sm", "Sc", "Sk", "So", "Cc", "Cf", "Co", "Cn"];
|
|
1160
|
+
var J = {};
|
|
1161
|
+
function S(A) {
|
|
1162
|
+
return A.codePointAt(0);
|
|
1163
|
+
}
|
|
1164
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("").forEach((A, B2) => {
|
|
1165
|
+
J[A] = B2;
|
|
1166
|
+
});
|
|
1167
|
+
var K = (A) => A === -1 || A === -2;
|
|
1168
|
+
function b(A) {
|
|
1169
|
+
return (B2) => !K(B2) && !A(B2);
|
|
1170
|
+
}
|
|
1171
|
+
function y(A, B2) {
|
|
1172
|
+
return B2 === null ? A : (a3) => A(a3) && !B2(a3);
|
|
1173
|
+
}
|
|
1174
|
+
var Q = function(A, B2) {
|
|
1175
|
+
const n3 = new Map;
|
|
1176
|
+
let e3 = 0;
|
|
1177
|
+
return A.forEach((A2, G2) => {
|
|
1178
|
+
const i3 = B2[G2];
|
|
1179
|
+
A2 !== null && A2.split("|").forEach((A3) => {
|
|
1180
|
+
const B3 = n3.get(A3), G3 = a2(e3, e3 + i3 - 1);
|
|
1181
|
+
n3.set(A3, B3 ? t2(B3, G3) : G3);
|
|
1182
|
+
}), e3 += i3;
|
|
1183
|
+
}), n3;
|
|
1184
|
+
}(["BasicLatin", "Latin-1Supplement", "LatinExtended-A", "LatinExtended-B", "IPAExtensions", "SpacingModifierLetters", "CombiningDiacriticalMarks", "GreekandCoptic|Greek", "Cyrillic", "CyrillicSupplement", "Armenian", "Hebrew", "Arabic", "Syriac", "ArabicSupplement", "Thaana", "NKo", "Samaritan", "Mandaic", "SyriacSupplement", "ArabicExtended-B", "ArabicExtended-A", "Devanagari", "Bengali", "Gurmukhi", "Gujarati", "Oriya", "Tamil", "Telugu", "Kannada", "Malayalam", "Sinhala", "Thai", "Lao", "Tibetan", "Myanmar", "Georgian", "HangulJamo", "Ethiopic", "EthiopicSupplement", "Cherokee", "UnifiedCanadianAboriginalSyllabics", "Ogham", "Runic", "Tagalog", "Hanunoo", "Buhid", "Tagbanwa", "Khmer", "Mongolian", "UnifiedCanadianAboriginalSyllabicsExtended", "Limbu", "TaiLe", "NewTaiLue", "KhmerSymbols", "Buginese", "TaiTham", "CombiningDiacriticalMarksExtended", "Balinese", "Sundanese", "Batak", "Lepcha", "OlChiki", "CyrillicExtended-C", "GeorgianExtended", "SundaneseSupplement", "VedicExtensions", "PhoneticExtensions", "PhoneticExtensionsSupplement", "CombiningDiacriticalMarksSupplement", "LatinExtendedAdditional", "GreekExtended", "GeneralPunctuation", "SuperscriptsandSubscripts", "CurrencySymbols", "CombiningDiacriticalMarksforSymbols|CombiningMarksforSymbols", "LetterlikeSymbols", "NumberForms", "Arrows", "MathematicalOperators", "MiscellaneousTechnical", "ControlPictures", "OpticalCharacterRecognition", "EnclosedAlphanumerics", "BoxDrawing", "BlockElements", "GeometricShapes", "MiscellaneousSymbols", "Dingbats", "MiscellaneousMathematicalSymbols-A", "SupplementalArrows-A", "BraillePatterns", "SupplementalArrows-B", "MiscellaneousMathematicalSymbols-B", "SupplementalMathematicalOperators", "MiscellaneousSymbolsandArrows", "Glagolitic", "LatinExtended-C", "Coptic", "GeorgianSupplement", "Tifinagh", "EthiopicExtended", "CyrillicExtended-A", "SupplementalPunctuation", "CJKRadicalsSupplement", "KangxiRadicals", null, "IdeographicDescriptionCharacters", "CJKSymbolsandPunctuation", "Hiragana", "Katakana", "Bopomofo", "HangulCompatibilityJamo", "Kanbun", "BopomofoExtended", "CJKStrokes", "KatakanaPhoneticExtensions", "EnclosedCJKLettersandMonths", "CJKCompatibility", "CJKUnifiedIdeographsExtensionA", "YijingHexagramSymbols", "CJKUnifiedIdeographs", "YiSyllables", "YiRadicals", "Lisu", "Vai", "CyrillicExtended-B", "Bamum", "ModifierToneLetters", "LatinExtended-D", "SylotiNagri", "CommonIndicNumberForms", "Phags-pa", "Saurashtra", "DevanagariExtended", "KayahLi", "Rejang", "HangulJamoExtended-A", "Javanese", "MyanmarExtended-B", "Cham", "MyanmarExtended-A", "TaiViet", "MeeteiMayekExtensions", "EthiopicExtended-A", "LatinExtended-E", "CherokeeSupplement", "MeeteiMayek", "HangulSyllables", "HangulJamoExtended-B", "HighSurrogates", "HighPrivateUseSurrogates", "LowSurrogates", "PrivateUseArea|PrivateUse", "CJKCompatibilityIdeographs", "AlphabeticPresentationForms", "ArabicPresentationForms-A", "VariationSelectors", "VerticalForms", "CombiningHalfMarks", "CJKCompatibilityForms", "SmallFormVariants", "ArabicPresentationForms-B", "HalfwidthandFullwidthForms", "Specials", "LinearBSyllabary", "LinearBIdeograms", "AegeanNumbers", "AncientGreekNumbers", "AncientSymbols", "PhaistosDisc", null, "Lycian", "Carian", "CopticEpactNumbers", "OldItalic", "Gothic", "OldPermic", "Ugaritic", "OldPersian", null, "Deseret", "Shavian", "Osmanya", "Osage", "Elbasan", "CaucasianAlbanian", "Vithkuqi", null, "LinearA", "LatinExtended-F", null, "CypriotSyllabary", "ImperialAramaic", "Palmyrene", "Nabataean", null, "Hatran", "Phoenician", "Lydian", null, "MeroiticHieroglyphs", "MeroiticCursive", "Kharoshthi", "OldSouthArabian", "OldNorthArabian", null, "Manichaean", "Avestan", "InscriptionalParthian", "InscriptionalPahlavi", "PsalterPahlavi", null, "OldTurkic", null, "OldHungarian", "HanifiRohingya", null, "RumiNumeralSymbols", "Yezidi", "ArabicExtended-C", "OldSogdian", "Sogdian", "OldUyghur", "Chorasmian", "Elymaic", "Brahmi", "Kaithi", "SoraSompeng", "Chakma", "Mahajani", "Sharada", "SinhalaArchaicNumbers", "Khojki", null, "Multani", "Khudawadi", "Grantha", null, "Newa", "Tirhuta", null, "Siddham", "Modi", "MongolianSupplement", "Takri", null, "Ahom", null, "Dogra", null, "WarangCiti", "DivesAkuru", null, "Nandinagari", "ZanabazarSquare", "Soyombo", "UnifiedCanadianAboriginalSyllabicsExtended-A", "PauCinHau", "DevanagariExtended-A", null, "Bhaiksuki", "Marchen", null, "MasaramGondi", "GunjalaGondi", null, "Makasar", "Kawi", null, "LisuSupplement", "TamilSupplement", "Cuneiform", "CuneiformNumbersandPunctuation", "EarlyDynasticCuneiform", null, "Cypro-Minoan", "EgyptianHieroglyphs", "EgyptianHieroglyphFormatControls", null, "AnatolianHieroglyphs", null, "BamumSupplement", "Mro", "Tangsa", "BassaVah", "PahawhHmong", null, "Medefaidrin", null, "Miao", null, "IdeographicSymbolsandPunctuation", "Tangut", "TangutComponents", "KhitanSmallScript", "TangutSupplement", null, "KanaExtended-B", "KanaSupplement", "KanaExtended-A", "SmallKanaExtension", "Nushu", null, "Duployan", "ShorthandFormatControls", null, "ZnamennyMusicalNotation", null, "ByzantineMusicalSymbols", "MusicalSymbols", "AncientGreekMusicalNotation", null, "KaktovikNumerals", "MayanNumerals", "TaiXuanJingSymbols", "CountingRodNumerals", null, "MathematicalAlphanumericSymbols", "SuttonSignWriting", null, "LatinExtended-G", "GlagoliticSupplement", "CyrillicExtended-D", null, "NyiakengPuachueHmong", null, "Toto", "Wancho", null, "NagMundari", null, "EthiopicExtended-B", "MendeKikakui", null, "Adlam", null, "IndicSiyaqNumbers", null, "OttomanSiyaqNumbers", null, "ArabicMathematicalAlphabeticSymbols", null, "MahjongTiles", "DominoTiles", "PlayingCards", "EnclosedAlphanumericSupplement", "EnclosedIdeographicSupplement", "MiscellaneousSymbolsandPictographs", "Emoticons", "OrnamentalDingbats", "TransportandMapSymbols", "AlchemicalSymbols", "GeometricShapesExtended", "SupplementalArrows-C", "SupplementalSymbolsandPictographs", "ChessSymbols", "SymbolsandPictographsExtended-A", "SymbolsforLegacyComputing", null, "CJKUnifiedIdeographsExtensionB", null, "CJKUnifiedIdeographsExtensionC", "CJKUnifiedIdeographsExtensionD", "CJKUnifiedIdeographsExtensionE", "CJKUnifiedIdeographsExtensionF", null, "CJKCompatibilityIdeographsSupplement", null, "CJKUnifiedIdeographsExtensionG", "CJKUnifiedIdeographsExtensionH", null, "Tags", null, "VariationSelectorsSupplement", null, "SupplementaryPrivateUseArea-A|PrivateUse", "SupplementaryPrivateUseArea-B|PrivateUse"], [128, 128, 128, 208, 96, 80, 112, 144, 256, 48, 96, 112, 256, 80, 48, 64, 64, 64, 32, 16, 48, 96, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 256, 160, 96, 256, 384, 32, 96, 640, 32, 96, 32, 32, 32, 32, 128, 176, 80, 80, 48, 96, 32, 32, 144, 80, 128, 64, 64, 80, 48, 16, 48, 16, 48, 128, 64, 64, 256, 256, 112, 48, 48, 48, 80, 64, 112, 256, 256, 64, 32, 160, 128, 32, 96, 256, 192, 48, 16, 256, 128, 128, 256, 256, 96, 32, 128, 48, 80, 96, 32, 128, 128, 224, 16, 16, 64, 96, 96, 48, 96, 16, 32, 48, 16, 256, 256, 6592, 64, 20992, 1168, 64, 48, 320, 96, 96, 32, 224, 48, 16, 64, 96, 32, 48, 48, 32, 96, 32, 96, 32, 96, 32, 48, 64, 80, 64, 11184, 80, 896, 128, 1024, 6400, 512, 80, 688, 16, 16, 16, 32, 32, 144, 240, 16, 128, 128, 64, 80, 64, 48, 128, 32, 64, 32, 48, 32, 48, 32, 64, 32, 80, 48, 48, 80, 48, 64, 80, 64, 384, 64, 64, 64, 32, 32, 48, 48, 32, 32, 32, 64, 32, 96, 96, 32, 32, 32, 64, 64, 32, 32, 48, 80, 80, 48, 128, 64, 288, 32, 64, 64, 48, 64, 64, 48, 32, 128, 80, 48, 80, 48, 96, 32, 80, 48, 48, 80, 128, 128, 128, 96, 160, 128, 96, 32, 80, 48, 80, 176, 80, 80, 96, 96, 64, 96, 80, 96, 16, 64, 96, 160, 112, 80, 64, 96, 80, 304, 32, 96, 80, 16, 64, 1024, 128, 208, 2624, 112, 1072, 48, 4000, 640, 8576, 576, 48, 96, 48, 144, 688, 96, 96, 160, 64, 32, 6144, 768, 512, 128, 8816, 16, 256, 48, 64, 400, 2304, 160, 16, 4688, 208, 48, 256, 256, 80, 112, 32, 32, 96, 32, 128, 1024, 688, 1104, 256, 48, 96, 112, 80, 320, 48, 64, 464, 48, 736, 32, 224, 32, 96, 784, 80, 64, 80, 176, 256, 256, 48, 112, 96, 256, 256, 768, 80, 48, 128, 128, 128, 256, 256, 112, 144, 256, 1024, 42720, 32, 4160, 224, 5776, 7488, 3088, 544, 1504, 4944, 4192, 711760, 128, 128, 240, 65040, 65536, 65536]);
|
|
1185
|
+
var x = function(A) {
|
|
1186
|
+
const n3 = new Map, G2 = A.split(""), i3 = M.map(() => []);
|
|
1187
|
+
let r3 = 0, o3 = 0;
|
|
1188
|
+
for (;o3 < G2.length; ) {
|
|
1189
|
+
const A2 = J[G2[o3]], n4 = (31 & A2) - 2;
|
|
1190
|
+
let e3 = 1 + J[G2[o3 + 1]];
|
|
1191
|
+
switch (32 & A2 ? (e3 += J[G2[o3 + 2]] << 6, e3 += J[G2[o3 + 3]] << 12, e3 += J[G2[o3 + 4]] << 18, o3 += 5) : o3 += 2, n4) {
|
|
1192
|
+
case -2: {
|
|
1193
|
+
let A3 = 0;
|
|
1194
|
+
for (let a3 = r3;a3 < r3 + e3; ++a3) {
|
|
1195
|
+
i3[A3].push(B(a3)), A3 = (A3 + 1) % 2;
|
|
1196
|
+
}
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
case -1:
|
|
1200
|
+
break;
|
|
1201
|
+
default: {
|
|
1202
|
+
const A3 = i3[n4];
|
|
1203
|
+
e3 === 1 ? A3.push(B(r3)) : A3.push(a2(r3, r3 + e3 - 1));
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
r3 += e3;
|
|
1208
|
+
}
|
|
1209
|
+
const l3 = new Map;
|
|
1210
|
+
return M.forEach((A2, B2) => {
|
|
1211
|
+
const a3 = i3[B2].reduce(t2, e2);
|
|
1212
|
+
n3.set(A2, a3);
|
|
1213
|
+
const G3 = A2.charAt(0), r4 = l3.get(G3) || [];
|
|
1214
|
+
l3.set(G3, r4), r4.push(a3);
|
|
1215
|
+
}), l3.forEach((A2, B2) => {
|
|
1216
|
+
n3.set(B2, A2.reduce(t2, e2));
|
|
1217
|
+
}), n3;
|
|
1218
|
+
}("bfUATCYATCPAQATAXATAOATBKJTBXCTBCZPATAQAZANAZADZPAXAQAXAbgUATAYDaATAZAaAGARAXAcAaAZAaAXAMBZADATBZAMAGASAMCTACWXACGDXXADHA3DAAPDAAtCAAFDBCAADCAABCCDBCCABCAABCCDCCAABCAAFCAADDAABCAABCBADCBDBGACADCGDCAEADACAEADACAEADAAPDAARDACAEADAABCBA7DFCAABCBDBABCCAJjDBAAGADaFRZDFLZNFEZGFAZAFAZQnvBAAADFAZACADABBFADCTACABDZBCATACCBACABACAABCQBACIDiCADBCCDCAXDDCADAXAABCBDBCyDvAhaAHEJBA1CAANDAgfBAABAClBBFATFDoTAOABBaBYABAHsOAHATAHBTAHBTAHABHGaBDGDTBBKcFXCTBYATBaBHKTAcATCGfFAGJHUKJTDGBHAmiBAATAGAHGcAaAHFFBHBaAHDGBKJGCaBGATNBAcAGAHAGdHaBBmYBAAHKGABNKJGgHIFBaATCFABBHAYBGVHDFAHIFAHCFAHEBBTOBAGYHCBBTABAGKBEGXZAGFBAcBBFHHGoFAHXcAHfIAG1HAIAHAGAICHHIDHAIBGAHGGJHBTBKJTAFAGOHAIBBAGHBBGBBBGVBAGGBAGABCGDBBHAGAICHDBBIBBBIBHAGABHIABDGBBAGCHBBBKJGBYBMFaAYAGATAHABBHBIABAGFBDGBBBGVBAGGBAGBBAGBBAGBBBHABAICHBBDHBBBHCBCHABGGDBAGABGKJHBGCHATABJHBIABAGIBAGCBAGVBAGGBAGBBAGEBBHAGAICHEBAHBIABAIBHABBGABOGBHBBBKJTAYABGGAHFBAHAIBBAGHBBGBBBGVBAGGBAGBBAGEBBHAGAIAHAIAHDBBIBBBIBHABGHBIABDGBBAGCHBBBKJaAGAMFBJHAGABAGFBCGCBAGDBCGBBAGABAGBBCGBBCGCBCGLBDIBHAIBBCICBAICHABBGABFIABNKJMCaFYAaABEHAICHAGHBAGCBAGWBAGPBBHAGAHCIDBAHCBAHDBGHBBAGCBBGABBGBHBBBKJBGTAMGaAGAHAIBTAGHBAGCBAGWBAGJBAGEBBHAGAIAHAIEBAHAIBBAIBHBBGIBBFGBBAGBHBBBKJBAGBIABLHBIBGIBAGCBAGoHBGAICHDBAICBAICHAGAaABDGCIAMGGCHBBBKJMIaAGFBAHAIBBAGRBCGXBAGIBAGABBGGBCHABDICHCBAHABAIHBFKJBBIBTABLGvHAGBHGBDYAGFFAHHTAKJTBBkGBBAGABAGEBAGXBAGABAGJHAGBHIGABBGEBAFABAHGBAKJBBGDBfGAaCTOaATAaCHBaFKJMJaAHAaAHAaAHAPAQAPAQAIBGHBAGjBDHNIAHETAHBGEHKBAHjBAaHHAaFBAaBTEaDTBBkGqIBHDIAHFIAHBIBHBGAKJTFGFIBHBGDHCGAICGBIGGCHDGMHAIBHBIFHAGAIAKJICHAaBClBACABECABBDqTAFADCmIFAABAGDBBGGBAGABAGDBBGoBAGDBBGgBAGDBBGGBAGABAGDBBGOBAG4BAGDBBmCBAABBHCTIMTBCGPaJBFiVBAABBDFBBOAmrJAAaATAGQUAGZPAQABCmKBAATCLCGHBGGRHCIABIGSHBIATBBIGRHBBLGMBAGCBAHBBLGzHBIAHGIHHAIBHKTCFATCYAGAHABBKJBFMJBFTFOATDHCcAHAKJBFGiFAG0BGGEHBGhHAGABEmFBAABJGeBAHCIDHBICBDIBHAIFHCBDaABCTBKJGdBBGEBKGrBDGZBFKJMABCahGWHBIBHABBTBG0IAHAIAHGBAHAIAHAIBHHIFHJBBHAKJBFKJBFTGFATFBBHNJAHPBwHDIAGuHAIAHEIAHAIEHAIBGHBCKJTGaJHIaITBBAHBIAGdIAHDIBHBIAHCGBKJGrHAIAHBICHAIAHCIBBHTDGjIHHHIBHBBCTEKJBCGCKJGdFFTBDIBGCqBBCCTHBHHCTAHMIAHGGDHAGFHAGBIAHBGABEDrF+DMFADhFkH/gVCAADHghBAADHCHDFBBCFBBDHCHDHCHDFBBCFBBDHBACABACABACABACADHCHDNBBDHEHDHEHDHEHDEBADBCDEAZADAZCDCBADBCDEAZCDDBBDBCDBAZCDHCEZCBBDCBADBCDEAZBBAUKcEOFTBRASAPARBSAPARATHVAWAcEUATIRASATDNBTCXAPAQATKXATANATJUAcEBAcJMAFABBMFXCPAQAFAMJXCPAQABAFMBCYgBOHMJDHAJCHLBOaBCAaDCAaBDACCDBCCDAaACAaBXACEaFCAaACAaACAaACDaADACDDAGDDAaBDBCBXECADDaAXAaBDAaAMPLiCADALDMAaBBDXEaEXBaDXAaBXAaBXAaGXAaeXBaBXAaAXAae3LEAAaHPAQAPAQAaTXBaGPAQA6QBAAXAadXYanXF6EBAABYaKBUM76NBAAMV62CAAXAaIXAa1XH6uBAAXA63DAAPAQAPAQAPAQAPAQAPAQAPAQAPAQAMdarXEPAQAXePAQAPAQAPAQAPAQAPAQAXP6/DAA3CCAAPAQAPAQAPAQAPAQAPAQAPAQAPAQAPAQAPAQAPAQAPAQAX+PAQAPAQAXfPAQA3BEAAavXUaBXFamBBafBA6oBAACvDvABCCDBAFCCADDACADFFBCBgjBAADAaFADHCCADABETDMATBDlBADABEDABBG3BGFATABNHAGWBIGGBAGGBAGGBAGGBAGGBAGGBAGGBAGGBAHfTBRASARASATCRASATARASATIOATBOATARASATBRASAPAQAPAQAPAQAPAQATEFATJOBTDOATAPATMaBTCPAQAPAQAPAQAPAQAOABhaZBA6YBAABL6VDAABZaLBDUATCaAFAGALAPAQAPAQAPAQAPAQAPAQAaBPAQAPAQAPAQAPAQAOAPAQBaALIHDIBOAFEaBLCFAGATAaBBAmVBAABBHBZBFBGAOAmZBAATAFCGABEGqBAmdBAABAaBMDaJGfajBLGPaeBAMJadMHaAMOafMJamMO6/EAAm/mBAa/mUIFAFAm2RAABCa2BIGnFFTBmLEAAFATCGPKJGBBTAtGAHAJCTAHJTAFAAbFBHBmFBAALJHBTFBHZWFIZBANDBA9FADHADCAAJFAZBADGAADDBATCDABCDAPCCADBECADABADABADAADBXFCCADAGAFBDAGGHAGCHAGDHAGWIBHBIAaDHABCMFaBYAaABFGzTDBHIBGxIPHBBHTBKJBFHRGFTCGATAGBHAKJGbHHTBGWHKIBBKTAGcBCHCIAGuHAIBHDIBHBICTMBAFAKJBDTBGEHAFAGIKJGEBAGoHFIBHBIBHBBIGCHAGHHAIABBKJBBTDGPFAGFaCGAIAHAIAGxHAGAHCGBHBGEHBGAHAGABXGBFATBGKIAHBIBTBGAFBIAHABJGFBBGFBBGFBIGGBAGGBADqZAFDDIFAZBBDjPBAAGiIBHAIBHAIBTAIAHABBKJBFmjuCABLGWBDGwhDgAA9/jBAmtFAABBmpBAABlDGBLDEBEGAHAGJXAGMBAGEBAGABAGBBAGBBAmrBAAZQBPmqFAAQAPAaPG/BBG1BGaABfGLYAaCHPTGPAQATABFHPTAOBNBPAQAPAQAPAQAPAQAPAQAPAQAPAQAPAQATBPAQATDNCTCBATDOAPAQAPAQAPAQATCXAOAXCBATAYATBBDGEBAmGCAABBcABATCYATCPAQATAXATAOATBKJTBXCTBCZPATAQAZANAZADZPAXAQAXAPAQATAPAQATBGJFAGsFBGeBCGFBBGFBBGFBBGCBCYBXAZAaAYBBAaAXDaBBJcCaBBBGLBAGZBAGSBAGBBAGOBBGNBhm6BAABETCBDMsBCaIL0MDaQMBaCBAaMBCaABuasHAhBCAAGcBCGwBOHAMaBDGfMDBIGTLAGHLABEGlHEBEGdBATAGjBDGHTALEBpCnDnmNBAABBKJBFCjBDDjBDGnBHGzBKTACKBACOBACGBACBBADKBADOBADGBADBhCBAAm2EAABIGVBJGHBXFFBAFpBAFIhEBAAGFBBGABAGrBAGBBCGABBGWBATAMHGWaBMGGeBHMIBvGSBAGBBEMEGVMFBCTAGZBETAB/G3BDMBGBMPBBMtGAHCBAHBBEHDGDBAGCBAGcBBHCBDHAMIBGTIBGGcMBTAGcMCBfGHaAGbHBBDMETGBIG1BCTGGVBBMHGSBEMHGRBGTDBLMGhPBAAmIBAAB2CyBMDyBGMFGjHDBHKJhlEAAMeBAGpBAHBOABBGBhKBAAHCGcMJGABHGVHKMDTEBVGRHDTDBlGUMGBTGWBIIAHAIAG0HOTGBDMTKJHAGBHBGABIHCIAGsICHDIBHBTBcATDHABJcABBGYBGKJBFHCGjHEIAHHBAKJTDGAIBGABHGiHATBGABIHBIAGvICHIIBGDTDHDTAIAHAKJGATAGATCBAMTBKGRBAGYICHCIBHAIAHBTFHAGBHAB9GGBAGABAGDBAGOBAGJTABFGuHAICHHBEKJBFHBIBBAGHBBGBBBGVBAGGBAGBBAGEBAHBGAIBHAIDBBIBBBICBBGABFIABEGEIBBBHGBCHEhKCAAG0ICHHIBHCIAHAGDTEKJTBBATAHAGCBdGvICHFIAHAIDHBIAHBGBTAGABHKJhlCAAGuICHDBBIDHBIAHBTWGDHBBhGvICHHIBHAIAHBTCGABKKJBFTMBSGqHAIAHAIBHFIAHAGATABFKJB1GaBBHCIBHDIAHEBDKJMBTCaAGGh4CAAGrICHIIAHBTAhjBAACfDfKJMIBLGHBBGABBGHBAGBBAGXIFBAIBBBHBIAHAGAIAGAIAHATCBIKJhFBAAGHBBGmICHDBBHBIDHAGATAGAIABaGAHJGnHFIAGAHDTHHABHGAHFIBHCGtHMIAHBTCGATEBMmIBAABGTJh1DAAGIBAGkIAHGBAHFIAHAGATEBJKJMSBCTBGdBBHVBAIAHGIAHBIAHBhIBAAGGBAGBBAGlHFBCHABAHBBAHGGAHABHKJBFGFBAGBBAGfIEBAHBBAIBHAIAHAGABGKJh1EAAGSHBIBTBBGHBGAIAGMBAGhIBHEBCIBHAIAHATMKJhVBAAGABOMUaHYDaQBMTAmZOAAhlBAAruBAABATEBKmDDAAhLpAAmgBAATBBMmvQAAcPHAGFHOhp+AAmGJAAh4GCAm4IAABGGeBAKJBDTBmOBAABAKJBFGdBBHETABJGvHGTEaDFDTAaABJKJBAMGBAGUBEGShvKAACfDfMWTDhkBAAmKBAABDHAGAI2BGHDFMB/FBTAFAHABKIBBNm3fBABHmVTAABpGIhmLCAFDBAFGBAFBBAmiEAABOGABcGCBBGABNGDBHmLGAAhDkAAmqBAABEGMBCGIBGGJBBaAHBTAcDhbJBAHtBBHWBI6zBAAB761DAABJamBBa7IBHCaCIFcHHHaBHGadHDa8BU6BBAAHCaAh5BAAMTBLMTBL6WBAABIMYhGCAACZDZCZDGBADRCZDZCABACBBBCABBCBBBCDBACHDDBADABADGBADKCZDZCBBACDBBCHBACGBADZCBBACDBACEBACABCCGBADZCZDZCZDZCZDZCZDZCZDZCZDbBBCYXADYXADFCYXADYXADFCYXADYXADFCYXADYXADFCYXADYXADFCADABBKx6/HAAH2aDHxaHHAaNHAaBTEBOHEBAHOhPRAADJGADTBFDFhUDAAHGBAHQBBHGBAHBBAHEBEF9BgHAhvBAAGsBCHGFGBBKJBDGAaAh/EAAGdHABQGrHDKJBEYAhPHAAGaFAHDKJhlLAAGGBAGDBAGBBAGOBAmEDAABBMIHGBoChDhHGFABDKJBDTBhQMAAM6aAMCYAMDhLBAAMsaAMOhBDAAGDBAGaBAGBBAGABBGABAGJBAGDBAGABAGABFGABDGABAGABAGABAGCBAGBBAGABBGABAGABAGABAGABAGABAGBBAGABBGDBAGGBAGDBAGDBAGABAGJBAGQBEGCBAGEBAGQBzXBhNEAAarBD6jBAABLaOBBaOBAaOBAakBJMM6gCAAB3acBMarBDaIBGaBBNaFhZCAA66DAAZE6XLAABDaQBCaMBC62BAABD6eBAABFaLBDaABOaLBDa3BHaJBFanBHadBBaBhNBAA6TFAABLaNBBaMBCaIBGatBAaGBHaNBDaIBGaIBG6SCAABAa2BkKJhFQAAmfbKABfm5ABABFmdDAABBmBaBABNmw0BAhewAAmdIAAhhXAAmKNBABEmfBBAhQxtCcABd8fBAAh/BAAnvDAAhP4PA99/PABB99/PA");
|
|
1219
|
+
function L(A) {
|
|
1220
|
+
return A === 32 || A === 9 || A === 10 || A === 13;
|
|
1221
|
+
}
|
|
1222
|
+
var X = [B(S(":")), a2(S("A"), S("Z")), B(S("_")), a2(S("a"), S("z")), a2(192, 214), a2(216, 246), a2(192, 214), a2(216, 246), a2(248, 767), a2(880, 893), a2(895, 8191), a2(8204, 8205), a2(8304, 8591), a2(11264, 12271), a2(12289, 55295), a2(63744, 64975), a2(65008, 65533), a2(65536, 983039)].reduce(t2);
|
|
1223
|
+
var Z = [X, B(S("-")), B(S(".")), a2(S("0"), S("9")), B(183), a2(768, 879), a2(8255, 8256)].reduce(t2);
|
|
1224
|
+
var O = x.get("Nd");
|
|
1225
|
+
var k = b(O);
|
|
1226
|
+
var N = y(a2(0, 1114111), [x.get("P"), x.get("Z"), x.get("C")].reduce(t2));
|
|
1227
|
+
var v = b(N);
|
|
1228
|
+
function w2(A) {
|
|
1229
|
+
return A !== 10 && A !== 13 && !K(A);
|
|
1230
|
+
}
|
|
1231
|
+
var Y = { s: L, S: b(L), i: X, I: b(X), c: Z, C: b(Z), d: O, D: k, w: N, W: v };
|
|
1232
|
+
var U = C("*");
|
|
1233
|
+
var j = C("\\");
|
|
1234
|
+
var R = C("{");
|
|
1235
|
+
var V = C("}");
|
|
1236
|
+
var W = C("[");
|
|
1237
|
+
var q = C("]");
|
|
1238
|
+
var z = C("^");
|
|
1239
|
+
var $ = C("$");
|
|
1240
|
+
var _ = C(",");
|
|
1241
|
+
var AA = C("-");
|
|
1242
|
+
var BA = C("(");
|
|
1243
|
+
var aA = C(")");
|
|
1244
|
+
var nA = C(".");
|
|
1245
|
+
var eA = C("|");
|
|
1246
|
+
var tA = C("+");
|
|
1247
|
+
var GA = C("?");
|
|
1248
|
+
var iA = C("-[");
|
|
1249
|
+
var rA = S("0");
|
|
1250
|
+
function oA(A) {
|
|
1251
|
+
function e3(A2) {
|
|
1252
|
+
return new Set(A2.split("").map((A3) => S(A3)));
|
|
1253
|
+
}
|
|
1254
|
+
function G2(A2, B2) {
|
|
1255
|
+
const a3 = A2.codePointAt(B2);
|
|
1256
|
+
return a3 === undefined ? H(B2, ["any character"]) : o2(B2 + String.fromCodePoint(a3).length, a3);
|
|
1257
|
+
}
|
|
1258
|
+
const i3 = A.language === "xpath" ? T(j, c2([u2(C("n"), () => 10), u2(C("r"), () => 13), u2(C("t"), () => 9), u2(c2([j, eA, nA, AA, z, GA, U, tA, R, V, $, BA, aA, W, q]), (A2) => S(A2))])) : T(j, c2([u2(C("n"), () => 10), u2(C("r"), () => 13), u2(C("t"), () => 9), u2(c2([j, eA, nA, AA, z, GA, U, tA, R, V, BA, aA, W, q]), (A2) => S(A2))]));
|
|
1259
|
+
function r3(A2, B2) {
|
|
1260
|
+
const a3 = e3(B2);
|
|
1261
|
+
return I(C(A2), D(s2(G2, (A3) => a3.has(A3), B2.split(""))), (A3, B3) => function(A4) {
|
|
1262
|
+
const B4 = x.get(A4);
|
|
1263
|
+
if (B4 == null)
|
|
1264
|
+
throw new Error(A4 + " is not a valid unicode category");
|
|
1265
|
+
return B4;
|
|
1266
|
+
}(B3 === null ? A3 : A3 + String.fromCodePoint(B3)));
|
|
1267
|
+
}
|
|
1268
|
+
const l3 = c2([r3("L", "ultmo"), r3("M", "nce"), r3("N", "dlo"), r3("P", "cdseifo"), r3("Z", "slp"), r3("S", "mcko"), r3("C", "cfon")]), p2 = [a2(S("a"), S("z")), a2(S("A"), S("Z")), a2(S("0"), S("9")), B(45)].reduce(t2), M2 = c2([l3, u2(T(C("Is"), function(A2) {
|
|
1269
|
+
return (B2, a3) => {
|
|
1270
|
+
const n3 = A2(B2, a3);
|
|
1271
|
+
return n3.success ? o2(n3.offset, B2.slice(a3, n3.offset)) : n3;
|
|
1272
|
+
};
|
|
1273
|
+
}(d2(s2(G2, p2, ["block identifier"])))), (B2) => function(A2, B3) {
|
|
1274
|
+
const a3 = Q.get(A2);
|
|
1275
|
+
if (a3 === undefined) {
|
|
1276
|
+
if (B3)
|
|
1277
|
+
return n2;
|
|
1278
|
+
throw new Error(`The unicode block identifier "${A2}" is not known.`);
|
|
1279
|
+
}
|
|
1280
|
+
return a3;
|
|
1281
|
+
}(B2, A.language !== "xpath"))]), J2 = E(C("\\p{"), M2, V, true), K2 = u2(E(C("\\P{"), M2, V, true), b), L2 = T(j, u2(c2("sSiIcCdDwW".split("").map((A2) => C(A2))), (A2) => Y[A2])), X2 = u2(nA, () => w2), Z2 = c2([L2, J2, K2]), O2 = e3("\\[]"), k2 = c2([i3, s2(G2, (A2) => !O2.has(A2), ["unescaped character"])]), N2 = c2([u2(AA, () => null), k2]), v2 = I(N2, T(AA, N2), a2);
|
|
1282
|
+
function oA2(A2, B2) {
|
|
1283
|
+
return [A2].concat(B2 || []);
|
|
1284
|
+
}
|
|
1285
|
+
const lA = u2(function(A2) {
|
|
1286
|
+
return (B2, a3) => {
|
|
1287
|
+
const n3 = A2(B2, a3);
|
|
1288
|
+
return n3.success ? o2(a3, n3.value) : n3;
|
|
1289
|
+
};
|
|
1290
|
+
}(c2([q, iA])), () => null), HA = S("-"), CA = c2([u2(F(F(AA, g(W, ["not ["])), lA), () => HA), T(g(AA, ["not -"]), k2)]), uA = c2([I(u2(CA, B), c2([function(A2, B2) {
|
|
1291
|
+
return uA(A2, B2);
|
|
1292
|
+
}, lA]), oA2), I(c2([v2, Z2]), c2([cA, lA]), oA2)]);
|
|
1293
|
+
const sA = c2([I(u2(k2, B), c2([uA, lA]), oA2), I(c2([v2, Z2]), c2([cA, lA]), oA2)]);
|
|
1294
|
+
function cA(A2, B2) {
|
|
1295
|
+
return sA(A2, B2);
|
|
1296
|
+
}
|
|
1297
|
+
const DA = u2(sA, (A2) => A2.reduce(t2)), mA = u2(T(z, DA), b), IA = I(c2([T(g(z, ["not ^"]), DA), mA]), D(T(AA, function(A2, B2) {
|
|
1298
|
+
return dA(A2, B2);
|
|
1299
|
+
})), y), dA = E(W, IA, q, true);
|
|
1300
|
+
const hA = A.language === "xpath" ? c2([u2(i3, B), Z2, dA, X2, u2(z, () => (A2) => A2 === -1), u2($, () => (A2) => A2 === -2)]) : c2([u2(i3, B), Z2, dA, X2]), pA = A.language === "xpath" ? e3(".\\?*+{}()|^$[]") : e3(".\\?*+{}()|[]"), TA = s2(G2, (A2) => !pA.has(A2), ["NormalChar"]), FA = u2(T(j, I(u2(s2(G2, a2(S("1"), S("9")), ["digit"]), (A2) => A2 - rA), m(u2(s2(G2, a2(rA, S("9")), ["digit"]), (A2) => A2 - rA)), (A2, B2) => {
|
|
1301
|
+
B2.reduce((A3, B3) => 10 * A3 + B3, A2);
|
|
1302
|
+
})), (A2) => {
|
|
1303
|
+
throw new Error("Backreferences in XPath patterns are not yet implemented.");
|
|
1304
|
+
}), EA = A.language === "xpath" ? c2([u2(TA, (A2) => ({ kind: "predicate", value: B(A2) })), u2(hA, (A2) => ({ kind: "predicate", value: A2 })), u2(E(BA, T(D(C("?:")), SA), aA, true), (A2) => ({ kind: "regexp", value: A2 })), FA]) : c2([u2(TA, (A2) => ({ kind: "predicate", value: B(A2) })), u2(hA, (A2) => ({ kind: "predicate", value: A2 })), u2(E(BA, SA, aA, true), (A2) => ({ kind: "regexp", value: A2 }))]), gA = u2(d2(u2(s2(G2, a2(rA, S("9")), ["digit"]), (A2) => A2 - rA)), (A2) => A2.reduce((A3, B2) => 10 * A3 + B2)), fA = c2([I(gA, T(_, gA), (A2, B2) => {
|
|
1305
|
+
if (B2 < A2)
|
|
1306
|
+
throw new Error("quantifier range is in the wrong order");
|
|
1307
|
+
return { min: A2, max: B2 };
|
|
1308
|
+
}), I(gA, _, (A2) => ({ min: A2, max: null })), u2(gA, (A2) => ({ min: A2, max: A2 }))]), PA = A.language === "xpath" ? I(c2([u2(GA, () => ({ min: 0, max: 1 })), u2(U, () => ({ min: 0, max: null })), u2(tA, () => ({ min: 1, max: null })), E(R, fA, V, true)]), D(GA), (A2, B2) => A2) : c2([u2(GA, () => ({ min: 0, max: 1 })), u2(U, () => ({ min: 0, max: null })), u2(tA, () => ({ min: 1, max: null })), E(R, fA, V, true)]), MA = m(I(EA, u2(D(PA), (A2) => A2 === null ? { min: 1, max: 1 } : A2), (A2, B2) => [A2, B2])), JA = I(MA, m(T(eA, f2(MA))), (A2, B2) => [A2].concat(B2));
|
|
1309
|
+
function SA(A2, B2) {
|
|
1310
|
+
return JA(A2, B2);
|
|
1311
|
+
}
|
|
1312
|
+
const KA = function(A2) {
|
|
1313
|
+
return I(A2, P, h2);
|
|
1314
|
+
}(JA);
|
|
1315
|
+
return function(A2) {
|
|
1316
|
+
let B2;
|
|
1317
|
+
try {
|
|
1318
|
+
B2 = KA(A2, 0);
|
|
1319
|
+
} catch (B3) {
|
|
1320
|
+
throw new Error(`Error parsing pattern "${A2}": ${B3 instanceof Error ? B3.message : B3}`);
|
|
1321
|
+
}
|
|
1322
|
+
return B2.success ? B2.value : function(A3, B3, a3) {
|
|
1323
|
+
const n3 = a3.map((A4) => `"${A4}"`);
|
|
1324
|
+
throw new Error(`Error parsing pattern "${A3}" at offset ${B3}: expected ${n3.length > 1 ? "one of " + n3.join(", ") : n3[0]} but found "${A3.slice(B3, B3 + 1)}"`);
|
|
1325
|
+
}(A2, B2.offset, B2.expected);
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function lA(A) {
|
|
1329
|
+
return [...A].map((A2) => A2.codePointAt(0));
|
|
1330
|
+
}
|
|
1331
|
+
function HA(B2, a3 = { language: "xsd" }) {
|
|
1332
|
+
const n3 = oA(a3)(B2), e3 = w((A) => {
|
|
1333
|
+
r2(A, n3, a3.language === "xpath"), A.accept();
|
|
1334
|
+
});
|
|
1335
|
+
return function(A) {
|
|
1336
|
+
const B3 = a3.language === "xpath" ? [-1, ...lA(A), -2] : lA(A);
|
|
1337
|
+
return e3.execute(B3).success;
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/response-processing.ts
|
|
1342
|
+
function foldString(value) {
|
|
1343
|
+
return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLocaleLowerCase();
|
|
1344
|
+
}
|
|
1345
|
+
function asList(response) {
|
|
1346
|
+
if (response === null) {
|
|
1347
|
+
return [];
|
|
1348
|
+
}
|
|
1349
|
+
return typeof response === "string" ? [response] : Array.isArray(response) ? [...response] : [];
|
|
1350
|
+
}
|
|
1351
|
+
function isStringBaseType(declaration) {
|
|
1352
|
+
return declaration.baseType === "string" || declaration.baseType === undefined;
|
|
1353
|
+
}
|
|
1354
|
+
function pairsEqual(a3, b2, directed) {
|
|
1355
|
+
const [a1, a22] = a3.trim().split(/\s+/u);
|
|
1356
|
+
const [b1, b22] = b2.trim().split(/\s+/u);
|
|
1357
|
+
if (a1 === undefined || a22 === undefined || b1 === undefined || b22 === undefined) {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
if (a1 === b1 && a22 === b22) {
|
|
1361
|
+
return true;
|
|
1362
|
+
}
|
|
1363
|
+
return !directed && a1 === b22 && a22 === b1;
|
|
1364
|
+
}
|
|
1365
|
+
var numericBaseTypes = new Set(["float", "integer"]);
|
|
1366
|
+
function makeValueComparator(declaration, normalize) {
|
|
1367
|
+
if (declaration.baseType === "pair") {
|
|
1368
|
+
return (a3, b2) => pairsEqual(a3, b2, false);
|
|
1369
|
+
}
|
|
1370
|
+
if (declaration.baseType === "directedPair") {
|
|
1371
|
+
return (a3, b2) => pairsEqual(a3, b2, true);
|
|
1372
|
+
}
|
|
1373
|
+
if (declaration.baseType !== undefined && numericBaseTypes.has(declaration.baseType)) {
|
|
1374
|
+
return (a3, b2) => a3.trim() !== "" && b2.trim() !== "" && Number(a3) === Number(b2);
|
|
1375
|
+
}
|
|
1376
|
+
if (declaration.baseType === "point") {
|
|
1377
|
+
return (a3, b2) => {
|
|
1378
|
+
const pointA = parsePoint(a3);
|
|
1379
|
+
const pointB = parsePoint(b2);
|
|
1380
|
+
return pointA !== null && pointB !== null && pointA.x === pointB.x && pointA.y === pointB.y;
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
if (normalize && isStringBaseType(declaration)) {
|
|
1384
|
+
return (a3, b2) => normalize(a3, declaration) === normalize(b2, declaration);
|
|
1385
|
+
}
|
|
1386
|
+
return (a3, b2) => a3 === b2;
|
|
1387
|
+
}
|
|
1388
|
+
function matchCorrect(declaration, response, normalize) {
|
|
1389
|
+
const correct = declaration.correctResponse;
|
|
1390
|
+
if (!correct) {
|
|
1391
|
+
return false;
|
|
1392
|
+
}
|
|
1393
|
+
const equals = makeValueComparator(declaration, normalize);
|
|
1394
|
+
const expected = correct.values.map((entry) => entry.value);
|
|
1395
|
+
const actual = asList(response);
|
|
1396
|
+
if (expected.length !== actual.length) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
if (declaration.cardinality === "ordered") {
|
|
1400
|
+
return expected.every((value, index) => equals(value, actual[index] ?? ""));
|
|
1401
|
+
}
|
|
1402
|
+
const remaining = [...actual];
|
|
1403
|
+
for (const value of expected) {
|
|
1404
|
+
const matchIndex = remaining.findIndex((candidate) => equals(candidate, value));
|
|
1405
|
+
if (matchIndex === -1) {
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
remaining.splice(matchIndex, 1);
|
|
1409
|
+
}
|
|
1410
|
+
return remaining.length === 0;
|
|
1411
|
+
}
|
|
1412
|
+
function clamp(value, lower, upper) {
|
|
1413
|
+
let result = value;
|
|
1414
|
+
if (lower !== undefined && result < lower) {
|
|
1415
|
+
result = lower;
|
|
1416
|
+
}
|
|
1417
|
+
if (upper !== undefined && result > upper) {
|
|
1418
|
+
result = upper;
|
|
1419
|
+
}
|
|
1420
|
+
return result;
|
|
1421
|
+
}
|
|
1422
|
+
function mapResponse(declaration, response, normalize) {
|
|
1423
|
+
const mapping = declaration.mapping;
|
|
1424
|
+
if (!mapping) {
|
|
1425
|
+
return 0;
|
|
1426
|
+
}
|
|
1427
|
+
const isPairType = declaration.baseType === "pair" || declaration.baseType === "directedPair";
|
|
1428
|
+
const applyNormalize = normalize && isStringBaseType(declaration) ? normalize : undefined;
|
|
1429
|
+
const defaultValue = mapping.defaultValue ?? 0;
|
|
1430
|
+
let total = 0;
|
|
1431
|
+
for (const member of asList(response)) {
|
|
1432
|
+
const entry = mapping.mapEntries.find((candidate) => {
|
|
1433
|
+
if (isPairType) {
|
|
1434
|
+
return pairsEqual(candidate.mapKey, member, declaration.baseType === "directedPair");
|
|
1435
|
+
}
|
|
1436
|
+
let key = candidate.mapKey;
|
|
1437
|
+
let candidateMember = member;
|
|
1438
|
+
if (candidate.caseSensitive === false) {
|
|
1439
|
+
key = key.toLocaleLowerCase();
|
|
1440
|
+
candidateMember = candidateMember.toLocaleLowerCase();
|
|
1441
|
+
}
|
|
1442
|
+
if (applyNormalize) {
|
|
1443
|
+
key = applyNormalize(key, declaration);
|
|
1444
|
+
candidateMember = applyNormalize(candidateMember, declaration);
|
|
1445
|
+
}
|
|
1446
|
+
return key === candidateMember;
|
|
1447
|
+
});
|
|
1448
|
+
total += entry ? entry.mappedValue : defaultValue;
|
|
1449
|
+
}
|
|
1450
|
+
return clamp(total, mapping.lowerBound, mapping.upperBound);
|
|
1451
|
+
}
|
|
1452
|
+
function mapResponsePoint(declaration, response) {
|
|
1453
|
+
const areaMapping = declaration.areaMapping;
|
|
1454
|
+
if (!areaMapping) {
|
|
1455
|
+
return 0;
|
|
1456
|
+
}
|
|
1457
|
+
const defaultValue = areaMapping.defaultValue ?? 0;
|
|
1458
|
+
const usedAreas = new Set;
|
|
1459
|
+
let total = 0;
|
|
1460
|
+
for (const member of asList(response)) {
|
|
1461
|
+
const point = parsePoint(member);
|
|
1462
|
+
if (!point) {
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
const areaIndex = areaMapping.areaMapEntries.findIndex((entry, index) => !usedAreas.has(index) && pointInShape(entry.shape, entry.coords, point));
|
|
1466
|
+
if (areaIndex === -1) {
|
|
1467
|
+
total += defaultValue;
|
|
1468
|
+
} else {
|
|
1469
|
+
usedAreas.add(areaIndex);
|
|
1470
|
+
total += areaMapping.areaMapEntries[areaIndex].mappedValue;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return clamp(total, areaMapping.lowerBound, areaMapping.upperBound);
|
|
1474
|
+
}
|
|
1475
|
+
function scoreResponse(declaration, response, normalize) {
|
|
1476
|
+
if (declaration.areaMapping) {
|
|
1477
|
+
const score = mapResponsePoint(declaration, response);
|
|
1478
|
+
const positiveSum = declaration.areaMapping.areaMapEntries.reduce((sum, entry) => sum + Math.max(entry.mappedValue, 0), 0);
|
|
1479
|
+
const maxScore = declaration.areaMapping.upperBound ?? positiveSum;
|
|
1480
|
+
return {
|
|
1481
|
+
identifier: declaration.identifier,
|
|
1482
|
+
score,
|
|
1483
|
+
maxScore,
|
|
1484
|
+
correct: maxScore > 0 && score >= maxScore
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
if (declaration.mapping) {
|
|
1488
|
+
const score = mapResponse(declaration, response, normalize);
|
|
1489
|
+
const positiveSum = declaration.mapping.mapEntries.reduce((sum, entry) => sum + Math.max(entry.mappedValue, 0), 0);
|
|
1490
|
+
const maxScore = declaration.mapping.upperBound ?? positiveSum;
|
|
1491
|
+
return {
|
|
1492
|
+
identifier: declaration.identifier,
|
|
1493
|
+
score,
|
|
1494
|
+
maxScore,
|
|
1495
|
+
correct: maxScore > 0 && score >= maxScore
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
const correct = matchCorrect(declaration, response, normalize);
|
|
1499
|
+
return {
|
|
1500
|
+
identifier: declaration.identifier,
|
|
1501
|
+
score: correct ? 1 : 0,
|
|
1502
|
+
maxScore: 1,
|
|
1503
|
+
correct
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/types.ts
|
|
1508
|
+
function isResponseRecord(value) {
|
|
1509
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/rp/values.ts
|
|
1513
|
+
var numericBaseTypes2 = new Set(["float", "integer", "duration"]);
|
|
1514
|
+
function isNumericBaseType(baseType) {
|
|
1515
|
+
return baseType !== undefined && numericBaseTypes2.has(baseType);
|
|
1516
|
+
}
|
|
1517
|
+
function coerceScalar(value, baseType) {
|
|
1518
|
+
if (isNumericBaseType(baseType) && typeof value === "string" && value.trim() !== "") {
|
|
1519
|
+
const parsed = Number(value);
|
|
1520
|
+
if (!Number.isNaN(parsed)) {
|
|
1521
|
+
return parsed;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (baseType === "boolean" && typeof value === "string") {
|
|
1525
|
+
return value === "true";
|
|
1526
|
+
}
|
|
1527
|
+
return value;
|
|
1528
|
+
}
|
|
1529
|
+
function fieldBaseType(value) {
|
|
1530
|
+
return typeof value === "boolean" ? "boolean" : typeof value === "number" ? "float" : "string";
|
|
1531
|
+
}
|
|
1532
|
+
function fromResponse(declaration, response) {
|
|
1533
|
+
if (response === null) {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
if (isResponseRecord(response)) {
|
|
1537
|
+
const fields = Object.entries(response).flatMap(([name, member]) => member === null ? [] : [{ name, baseType: fieldBaseType(member), value: member }]);
|
|
1538
|
+
return fields.length === 0 ? null : { cardinality: "record", fields, values: fields.map((field) => field.value) };
|
|
1539
|
+
}
|
|
1540
|
+
const raw = typeof response === "string" ? [response] : [...response];
|
|
1541
|
+
if (raw.length === 0) {
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
return rpValue(declaration.cardinality, raw.map((value) => coerceScalar(value, declaration.baseType)), declaration.baseType);
|
|
1545
|
+
}
|
|
1546
|
+
function singleNumber(value) {
|
|
1547
|
+
if (value === null || value.values.length !== 1) {
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1550
|
+
const member = value.values[0];
|
|
1551
|
+
return typeof member === "number" ? member : null;
|
|
1552
|
+
}
|
|
1553
|
+
function singleBoolean(value) {
|
|
1554
|
+
if (value === null || value.values.length !== 1) {
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
const member = value.values[0];
|
|
1558
|
+
return typeof member === "boolean" ? member : null;
|
|
1559
|
+
}
|
|
1560
|
+
function rpValue(cardinality, values, baseType) {
|
|
1561
|
+
return {
|
|
1562
|
+
cardinality,
|
|
1563
|
+
values,
|
|
1564
|
+
...baseType !== undefined ? { baseType } : {}
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
function booleanValue(value) {
|
|
1568
|
+
return { cardinality: "single", baseType: "boolean", values: [value] };
|
|
1569
|
+
}
|
|
1570
|
+
function floatValue(value) {
|
|
1571
|
+
return { cardinality: "single", baseType: "float", values: [value] };
|
|
1572
|
+
}
|
|
1573
|
+
function pairsEqual2(a3, b2, directed) {
|
|
1574
|
+
const [a1, a22] = a3.trim().split(/\s+/u);
|
|
1575
|
+
const [b1, b22] = b2.trim().split(/\s+/u);
|
|
1576
|
+
if (a1 === undefined || a22 === undefined || b1 === undefined || b22 === undefined) {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1579
|
+
if (a1 === b1 && a22 === b22) {
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
return !directed && a1 === b22 && a22 === b1;
|
|
1583
|
+
}
|
|
1584
|
+
function scalarsEqual(a3, b2, baseType, normalize) {
|
|
1585
|
+
if (baseType === "pair" || baseType === "directedPair") {
|
|
1586
|
+
return typeof a3 === "string" && typeof b2 === "string" && pairsEqual2(a3, b2, baseType === "directedPair");
|
|
1587
|
+
}
|
|
1588
|
+
if (baseType === "point") {
|
|
1589
|
+
if (typeof a3 !== "string" || typeof b2 !== "string") {
|
|
1590
|
+
return false;
|
|
1591
|
+
}
|
|
1592
|
+
const pointA = parsePoint(a3);
|
|
1593
|
+
const pointB = parsePoint(b2);
|
|
1594
|
+
return pointA !== null && pointB !== null && pointA.x === pointB.x && pointA.y === pointB.y;
|
|
1595
|
+
}
|
|
1596
|
+
if (typeof a3 === "number" || typeof b2 === "number") {
|
|
1597
|
+
return Number(a3) === Number(b2);
|
|
1598
|
+
}
|
|
1599
|
+
if (normalize && baseType === "string" && typeof a3 === "string" && typeof b2 === "string") {
|
|
1600
|
+
return normalize(a3) === normalize(b2);
|
|
1601
|
+
}
|
|
1602
|
+
return a3 === b2;
|
|
1603
|
+
}
|
|
1604
|
+
function valuesMatch(a3, b2, normalize) {
|
|
1605
|
+
const baseType = a3.baseType ?? b2.baseType;
|
|
1606
|
+
if (a3.values.length !== b2.values.length) {
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
if (a3.cardinality === "ordered" || b2.cardinality === "ordered") {
|
|
1610
|
+
return a3.values.every((value, index) => scalarsEqual(value, b2.values[index], baseType, normalize));
|
|
1611
|
+
}
|
|
1612
|
+
const remaining = [...b2.values];
|
|
1613
|
+
for (const value of a3.values) {
|
|
1614
|
+
const matchIndex = remaining.findIndex((candidate) => scalarsEqual(candidate, value, baseType, normalize));
|
|
1615
|
+
if (matchIndex === -1) {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
remaining.splice(matchIndex, 1);
|
|
1619
|
+
}
|
|
1620
|
+
return true;
|
|
1621
|
+
}
|
|
1622
|
+
function fromFlatValue(value, cardinality, baseType) {
|
|
1623
|
+
if (value === null) {
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
const values = Array.isArray(value) ? [...value] : [value];
|
|
1627
|
+
if (values.length === 0) {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
return rpValue(cardinality, values.map((member) => coerceScalar(member, baseType)), baseType);
|
|
1631
|
+
}
|
|
1632
|
+
function toOutcomeValue(value) {
|
|
1633
|
+
if (value === null || value.values.length === 0) {
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
if (value.cardinality === "single") {
|
|
1637
|
+
return value.values[0] ?? null;
|
|
1638
|
+
}
|
|
1639
|
+
return value.values;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/rp/evaluate.ts
|
|
1643
|
+
var deterministicExpressionKinds = new Set([
|
|
1644
|
+
"and",
|
|
1645
|
+
"anyN",
|
|
1646
|
+
"baseValue",
|
|
1647
|
+
"containerSize",
|
|
1648
|
+
"contains",
|
|
1649
|
+
"correct",
|
|
1650
|
+
"default",
|
|
1651
|
+
"delete",
|
|
1652
|
+
"divide",
|
|
1653
|
+
"durationGte",
|
|
1654
|
+
"durationLt",
|
|
1655
|
+
"equal",
|
|
1656
|
+
"equalRounded",
|
|
1657
|
+
"fieldValue",
|
|
1658
|
+
"gcd",
|
|
1659
|
+
"gt",
|
|
1660
|
+
"gte",
|
|
1661
|
+
"index",
|
|
1662
|
+
"inside",
|
|
1663
|
+
"lcm",
|
|
1664
|
+
"integerDivide",
|
|
1665
|
+
"integerModulus",
|
|
1666
|
+
"integerToFloat",
|
|
1667
|
+
"isNull",
|
|
1668
|
+
"lt",
|
|
1669
|
+
"lte",
|
|
1670
|
+
"mapResponse",
|
|
1671
|
+
"mapResponsePoint",
|
|
1672
|
+
"match",
|
|
1673
|
+
"mathConstant",
|
|
1674
|
+
"mathOperator",
|
|
1675
|
+
"max",
|
|
1676
|
+
"member",
|
|
1677
|
+
"min",
|
|
1678
|
+
"multiple",
|
|
1679
|
+
"not",
|
|
1680
|
+
"null",
|
|
1681
|
+
"or",
|
|
1682
|
+
"ordered",
|
|
1683
|
+
"patternMatch",
|
|
1684
|
+
"power",
|
|
1685
|
+
"product",
|
|
1686
|
+
"repeat",
|
|
1687
|
+
"round",
|
|
1688
|
+
"roundTo",
|
|
1689
|
+
"statsOperator",
|
|
1690
|
+
"stringMatch",
|
|
1691
|
+
"substring",
|
|
1692
|
+
"subtract",
|
|
1693
|
+
"sum",
|
|
1694
|
+
"truncate",
|
|
1695
|
+
"variable"
|
|
1696
|
+
]);
|
|
1697
|
+
var randomExpressionKinds = new Set(["random", "randomFloat", "randomInteger"]);
|
|
1698
|
+
|
|
1699
|
+
class RpUnsupportedError extends Error {
|
|
1700
|
+
kindName;
|
|
1701
|
+
constructor(kindName) {
|
|
1702
|
+
super(`Unsupported response-processing construct: ${kindName}`);
|
|
1703
|
+
this.kindName = kindName;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
var mathConstants = { pi: Math.PI, e: Math.E };
|
|
1707
|
+
var xsdPatternMatchers = new Map;
|
|
1708
|
+
function xsdPatternMatcher(pattern) {
|
|
1709
|
+
const cached = xsdPatternMatchers.get(pattern);
|
|
1710
|
+
if (cached !== undefined) {
|
|
1711
|
+
return cached;
|
|
1712
|
+
}
|
|
1713
|
+
let matcher;
|
|
1714
|
+
try {
|
|
1715
|
+
matcher = HA(pattern);
|
|
1716
|
+
} catch {
|
|
1717
|
+
matcher = null;
|
|
1718
|
+
}
|
|
1719
|
+
xsdPatternMatchers.set(pattern, matcher);
|
|
1720
|
+
return matcher;
|
|
1721
|
+
}
|
|
1722
|
+
var encVariableStringPattern = /^\{[^{}]+\}$/u;
|
|
1723
|
+
function applyMathOperator(name, x2, y2) {
|
|
1724
|
+
switch (name) {
|
|
1725
|
+
case "sin":
|
|
1726
|
+
return Math.sin(x2);
|
|
1727
|
+
case "cos":
|
|
1728
|
+
return Math.cos(x2);
|
|
1729
|
+
case "tan":
|
|
1730
|
+
return Math.tan(x2);
|
|
1731
|
+
case "sec":
|
|
1732
|
+
return 1 / Math.cos(x2);
|
|
1733
|
+
case "csc":
|
|
1734
|
+
return 1 / Math.sin(x2);
|
|
1735
|
+
case "cot":
|
|
1736
|
+
return Math.cos(x2) / Math.sin(x2);
|
|
1737
|
+
case "asin":
|
|
1738
|
+
return Math.asin(x2);
|
|
1739
|
+
case "acos":
|
|
1740
|
+
return Math.acos(x2);
|
|
1741
|
+
case "atan":
|
|
1742
|
+
return Math.atan(x2);
|
|
1743
|
+
case "atan2":
|
|
1744
|
+
return Math.atan2(x2, y2);
|
|
1745
|
+
case "asec":
|
|
1746
|
+
return Math.acos(1 / x2);
|
|
1747
|
+
case "acsc":
|
|
1748
|
+
return Math.asin(1 / x2);
|
|
1749
|
+
case "acot":
|
|
1750
|
+
return Math.atan(1 / x2);
|
|
1751
|
+
case "sinh":
|
|
1752
|
+
return Math.sinh(x2);
|
|
1753
|
+
case "cosh":
|
|
1754
|
+
return Math.cosh(x2);
|
|
1755
|
+
case "tanh":
|
|
1756
|
+
return Math.tanh(x2);
|
|
1757
|
+
case "sech":
|
|
1758
|
+
return 1 / Math.cosh(x2);
|
|
1759
|
+
case "csch":
|
|
1760
|
+
return 1 / Math.sinh(x2);
|
|
1761
|
+
case "coth":
|
|
1762
|
+
return Math.cosh(x2) / Math.sinh(x2);
|
|
1763
|
+
case "log":
|
|
1764
|
+
return Math.log10(x2);
|
|
1765
|
+
case "ln":
|
|
1766
|
+
return Math.log(x2);
|
|
1767
|
+
case "exp":
|
|
1768
|
+
return Math.exp(x2);
|
|
1769
|
+
case "abs":
|
|
1770
|
+
return Math.abs(x2);
|
|
1771
|
+
case "signum":
|
|
1772
|
+
return Math.sign(x2);
|
|
1773
|
+
case "floor":
|
|
1774
|
+
return Math.floor(x2);
|
|
1775
|
+
case "ceil":
|
|
1776
|
+
return Math.ceil(x2);
|
|
1777
|
+
case "toDegrees":
|
|
1778
|
+
return x2 * 180 / Math.PI;
|
|
1779
|
+
case "toRadians":
|
|
1780
|
+
return x2 * Math.PI / 180;
|
|
1781
|
+
default:
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function resolveNumericAttribute(raw, env) {
|
|
1786
|
+
if (raw === undefined) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (typeof raw === "number") {
|
|
1790
|
+
return raw;
|
|
1791
|
+
}
|
|
1792
|
+
const identifier = raw.startsWith("{") && raw.endsWith("}") ? raw.slice(1, -1) : raw;
|
|
1793
|
+
return singleNumber(env.lookupVariable(identifier));
|
|
1794
|
+
}
|
|
1795
|
+
function roundToFigures(value, mode, figures) {
|
|
1796
|
+
if (mode === "decimalPlaces") {
|
|
1797
|
+
const scale2 = 10 ** figures;
|
|
1798
|
+
return Math.round(value * scale2) / scale2;
|
|
1799
|
+
}
|
|
1800
|
+
if (figures < 1) {
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
if (value === 0) {
|
|
1804
|
+
return 0;
|
|
1805
|
+
}
|
|
1806
|
+
const magnitude = Math.floor(Math.log10(Math.abs(value)));
|
|
1807
|
+
const scale = 10 ** (figures - 1 - magnitude);
|
|
1808
|
+
return Math.round(value * scale) / scale;
|
|
1809
|
+
}
|
|
1810
|
+
function evaluateExpression(expression, env) {
|
|
1811
|
+
function evaluate(child) {
|
|
1812
|
+
return evaluateExpression(child, env);
|
|
1813
|
+
}
|
|
1814
|
+
switch (expression.kind) {
|
|
1815
|
+
case "baseValue": {
|
|
1816
|
+
const baseType = expression.baseType;
|
|
1817
|
+
const value = expression.value;
|
|
1818
|
+
return value === undefined ? null : rpValue("single", [coerceScalar(value, baseType)], baseType);
|
|
1819
|
+
}
|
|
1820
|
+
case "variable":
|
|
1821
|
+
return env.lookupVariable(expression.identifier ?? "");
|
|
1822
|
+
case "correct": {
|
|
1823
|
+
const declaration = env.responseDeclaration(expression.identifier ?? "");
|
|
1824
|
+
if (!declaration?.correctResponse) {
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
return rpValue(declaration.cardinality, declaration.correctResponse.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType);
|
|
1828
|
+
}
|
|
1829
|
+
case "mapResponse": {
|
|
1830
|
+
const identifier = expression.identifier ?? "";
|
|
1831
|
+
const declaration = env.responseDeclaration(identifier);
|
|
1832
|
+
if (!declaration) {
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1835
|
+
return floatValue(mapResponse(declaration, env.responseValue(identifier), env.normalization));
|
|
1836
|
+
}
|
|
1837
|
+
case "mapResponsePoint": {
|
|
1838
|
+
const identifier = expression.identifier ?? "";
|
|
1839
|
+
const declaration = env.responseDeclaration(identifier);
|
|
1840
|
+
if (!declaration) {
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
return floatValue(mapResponsePoint(declaration, env.responseValue(identifier)));
|
|
1844
|
+
}
|
|
1845
|
+
case "match": {
|
|
1846
|
+
const [a3, b2] = (expression.expressions ?? []).map(evaluate);
|
|
1847
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null) {
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
return booleanValue(valuesMatch(a3, b2, env.normalization));
|
|
1851
|
+
}
|
|
1852
|
+
case "isNull": {
|
|
1853
|
+
const operand = expression.expressions?.[0];
|
|
1854
|
+
return booleanValue(operand === undefined || evaluate(operand) === null);
|
|
1855
|
+
}
|
|
1856
|
+
case "not": {
|
|
1857
|
+
const operand = expression.expressions?.[0];
|
|
1858
|
+
const value = operand === undefined ? null : singleBoolean(evaluate(operand));
|
|
1859
|
+
return value === null ? null : booleanValue(!value);
|
|
1860
|
+
}
|
|
1861
|
+
case "fieldValue": {
|
|
1862
|
+
const operand = expression.expressions?.[0];
|
|
1863
|
+
const value = operand === undefined ? null : evaluate(operand);
|
|
1864
|
+
const field = value?.fields?.find((entry) => entry.name === expression.fieldIdentifier);
|
|
1865
|
+
return field === undefined ? null : rpValue("single", [field.value], field.baseType);
|
|
1866
|
+
}
|
|
1867
|
+
case "and":
|
|
1868
|
+
case "or": {
|
|
1869
|
+
const members = (expression.expressions ?? []).map((child) => singleBoolean(evaluate(child)));
|
|
1870
|
+
if (expression.kind === "and") {
|
|
1871
|
+
if (members.some((member) => member === false)) {
|
|
1872
|
+
return booleanValue(false);
|
|
1873
|
+
}
|
|
1874
|
+
return members.some((member) => member === null) ? null : booleanValue(true);
|
|
1875
|
+
}
|
|
1876
|
+
if (members.some((member) => member === true)) {
|
|
1877
|
+
return booleanValue(true);
|
|
1878
|
+
}
|
|
1879
|
+
return members.some((member) => member === null) ? null : booleanValue(false);
|
|
1880
|
+
}
|
|
1881
|
+
case "sum":
|
|
1882
|
+
case "product": {
|
|
1883
|
+
let result = expression.kind === "sum" ? 0 : 1;
|
|
1884
|
+
for (const child of expression.expressions ?? []) {
|
|
1885
|
+
const value = evaluate(child);
|
|
1886
|
+
if (value === null) {
|
|
1887
|
+
return null;
|
|
1888
|
+
}
|
|
1889
|
+
for (const member of value.values) {
|
|
1890
|
+
if (typeof member !== "number") {
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
result = expression.kind === "sum" ? result + member : result * member;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
return floatValue(result);
|
|
1897
|
+
}
|
|
1898
|
+
case "subtract":
|
|
1899
|
+
case "divide": {
|
|
1900
|
+
const [a3, b2] = (expression.expressions ?? []).map((child) => singleNumber(evaluate(child)));
|
|
1901
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null) {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
if (expression.kind === "divide") {
|
|
1905
|
+
return b2 === 0 ? null : floatValue(a3 / b2);
|
|
1906
|
+
}
|
|
1907
|
+
return floatValue(a3 - b2);
|
|
1908
|
+
}
|
|
1909
|
+
case "gt":
|
|
1910
|
+
case "gte":
|
|
1911
|
+
case "lt":
|
|
1912
|
+
case "lte": {
|
|
1913
|
+
const [a3, b2] = (expression.expressions ?? []).map((child) => singleNumber(evaluate(child)));
|
|
1914
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null) {
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
const comparisons = { gt: a3 > b2, gte: a3 >= b2, lt: a3 < b2, lte: a3 <= b2 };
|
|
1918
|
+
return booleanValue(comparisons[expression.kind]);
|
|
1919
|
+
}
|
|
1920
|
+
case "equal": {
|
|
1921
|
+
const [a3, b2] = (expression.expressions ?? []).map((child) => singleNumber(evaluate(child)));
|
|
1922
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null) {
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
const mode = expression.toleranceMode ?? "exact";
|
|
1926
|
+
if (mode === "exact") {
|
|
1927
|
+
return booleanValue(a3 === b2);
|
|
1928
|
+
}
|
|
1929
|
+
const t0 = resolveNumericAttribute(expression.tolerance?.[0], env);
|
|
1930
|
+
const t1raw = expression.tolerance?.[1];
|
|
1931
|
+
const t1 = t1raw === undefined ? t0 : resolveNumericAttribute(t1raw, env);
|
|
1932
|
+
if (typeof t0 !== "number" || typeof t1 !== "number") {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
const lower = mode === "absolute" ? a3 - t0 : a3 * (1 - t0 / 100);
|
|
1936
|
+
const upper = mode === "absolute" ? a3 + t1 : a3 * (1 + t1 / 100);
|
|
1937
|
+
const aboveLower = expression.includeLowerBound ?? true ? b2 >= lower : b2 > lower;
|
|
1938
|
+
const belowUpper = expression.includeUpperBound ?? true ? b2 <= upper : b2 < upper;
|
|
1939
|
+
return booleanValue(aboveLower && belowUpper);
|
|
1940
|
+
}
|
|
1941
|
+
case "round":
|
|
1942
|
+
case "truncate": {
|
|
1943
|
+
const operand = expression.expressions?.[0];
|
|
1944
|
+
const value = operand === undefined ? null : singleNumber(evaluate(operand));
|
|
1945
|
+
if (value === null) {
|
|
1946
|
+
return null;
|
|
1947
|
+
}
|
|
1948
|
+
const rounded = expression.kind === "round" ? Math.round(value) : Math.trunc(value);
|
|
1949
|
+
return { cardinality: "single", baseType: "integer", values: [rounded] };
|
|
1950
|
+
}
|
|
1951
|
+
case "index": {
|
|
1952
|
+
const n3 = resolveNumericAttribute(expression.n, env);
|
|
1953
|
+
if (typeof n3 !== "number") {
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
const operand = expression.expressions?.[0];
|
|
1957
|
+
const container = operand === undefined ? null : evaluate(operand);
|
|
1958
|
+
if (container === null) {
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
const member = container.values[n3 - 1];
|
|
1962
|
+
if (member === undefined) {
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
return rpValue("single", [member], container.baseType);
|
|
1966
|
+
}
|
|
1967
|
+
case "mathConstant": {
|
|
1968
|
+
const constant = expression.name === undefined ? undefined : mathConstants[expression.name];
|
|
1969
|
+
if (constant === undefined) {
|
|
1970
|
+
throw new RpUnsupportedError("mathConstant");
|
|
1971
|
+
}
|
|
1972
|
+
return floatValue(constant);
|
|
1973
|
+
}
|
|
1974
|
+
case "mathOperator": {
|
|
1975
|
+
const [x2, y2] = (expression.expressions ?? []).map((child) => singleNumber(evaluate(child)));
|
|
1976
|
+
if (x2 === undefined || x2 === null || expression.name === "atan2" && (y2 === undefined || y2 === null)) {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
const result = expression.name === undefined ? undefined : applyMathOperator(expression.name, x2, y2 ?? NaN);
|
|
1980
|
+
if (result === undefined) {
|
|
1981
|
+
throw new RpUnsupportedError("mathOperator");
|
|
1982
|
+
}
|
|
1983
|
+
return Number.isFinite(result) ? floatValue(result) : null;
|
|
1984
|
+
}
|
|
1985
|
+
case "integerDivide":
|
|
1986
|
+
case "integerModulus": {
|
|
1987
|
+
const [a3, b2] = (expression.expressions ?? []).map((child) => singleNumber(evaluate(child)));
|
|
1988
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null || b2 === 0) {
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
const result = expression.kind === "integerDivide" ? Math.trunc(a3 / b2) : a3 % b2;
|
|
1992
|
+
return { cardinality: "single", baseType: "integer", values: [result] };
|
|
1993
|
+
}
|
|
1994
|
+
case "integerToFloat": {
|
|
1995
|
+
const operand = expression.expressions?.[0];
|
|
1996
|
+
const value = operand === undefined ? null : singleNumber(evaluate(operand));
|
|
1997
|
+
return value === null ? null : floatValue(value);
|
|
1998
|
+
}
|
|
1999
|
+
case "min":
|
|
2000
|
+
case "max": {
|
|
2001
|
+
const members = [];
|
|
2002
|
+
for (const child of expression.expressions ?? []) {
|
|
2003
|
+
const value = evaluate(child);
|
|
2004
|
+
if (value === null) {
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
2007
|
+
for (const member of value.values) {
|
|
2008
|
+
if (typeof member !== "number") {
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
members.push(member);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (members.length === 0) {
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
return floatValue(expression.kind === "min" ? Math.min(...members) : Math.max(...members));
|
|
2018
|
+
}
|
|
2019
|
+
case "gcd":
|
|
2020
|
+
case "lcm": {
|
|
2021
|
+
const members = [];
|
|
2022
|
+
for (const child of expression.expressions ?? []) {
|
|
2023
|
+
const value = evaluate(child);
|
|
2024
|
+
if (value === null) {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
for (const member of value.values) {
|
|
2028
|
+
if (typeof member !== "number" || !Number.isInteger(member)) {
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
members.push(Math.abs(member));
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (members.length === 0) {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
const gcdOf = (a3, b2) => b2 === 0 ? a3 : gcdOf(b2, a3 % b2);
|
|
2038
|
+
const result = expression.kind === "gcd" ? members.reduce(gcdOf) : members.reduce((a3, b2) => a3 === 0 || b2 === 0 ? 0 : a3 / gcdOf(a3, b2) * b2);
|
|
2039
|
+
return { cardinality: "single", baseType: "integer", values: [result] };
|
|
2040
|
+
}
|
|
2041
|
+
case "roundTo": {
|
|
2042
|
+
const figures = resolveNumericAttribute(expression.figures, env);
|
|
2043
|
+
if (typeof figures !== "number") {
|
|
2044
|
+
return null;
|
|
2045
|
+
}
|
|
2046
|
+
const operand = expression.expressions?.[0];
|
|
2047
|
+
const value = operand === undefined ? null : singleNumber(evaluate(operand));
|
|
2048
|
+
if (value === null) {
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
const rounded = roundToFigures(value, expression.roundingMode ?? "significantFigures", figures);
|
|
2052
|
+
return rounded === null ? null : floatValue(rounded);
|
|
2053
|
+
}
|
|
2054
|
+
case "equalRounded": {
|
|
2055
|
+
const figures = resolveNumericAttribute(expression.figures, env);
|
|
2056
|
+
if (typeof figures !== "number") {
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
const [a3, b2] = (expression.expressions ?? []).map((child) => singleNumber(evaluate(child)));
|
|
2060
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null) {
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
const mode = expression.roundingMode ?? "significantFigures";
|
|
2064
|
+
const roundedA = roundToFigures(a3, mode, figures);
|
|
2065
|
+
const roundedB = roundToFigures(b2, mode, figures);
|
|
2066
|
+
return roundedA === null || roundedB === null ? null : booleanValue(roundedA === roundedB);
|
|
2067
|
+
}
|
|
2068
|
+
case "statsOperator": {
|
|
2069
|
+
const operand = expression.expressions?.[0];
|
|
2070
|
+
const container = operand === undefined ? null : evaluate(operand);
|
|
2071
|
+
if (container === null) {
|
|
2072
|
+
return null;
|
|
2073
|
+
}
|
|
2074
|
+
const members = [];
|
|
2075
|
+
for (const member of container.values) {
|
|
2076
|
+
if (typeof member !== "number") {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
members.push(member);
|
|
2080
|
+
}
|
|
2081
|
+
const count = members.length;
|
|
2082
|
+
if (count === 0) {
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
const mean = members.reduce((sum, member) => sum + member, 0) / count;
|
|
2086
|
+
const sumSquares = members.reduce((sum, member) => sum + (member - mean) ** 2, 0);
|
|
2087
|
+
switch (expression.name) {
|
|
2088
|
+
case "mean":
|
|
2089
|
+
return floatValue(mean);
|
|
2090
|
+
case "popVariance":
|
|
2091
|
+
return floatValue(sumSquares / count);
|
|
2092
|
+
case "popSD":
|
|
2093
|
+
return floatValue(Math.sqrt(sumSquares / count));
|
|
2094
|
+
case "sampleVariance":
|
|
2095
|
+
return count < 2 ? null : floatValue(sumSquares / (count - 1));
|
|
2096
|
+
case "sampleSD":
|
|
2097
|
+
return count < 2 ? null : floatValue(Math.sqrt(sumSquares / (count - 1)));
|
|
2098
|
+
default:
|
|
2099
|
+
throw new RpUnsupportedError("statsOperator");
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
case "delete": {
|
|
2103
|
+
const [valueExpression, containerExpression] = expression.expressions ?? [];
|
|
2104
|
+
if (valueExpression === undefined || containerExpression === undefined) {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
const value = evaluate(valueExpression);
|
|
2108
|
+
const container = evaluate(containerExpression);
|
|
2109
|
+
if (value === null || container === null) {
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
const scalar = value.values[0];
|
|
2113
|
+
if (scalar === undefined) {
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
const baseType = container.baseType ?? value.baseType;
|
|
2117
|
+
const remaining = container.values.filter((member) => !scalarsEqual(member, scalar, baseType, env.normalization));
|
|
2118
|
+
return remaining.length === 0 ? null : rpValue(container.cardinality, remaining, container.baseType);
|
|
2119
|
+
}
|
|
2120
|
+
case "repeat": {
|
|
2121
|
+
const numberRepeats = resolveNumericAttribute(expression.numberRepeats, env);
|
|
2122
|
+
if (typeof numberRepeats !== "number" || numberRepeats < 1) {
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
2125
|
+
const members = [];
|
|
2126
|
+
let baseType;
|
|
2127
|
+
for (let pass = 0;pass < numberRepeats; pass += 1) {
|
|
2128
|
+
for (const child of expression.expressions ?? []) {
|
|
2129
|
+
const value = evaluate(child);
|
|
2130
|
+
if (value === null) {
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
baseType ??= value.baseType;
|
|
2134
|
+
members.push(...value.values);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return members.length === 0 ? null : rpValue("ordered", members, baseType);
|
|
2138
|
+
}
|
|
2139
|
+
case "null":
|
|
2140
|
+
return null;
|
|
2141
|
+
case "durationGte":
|
|
2142
|
+
case "durationLt": {
|
|
2143
|
+
const [a3, b2] = (expression.expressions ?? []).map((child) => singleNumber(evaluate(child)));
|
|
2144
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null) {
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
return booleanValue(expression.kind === "durationGte" ? a3 >= b2 : a3 < b2);
|
|
2148
|
+
}
|
|
2149
|
+
case "default":
|
|
2150
|
+
return env.variableDefault?.(expression.identifier ?? "") ?? null;
|
|
2151
|
+
case "patternMatch": {
|
|
2152
|
+
const operand = expression.expressions?.[0];
|
|
2153
|
+
const value = operand === undefined ? null : evaluate(operand);
|
|
2154
|
+
const member = value?.values[0];
|
|
2155
|
+
if (value === null || typeof member !== "string") {
|
|
2156
|
+
return null;
|
|
2157
|
+
}
|
|
2158
|
+
if (expression.pattern === undefined) {
|
|
2159
|
+
throw new RpUnsupportedError("patternMatch");
|
|
2160
|
+
}
|
|
2161
|
+
const pattern = encVariableStringPattern.test(expression.pattern) ? env.lookupVariable(expression.pattern.slice(1, -1))?.values[0] : expression.pattern;
|
|
2162
|
+
if (typeof pattern !== "string") {
|
|
2163
|
+
return null;
|
|
2164
|
+
}
|
|
2165
|
+
const matcher = xsdPatternMatcher(pattern);
|
|
2166
|
+
if (matcher === null) {
|
|
2167
|
+
throw new RpUnsupportedError("patternMatch");
|
|
2168
|
+
}
|
|
2169
|
+
return booleanValue(matcher(member));
|
|
2170
|
+
}
|
|
2171
|
+
case "power": {
|
|
2172
|
+
const [a3, b2] = (expression.expressions ?? []).map((child) => singleNumber(evaluate(child)));
|
|
2173
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null) {
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
const result = a3 ** b2;
|
|
2177
|
+
return Number.isFinite(result) ? floatValue(result) : null;
|
|
2178
|
+
}
|
|
2179
|
+
case "containerSize": {
|
|
2180
|
+
const operand = expression.expressions?.[0];
|
|
2181
|
+
const container = operand === undefined ? null : evaluate(operand);
|
|
2182
|
+
return {
|
|
2183
|
+
cardinality: "single",
|
|
2184
|
+
baseType: "integer",
|
|
2185
|
+
values: [container === null ? 0 : container.values.length]
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
case "contains": {
|
|
2189
|
+
const [firstExpression, secondExpression] = expression.expressions ?? [];
|
|
2190
|
+
if (firstExpression === undefined || secondExpression === undefined) {
|
|
2191
|
+
return null;
|
|
2192
|
+
}
|
|
2193
|
+
const first = evaluate(firstExpression);
|
|
2194
|
+
const second = evaluate(secondExpression);
|
|
2195
|
+
if (first === null || second === null || first.cardinality === "record" || second.cardinality === "record") {
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
const baseType = first.baseType ?? second.baseType;
|
|
2199
|
+
if (first.cardinality === "ordered") {
|
|
2200
|
+
const found = first.values.some((_2, start) => start + second.values.length <= first.values.length && second.values.every((member, offset) => scalarsEqual(first.values[start + offset], member, baseType, env.normalization)));
|
|
2201
|
+
return booleanValue(found);
|
|
2202
|
+
}
|
|
2203
|
+
const remaining = [...first.values];
|
|
2204
|
+
for (const member of second.values) {
|
|
2205
|
+
const at = remaining.findIndex((candidate) => scalarsEqual(candidate, member, baseType, env.normalization));
|
|
2206
|
+
if (at === -1) {
|
|
2207
|
+
return booleanValue(false);
|
|
2208
|
+
}
|
|
2209
|
+
remaining.splice(at, 1);
|
|
2210
|
+
}
|
|
2211
|
+
return booleanValue(true);
|
|
2212
|
+
}
|
|
2213
|
+
case "anyN": {
|
|
2214
|
+
const min = resolveNumericAttribute(expression.min, env);
|
|
2215
|
+
const max = resolveNumericAttribute(expression.max, env);
|
|
2216
|
+
if (typeof min !== "number" || typeof max !== "number") {
|
|
2217
|
+
return null;
|
|
2218
|
+
}
|
|
2219
|
+
const members = (expression.expressions ?? []).map((child) => singleBoolean(evaluate(child)));
|
|
2220
|
+
const trueCount = members.filter((member) => member === true).length;
|
|
2221
|
+
const nullCount = members.filter((member) => member === null).length;
|
|
2222
|
+
if (trueCount >= min && trueCount + nullCount <= max) {
|
|
2223
|
+
return booleanValue(true);
|
|
2224
|
+
}
|
|
2225
|
+
if (trueCount + nullCount < min || trueCount > max) {
|
|
2226
|
+
return booleanValue(false);
|
|
2227
|
+
}
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
case "stringMatch":
|
|
2231
|
+
case "substring": {
|
|
2232
|
+
const [a3, b2] = (expression.expressions ?? []).map((child) => {
|
|
2233
|
+
const value = evaluate(child);
|
|
2234
|
+
const member = value?.values[0];
|
|
2235
|
+
return typeof member === "string" ? member : null;
|
|
2236
|
+
});
|
|
2237
|
+
if (a3 === undefined || b2 === undefined || a3 === null || b2 === null) {
|
|
2238
|
+
return null;
|
|
2239
|
+
}
|
|
2240
|
+
const normalize = (input) => {
|
|
2241
|
+
const normalized = env.normalization?.(input) ?? input;
|
|
2242
|
+
return expression.caseSensitive === false ? normalized.toLowerCase() : normalized;
|
|
2243
|
+
};
|
|
2244
|
+
const [left, right] = [normalize(a3), normalize(b2)];
|
|
2245
|
+
const contains = expression.kind === "substring" || expression.substring === true;
|
|
2246
|
+
return booleanValue(contains ? right.includes(left) : left === right);
|
|
2247
|
+
}
|
|
2248
|
+
case "inside": {
|
|
2249
|
+
if (typeof expression.shape !== "string" || typeof expression.coords !== "string") {
|
|
2250
|
+
throw new RpUnsupportedError("inside");
|
|
2251
|
+
}
|
|
2252
|
+
const operand = expression.expressions?.[0];
|
|
2253
|
+
const value = operand === undefined ? null : evaluate(operand);
|
|
2254
|
+
const member = value?.values[0];
|
|
2255
|
+
if (value === null || typeof member !== "string") {
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
const point = parsePoint(member);
|
|
2259
|
+
if (point === null) {
|
|
2260
|
+
return null;
|
|
2261
|
+
}
|
|
2262
|
+
return booleanValue(pointInShape(expression.shape, parseCoords(expression.coords), point));
|
|
2263
|
+
}
|
|
2264
|
+
case "member": {
|
|
2265
|
+
const [needleExpression, containerExpression] = expression.expressions ?? [];
|
|
2266
|
+
if (needleExpression === undefined || containerExpression === undefined) {
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
const needle = evaluate(needleExpression);
|
|
2270
|
+
const container = evaluate(containerExpression);
|
|
2271
|
+
if (needle === null || container === null) {
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
const scalar = needle.values[0];
|
|
2275
|
+
if (scalar === undefined) {
|
|
2276
|
+
return null;
|
|
2277
|
+
}
|
|
2278
|
+
const baseType = container.baseType ?? needle.baseType;
|
|
2279
|
+
return booleanValue(container.values.some((member) => scalarsEqual(member, scalar, baseType, env.normalization)));
|
|
2280
|
+
}
|
|
2281
|
+
case "multiple":
|
|
2282
|
+
case "ordered": {
|
|
2283
|
+
const members = [];
|
|
2284
|
+
let baseType;
|
|
2285
|
+
for (const child of expression.expressions ?? []) {
|
|
2286
|
+
const value = evaluate(child);
|
|
2287
|
+
if (value === null) {
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
baseType ??= value.baseType;
|
|
2291
|
+
members.push(...value.values);
|
|
2292
|
+
}
|
|
2293
|
+
if (members.length === 0) {
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
2296
|
+
return rpValue(expression.kind, members, baseType);
|
|
2297
|
+
}
|
|
2298
|
+
case "randomInteger": {
|
|
2299
|
+
if (!env.random) {
|
|
2300
|
+
throw new RpUnsupportedError(expression.kind);
|
|
2301
|
+
}
|
|
2302
|
+
const min = resolveNumericAttribute(expression.min, env) ?? 0;
|
|
2303
|
+
const max = resolveNumericAttribute(expression.max, env) ?? min;
|
|
2304
|
+
const step = resolveNumericAttribute(expression.step, env) ?? 1;
|
|
2305
|
+
const count = Math.max(1, Math.floor((max - min) / step) + 1);
|
|
2306
|
+
return { cardinality: "single", baseType: "integer", values: [min + Math.floor(env.random() * count) * step] };
|
|
2307
|
+
}
|
|
2308
|
+
case "randomFloat": {
|
|
2309
|
+
if (!env.random) {
|
|
2310
|
+
throw new RpUnsupportedError(expression.kind);
|
|
2311
|
+
}
|
|
2312
|
+
const min = resolveNumericAttribute(expression.min, env) ?? 0;
|
|
2313
|
+
const max = resolveNumericAttribute(expression.max, env) ?? min;
|
|
2314
|
+
return floatValue(min + env.random() * (max - min));
|
|
2315
|
+
}
|
|
2316
|
+
case "testVariables": {
|
|
2317
|
+
if (!env.testVariables) {
|
|
2318
|
+
throw new RpUnsupportedError(expression.kind);
|
|
2319
|
+
}
|
|
2320
|
+
return env.testVariables(expression);
|
|
2321
|
+
}
|
|
2322
|
+
case "customOperator": {
|
|
2323
|
+
const implementation = env.customOperators?.[expression.class ?? ""];
|
|
2324
|
+
if (!implementation) {
|
|
2325
|
+
throw new RpUnsupportedError(expression.kind);
|
|
2326
|
+
}
|
|
2327
|
+
return implementation((expression.expressions ?? []).map(evaluate), expression);
|
|
2328
|
+
}
|
|
2329
|
+
case "numberCorrect":
|
|
2330
|
+
case "numberIncorrect":
|
|
2331
|
+
case "numberPresented":
|
|
2332
|
+
case "numberResponded":
|
|
2333
|
+
case "numberSelected":
|
|
2334
|
+
case "outcomeMinimum":
|
|
2335
|
+
case "outcomeMaximum": {
|
|
2336
|
+
if (!env.testAggregate) {
|
|
2337
|
+
throw new RpUnsupportedError(expression.kind);
|
|
2338
|
+
}
|
|
2339
|
+
return env.testAggregate(expression);
|
|
2340
|
+
}
|
|
2341
|
+
case "random": {
|
|
2342
|
+
if (!env.random) {
|
|
2343
|
+
throw new RpUnsupportedError(expression.kind);
|
|
2344
|
+
}
|
|
2345
|
+
const container = expression.expressions?.[0] === undefined ? null : evaluate(expression.expressions[0]);
|
|
2346
|
+
if (container === null || container.values.length === 0) {
|
|
2347
|
+
return null;
|
|
2348
|
+
}
|
|
2349
|
+
const pick = container.values[Math.floor(env.random() * container.values.length)];
|
|
2350
|
+
return rpValue("single", [pick], container.baseType);
|
|
2351
|
+
}
|
|
2352
|
+
default:
|
|
2353
|
+
throw new RpUnsupportedError(expression.kind);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
function collectExpressionIssues(expression, allowedKinds, report, customOperatorClasses) {
|
|
2357
|
+
if (expression.kind === "customOperator") {
|
|
2358
|
+
if (!customOperatorClasses?.has(expression.class ?? "")) {
|
|
2359
|
+
report(expression.kind);
|
|
2360
|
+
}
|
|
2361
|
+
} else if (!allowedKinds.has(expression.kind)) {
|
|
2362
|
+
report(expression.kind);
|
|
2363
|
+
} else if (expression.kind === "patternMatch" && expression.pattern !== undefined && !encVariableStringPattern.test(expression.pattern) && xsdPatternMatcher(expression.pattern) === null) {
|
|
2364
|
+
report(expression.kind);
|
|
2365
|
+
}
|
|
2366
|
+
for (const child of expression.expressions ?? []) {
|
|
2367
|
+
collectExpressionIssues(child, allowedKinds, report, customOperatorClasses);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// src/rp/lookup-table.ts
|
|
2372
|
+
function hasLookupTable(declaration) {
|
|
2373
|
+
return declaration?.matchTable !== undefined || declaration?.interpolationTable !== undefined;
|
|
2374
|
+
}
|
|
2375
|
+
function lookupTableValue(declaration, source) {
|
|
2376
|
+
const value = singleNumber(source);
|
|
2377
|
+
const table = declaration.matchTable ?? declaration.interpolationTable;
|
|
2378
|
+
const target = value === null ? undefined : matchedTarget(declaration, value);
|
|
2379
|
+
const result = target ?? table?.defaultValue;
|
|
2380
|
+
if (result === undefined) {
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
return rpValue("single", [coerceScalar(result, declaration.baseType)], declaration.baseType);
|
|
2384
|
+
}
|
|
2385
|
+
function matchedTarget(declaration, value) {
|
|
2386
|
+
if (declaration.matchTable) {
|
|
2387
|
+
return declaration.matchTable.matchTableEntries.find((entry) => entry.sourceValue === value)?.targetValue;
|
|
2388
|
+
}
|
|
2389
|
+
return declaration.interpolationTable?.interpolationTableEntries.find((entry) => entry.includeBoundary ?? true ? entry.sourceValue <= value : entry.sourceValue < value)?.targetValue;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// src/rp/templates.ts
|
|
2393
|
+
var matchCorrectRules = [
|
|
2394
|
+
{
|
|
2395
|
+
kind: "responseCondition",
|
|
2396
|
+
responseIf: {
|
|
2397
|
+
expression: {
|
|
2398
|
+
kind: "match",
|
|
2399
|
+
expressions: [
|
|
2400
|
+
{ kind: "variable", identifier: "RESPONSE" },
|
|
2401
|
+
{ kind: "correct", identifier: "RESPONSE" }
|
|
2402
|
+
]
|
|
2403
|
+
},
|
|
2404
|
+
rules: [
|
|
2405
|
+
{
|
|
2406
|
+
kind: "setOutcomeValue",
|
|
2407
|
+
identifier: "SCORE",
|
|
2408
|
+
expression: { kind: "baseValue", baseType: "float", value: 1 }
|
|
2409
|
+
}
|
|
2410
|
+
]
|
|
2411
|
+
},
|
|
2412
|
+
responseElse: {
|
|
2413
|
+
rules: [
|
|
2414
|
+
{
|
|
2415
|
+
kind: "setOutcomeValue",
|
|
2416
|
+
identifier: "SCORE",
|
|
2417
|
+
expression: { kind: "baseValue", baseType: "float", value: 0 }
|
|
2418
|
+
}
|
|
2419
|
+
]
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
];
|
|
2423
|
+
var mapResponseRules = [
|
|
2424
|
+
{
|
|
2425
|
+
kind: "responseCondition",
|
|
2426
|
+
responseIf: {
|
|
2427
|
+
expression: { kind: "isNull", expressions: [{ kind: "variable", identifier: "RESPONSE" }] },
|
|
2428
|
+
rules: [
|
|
2429
|
+
{
|
|
2430
|
+
kind: "setOutcomeValue",
|
|
2431
|
+
identifier: "SCORE",
|
|
2432
|
+
expression: { kind: "baseValue", baseType: "float", value: 0 }
|
|
2433
|
+
}
|
|
2434
|
+
]
|
|
2435
|
+
},
|
|
2436
|
+
responseElse: {
|
|
2437
|
+
rules: [
|
|
2438
|
+
{ kind: "setOutcomeValue", identifier: "SCORE", expression: { kind: "mapResponse", identifier: "RESPONSE" } }
|
|
2439
|
+
]
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
];
|
|
2443
|
+
var mapResponsePointRules = [
|
|
2444
|
+
{
|
|
2445
|
+
kind: "responseCondition",
|
|
2446
|
+
responseIf: {
|
|
2447
|
+
expression: { kind: "isNull", expressions: [{ kind: "variable", identifier: "RESPONSE" }] },
|
|
2448
|
+
rules: [
|
|
2449
|
+
{
|
|
2450
|
+
kind: "setOutcomeValue",
|
|
2451
|
+
identifier: "SCORE",
|
|
2452
|
+
expression: { kind: "baseValue", baseType: "float", value: 0 }
|
|
2453
|
+
}
|
|
2454
|
+
]
|
|
2455
|
+
},
|
|
2456
|
+
responseElse: {
|
|
2457
|
+
rules: [
|
|
2458
|
+
{
|
|
2459
|
+
kind: "setOutcomeValue",
|
|
2460
|
+
identifier: "SCORE",
|
|
2461
|
+
expression: { kind: "mapResponsePoint", identifier: "RESPONSE" }
|
|
2462
|
+
}
|
|
2463
|
+
]
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
];
|
|
2467
|
+
var cc2MatchBasicRules = [
|
|
2468
|
+
{
|
|
2469
|
+
kind: "responseCondition",
|
|
2470
|
+
responseIf: {
|
|
2471
|
+
expression: {
|
|
2472
|
+
kind: "match",
|
|
2473
|
+
expressions: [
|
|
2474
|
+
{ kind: "variable", identifier: "RESPONSE" },
|
|
2475
|
+
{ kind: "correct", identifier: "RESPONSE" }
|
|
2476
|
+
]
|
|
2477
|
+
},
|
|
2478
|
+
rules: [
|
|
2479
|
+
{ kind: "setOutcomeValue", identifier: "SCORE", expression: { kind: "variable", identifier: "MAXSCORE" } },
|
|
2480
|
+
{
|
|
2481
|
+
kind: "setOutcomeValue",
|
|
2482
|
+
identifier: "FEEDBACKBASIC",
|
|
2483
|
+
expression: { kind: "baseValue", baseType: "identifier", value: "correct" }
|
|
2484
|
+
}
|
|
2485
|
+
]
|
|
2486
|
+
},
|
|
2487
|
+
responseElse: {
|
|
2488
|
+
rules: [
|
|
2489
|
+
{
|
|
2490
|
+
kind: "setOutcomeValue",
|
|
2491
|
+
identifier: "FEEDBACKBASIC",
|
|
2492
|
+
expression: { kind: "baseValue", baseType: "identifier", value: "incorrect" }
|
|
2493
|
+
}
|
|
2494
|
+
]
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
];
|
|
2498
|
+
var cc2MapResponseRules = [
|
|
2499
|
+
{
|
|
2500
|
+
kind: "responseCondition",
|
|
2501
|
+
responseIf: {
|
|
2502
|
+
expression: { kind: "isNull", expressions: [{ kind: "variable", identifier: "RESPONSE" }] },
|
|
2503
|
+
rules: [
|
|
2504
|
+
{
|
|
2505
|
+
kind: "setOutcomeValue",
|
|
2506
|
+
identifier: "SCORE",
|
|
2507
|
+
expression: { kind: "baseValue", baseType: "float", value: 0 }
|
|
2508
|
+
}
|
|
2509
|
+
]
|
|
2510
|
+
},
|
|
2511
|
+
responseElse: {
|
|
2512
|
+
rules: [
|
|
2513
|
+
{ kind: "setOutcomeValue", identifier: "SCORE", expression: { kind: "mapResponse", identifier: "RESPONSE" } }
|
|
2514
|
+
]
|
|
2515
|
+
}
|
|
2516
|
+
},
|
|
2517
|
+
{
|
|
2518
|
+
kind: "responseCondition",
|
|
2519
|
+
responseIf: {
|
|
2520
|
+
expression: {
|
|
2521
|
+
kind: "equal",
|
|
2522
|
+
toleranceMode: "exact",
|
|
2523
|
+
expressions: [
|
|
2524
|
+
{ kind: "variable", identifier: "MAXSCORE" },
|
|
2525
|
+
{ kind: "variable", identifier: "SCORE" }
|
|
2526
|
+
]
|
|
2527
|
+
},
|
|
2528
|
+
rules: [
|
|
2529
|
+
{
|
|
2530
|
+
kind: "setOutcomeValue",
|
|
2531
|
+
identifier: "FEEDBACK",
|
|
2532
|
+
expression: { kind: "baseValue", baseType: "identifier", value: "correct" }
|
|
2533
|
+
}
|
|
2534
|
+
]
|
|
2535
|
+
},
|
|
2536
|
+
responseElse: {
|
|
2537
|
+
rules: [
|
|
2538
|
+
{
|
|
2539
|
+
kind: "setOutcomeValue",
|
|
2540
|
+
identifier: "FEEDBACK",
|
|
2541
|
+
expression: { kind: "baseValue", baseType: "identifier", value: "incorrect" }
|
|
2542
|
+
}
|
|
2543
|
+
]
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
];
|
|
2547
|
+
var templatesBySegment = new Map([
|
|
2548
|
+
["match_correct", matchCorrectRules],
|
|
2549
|
+
["map_response", mapResponseRules],
|
|
2550
|
+
["map_response_point", mapResponsePointRules],
|
|
2551
|
+
["CC2_match", matchCorrectRules],
|
|
2552
|
+
["CC2_match_basic", cc2MatchBasicRules],
|
|
2553
|
+
["CC2_map_response", cc2MapResponseRules]
|
|
2554
|
+
]);
|
|
2555
|
+
function resolveTemplate(uri) {
|
|
2556
|
+
const segment = uri.split("/").at(-1)?.replace(/\.xml$/u, "") ?? "";
|
|
2557
|
+
return templatesBySegment.get(segment) ?? null;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/rp/interpreter.ts
|
|
2561
|
+
var supportedRuleKinds = new Set([
|
|
2562
|
+
"responseCondition",
|
|
2563
|
+
"setOutcomeValue",
|
|
2564
|
+
"lookupOutcomeValue",
|
|
2565
|
+
"responseProcessingFragment",
|
|
2566
|
+
"exitResponse"
|
|
2567
|
+
]);
|
|
2568
|
+
var rpExpressionKinds = new Set([...deterministicExpressionKinds, "random", "randomInteger", "randomFloat"]);
|
|
2569
|
+
|
|
2570
|
+
class ExitResponseSignal extends Error {
|
|
2571
|
+
}
|
|
2572
|
+
function defaultOutcomes(declarations) {
|
|
2573
|
+
const outcomes = new Map;
|
|
2574
|
+
for (const declaration of declarations) {
|
|
2575
|
+
if (declaration.defaultValue) {
|
|
2576
|
+
outcomes.set(declaration.identifier, rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType));
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
outcomes.set(declaration.identifier, isNumericBaseType(declaration.baseType) ? floatValue(0) : null);
|
|
2580
|
+
}
|
|
2581
|
+
return outcomes;
|
|
2582
|
+
}
|
|
2583
|
+
function executeResponseProcessing(view, context) {
|
|
2584
|
+
const issues = [];
|
|
2585
|
+
const declarationsById = new Map(context.responseDeclarations.map((declaration) => [declaration.identifier, declaration]));
|
|
2586
|
+
const templateDeclarationsById = new Map((context.templateDeclarations ?? []).map((declaration) => [declaration.identifier, declaration]));
|
|
2587
|
+
function initialOutcomes() {
|
|
2588
|
+
const outcomes2 = defaultOutcomes(context.outcomeDeclarations);
|
|
2589
|
+
if (!outcomes2.has("completionStatus")) {
|
|
2590
|
+
outcomes2.set("completionStatus", rpValue("single", [context.completionStatus ?? "not_attempted"], "identifier"));
|
|
2591
|
+
}
|
|
2592
|
+
for (const [identifier, prior] of Object.entries(context.priorOutcomes ?? {})) {
|
|
2593
|
+
const declaration = context.outcomeDeclarations.find((entry) => entry.identifier === identifier);
|
|
2594
|
+
outcomes2.set(identifier, fromFlatValue(prior, declaration?.cardinality ?? "single", declaration?.baseType));
|
|
2595
|
+
}
|
|
2596
|
+
return outcomes2;
|
|
2597
|
+
}
|
|
2598
|
+
let outcomes = initialOutcomes();
|
|
2599
|
+
let rules = view?.rules ?? [];
|
|
2600
|
+
if (view && !view.rules && view.template) {
|
|
2601
|
+
const resolved = resolveTemplate(view.template);
|
|
2602
|
+
if (resolved) {
|
|
2603
|
+
rules = resolved;
|
|
2604
|
+
} else {
|
|
2605
|
+
issues.push({ type: "unsupported-rp", name: view.template, detail: "Unknown response-processing template URI." });
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
const env = {
|
|
2609
|
+
lookupVariable: (identifier) => {
|
|
2610
|
+
if (identifier === "duration") {
|
|
2611
|
+
return context.duration === undefined ? null : rpValue("single", [context.duration], "duration");
|
|
2612
|
+
}
|
|
2613
|
+
if (identifier === "numAttempts") {
|
|
2614
|
+
return context.numAttempts === undefined ? null : rpValue("single", [context.numAttempts], "integer");
|
|
2615
|
+
}
|
|
2616
|
+
const declaration = declarationsById.get(identifier);
|
|
2617
|
+
if (declaration) {
|
|
2618
|
+
return fromResponse(declaration, context.responses[identifier] ?? null);
|
|
2619
|
+
}
|
|
2620
|
+
const templateDeclaration = templateDeclarationsById.get(identifier);
|
|
2621
|
+
if (templateDeclaration) {
|
|
2622
|
+
return fromFlatValue(context.templateValues?.[identifier] ?? null, templateDeclaration.cardinality, templateDeclaration.baseType);
|
|
2623
|
+
}
|
|
2624
|
+
return outcomes.get(identifier) ?? null;
|
|
2625
|
+
},
|
|
2626
|
+
responseDeclaration: (identifier) => declarationsById.get(identifier),
|
|
2627
|
+
responseValue: (identifier) => context.responses[identifier] ?? null,
|
|
2628
|
+
variableDefault: (identifier) => {
|
|
2629
|
+
const declaration = declarationsById.get(identifier) ?? context.outcomeDeclarations.find((entry) => entry.identifier === identifier) ?? templateDeclarationsById.get(identifier);
|
|
2630
|
+
if (!declaration?.defaultValue) {
|
|
2631
|
+
return null;
|
|
2632
|
+
}
|
|
2633
|
+
return rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType);
|
|
2634
|
+
},
|
|
2635
|
+
normalization: context.normalization,
|
|
2636
|
+
random: context.random,
|
|
2637
|
+
customOperators: context.customOperators
|
|
2638
|
+
};
|
|
2639
|
+
function branchTaken(branch) {
|
|
2640
|
+
if (singleBoolean(evaluateExpression(branch.expression, env)) !== true) {
|
|
2641
|
+
return false;
|
|
2642
|
+
}
|
|
2643
|
+
executeRules(branch.rules);
|
|
2644
|
+
return true;
|
|
2645
|
+
}
|
|
2646
|
+
function executeRules(rules_) {
|
|
2647
|
+
for (const rule of rules_) {
|
|
2648
|
+
if (!supportedRuleKinds.has(rule.kind)) {
|
|
2649
|
+
throw new RpUnsupportedError(rule.kind);
|
|
2650
|
+
}
|
|
2651
|
+
if (rule.kind === "exitResponse") {
|
|
2652
|
+
throw new ExitResponseSignal;
|
|
2653
|
+
}
|
|
2654
|
+
if (rule.kind === "setOutcomeValue") {
|
|
2655
|
+
if (rule.identifier !== undefined && rule.expression !== undefined) {
|
|
2656
|
+
outcomes.set(rule.identifier, evaluateExpression(rule.expression, env));
|
|
2657
|
+
}
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
if (rule.kind === "responseProcessingFragment") {
|
|
2661
|
+
executeRules(rule.rules ?? []);
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
if (rule.kind === "lookupOutcomeValue") {
|
|
2665
|
+
if (rule.identifier !== undefined && rule.expression !== undefined) {
|
|
2666
|
+
const declaration = context.outcomeDeclarations.find((entry) => entry.identifier === rule.identifier);
|
|
2667
|
+
if (!hasLookupTable(declaration)) {
|
|
2668
|
+
throw new RpUnsupportedError("lookupOutcomeValue");
|
|
2669
|
+
}
|
|
2670
|
+
outcomes.set(rule.identifier, lookupTableValue(declaration, evaluateExpression(rule.expression, env)));
|
|
2671
|
+
}
|
|
2672
|
+
continue;
|
|
2673
|
+
}
|
|
2674
|
+
if (rule.responseIf && branchTaken(rule.responseIf)) {
|
|
2675
|
+
continue;
|
|
2676
|
+
}
|
|
2677
|
+
const elseIfTaken = (rule.responseElseIfs ?? []).some((branch) => branchTaken(branch));
|
|
2678
|
+
if (!elseIfTaken && rule.responseElse) {
|
|
2679
|
+
executeRules(rule.responseElse.rules);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
try {
|
|
2684
|
+
executeRules(rules);
|
|
2685
|
+
} catch (error) {
|
|
2686
|
+
if (error instanceof RpUnsupportedError) {
|
|
2687
|
+
issues.push({ type: "unsupported-rp", name: error.kindName });
|
|
2688
|
+
outcomes = initialOutcomes();
|
|
2689
|
+
} else if (!(error instanceof ExitResponseSignal)) {
|
|
2690
|
+
throw error;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
return {
|
|
2694
|
+
outcomes: Object.fromEntries([...outcomes].map(([identifier, value]) => [identifier, toOutcomeValue(value)])),
|
|
2695
|
+
issues
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
function collectRpIssues(view, options) {
|
|
2699
|
+
if (!view) {
|
|
2700
|
+
return [];
|
|
2701
|
+
}
|
|
2702
|
+
const issues = [];
|
|
2703
|
+
const seen = new Set;
|
|
2704
|
+
function report(name, detail) {
|
|
2705
|
+
if (!seen.has(name)) {
|
|
2706
|
+
seen.add(name);
|
|
2707
|
+
issues.push({ type: "unsupported-rp", name, ...detail === undefined ? {} : { detail } });
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
function walkRules(rules) {
|
|
2711
|
+
for (const rule of rules) {
|
|
2712
|
+
if (!supportedRuleKinds.has(rule.kind)) {
|
|
2713
|
+
report(rule.kind);
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
if (rule.kind === "lookupOutcomeValue" && options?.outcomeDeclarations !== undefined) {
|
|
2717
|
+
const declaration = options.outcomeDeclarations.find((entry) => entry.identifier === rule.identifier);
|
|
2718
|
+
if (!hasLookupTable(declaration)) {
|
|
2719
|
+
report("lookupOutcomeValue", "Outcome declaration has no matchTable/interpolationTable.");
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
if (rule.expression) {
|
|
2723
|
+
collectExpressionIssues(rule.expression, rpExpressionKinds, report, options?.customOperatorClasses);
|
|
2724
|
+
}
|
|
2725
|
+
if (rule.rules) {
|
|
2726
|
+
walkRules(rule.rules);
|
|
2727
|
+
}
|
|
2728
|
+
for (const branch of [rule.responseIf, ...rule.responseElseIfs ?? []]) {
|
|
2729
|
+
if (branch) {
|
|
2730
|
+
collectExpressionIssues(branch.expression, rpExpressionKinds, report, options?.customOperatorClasses);
|
|
2731
|
+
walkRules(branch.rules);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
if (rule.responseElse) {
|
|
2735
|
+
walkRules(rule.responseElse.rules);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
if (view.rules) {
|
|
2740
|
+
walkRules(view.rules);
|
|
2741
|
+
} else if (view.template) {
|
|
2742
|
+
const resolved = resolveTemplate(view.template);
|
|
2743
|
+
if (resolved === null) {
|
|
2744
|
+
report(view.template, "Unknown response-processing template URI.");
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return issues;
|
|
2748
|
+
}
|
|
2749
|
+
// src/rp/template-processing.ts
|
|
2750
|
+
var supportedTemplateRuleKinds = new Set([
|
|
2751
|
+
"setTemplateValue",
|
|
2752
|
+
"templateCondition",
|
|
2753
|
+
"templateConstraint",
|
|
2754
|
+
"setCorrectResponse",
|
|
2755
|
+
"exitTemplate"
|
|
2756
|
+
]);
|
|
2757
|
+
var templateExpressionKinds = new Set([...deterministicExpressionKinds, ...randomExpressionKinds]);
|
|
2758
|
+
|
|
2759
|
+
class ExitTemplateSignal extends Error {
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
class TemplateConstraintSignal extends Error {
|
|
2763
|
+
}
|
|
2764
|
+
var maxConstraintAttempts = 100;
|
|
2765
|
+
function mulberry32(seed) {
|
|
2766
|
+
let state = seed >>> 0;
|
|
2767
|
+
return () => {
|
|
2768
|
+
state = state + 1831565813 >>> 0;
|
|
2769
|
+
let t3 = state;
|
|
2770
|
+
t3 = Math.imul(t3 ^ t3 >>> 15, t3 | 1);
|
|
2771
|
+
t3 ^= t3 + Math.imul(t3 ^ t3 >>> 7, t3 | 61);
|
|
2772
|
+
return ((t3 ^ t3 >>> 14) >>> 0) / 4294967296;
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
function executeTemplateProcessing(view, context) {
|
|
2776
|
+
const issues = [];
|
|
2777
|
+
const declarationsById = new Map(context.templateDeclarations.map((entry) => [entry.identifier, entry]));
|
|
2778
|
+
const responseDeclarationsById = new Map(context.responseDeclarations.map((entry) => [entry.identifier, entry]));
|
|
2779
|
+
const correctResponseOverrides = {};
|
|
2780
|
+
function initialValues() {
|
|
2781
|
+
const values = new Map;
|
|
2782
|
+
for (const declaration of context.templateDeclarations) {
|
|
2783
|
+
values.set(declaration.identifier, declaration.defaultValue ? rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType) : null);
|
|
2784
|
+
}
|
|
2785
|
+
return values;
|
|
2786
|
+
}
|
|
2787
|
+
let templateValues = initialValues();
|
|
2788
|
+
const env = {
|
|
2789
|
+
lookupVariable: (identifier) => templateValues.get(identifier) ?? null,
|
|
2790
|
+
responseDeclaration: (identifier) => responseDeclarationsById.get(identifier),
|
|
2791
|
+
responseValue: () => null,
|
|
2792
|
+
variableDefault: (identifier) => {
|
|
2793
|
+
const declaration = declarationsById.get(identifier) ?? responseDeclarationsById.get(identifier);
|
|
2794
|
+
if (!declaration?.defaultValue) {
|
|
2795
|
+
return null;
|
|
2796
|
+
}
|
|
2797
|
+
return rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType);
|
|
2798
|
+
},
|
|
2799
|
+
random: mulberry32(context.seed),
|
|
2800
|
+
customOperators: context.customOperators
|
|
2801
|
+
};
|
|
2802
|
+
function branchTaken(branch) {
|
|
2803
|
+
if (singleBoolean(evaluateExpression(branch.expression, env)) !== true) {
|
|
2804
|
+
return false;
|
|
2805
|
+
}
|
|
2806
|
+
executeRules(branch.rules);
|
|
2807
|
+
return true;
|
|
2808
|
+
}
|
|
2809
|
+
function executeRules(rules) {
|
|
2810
|
+
for (const rule of rules) {
|
|
2811
|
+
if (!supportedTemplateRuleKinds.has(rule.kind)) {
|
|
2812
|
+
throw new RpUnsupportedError(rule.kind);
|
|
2813
|
+
}
|
|
2814
|
+
if (rule.kind === "exitTemplate") {
|
|
2815
|
+
throw new ExitTemplateSignal;
|
|
2816
|
+
}
|
|
2817
|
+
if (rule.kind === "setTemplateValue") {
|
|
2818
|
+
if (rule.identifier !== undefined && rule.expression !== undefined) {
|
|
2819
|
+
const declaration = declarationsById.get(rule.identifier);
|
|
2820
|
+
const value = evaluateExpression(rule.expression, env);
|
|
2821
|
+
templateValues.set(rule.identifier, value === null || !declaration ? value : rpValue(declaration.cardinality, value.values, declaration.baseType ?? value.baseType));
|
|
2822
|
+
}
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
if (rule.kind === "setCorrectResponse") {
|
|
2826
|
+
if (rule.identifier !== undefined && rule.expression !== undefined) {
|
|
2827
|
+
const value = evaluateExpression(rule.expression, env);
|
|
2828
|
+
if (value !== null) {
|
|
2829
|
+
correctResponseOverrides[rule.identifier] = {
|
|
2830
|
+
values: value.values.map((member) => ({ value: String(member) }))
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
if (rule.kind === "templateConstraint") {
|
|
2837
|
+
if (rule.expression !== undefined && singleBoolean(evaluateExpression(rule.expression, env)) !== true) {
|
|
2838
|
+
throw new TemplateConstraintSignal;
|
|
2839
|
+
}
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
if (rule.templateIf && branchTaken(rule.templateIf)) {
|
|
2843
|
+
continue;
|
|
2844
|
+
}
|
|
2845
|
+
const elseIfTaken = (rule.templateElseIfs ?? []).some((branch) => branchTaken(branch));
|
|
2846
|
+
if (!elseIfTaken && rule.templateElse) {
|
|
2847
|
+
executeRules(rule.templateElse.rules);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
for (let attempt = 0;attempt < maxConstraintAttempts; attempt += 1) {
|
|
2852
|
+
try {
|
|
2853
|
+
executeRules(view?.rules ?? []);
|
|
2854
|
+
break;
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
if (error instanceof TemplateConstraintSignal) {
|
|
2857
|
+
templateValues = initialValues();
|
|
2858
|
+
for (const key of Object.keys(correctResponseOverrides)) {
|
|
2859
|
+
delete correctResponseOverrides[key];
|
|
2860
|
+
}
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
if (error instanceof RpUnsupportedError) {
|
|
2864
|
+
issues.push({ type: "unsupported-rp", name: error.kindName });
|
|
2865
|
+
templateValues = initialValues();
|
|
2866
|
+
} else if (!(error instanceof ExitTemplateSignal)) {
|
|
2867
|
+
throw error;
|
|
2868
|
+
}
|
|
2869
|
+
break;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
return {
|
|
2873
|
+
templateValues: Object.fromEntries([...templateValues].map(([identifier, value]) => [identifier, toOutcomeValue(value)])),
|
|
2874
|
+
correctResponseOverrides,
|
|
2875
|
+
issues
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
function applyTemplateDefaultOverrides(declarations, overrides) {
|
|
2879
|
+
return declarations.map((declaration) => {
|
|
2880
|
+
if (!(declaration.identifier in overrides)) {
|
|
2881
|
+
return declaration;
|
|
2882
|
+
}
|
|
2883
|
+
const value = overrides[declaration.identifier];
|
|
2884
|
+
if (value === null || value === undefined) {
|
|
2885
|
+
const { defaultValue: _cleared, ...rest } = declaration;
|
|
2886
|
+
return rest;
|
|
2887
|
+
}
|
|
2888
|
+
return {
|
|
2889
|
+
...declaration,
|
|
2890
|
+
defaultValue: { values: (Array.isArray(value) ? value : [value]).map((member) => ({ value: member })) }
|
|
2891
|
+
};
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
function applyCorrectResponseOverrides(declarations, overrides) {
|
|
2895
|
+
return declarations.map((declaration) => {
|
|
2896
|
+
const override = overrides[declaration.identifier];
|
|
2897
|
+
return override ? { ...declaration, correctResponse: override } : declaration;
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
function collectTemplateIssues(view, options) {
|
|
2901
|
+
if (!view) {
|
|
2902
|
+
return [];
|
|
2903
|
+
}
|
|
2904
|
+
const issues = [];
|
|
2905
|
+
const seen = new Set;
|
|
2906
|
+
function report(name) {
|
|
2907
|
+
if (!seen.has(name)) {
|
|
2908
|
+
seen.add(name);
|
|
2909
|
+
issues.push({ type: "unsupported-rp", name });
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
function walkRules(rules) {
|
|
2913
|
+
for (const rule of rules) {
|
|
2914
|
+
if (!supportedTemplateRuleKinds.has(rule.kind)) {
|
|
2915
|
+
report(rule.kind);
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
if (rule.expression) {
|
|
2919
|
+
collectExpressionIssues(rule.expression, templateExpressionKinds, report, options?.customOperatorClasses);
|
|
2920
|
+
}
|
|
2921
|
+
for (const branch of [rule.templateIf, ...rule.templateElseIfs ?? []]) {
|
|
2922
|
+
if (branch) {
|
|
2923
|
+
collectExpressionIssues(branch.expression, templateExpressionKinds, report, options?.customOperatorClasses);
|
|
2924
|
+
walkRules(branch.rules);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
if (rule.templateElse) {
|
|
2928
|
+
walkRules(rule.templateElse.rules);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
walkRules(view.rules);
|
|
2933
|
+
return issues;
|
|
2934
|
+
}
|
|
2935
|
+
// src/item-capability.ts
|
|
2936
|
+
var feedbackKinds = new Set(["feedbackInline", "feedbackBlock"]);
|
|
2937
|
+
var templateContentKinds = new Set(["templateInline", "templateBlock"]);
|
|
2938
|
+
var intrinsicLeafKinds = new Set(["text", "printedVariable"]);
|
|
2939
|
+
function isFeedbackNode(node) {
|
|
2940
|
+
return feedbackKinds.has(node.kind);
|
|
2941
|
+
}
|
|
2942
|
+
function isTemplateContentNode(node) {
|
|
2943
|
+
return templateContentKinds.has(node.kind);
|
|
2944
|
+
}
|
|
2945
|
+
function isInteractionNode(node) {
|
|
2946
|
+
return node.kind !== "xml" && typeof node.responseIdentifier === "string";
|
|
2947
|
+
}
|
|
2948
|
+
function reportItemCapability(item, options) {
|
|
2949
|
+
const model = options.model ?? v0ContentModel;
|
|
2950
|
+
const customOperatorClasses = options.customOperatorClasses ?? new Set;
|
|
2951
|
+
const issues = [];
|
|
2952
|
+
const seen = new Set;
|
|
2953
|
+
function report(issue) {
|
|
2954
|
+
const dedupeKey = `${issue.type}:${issue.name}:${issue.responseIdentifier ?? ""}`;
|
|
2955
|
+
if (!seen.has(dedupeKey)) {
|
|
2956
|
+
seen.add(dedupeKey);
|
|
2957
|
+
issues.push(issue);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
function walk(node) {
|
|
2961
|
+
if (isFeedbackNode(node) || isTemplateContentNode(node) || node.kind === "rubricBlock") {
|
|
2962
|
+
for (const child of node.content ?? []) {
|
|
2963
|
+
walk(child);
|
|
2964
|
+
}
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
if (isInteractionNode(node)) {
|
|
2968
|
+
if (!options.supportedInteractions.has(node.kind)) {
|
|
2969
|
+
report({ type: "unsupported-interaction", name: node.kind, responseIdentifier: node.responseIdentifier });
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
const schema = options.interactionSchemas?.get(node.kind);
|
|
2973
|
+
if (schema) {
|
|
2974
|
+
const parsed = schema.safeParse(node);
|
|
2975
|
+
if (!parsed.success) {
|
|
2976
|
+
const detail = parsed.error.issues[0]?.message;
|
|
2977
|
+
report({
|
|
2978
|
+
type: "invalid-interaction",
|
|
2979
|
+
name: node.kind,
|
|
2980
|
+
responseIdentifier: node.responseIdentifier,
|
|
2981
|
+
...detail !== undefined ? { detail } : {}
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
return;
|
|
2986
|
+
}
|
|
2987
|
+
if (node.kind === "xml") {
|
|
2988
|
+
const xmlNode = node;
|
|
2989
|
+
if (xmlNode.name === model.mathRoot) {
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (!isAllowedFlowElement(model, xmlNode.name)) {
|
|
2993
|
+
report({ type: "unsupported-element", name: xmlNode.name });
|
|
2994
|
+
}
|
|
2995
|
+
for (const child of xmlNode.children ?? []) {
|
|
2996
|
+
walk(child);
|
|
2997
|
+
}
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
if (intrinsicLeafKinds.has(node.kind)) {
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
report({ type: "unsupported-element", name: node.kind });
|
|
3004
|
+
}
|
|
3005
|
+
for (const node of item.itemBody.content ?? []) {
|
|
3006
|
+
walk(node);
|
|
3007
|
+
}
|
|
3008
|
+
for (const ref of item.assessmentStimulusRefs ?? []) {
|
|
3009
|
+
const stimulus = options.resolveStimulus?.(ref) ?? null;
|
|
3010
|
+
if (stimulus === null) {
|
|
3011
|
+
report({ type: "unsupported-element", name: "assessmentStimulusRef", detail: ref.href });
|
|
3012
|
+
continue;
|
|
3013
|
+
}
|
|
3014
|
+
for (const node of stimulus.content) {
|
|
3015
|
+
walk(node);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
for (const feedback of item.modalFeedbacks ?? []) {
|
|
3019
|
+
for (const child of feedback.content ?? []) {
|
|
3020
|
+
walk(child);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
for (const issue of collectRpIssues(item.responseProcessing, {
|
|
3024
|
+
customOperatorClasses,
|
|
3025
|
+
outcomeDeclarations: item.outcomeDeclarations ?? []
|
|
3026
|
+
})) {
|
|
3027
|
+
report(issue);
|
|
3028
|
+
}
|
|
3029
|
+
for (const issue of collectTemplateIssues(item.templateProcessing, { customOperatorClasses })) {
|
|
3030
|
+
report(issue);
|
|
3031
|
+
}
|
|
3032
|
+
return { deliverable: issues.length === 0, issues };
|
|
3033
|
+
}
|
|
3034
|
+
var referenceInteractionKinds = [
|
|
3035
|
+
"associateInteraction",
|
|
3036
|
+
"choiceInteraction",
|
|
3037
|
+
"drawingInteraction",
|
|
3038
|
+
"endAttemptInteraction",
|
|
3039
|
+
"extendedTextInteraction",
|
|
3040
|
+
"gapMatchInteraction",
|
|
3041
|
+
"graphicAssociateInteraction",
|
|
3042
|
+
"graphicGapMatchInteraction",
|
|
3043
|
+
"graphicOrderInteraction",
|
|
3044
|
+
"hotspotInteraction",
|
|
3045
|
+
"hottextInteraction",
|
|
3046
|
+
"inlineChoiceInteraction",
|
|
3047
|
+
"matchInteraction",
|
|
3048
|
+
"mediaInteraction",
|
|
3049
|
+
"orderInteraction",
|
|
3050
|
+
"positionObjectStage",
|
|
3051
|
+
"selectPointInteraction",
|
|
3052
|
+
"sliderInteraction",
|
|
3053
|
+
"textEntryInteraction",
|
|
3054
|
+
"uploadInteraction"
|
|
3055
|
+
];
|
|
3056
|
+
// src/item-score.ts
|
|
3057
|
+
function numericOutcome(value) {
|
|
3058
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3059
|
+
}
|
|
3060
|
+
function effectiveItemScore(scores, outcomes) {
|
|
3061
|
+
const scoreOutcome = numericOutcome(outcomes["SCORE"]);
|
|
3062
|
+
const maxOutcome = numericOutcome(outcomes["MAXSCORE"]);
|
|
3063
|
+
const summedMax = scores.reduce((total, score) => total + score.maxScore, 0);
|
|
3064
|
+
if (scoreOutcome !== null) {
|
|
3065
|
+
return { raw: scoreOutcome, max: maxOutcome ?? summedMax, fromOutcomes: true };
|
|
3066
|
+
}
|
|
3067
|
+
return {
|
|
3068
|
+
raw: scores.reduce((total, score) => total + score.score, 0),
|
|
3069
|
+
max: maxOutcome ?? summedMax,
|
|
3070
|
+
fromOutcomes: false
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
// src/response-validity.ts
|
|
3074
|
+
var countConstraintKinds = ["minChoices", "maxChoices", "minAssociations", "maxAssociations", "minStrings"];
|
|
3075
|
+
function collectInteractionConstraints(content) {
|
|
3076
|
+
const constraints = [];
|
|
3077
|
+
function walk(node) {
|
|
3078
|
+
const record = node;
|
|
3079
|
+
if (isInteractionKind(v0ContentModel, node.kind) && typeof record["responseIdentifier"] === "string") {
|
|
3080
|
+
const responseIdentifier = record["responseIdentifier"];
|
|
3081
|
+
for (const kind of [...countConstraintKinds, "minPlays"]) {
|
|
3082
|
+
const bound = record[kind];
|
|
3083
|
+
if (typeof bound === "number" && bound > 0) {
|
|
3084
|
+
constraints.push({ responseIdentifier, kind, bound });
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
const patternMask = record["patternMask"];
|
|
3088
|
+
if (typeof patternMask === "string" && patternMask !== "") {
|
|
3089
|
+
constraints.push({ responseIdentifier, kind: "patternMask", bound: patternMask });
|
|
3090
|
+
}
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
for (const key of ["content", "children"]) {
|
|
3094
|
+
const nested = record[key];
|
|
3095
|
+
if (Array.isArray(nested)) {
|
|
3096
|
+
for (const child of nested) {
|
|
3097
|
+
walk(child);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
for (const node of content ?? []) {
|
|
3103
|
+
walk(node);
|
|
3104
|
+
}
|
|
3105
|
+
return constraints;
|
|
3106
|
+
}
|
|
3107
|
+
function memberCount(value) {
|
|
3108
|
+
if (value === null || value === undefined || value === "") {
|
|
3109
|
+
return 0;
|
|
3110
|
+
}
|
|
3111
|
+
if (Array.isArray(value)) {
|
|
3112
|
+
return value.filter((member) => member !== null && member !== "").length;
|
|
3113
|
+
}
|
|
3114
|
+
return 1;
|
|
3115
|
+
}
|
|
3116
|
+
function stringMembers(value) {
|
|
3117
|
+
if (typeof value === "string") {
|
|
3118
|
+
return value === "" ? [] : [value];
|
|
3119
|
+
}
|
|
3120
|
+
if (Array.isArray(value)) {
|
|
3121
|
+
return value.filter((member) => typeof member === "string" && member !== "");
|
|
3122
|
+
}
|
|
3123
|
+
return [];
|
|
3124
|
+
}
|
|
3125
|
+
function violates(constraint, value) {
|
|
3126
|
+
switch (constraint.kind) {
|
|
3127
|
+
case "minChoices":
|
|
3128
|
+
case "minAssociations":
|
|
3129
|
+
case "minStrings":
|
|
3130
|
+
return memberCount(value) < Number(constraint.bound);
|
|
3131
|
+
case "maxChoices":
|
|
3132
|
+
case "maxAssociations":
|
|
3133
|
+
return memberCount(value) > Number(constraint.bound);
|
|
3134
|
+
case "minPlays": {
|
|
3135
|
+
const plays = typeof value === "number" ? value : 0;
|
|
3136
|
+
return plays < Number(constraint.bound);
|
|
3137
|
+
}
|
|
3138
|
+
case "patternMask": {
|
|
3139
|
+
const members = stringMembers(value);
|
|
3140
|
+
if (members.length === 0) {
|
|
3141
|
+
return false;
|
|
3142
|
+
}
|
|
3143
|
+
try {
|
|
3144
|
+
const matches = HA(String(constraint.bound), { language: "xsd" });
|
|
3145
|
+
return !members.every((member) => matches(member));
|
|
3146
|
+
} catch {
|
|
3147
|
+
return false;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
function collectResponseViolations(constraints, responses) {
|
|
3153
|
+
return constraints.filter((constraint) => {
|
|
3154
|
+
const value = responses[constraint.responseIdentifier] ?? null;
|
|
3155
|
+
return !isResponseRecord(value) && violates(constraint, value);
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// src/store.ts
|
|
3160
|
+
function createAttemptStore(declarations, initialResponses, options) {
|
|
3161
|
+
const seed = options?.seed ?? Math.floor(Math.random() * 2 ** 31);
|
|
3162
|
+
const templateDeclarations = options?.templateDefaultValues ? applyTemplateDefaultOverrides(options.templateDeclarations ?? [], options.templateDefaultValues) : options?.templateDeclarations ?? [];
|
|
3163
|
+
const templateResult = options?.templateProcessing ? executeTemplateProcessing(options.templateProcessing, {
|
|
3164
|
+
templateDeclarations,
|
|
3165
|
+
responseDeclarations: declarations,
|
|
3166
|
+
seed,
|
|
3167
|
+
customOperators: options.customOperators
|
|
3168
|
+
}) : null;
|
|
3169
|
+
const effectiveDeclarations = templateResult ? applyCorrectResponseOverrides(declarations, templateResult.correctResponseOverrides) : declarations;
|
|
3170
|
+
const declarationsById = new Map(effectiveDeclarations.map((declaration) => [declaration.identifier, declaration]));
|
|
3171
|
+
const listeners = new Set;
|
|
3172
|
+
const responseCollectors = new Map;
|
|
3173
|
+
const rpRandom = mulberry32((seed ^ 2654435769) >>> 0);
|
|
3174
|
+
const now = options?.now ?? Date.now;
|
|
3175
|
+
let activeMs = 0;
|
|
3176
|
+
let runningSinceMs = now();
|
|
3177
|
+
const activeSeconds = () => (activeMs + (runningSinceMs === null ? 0 : now() - runningSinceMs)) / 1000;
|
|
3178
|
+
const completionStatusDeclared = (options?.outcomeDeclarations ?? []).some((declaration) => declaration.identifier === "completionStatus");
|
|
3179
|
+
const maintainedOutcomes = () => completionStatusDeclared ? {} : { completionStatus: "unknown" };
|
|
3180
|
+
const violationsOf = (responses) => options?.constraints ? collectResponseViolations(options.constraints, responses) : [];
|
|
3181
|
+
const correctResponses = {};
|
|
3182
|
+
for (const declaration of effectiveDeclarations) {
|
|
3183
|
+
const values = declaration.correctResponse?.values;
|
|
3184
|
+
if (values !== undefined) {
|
|
3185
|
+
correctResponses[declaration.identifier] = declaration.cardinality === "single" ? values[0]?.value ?? null : values.map((entry) => entry.value);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
let snapshot = {
|
|
3189
|
+
responses: { ...initialResponses },
|
|
3190
|
+
submitted: false,
|
|
3191
|
+
scores: [],
|
|
3192
|
+
outcomes: maintainedOutcomes(),
|
|
3193
|
+
templateValues: templateResult?.templateValues ?? {},
|
|
3194
|
+
attemptCount: 0,
|
|
3195
|
+
durationSeconds: null,
|
|
3196
|
+
responseViolations: violationsOf(initialResponses),
|
|
3197
|
+
correctResponses
|
|
3198
|
+
};
|
|
3199
|
+
function emit(next) {
|
|
3200
|
+
snapshot = next;
|
|
3201
|
+
for (const listener of listeners) {
|
|
3202
|
+
listener();
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
function computeScores(responses) {
|
|
3206
|
+
return [...declarationsById.values()].map((declaration) => scoreResponse(declaration, responses[declaration.identifier] ?? null, options?.normalization));
|
|
3207
|
+
}
|
|
3208
|
+
function computeOutcomes(responses, durationSeconds, priorOutcomes) {
|
|
3209
|
+
if (!options?.responseProcessing) {
|
|
3210
|
+
return {};
|
|
3211
|
+
}
|
|
3212
|
+
return executeResponseProcessing(options.responseProcessing, {
|
|
3213
|
+
responseDeclarations: effectiveDeclarations,
|
|
3214
|
+
outcomeDeclarations: options.outcomeDeclarations ?? [],
|
|
3215
|
+
responses,
|
|
3216
|
+
normalization: options.normalization,
|
|
3217
|
+
templateDeclarations,
|
|
3218
|
+
templateValues: snapshot.templateValues,
|
|
3219
|
+
priorOutcomes,
|
|
3220
|
+
random: rpRandom,
|
|
3221
|
+
customOperators: options.customOperators,
|
|
3222
|
+
duration: durationSeconds,
|
|
3223
|
+
numAttempts: snapshot.attemptCount + 1,
|
|
3224
|
+
...typeof snapshot.outcomes["completionStatus"] === "string" ? { completionStatus: snapshot.outcomes["completionStatus"] } : {}
|
|
3225
|
+
}).outcomes;
|
|
3226
|
+
}
|
|
3227
|
+
return {
|
|
3228
|
+
getSnapshot: () => snapshot,
|
|
3229
|
+
subscribe: (listener) => {
|
|
3230
|
+
listeners.add(listener);
|
|
3231
|
+
return () => {
|
|
3232
|
+
listeners.delete(listener);
|
|
3233
|
+
};
|
|
3234
|
+
},
|
|
3235
|
+
setResponse: (responseIdentifier, value) => {
|
|
3236
|
+
if (snapshot.submitted) {
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
const responses = { ...snapshot.responses, [responseIdentifier]: value };
|
|
3240
|
+
emit({
|
|
3241
|
+
...snapshot,
|
|
3242
|
+
responses,
|
|
3243
|
+
responseViolations: violationsOf(responses)
|
|
3244
|
+
});
|
|
3245
|
+
},
|
|
3246
|
+
registerResponseCollector: (responseIdentifier, collector) => {
|
|
3247
|
+
responseCollectors.set(responseIdentifier, collector);
|
|
3248
|
+
return () => {
|
|
3249
|
+
if (responseCollectors.get(responseIdentifier) === collector) {
|
|
3250
|
+
responseCollectors.delete(responseIdentifier);
|
|
3251
|
+
}
|
|
3252
|
+
};
|
|
3253
|
+
},
|
|
3254
|
+
submit: () => {
|
|
3255
|
+
if (snapshot.submitted) {
|
|
3256
|
+
return snapshot.scores;
|
|
3257
|
+
}
|
|
3258
|
+
let collected = snapshot.responses;
|
|
3259
|
+
for (const [responseIdentifier, collector] of responseCollectors) {
|
|
3260
|
+
const value = collector();
|
|
3261
|
+
if (value !== undefined) {
|
|
3262
|
+
collected = { ...collected, [responseIdentifier]: value };
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
if (collected !== snapshot.responses) {
|
|
3266
|
+
snapshot = { ...snapshot, responses: collected, responseViolations: violationsOf(collected) };
|
|
3267
|
+
}
|
|
3268
|
+
if (options?.validateResponses && snapshot.responseViolations.length > 0) {
|
|
3269
|
+
emit(snapshot);
|
|
3270
|
+
return snapshot.scores;
|
|
3271
|
+
}
|
|
3272
|
+
const scores = computeScores(snapshot.responses);
|
|
3273
|
+
const durationSeconds = activeSeconds();
|
|
3274
|
+
const priorOutcomes = options?.adaptive && snapshot.attemptCount > 0 ? snapshot.outcomes : undefined;
|
|
3275
|
+
const rpOutcomes = computeOutcomes(snapshot.responses, durationSeconds, priorOutcomes);
|
|
3276
|
+
const outcomes = options?.responseProcessing ? rpOutcomes : { ...maintainedOutcomes(), ...rpOutcomes };
|
|
3277
|
+
const completionStatus = outcomes["completionStatus"] ?? outcomes["completion_status"];
|
|
3278
|
+
const completed = !options?.adaptive || completionStatus === "completed";
|
|
3279
|
+
let responses = snapshot.responses;
|
|
3280
|
+
if (options?.adaptive && !completed) {
|
|
3281
|
+
responses = { ...responses };
|
|
3282
|
+
for (const declaration of effectiveDeclarations) {
|
|
3283
|
+
if (declaration.baseType === "boolean") {
|
|
3284
|
+
responses = { ...responses, [declaration.identifier]: null };
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
emit({
|
|
3289
|
+
...snapshot,
|
|
3290
|
+
responses,
|
|
3291
|
+
submitted: completed,
|
|
3292
|
+
scores,
|
|
3293
|
+
outcomes,
|
|
3294
|
+
attemptCount: snapshot.attemptCount + 1,
|
|
3295
|
+
durationSeconds
|
|
3296
|
+
});
|
|
3297
|
+
return scores;
|
|
3298
|
+
},
|
|
3299
|
+
suspend: () => {
|
|
3300
|
+
if (runningSinceMs !== null) {
|
|
3301
|
+
activeMs += now() - runningSinceMs;
|
|
3302
|
+
runningSinceMs = null;
|
|
3303
|
+
}
|
|
3304
|
+
},
|
|
3305
|
+
resume: () => {
|
|
3306
|
+
runningSinceMs ??= now();
|
|
3307
|
+
},
|
|
3308
|
+
reset: () => {
|
|
3309
|
+
activeMs = 0;
|
|
3310
|
+
runningSinceMs = now();
|
|
3311
|
+
emit({
|
|
3312
|
+
responses: { ...initialResponses },
|
|
3313
|
+
submitted: false,
|
|
3314
|
+
scores: [],
|
|
3315
|
+
outcomes: maintainedOutcomes(),
|
|
3316
|
+
templateValues: snapshot.templateValues,
|
|
3317
|
+
attemptCount: 0,
|
|
3318
|
+
durationSeconds: null,
|
|
3319
|
+
responseViolations: violationsOf(initialResponses),
|
|
3320
|
+
correctResponses
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
// src/pnp.ts
|
|
3326
|
+
var pnpFeatureFields = {
|
|
3327
|
+
"linguistic-guidance": "linguisticGuidance",
|
|
3328
|
+
"keyword-emphasis": "keywordEmphasis",
|
|
3329
|
+
"keyword-translation": "keywordTranslation",
|
|
3330
|
+
"simplified-language-portions": "simplifiedLanguagePortions",
|
|
3331
|
+
"simplified-graphics": "simplifiedGraphics",
|
|
3332
|
+
"item-translation": "itemTranslation",
|
|
3333
|
+
"sign-language": "signLanguage",
|
|
3334
|
+
encouragement: "encouragement",
|
|
3335
|
+
"additional-testing-time": "additionalTestingTime",
|
|
3336
|
+
"line-reader": "lineReader",
|
|
3337
|
+
"invert-display-polarity": "invertDisplayPolarity",
|
|
3338
|
+
magnification: "magnification",
|
|
3339
|
+
spoken: "spoken",
|
|
3340
|
+
tactile: "tactile",
|
|
3341
|
+
braille: "braille",
|
|
3342
|
+
"answer-masking": "answerMasking",
|
|
3343
|
+
"keyboard-directions": "keyboardDirections",
|
|
3344
|
+
"additional-directions": "additionalDirections",
|
|
3345
|
+
"long-description": "longDescription",
|
|
3346
|
+
captions: "captions",
|
|
3347
|
+
transcript: "transcript",
|
|
3348
|
+
"alternative-text": "alternativeText",
|
|
3349
|
+
"audio-description": "audioDescription",
|
|
3350
|
+
"high-contrast": "highContrast",
|
|
3351
|
+
"input-requirements": "inputRequirements",
|
|
3352
|
+
"language-of-interface": "languageOfInterface",
|
|
3353
|
+
"layout-single-column": "layoutSingleColumn",
|
|
3354
|
+
"text-appearance": "textAppearance",
|
|
3355
|
+
"calculator-on-screen": "calculatorOnScreen",
|
|
3356
|
+
"dictionary-on-screen": "dictionaryOnScreen",
|
|
3357
|
+
"glossary-on-screen": "glossaryOnScreen",
|
|
3358
|
+
"thesaurus-on-screen": "thesaurusOnScreen",
|
|
3359
|
+
"homophone-checker-on-screen": "homophoneCheckerOnScreen",
|
|
3360
|
+
"note-taking-on-screen": "noteTakingOnScreen",
|
|
3361
|
+
"visual-organizer-on-screen": "visualOrganizerOnScreen",
|
|
3362
|
+
"outliner-on-screen": "outlinerOnScreen",
|
|
3363
|
+
"peer-interaction-on-screen": "peerInteractionOnScreen",
|
|
3364
|
+
"spell-checker-on-screen": "spellCheckerOnScreen"
|
|
3365
|
+
};
|
|
3366
|
+
function resolvePnpActivation(pnp) {
|
|
3367
|
+
const prohibited = new Set(pnp?.prohibitSet?.features ?? []);
|
|
3368
|
+
const active = new Set;
|
|
3369
|
+
const optional = new Set;
|
|
3370
|
+
if (!pnp) {
|
|
3371
|
+
return { active, optional, prohibited };
|
|
3372
|
+
}
|
|
3373
|
+
const optedIn = new Set(pnp.activateAsOptionSet?.features ?? []);
|
|
3374
|
+
for (const feature of pnp.activateAtInitializationSet?.features ?? []) {
|
|
3375
|
+
active.add(feature);
|
|
3376
|
+
}
|
|
3377
|
+
for (const [feature, field] of Object.entries(pnpFeatureFields)) {
|
|
3378
|
+
if (pnp[field] === undefined || active.has(feature) || optedIn.has(feature)) {
|
|
3379
|
+
continue;
|
|
3380
|
+
}
|
|
3381
|
+
active.add(feature);
|
|
3382
|
+
}
|
|
3383
|
+
for (const feature of optedIn) {
|
|
3384
|
+
if (!active.has(feature)) {
|
|
3385
|
+
optional.add(feature);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
for (const feature of prohibited) {
|
|
3389
|
+
active.delete(feature);
|
|
3390
|
+
optional.delete(feature);
|
|
3391
|
+
}
|
|
3392
|
+
return { active, optional, prohibited };
|
|
3393
|
+
}
|
|
3394
|
+
function languagesMatch(left, right) {
|
|
3395
|
+
const a3 = left.toLowerCase();
|
|
3396
|
+
const b2 = right.toLowerCase();
|
|
3397
|
+
return a3 === b2 || a3.split("-")[0] === b2.split("-")[0];
|
|
3398
|
+
}
|
|
3399
|
+
function camelCase(name) {
|
|
3400
|
+
return name.replace(/-([a-z])/gu, (_2, letter) => letter.toUpperCase());
|
|
3401
|
+
}
|
|
3402
|
+
function pnpFeaturePreference(pnp, feature) {
|
|
3403
|
+
const field = pnpFeatureFields[feature];
|
|
3404
|
+
if (!pnp || !field) {
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
const value = pnp[field];
|
|
3408
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
3409
|
+
return typeof first === "object" && first !== null ? first : undefined;
|
|
3410
|
+
}
|
|
3411
|
+
function entryMatches(entry, preference) {
|
|
3412
|
+
if (entry.xmlLang !== undefined) {
|
|
3413
|
+
const preferred = preference?.["xmlLang"];
|
|
3414
|
+
if (typeof preferred !== "string" || !languagesMatch(entry.xmlLang, preferred)) {
|
|
3415
|
+
return false;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
for (const [name, value] of Object.entries(entry.dataAttributes ?? {})) {
|
|
3419
|
+
const preferred = preference?.[camelCase(name)];
|
|
3420
|
+
const comparable = typeof preferred === "string" || typeof preferred === "number" || typeof preferred === "boolean" ? `${preferred}` : undefined;
|
|
3421
|
+
if (comparable === undefined || comparable !== value) {
|
|
3422
|
+
return false;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
return true;
|
|
3426
|
+
}
|
|
3427
|
+
function resolveCard(card, pnp) {
|
|
3428
|
+
if (!card.cardEntries) {
|
|
3429
|
+
const cardLang = card.xmlLang ?? card.htmlContent?.xmlLang;
|
|
3430
|
+
return {
|
|
3431
|
+
support: card.support,
|
|
3432
|
+
...cardLang !== undefined ? { xmlLang: cardLang } : {},
|
|
3433
|
+
...card.htmlContent?.content ? { content: card.htmlContent.content } : {},
|
|
3434
|
+
...card.fileHrefs ? { fileHrefs: card.fileHrefs } : {}
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
const preference = pnpFeaturePreference(pnp, card.support);
|
|
3438
|
+
const entry = card.cardEntries.find((candidate) => entryMatches(candidate, preference)) ?? card.cardEntries.find((candidate) => candidate.default === true);
|
|
3439
|
+
if (!entry) {
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
const xmlLang = entry.xmlLang ?? entry.htmlContent?.xmlLang ?? card.xmlLang;
|
|
3443
|
+
return {
|
|
3444
|
+
support: card.support,
|
|
3445
|
+
...xmlLang !== undefined ? { xmlLang } : {},
|
|
3446
|
+
...entry.htmlContent?.content ? { content: entry.htmlContent.content } : {},
|
|
3447
|
+
...entry.fileHrefs ? { fileHrefs: entry.fileHrefs } : {}
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
function resolveCatalogSupports(options) {
|
|
3451
|
+
const activation = resolvePnpActivation(options.pnp);
|
|
3452
|
+
const effective = new Set(activation.active);
|
|
3453
|
+
for (const support of options.activeSupports ?? []) {
|
|
3454
|
+
if (!activation.prohibited.has(support)) {
|
|
3455
|
+
effective.add(support);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
const byCatalogId = new Map;
|
|
3459
|
+
for (const catalog of options.catalogs ?? []) {
|
|
3460
|
+
const resolved = [];
|
|
3461
|
+
for (const card of catalog.cards) {
|
|
3462
|
+
if (!effective.has(card.support)) {
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
const support = resolveCard(card, options.pnp);
|
|
3466
|
+
if (support) {
|
|
3467
|
+
resolved.push(support);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
byCatalogId.set(catalog.id, resolved);
|
|
3471
|
+
}
|
|
3472
|
+
return { activation, byCatalogId };
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// src/test/controller.ts
|
|
3476
|
+
var supportedOutcomeRuleKinds = new Set([
|
|
3477
|
+
"outcomeCondition",
|
|
3478
|
+
"setOutcomeValue",
|
|
3479
|
+
"lookupOutcomeValue",
|
|
3480
|
+
"outcomeProcessingFragment",
|
|
3481
|
+
"exitTest"
|
|
3482
|
+
]);
|
|
3483
|
+
var testExpressionKinds = new Set([
|
|
3484
|
+
...deterministicExpressionKinds,
|
|
3485
|
+
"testVariables",
|
|
3486
|
+
"outcomeMinimum",
|
|
3487
|
+
"outcomeMaximum",
|
|
3488
|
+
"numberCorrect",
|
|
3489
|
+
"numberIncorrect",
|
|
3490
|
+
"numberPresented",
|
|
3491
|
+
"numberResponded",
|
|
3492
|
+
"numberSelected"
|
|
3493
|
+
]);
|
|
3494
|
+
|
|
3495
|
+
class ExitTestSignal extends Error {
|
|
3496
|
+
}
|
|
3497
|
+
function inferBaseType(value) {
|
|
3498
|
+
if (typeof value === "number") {
|
|
3499
|
+
return "float";
|
|
3500
|
+
}
|
|
3501
|
+
if (typeof value === "boolean") {
|
|
3502
|
+
return "boolean";
|
|
3503
|
+
}
|
|
3504
|
+
return;
|
|
3505
|
+
}
|
|
3506
|
+
function liftFlat(value) {
|
|
3507
|
+
if (value === null || value === undefined) {
|
|
3508
|
+
return null;
|
|
3509
|
+
}
|
|
3510
|
+
if (Array.isArray(value)) {
|
|
3511
|
+
return fromFlatValue(value, "multiple", inferBaseType(value[0]));
|
|
3512
|
+
}
|
|
3513
|
+
return fromFlatValue(value, "single", inferBaseType(value));
|
|
3514
|
+
}
|
|
3515
|
+
var specSessionControlDefaults = {
|
|
3516
|
+
maxAttempts: 1,
|
|
3517
|
+
showFeedback: false,
|
|
3518
|
+
allowReview: true,
|
|
3519
|
+
showSolution: false,
|
|
3520
|
+
allowComment: false,
|
|
3521
|
+
allowSkipping: true,
|
|
3522
|
+
validateResponses: false
|
|
3523
|
+
};
|
|
3524
|
+
function definedControl(control) {
|
|
3525
|
+
return control ? Object.fromEntries(Object.entries(control).filter(([, value]) => value !== undefined)) : {};
|
|
3526
|
+
}
|
|
3527
|
+
function seededPick(pool, count, random) {
|
|
3528
|
+
const indices = pool.map((_2, index) => index);
|
|
3529
|
+
for (let i3 = indices.length - 1;i3 > 0; i3 -= 1) {
|
|
3530
|
+
const j2 = Math.floor(random() * (i3 + 1));
|
|
3531
|
+
[indices[i3], indices[j2]] = [indices[j2], indices[i3]];
|
|
3532
|
+
}
|
|
3533
|
+
return indices.slice(0, Math.min(count, indices.length)).sort((a3, b2) => a3 - b2).map((index) => pool[index]);
|
|
3534
|
+
}
|
|
3535
|
+
function applySelection(children, selection, random) {
|
|
3536
|
+
const required = children.filter((child) => child.required === true);
|
|
3537
|
+
const needed = Math.max(0, selection.select - required.length);
|
|
3538
|
+
if (selection.withReplacement === true) {
|
|
3539
|
+
const counts = children.map((child) => child.required === true ? 1 : 0);
|
|
3540
|
+
for (let draw = 0;draw < needed; draw += 1) {
|
|
3541
|
+
const index = Math.floor(random() * children.length);
|
|
3542
|
+
counts[index] = (counts[index] ?? 0) + 1;
|
|
3543
|
+
}
|
|
3544
|
+
return children.flatMap((child, index) => Array.from({ length: counts[index] ?? 0 }, () => child));
|
|
3545
|
+
}
|
|
3546
|
+
const optional = children.filter((child) => child.required !== true);
|
|
3547
|
+
const picked = new Set([...required, ...seededPick(optional, needed, random)]);
|
|
3548
|
+
return children.filter((child) => picked.has(child));
|
|
3549
|
+
}
|
|
3550
|
+
function applyOrdering(units, random) {
|
|
3551
|
+
const result = units.map((unit) => unit.child.fixed === true ? unit : null);
|
|
3552
|
+
const movable = units.filter((unit) => unit.child.fixed !== true);
|
|
3553
|
+
const shuffled = seededPick(movable, movable.length, random);
|
|
3554
|
+
for (let i3 = shuffled.length - 1;i3 > 0; i3 -= 1) {
|
|
3555
|
+
const j2 = Math.floor(random() * (i3 + 1));
|
|
3556
|
+
[shuffled[i3], shuffled[j2]] = [shuffled[j2], shuffled[i3]];
|
|
3557
|
+
}
|
|
3558
|
+
let cursor = 0;
|
|
3559
|
+
return result.map((slot) => slot ?? shuffled[cursor++]);
|
|
3560
|
+
}
|
|
3561
|
+
function mixedUnits(children, random, sections) {
|
|
3562
|
+
return children.flatMap((child) => {
|
|
3563
|
+
if (child.kind !== "assessmentSection" || child.visible !== false || child.keepTogether !== false) {
|
|
3564
|
+
return [{ child, via: [] }];
|
|
3565
|
+
}
|
|
3566
|
+
sections[child.identifier] = {
|
|
3567
|
+
identifier: child.identifier,
|
|
3568
|
+
...child.timeLimits ? { timeLimits: child.timeLimits } : {}
|
|
3569
|
+
};
|
|
3570
|
+
const inner = child.selection ? applySelection(child.children, child.selection, random) : child.children;
|
|
3571
|
+
return mixedUnits(inner, random, sections).map((unit) => ({ child: unit.child, via: [child, ...unit.via] }));
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
function resolveSection(section, partIdentifier, sectionPath, inheritedPreConditions, inheritedControl, random, sections) {
|
|
3575
|
+
const path = [...sectionPath, section.identifier];
|
|
3576
|
+
sections[section.identifier] = {
|
|
3577
|
+
identifier: section.identifier,
|
|
3578
|
+
...section.timeLimits ? { timeLimits: section.timeLimits } : {}
|
|
3579
|
+
};
|
|
3580
|
+
const preConditions = [...inheritedPreConditions, ...section.preConditions ?? []];
|
|
3581
|
+
const control = { ...inheritedControl, ...definedControl(section.itemSessionControl) };
|
|
3582
|
+
let children = section.children;
|
|
3583
|
+
if (section.selection) {
|
|
3584
|
+
children = applySelection(children, section.selection, random);
|
|
3585
|
+
}
|
|
3586
|
+
let units = children.map((child) => ({ child, via: [] }));
|
|
3587
|
+
if (section.ordering?.shuffle) {
|
|
3588
|
+
units = applyOrdering(mixedUnits(children, random, sections), random);
|
|
3589
|
+
}
|
|
3590
|
+
const items = [];
|
|
3591
|
+
for (const { child, via } of units) {
|
|
3592
|
+
const viaPath = [...path, ...via.map((entry) => entry.identifier)];
|
|
3593
|
+
const viaPreConditions = [...preConditions, ...via.flatMap((entry) => entry.preConditions ?? [])];
|
|
3594
|
+
const viaControl = via.reduce((merged, entry) => ({ ...merged, ...definedControl(entry.itemSessionControl) }), control);
|
|
3595
|
+
if (child.kind === "assessmentSection") {
|
|
3596
|
+
items.push(...resolveSection(child, partIdentifier, viaPath, viaPreConditions, viaControl, random, sections));
|
|
3597
|
+
} else {
|
|
3598
|
+
items.push({
|
|
3599
|
+
key: child.identifier,
|
|
3600
|
+
ref: child,
|
|
3601
|
+
partIdentifier,
|
|
3602
|
+
sectionPath: viaPath,
|
|
3603
|
+
preConditions: [...viaPreConditions, ...child.preConditions ?? []],
|
|
3604
|
+
sessionControl: { ...specSessionControlDefaults, ...viaControl, ...definedControl(child.itemSessionControl) },
|
|
3605
|
+
...child.timeLimits ? { timeLimits: child.timeLimits } : {}
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
return items;
|
|
3610
|
+
}
|
|
3611
|
+
function resolvePlan(view, seed) {
|
|
3612
|
+
const random = mulberry32(seed);
|
|
3613
|
+
const sections = {};
|
|
3614
|
+
const parts = view.testParts.map((part) => ({
|
|
3615
|
+
identifier: part.identifier,
|
|
3616
|
+
navigationMode: part.navigationMode,
|
|
3617
|
+
submissionMode: part.submissionMode,
|
|
3618
|
+
...part.timeLimits ? { timeLimits: part.timeLimits } : {},
|
|
3619
|
+
items: part.assessmentSections.flatMap((section) => resolveSection(section, part.identifier, [], [], definedControl(part.itemSessionControl), random, sections))
|
|
3620
|
+
}));
|
|
3621
|
+
const totals = new Map;
|
|
3622
|
+
for (const part of parts) {
|
|
3623
|
+
for (const item of part.items) {
|
|
3624
|
+
totals.set(item.ref.identifier, (totals.get(item.ref.identifier) ?? 0) + 1);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
const ordinals = new Map;
|
|
3628
|
+
const keyedParts = parts.map((part) => ({
|
|
3629
|
+
...part,
|
|
3630
|
+
items: part.items.map((item) => {
|
|
3631
|
+
if ((totals.get(item.ref.identifier) ?? 0) < 2) {
|
|
3632
|
+
return item;
|
|
3633
|
+
}
|
|
3634
|
+
const instance = (ordinals.get(item.ref.identifier) ?? 0) + 1;
|
|
3635
|
+
ordinals.set(item.ref.identifier, instance);
|
|
3636
|
+
return { ...item, key: `${item.ref.identifier}.${instance}`, instance };
|
|
3637
|
+
})
|
|
3638
|
+
}));
|
|
3639
|
+
return {
|
|
3640
|
+
...view.timeLimits ? { timeLimits: view.timeLimits } : {},
|
|
3641
|
+
parts: keyedParts,
|
|
3642
|
+
sections
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
function createTestController(view, options) {
|
|
3646
|
+
const plan = resolvePlan(view, options.seed);
|
|
3647
|
+
const allItems = plan.parts.flatMap((part) => [...part.items]);
|
|
3648
|
+
const partIndexByItemKey = new Map;
|
|
3649
|
+
const itemsByKey = new Map;
|
|
3650
|
+
const instancesByRef = new Map;
|
|
3651
|
+
plan.parts.forEach((part, partIndex) => {
|
|
3652
|
+
for (const item of part.items) {
|
|
3653
|
+
partIndexByItemKey.set(item.key, partIndex);
|
|
3654
|
+
itemsByKey.set(item.key, item);
|
|
3655
|
+
const siblings = instancesByRef.get(item.ref.identifier);
|
|
3656
|
+
if (siblings) {
|
|
3657
|
+
siblings.push(item);
|
|
3658
|
+
} else {
|
|
3659
|
+
instancesByRef.set(item.ref.identifier, [item]);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
});
|
|
3663
|
+
function attemptsOf(state, itemKey) {
|
|
3664
|
+
return (state.attemptCounts ?? {})[itemKey] ?? 0;
|
|
3665
|
+
}
|
|
3666
|
+
function subsetItems(expression) {
|
|
3667
|
+
const asList2 = (value) => typeof value === "string" ? [value] : value;
|
|
3668
|
+
const includeCategory = asList2(expression.includeCategory);
|
|
3669
|
+
const excludeCategory = asList2(expression.excludeCategory);
|
|
3670
|
+
return allItems.filter((item) => {
|
|
3671
|
+
if (expression.sectionIdentifier !== undefined && !item.sectionPath.includes(expression.sectionIdentifier)) {
|
|
3672
|
+
return false;
|
|
3673
|
+
}
|
|
3674
|
+
const categories = item.ref.categories ?? [];
|
|
3675
|
+
if (includeCategory !== undefined && !includeCategory.some((category) => categories.includes(category))) {
|
|
3676
|
+
return false;
|
|
3677
|
+
}
|
|
3678
|
+
return !(excludeCategory !== undefined && excludeCategory.some((category) => categories.includes(category)));
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
function weightOf(item, weightIdentifier) {
|
|
3682
|
+
if (weightIdentifier === undefined) {
|
|
3683
|
+
return 1;
|
|
3684
|
+
}
|
|
3685
|
+
return item.ref.weights?.find((entry) => entry.identifier === weightIdentifier)?.value ?? 1;
|
|
3686
|
+
}
|
|
3687
|
+
function remainingAttempts(state, itemKey) {
|
|
3688
|
+
const item = itemsByKey.get(itemKey);
|
|
3689
|
+
if (!item) {
|
|
3690
|
+
return 0;
|
|
3691
|
+
}
|
|
3692
|
+
const max = item.sessionControl.maxAttempts;
|
|
3693
|
+
return max === 0 ? Number.POSITIVE_INFINITY : Math.max(0, max - attemptsOf(state, itemKey));
|
|
3694
|
+
}
|
|
3695
|
+
const now = options.now ?? Date.now;
|
|
3696
|
+
function touch(state) {
|
|
3697
|
+
if (state.status !== "in-progress") {
|
|
3698
|
+
return state;
|
|
3699
|
+
}
|
|
3700
|
+
const nowMs = now();
|
|
3701
|
+
const timing = state.timing ?? {
|
|
3702
|
+
lastTransitionAtMs: nowMs,
|
|
3703
|
+
testSeconds: 0,
|
|
3704
|
+
partSeconds: {},
|
|
3705
|
+
sectionSeconds: {},
|
|
3706
|
+
itemSeconds: {}
|
|
3707
|
+
};
|
|
3708
|
+
const elapsed = Math.max(0, nowMs - timing.lastTransitionAtMs) / 1000;
|
|
3709
|
+
const bump = (record, key) => ({
|
|
3710
|
+
...record,
|
|
3711
|
+
[key]: (record[key] ?? 0) + elapsed
|
|
3712
|
+
});
|
|
3713
|
+
const item = state.currentItemKey === null ? undefined : itemsByKey.get(state.currentItemKey);
|
|
3714
|
+
return {
|
|
3715
|
+
...state,
|
|
3716
|
+
timing: {
|
|
3717
|
+
lastTransitionAtMs: nowMs,
|
|
3718
|
+
testSeconds: timing.testSeconds + elapsed,
|
|
3719
|
+
partSeconds: item ? bump(timing.partSeconds, item.partIdentifier) : timing.partSeconds,
|
|
3720
|
+
sectionSeconds: item ? item.sectionPath.reduce((record, identifier) => bump(record, identifier), timing.sectionSeconds) : timing.sectionSeconds,
|
|
3721
|
+
itemSeconds: item ? bump(timing.itemSeconds, item.key) : timing.itemSeconds
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3725
|
+
function secondsOf(state, scope) {
|
|
3726
|
+
const timing = state.timing;
|
|
3727
|
+
if (!timing) {
|
|
3728
|
+
return 0;
|
|
3729
|
+
}
|
|
3730
|
+
switch (scope.kind) {
|
|
3731
|
+
case "test":
|
|
3732
|
+
return timing.testSeconds;
|
|
3733
|
+
case "part":
|
|
3734
|
+
return timing.partSeconds[scope.identifier] ?? 0;
|
|
3735
|
+
case "section":
|
|
3736
|
+
return timing.sectionSeconds[scope.identifier] ?? 0;
|
|
3737
|
+
case "item":
|
|
3738
|
+
return timing.itemSeconds[scope.key] ?? 0;
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
function timeLimitsOf(scope) {
|
|
3742
|
+
switch (scope.kind) {
|
|
3743
|
+
case "test":
|
|
3744
|
+
return plan.timeLimits;
|
|
3745
|
+
case "part":
|
|
3746
|
+
return plan.parts.find((part) => part.identifier === scope.identifier)?.timeLimits;
|
|
3747
|
+
case "section":
|
|
3748
|
+
return plan.sections[scope.identifier]?.timeLimits;
|
|
3749
|
+
case "item":
|
|
3750
|
+
return itemsByKey.get(scope.key)?.timeLimits;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
const additionalTestingTime = resolvePnpActivation(options.pnp).active.has("additional-testing-time") ? options.pnp?.additionalTestingTime : undefined;
|
|
3754
|
+
function effectiveMaxTime(scope) {
|
|
3755
|
+
if (additionalTestingTime?.unlimited === true) {
|
|
3756
|
+
return;
|
|
3757
|
+
}
|
|
3758
|
+
const declared = timeLimitsOf(scope)?.maxTime;
|
|
3759
|
+
if (declared === undefined) {
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
if (additionalTestingTime?.timeMultiplier !== undefined) {
|
|
3763
|
+
return declared * additionalTestingTime.timeMultiplier;
|
|
3764
|
+
}
|
|
3765
|
+
if (additionalTestingTime?.fixedMinutes !== undefined && scope.kind === "test") {
|
|
3766
|
+
return declared + additionalTestingTime.fixedMinutes * 60;
|
|
3767
|
+
}
|
|
3768
|
+
return declared;
|
|
3769
|
+
}
|
|
3770
|
+
function scopeExpired(state, scope) {
|
|
3771
|
+
const maxTime = effectiveMaxTime(scope);
|
|
3772
|
+
return maxTime !== undefined && secondsOf(state, scope) > maxTime;
|
|
3773
|
+
}
|
|
3774
|
+
function enclosingScopes(item) {
|
|
3775
|
+
return [
|
|
3776
|
+
{ kind: "item", key: item.key },
|
|
3777
|
+
...[...item.sectionPath].reverse().map((identifier) => ({ kind: "section", identifier })),
|
|
3778
|
+
{ kind: "part", identifier: item.partIdentifier },
|
|
3779
|
+
{ kind: "test" }
|
|
3780
|
+
];
|
|
3781
|
+
}
|
|
3782
|
+
function navigableInTime(state, item) {
|
|
3783
|
+
return !enclosingScopes(item).some((scope) => scopeExpired(state, scope));
|
|
3784
|
+
}
|
|
3785
|
+
function minTimeBlocked(state, from, to) {
|
|
3786
|
+
const itemMin = from.timeLimits?.minTime;
|
|
3787
|
+
if (itemMin !== undefined && secondsOf(state, { kind: "item", key: from.key }) < itemMin) {
|
|
3788
|
+
return true;
|
|
3789
|
+
}
|
|
3790
|
+
const destinationSections = new Set(to?.sectionPath ?? []);
|
|
3791
|
+
return from.sectionPath.some((identifier) => {
|
|
3792
|
+
if (destinationSections.has(identifier)) {
|
|
3793
|
+
return false;
|
|
3794
|
+
}
|
|
3795
|
+
const minTime = plan.sections[identifier]?.timeLimits?.minTime;
|
|
3796
|
+
return minTime !== undefined && secondsOf(state, { kind: "section", identifier }) < minTime;
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3799
|
+
function defaultTestOutcomes() {
|
|
3800
|
+
const outcomes = new Map;
|
|
3801
|
+
for (const declaration of view.outcomeDeclarations ?? []) {
|
|
3802
|
+
if (declaration.defaultValue) {
|
|
3803
|
+
outcomes.set(declaration.identifier, rpValue(declaration.cardinality, declaration.defaultValue.values.map((entry) => coerceScalar(entry.value, declaration.baseType)), declaration.baseType));
|
|
3804
|
+
continue;
|
|
3805
|
+
}
|
|
3806
|
+
outcomes.set(declaration.identifier, isNumericBaseType(declaration.baseType) ? floatValue(0) : null);
|
|
3807
|
+
}
|
|
3808
|
+
return outcomes;
|
|
3809
|
+
}
|
|
3810
|
+
const durationValue = (seconds) => rpValue("single", [seconds], "duration");
|
|
3811
|
+
function makeEnv(state, outcomes) {
|
|
3812
|
+
return {
|
|
3813
|
+
lookupVariable: (identifier) => {
|
|
3814
|
+
if (identifier === "duration") {
|
|
3815
|
+
return state.timing === undefined ? null : durationValue(state.timing.testSeconds);
|
|
3816
|
+
}
|
|
3817
|
+
const dot = identifier.indexOf(".");
|
|
3818
|
+
if (dot !== -1) {
|
|
3819
|
+
let itemKey = identifier.slice(0, dot);
|
|
3820
|
+
let variableName = identifier.slice(dot + 1);
|
|
3821
|
+
const secondDot = identifier.indexOf(".", dot + 1);
|
|
3822
|
+
if (secondDot !== -1 && itemsByKey.has(identifier.slice(0, secondDot))) {
|
|
3823
|
+
itemKey = identifier.slice(0, secondDot);
|
|
3824
|
+
variableName = identifier.slice(secondDot + 1);
|
|
3825
|
+
}
|
|
3826
|
+
const instances = instancesByRef.get(itemKey);
|
|
3827
|
+
if (instances !== undefined && instances.length > 1) {
|
|
3828
|
+
const partIndex = partIndexByItemKey.get(instances[0].key);
|
|
3829
|
+
if (partIndex === undefined || plan.parts[partIndex].submissionMode !== "simultaneous") {
|
|
3830
|
+
return null;
|
|
3831
|
+
}
|
|
3832
|
+
for (let index = instances.length - 1;index >= 0; index -= 1) {
|
|
3833
|
+
const instanceKey = instances[index].key;
|
|
3834
|
+
if (variableName === "duration") {
|
|
3835
|
+
const seconds = state.itemDurationSeconds?.[instanceKey];
|
|
3836
|
+
if (seconds !== undefined) {
|
|
3837
|
+
return durationValue(seconds);
|
|
3838
|
+
}
|
|
3839
|
+
} else if (state.itemOutcomes[instanceKey] !== undefined) {
|
|
3840
|
+
return liftFlat(state.itemOutcomes[instanceKey]?.[variableName] ?? null);
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
return null;
|
|
3844
|
+
}
|
|
3845
|
+
if (variableName === "duration") {
|
|
3846
|
+
if (itemsByKey.has(itemKey)) {
|
|
3847
|
+
const seconds = state.itemDurationSeconds?.[itemKey];
|
|
3848
|
+
return seconds === undefined ? null : durationValue(seconds);
|
|
3849
|
+
}
|
|
3850
|
+
const partSeconds = state.timing?.partSeconds[itemKey];
|
|
3851
|
+
if (plan.parts.some((part) => part.identifier === itemKey)) {
|
|
3852
|
+
return partSeconds === undefined ? null : durationValue(partSeconds);
|
|
3853
|
+
}
|
|
3854
|
+
if (plan.sections[itemKey]) {
|
|
3855
|
+
const seconds = state.timing?.sectionSeconds[itemKey];
|
|
3856
|
+
return seconds === undefined ? null : durationValue(seconds);
|
|
3857
|
+
}
|
|
3858
|
+
return null;
|
|
3859
|
+
}
|
|
3860
|
+
return liftFlat(state.itemOutcomes[itemKey]?.[variableName] ?? null);
|
|
3861
|
+
}
|
|
3862
|
+
if (outcomes?.has(identifier)) {
|
|
3863
|
+
return outcomes.get(identifier) ?? null;
|
|
3864
|
+
}
|
|
3865
|
+
return liftFlat(state.testOutcomes[identifier] ?? null);
|
|
3866
|
+
},
|
|
3867
|
+
responseDeclaration: () => {
|
|
3868
|
+
return;
|
|
3869
|
+
},
|
|
3870
|
+
responseValue: () => null,
|
|
3871
|
+
testVariables: (expression) => {
|
|
3872
|
+
const variableName = expression.variableIdentifier ?? expression.identifier ?? "";
|
|
3873
|
+
const weightIdentifier = expression.weightIdentifier;
|
|
3874
|
+
const members = [];
|
|
3875
|
+
let baseType = expression.baseType;
|
|
3876
|
+
for (const item of subsetItems(expression)) {
|
|
3877
|
+
const value = state.itemOutcomes[item.key]?.[variableName];
|
|
3878
|
+
if (value === undefined || value === null) {
|
|
3879
|
+
continue;
|
|
3880
|
+
}
|
|
3881
|
+
const lifted = liftFlat(value);
|
|
3882
|
+
if (lifted === null) {
|
|
3883
|
+
continue;
|
|
3884
|
+
}
|
|
3885
|
+
if (weightIdentifier !== undefined && isNumericBaseType(lifted.baseType)) {
|
|
3886
|
+
baseType = "float";
|
|
3887
|
+
members.push(...lifted.values.map((entry) => Number(entry) * weightOf(item, weightIdentifier)));
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3890
|
+
baseType ??= lifted.baseType;
|
|
3891
|
+
members.push(...lifted.values);
|
|
3892
|
+
}
|
|
3893
|
+
return members.length === 0 ? null : rpValue("multiple", members, baseType);
|
|
3894
|
+
},
|
|
3895
|
+
testAggregate: (expression) => {
|
|
3896
|
+
const subset = subsetItems(expression);
|
|
3897
|
+
const integer = (value) => ({
|
|
3898
|
+
cardinality: "single",
|
|
3899
|
+
baseType: "integer",
|
|
3900
|
+
values: [value]
|
|
3901
|
+
});
|
|
3902
|
+
const countIn = (list) => {
|
|
3903
|
+
const flagged = new Set(list ?? []);
|
|
3904
|
+
return subset.filter((item) => flagged.has(item.key)).length;
|
|
3905
|
+
};
|
|
3906
|
+
switch (expression.kind) {
|
|
3907
|
+
case "numberSelected":
|
|
3908
|
+
return integer(subset.length);
|
|
3909
|
+
case "numberPresented":
|
|
3910
|
+
return integer(countIn(state.presentedItems));
|
|
3911
|
+
case "numberResponded":
|
|
3912
|
+
return integer(countIn(state.respondedItems));
|
|
3913
|
+
case "numberCorrect":
|
|
3914
|
+
return integer(countIn(state.correctItems));
|
|
3915
|
+
case "numberIncorrect":
|
|
3916
|
+
return integer(countIn(state.incorrectItems));
|
|
3917
|
+
case "outcomeMinimum":
|
|
3918
|
+
case "outcomeMaximum": {
|
|
3919
|
+
const bound = expression.kind === "outcomeMaximum" ? "normalMaximum" : "normalMinimum";
|
|
3920
|
+
const members = [];
|
|
3921
|
+
for (const item of subset) {
|
|
3922
|
+
const declared = options.itemOutcomeDeclarations?.[item.ref.identifier]?.find((entry) => entry.identifier === expression.outcomeIdentifier)?.[bound];
|
|
3923
|
+
if (declared === undefined) {
|
|
3924
|
+
if (expression.kind === "outcomeMaximum") {
|
|
3925
|
+
return null;
|
|
3926
|
+
}
|
|
3927
|
+
continue;
|
|
3928
|
+
}
|
|
3929
|
+
members.push(declared * weightOf(item, expression.weightIdentifier));
|
|
3930
|
+
}
|
|
3931
|
+
return members.length === 0 ? null : rpValue("multiple", members, "float");
|
|
3932
|
+
}
|
|
3933
|
+
default:
|
|
3934
|
+
throw new RpUnsupportedError(expression.kind);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
function conditionPasses(expression, state) {
|
|
3940
|
+
try {
|
|
3941
|
+
return singleBoolean(evaluateExpression(expression, makeEnv(state))) === true;
|
|
3942
|
+
} catch (error) {
|
|
3943
|
+
if (error instanceof RpUnsupportedError) {
|
|
3944
|
+
return true;
|
|
3945
|
+
}
|
|
3946
|
+
throw error;
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
function preConditionsPass(item, state) {
|
|
3950
|
+
return item.preConditions.every((expression) => conditionPasses(expression, state));
|
|
3951
|
+
}
|
|
3952
|
+
function runOutcomeProcessing(state) {
|
|
3953
|
+
let outcomes = defaultTestOutcomes();
|
|
3954
|
+
const env = makeEnv(state, outcomes);
|
|
3955
|
+
function branchTaken(branch) {
|
|
3956
|
+
if (singleBoolean(evaluateExpression(branch.expression, env)) !== true) {
|
|
3957
|
+
return false;
|
|
3958
|
+
}
|
|
3959
|
+
executeRules(branch.rules);
|
|
3960
|
+
return true;
|
|
3961
|
+
}
|
|
3962
|
+
function executeRules(rules) {
|
|
3963
|
+
for (const rule of rules) {
|
|
3964
|
+
if (!supportedOutcomeRuleKinds.has(rule.kind)) {
|
|
3965
|
+
throw new RpUnsupportedError(rule.kind);
|
|
3966
|
+
}
|
|
3967
|
+
if (rule.kind === "exitTest") {
|
|
3968
|
+
throw new ExitTestSignal;
|
|
3969
|
+
}
|
|
3970
|
+
if (rule.kind === "setOutcomeValue") {
|
|
3971
|
+
if (rule.identifier !== undefined && rule.expression !== undefined) {
|
|
3972
|
+
outcomes.set(rule.identifier, evaluateExpression(rule.expression, env));
|
|
3973
|
+
}
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
3976
|
+
if (rule.kind === "outcomeProcessingFragment") {
|
|
3977
|
+
executeRules(rule.rules ?? []);
|
|
3978
|
+
continue;
|
|
3979
|
+
}
|
|
3980
|
+
if (rule.kind === "lookupOutcomeValue") {
|
|
3981
|
+
if (rule.identifier !== undefined && rule.expression !== undefined) {
|
|
3982
|
+
const declaration = (view.outcomeDeclarations ?? []).find((entry) => entry.identifier === rule.identifier);
|
|
3983
|
+
if (!hasLookupTable(declaration)) {
|
|
3984
|
+
throw new RpUnsupportedError("lookupOutcomeValue");
|
|
3985
|
+
}
|
|
3986
|
+
outcomes.set(rule.identifier, lookupTableValue(declaration, evaluateExpression(rule.expression, env)));
|
|
3987
|
+
}
|
|
3988
|
+
continue;
|
|
3989
|
+
}
|
|
3990
|
+
if (rule.outcomeIf && branchTaken(rule.outcomeIf)) {
|
|
3991
|
+
continue;
|
|
3992
|
+
}
|
|
3993
|
+
const elseIfTaken = (rule.outcomeElseIfs ?? []).some((branch) => branchTaken(branch));
|
|
3994
|
+
if (!elseIfTaken && rule.outcomeElse) {
|
|
3995
|
+
executeRules(rule.outcomeElse.rules);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
try {
|
|
4000
|
+
executeRules(view.outcomeProcessing?.rules ?? []);
|
|
4001
|
+
} catch (error) {
|
|
4002
|
+
if (error instanceof RpUnsupportedError) {
|
|
4003
|
+
outcomes = defaultTestOutcomes();
|
|
4004
|
+
} else if (!(error instanceof ExitTestSignal)) {
|
|
4005
|
+
throw error;
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
return Object.fromEntries([...outcomes].map(([identifier, value]) => [identifier, toOutcomeValue(value)]));
|
|
4009
|
+
}
|
|
4010
|
+
function firstNavigable(state, partIndex, itemIndex) {
|
|
4011
|
+
for (let p2 = partIndex;p2 < plan.parts.length; p2 += 1) {
|
|
4012
|
+
const items = plan.parts[p2].items;
|
|
4013
|
+
for (let i3 = p2 === partIndex ? itemIndex : 0;i3 < items.length; i3 += 1) {
|
|
4014
|
+
const item = items[i3];
|
|
4015
|
+
if (preConditionsPass(item, state) && navigableInTime(state, item)) {
|
|
4016
|
+
return item;
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
return null;
|
|
4021
|
+
}
|
|
4022
|
+
function positionOf(itemKey) {
|
|
4023
|
+
const partIndex = partIndexByItemKey.get(itemKey);
|
|
4024
|
+
if (partIndex === undefined) {
|
|
4025
|
+
return null;
|
|
4026
|
+
}
|
|
4027
|
+
const itemIndex = plan.parts[partIndex].items.findIndex((item) => item.key === itemKey);
|
|
4028
|
+
return itemIndex === -1 ? null : { partIndex, itemIndex };
|
|
4029
|
+
}
|
|
4030
|
+
function withFlag(list, itemKey, present) {
|
|
4031
|
+
const existing = list ?? [];
|
|
4032
|
+
if (existing.includes(itemKey) === present) {
|
|
4033
|
+
return existing;
|
|
4034
|
+
}
|
|
4035
|
+
return present ? [...existing, itemKey] : existing.filter((entry) => entry !== itemKey);
|
|
4036
|
+
}
|
|
4037
|
+
function applyResultFlags(state, itemKey, result) {
|
|
4038
|
+
return {
|
|
4039
|
+
...state,
|
|
4040
|
+
respondedItems: withFlag(state.respondedItems, itemKey, result.responded === true),
|
|
4041
|
+
correctItems: withFlag(state.correctItems, itemKey, result.correct === true),
|
|
4042
|
+
incorrectItems: withFlag(state.incorrectItems, itemKey, result.correct === false)
|
|
4043
|
+
};
|
|
4044
|
+
}
|
|
4045
|
+
function markPresented(state, itemKey) {
|
|
4046
|
+
return (state.presentedItems ?? []).includes(itemKey) ? state : { ...state, presentedItems: [...state.presentedItems ?? [], itemKey] };
|
|
4047
|
+
}
|
|
4048
|
+
function withRecordedAttempt(state, itemKey, result, atMs) {
|
|
4049
|
+
const entry = {
|
|
4050
|
+
atMs,
|
|
4051
|
+
outcomes: result.outcomes,
|
|
4052
|
+
...result.responses !== undefined ? { responses: result.responses } : {},
|
|
4053
|
+
...result.durationSeconds !== undefined ? { durationSeconds: result.durationSeconds } : {}
|
|
4054
|
+
};
|
|
4055
|
+
return {
|
|
4056
|
+
...state,
|
|
4057
|
+
attemptHistory: {
|
|
4058
|
+
...state.attemptHistory ?? {},
|
|
4059
|
+
[itemKey]: [...state.attemptHistory?.[itemKey] ?? [], entry]
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
function flushPending(state, partIndex) {
|
|
4064
|
+
const pending = state.pendingItemResults ?? {};
|
|
4065
|
+
const keys = Object.keys(pending).filter((key) => partIndex === null || partIndexByItemKey.get(key) === partIndex);
|
|
4066
|
+
if (keys.length === 0) {
|
|
4067
|
+
return state;
|
|
4068
|
+
}
|
|
4069
|
+
const itemOutcomes = { ...state.itemOutcomes };
|
|
4070
|
+
const attemptCounts = { ...state.attemptCounts ?? {} };
|
|
4071
|
+
const itemDurations = { ...state.itemDurationSeconds ?? {} };
|
|
4072
|
+
const remaining = { ...pending };
|
|
4073
|
+
let flagged = state;
|
|
4074
|
+
for (const key of keys) {
|
|
4075
|
+
const result = pending[key];
|
|
4076
|
+
itemOutcomes[key] = result.outcomes;
|
|
4077
|
+
attemptCounts[key] = (attemptCounts[key] ?? 0) + 1;
|
|
4078
|
+
delete remaining[key];
|
|
4079
|
+
if (result.durationSeconds !== undefined) {
|
|
4080
|
+
itemDurations[key] = result.durationSeconds;
|
|
4081
|
+
}
|
|
4082
|
+
flagged = withRecordedAttempt(applyResultFlags(flagged, key, result), key, result, result.submittedAtMs ?? now());
|
|
4083
|
+
}
|
|
4084
|
+
return {
|
|
4085
|
+
...flagged,
|
|
4086
|
+
itemOutcomes,
|
|
4087
|
+
attemptCounts,
|
|
4088
|
+
itemDurationSeconds: itemDurations,
|
|
4089
|
+
pendingItemResults: remaining
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
function ended(state) {
|
|
4093
|
+
const flushed = flushPending(state, null);
|
|
4094
|
+
return { ...flushed, status: "ended", currentItemKey: null, testOutcomes: runOutcomeProcessing(flushed) };
|
|
4095
|
+
}
|
|
4096
|
+
function evaluateTemplateDefaults(state, item) {
|
|
4097
|
+
const defaults = item.ref.templateDefaults;
|
|
4098
|
+
if (!defaults || defaults.length === 0) {
|
|
4099
|
+
return;
|
|
4100
|
+
}
|
|
4101
|
+
const env = makeEnv(state);
|
|
4102
|
+
const values = {};
|
|
4103
|
+
for (const entry of defaults) {
|
|
4104
|
+
try {
|
|
4105
|
+
values[entry.templateIdentifier] = toOutcomeValue(evaluateExpression(entry.expression, env));
|
|
4106
|
+
} catch (error) {
|
|
4107
|
+
if (!(error instanceof RpUnsupportedError)) {
|
|
4108
|
+
throw error;
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
return values;
|
|
4113
|
+
}
|
|
4114
|
+
function withTemplateDefaults(state, items) {
|
|
4115
|
+
let merged = state.templateDefaultValues ?? {};
|
|
4116
|
+
let changed = false;
|
|
4117
|
+
for (const item of items) {
|
|
4118
|
+
if (merged[item.key] !== undefined) {
|
|
4119
|
+
continue;
|
|
4120
|
+
}
|
|
4121
|
+
const values = evaluateTemplateDefaults(state, item);
|
|
4122
|
+
if (values) {
|
|
4123
|
+
merged = { ...merged, [item.key]: values };
|
|
4124
|
+
changed = true;
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
return changed ? { ...state, templateDefaultValues: merged } : state;
|
|
4128
|
+
}
|
|
4129
|
+
function moveToItem(state, item) {
|
|
4130
|
+
if (item === null) {
|
|
4131
|
+
return ended(state);
|
|
4132
|
+
}
|
|
4133
|
+
const fromPart = state.currentItemKey === null ? undefined : partIndexByItemKey.get(state.currentItemKey);
|
|
4134
|
+
const toPart = partIndexByItemKey.get(item.key);
|
|
4135
|
+
let next = state;
|
|
4136
|
+
if (fromPart !== undefined && toPart !== fromPart) {
|
|
4137
|
+
const flushed = flushPending(state, fromPart);
|
|
4138
|
+
if (flushed !== state) {
|
|
4139
|
+
next = { ...flushed, testOutcomes: runOutcomeProcessing(flushed) };
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
const part = toPart === undefined ? undefined : plan.parts[toPart];
|
|
4143
|
+
if (part) {
|
|
4144
|
+
next = withTemplateDefaults(next, part.navigationMode === "nonlinear" ? part.items : [item]);
|
|
4145
|
+
}
|
|
4146
|
+
return markPresented({ ...next, currentItemKey: item.key }, item.key);
|
|
4147
|
+
}
|
|
4148
|
+
function nextState(state) {
|
|
4149
|
+
if (state.status !== "in-progress" || state.currentItemKey === null) {
|
|
4150
|
+
return state;
|
|
4151
|
+
}
|
|
4152
|
+
const current = positionOf(state.currentItemKey);
|
|
4153
|
+
if (!current) {
|
|
4154
|
+
return ended(state);
|
|
4155
|
+
}
|
|
4156
|
+
const part = plan.parts[current.partIndex];
|
|
4157
|
+
const currentItem = part.items[current.itemIndex];
|
|
4158
|
+
if (part.navigationMode === "linear" && !currentItem.sessionControl.allowSkipping && !state.attemptedItems.includes(currentItem.key)) {
|
|
4159
|
+
return state;
|
|
4160
|
+
}
|
|
4161
|
+
if (part.navigationMode === "linear" && minTimeBlocked(state, currentItem, firstNavigable(state, current.partIndex, current.itemIndex + 1))) {
|
|
4162
|
+
return state;
|
|
4163
|
+
}
|
|
4164
|
+
for (const branchRule of currentItem.ref.branchRules ?? []) {
|
|
4165
|
+
if (!conditionPasses(branchRule.expression, state)) {
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
if (branchRule.target === "EXIT_TEST") {
|
|
4169
|
+
return ended(state);
|
|
4170
|
+
}
|
|
4171
|
+
if (branchRule.target === "EXIT_TESTPART") {
|
|
4172
|
+
return moveToItem(state, firstNavigable(state, current.partIndex + 1, 0));
|
|
4173
|
+
}
|
|
4174
|
+
if (branchRule.target === "EXIT_SECTION") {
|
|
4175
|
+
const items = part.items;
|
|
4176
|
+
const sectionKey = currentItem.sectionPath.join("/");
|
|
4177
|
+
let index = current.itemIndex + 1;
|
|
4178
|
+
while (index < items.length && items[index].sectionPath.join("/") === sectionKey) {
|
|
4179
|
+
index += 1;
|
|
4180
|
+
}
|
|
4181
|
+
return moveToItem(state, firstNavigable(state, current.partIndex, index));
|
|
4182
|
+
}
|
|
4183
|
+
const target = positionOf(branchRule.target) ?? (instancesByRef.get(branchRule.target) ?? []).map((instance) => positionOf(instance.key)).find((position) => position !== null && position.partIndex === current.partIndex && position.itemIndex > current.itemIndex) ?? null;
|
|
4184
|
+
if (target && target.partIndex === current.partIndex) {
|
|
4185
|
+
return moveToItem(state, firstNavigable(state, target.partIndex, target.itemIndex));
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
const destination = firstNavigable(state, current.partIndex, current.itemIndex + 1);
|
|
4189
|
+
if (part.navigationMode === "nonlinear" && (destination === null || partIndexByItemKey.get(destination.key) !== current.partIndex)) {
|
|
4190
|
+
const blocked = part.items.some((item) => !item.sessionControl.allowSkipping && !state.attemptedItems.includes(item.key) && preConditionsPass(item, state) && navigableInTime(state, item));
|
|
4191
|
+
if (blocked) {
|
|
4192
|
+
return state;
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
return moveToItem(state, destination);
|
|
4196
|
+
}
|
|
4197
|
+
function reviewBarred(state, item) {
|
|
4198
|
+
return item.sessionControl.allowReview === false && remainingAttempts(state, item.key) <= 0;
|
|
4199
|
+
}
|
|
4200
|
+
function reviewable(state, itemKey) {
|
|
4201
|
+
const item = itemsByKey.get(itemKey);
|
|
4202
|
+
return state.status === "ended" && item !== undefined && item.sessionControl.allowReview && (state.presentedItems ?? []).includes(itemKey);
|
|
4203
|
+
}
|
|
4204
|
+
function commentable(state, itemKey) {
|
|
4205
|
+
return state.status === "in-progress" && itemsByKey.get(itemKey)?.sessionControl.allowComment === true;
|
|
4206
|
+
}
|
|
4207
|
+
function submitBody(state, itemKey, result) {
|
|
4208
|
+
const partIndex = partIndexByItemKey.get(itemKey);
|
|
4209
|
+
if (partIndex !== undefined && plan.parts[partIndex].submissionMode === "simultaneous") {
|
|
4210
|
+
if (attemptsOf(state, itemKey) > 0) {
|
|
4211
|
+
return state;
|
|
4212
|
+
}
|
|
4213
|
+
return {
|
|
4214
|
+
...state,
|
|
4215
|
+
pendingItemResults: { ...state.pendingItemResults ?? {}, [itemKey]: { ...result, submittedAtMs: now() } },
|
|
4216
|
+
attemptedItems: state.attemptedItems.includes(itemKey) ? state.attemptedItems : [...state.attemptedItems, itemKey]
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
if (result.valid === false && itemsByKey.get(itemKey)?.sessionControl.validateResponses === true) {
|
|
4220
|
+
return state;
|
|
4221
|
+
}
|
|
4222
|
+
if (result.adaptive !== true && remainingAttempts(state, itemKey) <= 0) {
|
|
4223
|
+
return state;
|
|
4224
|
+
}
|
|
4225
|
+
const next = {
|
|
4226
|
+
...withRecordedAttempt(applyResultFlags(state, itemKey, result), itemKey, result, now()),
|
|
4227
|
+
itemOutcomes: { ...state.itemOutcomes, [itemKey]: result.outcomes },
|
|
4228
|
+
attemptedItems: state.attemptedItems.includes(itemKey) ? state.attemptedItems : [...state.attemptedItems, itemKey],
|
|
4229
|
+
attemptCounts: { ...state.attemptCounts ?? {}, [itemKey]: attemptsOf(state, itemKey) + 1 },
|
|
4230
|
+
...result.durationSeconds !== undefined ? { itemDurationSeconds: { ...state.itemDurationSeconds ?? {}, [itemKey]: result.durationSeconds } } : {}
|
|
4231
|
+
};
|
|
4232
|
+
return { ...next, testOutcomes: runOutcomeProcessing(next) };
|
|
4233
|
+
}
|
|
4234
|
+
function applyExpiries(state) {
|
|
4235
|
+
if (state.status !== "in-progress") {
|
|
4236
|
+
return state;
|
|
4237
|
+
}
|
|
4238
|
+
if (scopeExpired(state, { kind: "test" })) {
|
|
4239
|
+
return ended(state);
|
|
4240
|
+
}
|
|
4241
|
+
const currentKey = state.currentItemKey;
|
|
4242
|
+
const item = currentKey === null ? undefined : itemsByKey.get(currentKey);
|
|
4243
|
+
if (!item || navigableInTime(state, item)) {
|
|
4244
|
+
return state;
|
|
4245
|
+
}
|
|
4246
|
+
const position = positionOf(item.key);
|
|
4247
|
+
return position === null ? ended(state) : moveToItem(state, firstNavigable(state, position.partIndex, position.itemIndex + 1));
|
|
4248
|
+
}
|
|
4249
|
+
function withTransition(state, op) {
|
|
4250
|
+
if (state.status !== "in-progress") {
|
|
4251
|
+
return state;
|
|
4252
|
+
}
|
|
4253
|
+
const touched = touch(state);
|
|
4254
|
+
const settled = applyExpiries(touched);
|
|
4255
|
+
if (settled !== touched) {
|
|
4256
|
+
return settled;
|
|
4257
|
+
}
|
|
4258
|
+
const result = op(settled);
|
|
4259
|
+
return result === settled ? state : result;
|
|
4260
|
+
}
|
|
4261
|
+
const issues = [];
|
|
4262
|
+
const seenIssues = new Set;
|
|
4263
|
+
function report(name) {
|
|
4264
|
+
if (!seenIssues.has(name)) {
|
|
4265
|
+
seenIssues.add(name);
|
|
4266
|
+
issues.push({ type: "unsupported-rp", name });
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
function walkOutcomeRules(rules) {
|
|
4270
|
+
for (const rule of rules) {
|
|
4271
|
+
if (!supportedOutcomeRuleKinds.has(rule.kind)) {
|
|
4272
|
+
report(rule.kind);
|
|
4273
|
+
continue;
|
|
4274
|
+
}
|
|
4275
|
+
if (rule.kind === "lookupOutcomeValue") {
|
|
4276
|
+
const declaration = (view.outcomeDeclarations ?? []).find((entry) => entry.identifier === rule.identifier);
|
|
4277
|
+
if (!hasLookupTable(declaration)) {
|
|
4278
|
+
report("lookupOutcomeValue");
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
if (rule.expression) {
|
|
4282
|
+
collectExpressionIssues(rule.expression, testExpressionKinds, report);
|
|
4283
|
+
}
|
|
4284
|
+
if (rule.rules) {
|
|
4285
|
+
walkOutcomeRules(rule.rules);
|
|
4286
|
+
}
|
|
4287
|
+
for (const branch of [rule.outcomeIf, ...rule.outcomeElseIfs ?? []]) {
|
|
4288
|
+
if (branch) {
|
|
4289
|
+
collectExpressionIssues(branch.expression, testExpressionKinds, report);
|
|
4290
|
+
walkOutcomeRules(branch.rules);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
if (rule.outcomeElse) {
|
|
4294
|
+
walkOutcomeRules(rule.outcomeElse.rules);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
walkOutcomeRules(view.outcomeProcessing?.rules ?? []);
|
|
4299
|
+
for (const item of allItems) {
|
|
4300
|
+
for (const expression of item.preConditions) {
|
|
4301
|
+
collectExpressionIssues(expression, testExpressionKinds, report);
|
|
4302
|
+
}
|
|
4303
|
+
for (const branchRule of item.ref.branchRules ?? []) {
|
|
4304
|
+
collectExpressionIssues(branchRule.expression, testExpressionKinds, report);
|
|
4305
|
+
}
|
|
4306
|
+
for (const entry of item.ref.templateDefaults ?? []) {
|
|
4307
|
+
collectExpressionIssues(entry.expression, testExpressionKinds, report);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
return {
|
|
4311
|
+
test: view,
|
|
4312
|
+
plan,
|
|
4313
|
+
issues,
|
|
4314
|
+
start: () => {
|
|
4315
|
+
const initial = {
|
|
4316
|
+
status: "in-progress",
|
|
4317
|
+
currentItemKey: null,
|
|
4318
|
+
itemOutcomes: {},
|
|
4319
|
+
attemptedItems: [],
|
|
4320
|
+
attemptCounts: {},
|
|
4321
|
+
presentedItems: [],
|
|
4322
|
+
respondedItems: [],
|
|
4323
|
+
correctItems: [],
|
|
4324
|
+
incorrectItems: [],
|
|
4325
|
+
pendingItemResults: {},
|
|
4326
|
+
testOutcomes: {},
|
|
4327
|
+
timing: {
|
|
4328
|
+
lastTransitionAtMs: now(),
|
|
4329
|
+
testSeconds: 0,
|
|
4330
|
+
partSeconds: {},
|
|
4331
|
+
sectionSeconds: {},
|
|
4332
|
+
itemSeconds: {}
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
const opened = { ...initial, testOutcomes: runOutcomeProcessing(initial) };
|
|
4336
|
+
return moveToItem(opened, firstNavigable(opened, 0, 0));
|
|
4337
|
+
},
|
|
4338
|
+
currentItem: (state) => state.currentItemKey === null ? null : allItems.find((item) => item.key === state.currentItemKey) ?? null,
|
|
4339
|
+
canMoveTo: (state, itemKey) => {
|
|
4340
|
+
if (state.status !== "in-progress" || state.currentItemKey === null) {
|
|
4341
|
+
return false;
|
|
4342
|
+
}
|
|
4343
|
+
const current = positionOf(state.currentItemKey);
|
|
4344
|
+
const target = positionOf(itemKey);
|
|
4345
|
+
if (!current || !target || target.partIndex !== current.partIndex) {
|
|
4346
|
+
return false;
|
|
4347
|
+
}
|
|
4348
|
+
if (plan.parts[current.partIndex].navigationMode !== "nonlinear") {
|
|
4349
|
+
return false;
|
|
4350
|
+
}
|
|
4351
|
+
const item = plan.parts[target.partIndex].items[target.itemIndex];
|
|
4352
|
+
return preConditionsPass(item, state) && navigableInTime(state, item) && !reviewBarred(state, item);
|
|
4353
|
+
},
|
|
4354
|
+
moveTo: (state, itemKey) => withTransition(state, (settled) => {
|
|
4355
|
+
const current = positionOf(settled.currentItemKey ?? "");
|
|
4356
|
+
const target = positionOf(itemKey);
|
|
4357
|
+
const item = itemsByKey.get(itemKey);
|
|
4358
|
+
if (!current || !target || !item || target.partIndex !== current.partIndex || plan.parts[current.partIndex].navigationMode !== "nonlinear" || !navigableInTime(settled, item) || reviewBarred(settled, item)) {
|
|
4359
|
+
return settled;
|
|
4360
|
+
}
|
|
4361
|
+
return markPresented({ ...settled, currentItemKey: itemKey }, itemKey);
|
|
4362
|
+
}),
|
|
4363
|
+
canNext: (state) => nextState(state) !== state,
|
|
4364
|
+
next: (state) => withTransition(state, nextState),
|
|
4365
|
+
remainingAttempts,
|
|
4366
|
+
canSubmitItem: (state, itemKey) => {
|
|
4367
|
+
if (state.status !== "in-progress" || remainingAttempts(state, itemKey) <= 0) {
|
|
4368
|
+
return false;
|
|
4369
|
+
}
|
|
4370
|
+
const item = itemsByKey.get(itemKey);
|
|
4371
|
+
return item !== undefined && enclosingScopes(item).every((scope) => !scopeExpired(state, scope) || timeLimitsOf(scope)?.allowLateSubmission === true);
|
|
4372
|
+
},
|
|
4373
|
+
submitItem: (state, itemKey, result) => {
|
|
4374
|
+
const item = itemsByKey.get(itemKey);
|
|
4375
|
+
if (state.status !== "in-progress" || !item) {
|
|
4376
|
+
return state;
|
|
4377
|
+
}
|
|
4378
|
+
const touched = touch(state);
|
|
4379
|
+
const barring = enclosingScopes(item).find((scope) => scopeExpired(touched, scope) && timeLimitsOf(scope)?.allowLateSubmission !== true);
|
|
4380
|
+
if (barring) {
|
|
4381
|
+
return applyExpiries({
|
|
4382
|
+
...touched,
|
|
4383
|
+
rejectedSubmissions: [
|
|
4384
|
+
...touched.rejectedSubmissions ?? [],
|
|
4385
|
+
{ itemKey, scope: barring, atTestSeconds: touched.timing?.testSeconds ?? 0 }
|
|
4386
|
+
]
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
const accepted = submitBody(touched, itemKey, result);
|
|
4390
|
+
return accepted === touched ? state : applyExpiries(accepted);
|
|
4391
|
+
},
|
|
4392
|
+
end: (state) => state.status === "ended" ? state : ended(touch(state)),
|
|
4393
|
+
tick: (state) => state.status !== "in-progress" ? state : applyExpiries(touch(state)),
|
|
4394
|
+
suspend: (state) => {
|
|
4395
|
+
if (state.status !== "in-progress") {
|
|
4396
|
+
return state;
|
|
4397
|
+
}
|
|
4398
|
+
const settled = applyExpiries(touch(state));
|
|
4399
|
+
return settled.status === "in-progress" ? { ...settled, status: "suspended" } : settled;
|
|
4400
|
+
},
|
|
4401
|
+
resume: (state) => state.status !== "suspended" ? state : {
|
|
4402
|
+
...state,
|
|
4403
|
+
status: "in-progress",
|
|
4404
|
+
...state.timing ? { timing: { ...state.timing, lastTransitionAtMs: now() } } : {}
|
|
4405
|
+
},
|
|
4406
|
+
canReview: reviewable,
|
|
4407
|
+
review: (state, itemKey) => reviewable(state, itemKey) ? { ...state, currentItemKey: itemKey } : state,
|
|
4408
|
+
canComment: commentable,
|
|
4409
|
+
setItemComment: (state, itemKey, comment) => commentable(state, itemKey) ? { ...state, itemComments: { ...state.itemComments ?? {}, [itemKey]: comment } } : state,
|
|
4410
|
+
visibleTestFeedbacks: (state) => (view.testFeedbacks ?? []).filter((feedback) => {
|
|
4411
|
+
const accessOk = (feedback.access ?? "atEnd") === (state.status === "ended" ? "atEnd" : "during");
|
|
4412
|
+
if (!accessOk) {
|
|
4413
|
+
return false;
|
|
4414
|
+
}
|
|
4415
|
+
const outcome = state.testOutcomes[feedback.outcomeIdentifier] ?? null;
|
|
4416
|
+
const matched = Array.isArray(outcome) ? outcome.map(String).includes(feedback.identifier) : outcome !== null && String(outcome) === feedback.identifier;
|
|
4417
|
+
return matched !== (feedback.showHide === "hide");
|
|
4418
|
+
})
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
// src/test/results.ts
|
|
4422
|
+
function iso(ms) {
|
|
4423
|
+
return new Date(ms).toISOString();
|
|
4424
|
+
}
|
|
4425
|
+
function pnpSupports(pnp) {
|
|
4426
|
+
if (!pnp) {
|
|
4427
|
+
return [];
|
|
4428
|
+
}
|
|
4429
|
+
const activation = resolvePnpActivation(pnp);
|
|
4430
|
+
const names = [...new Set([...activation.active, ...activation.optional, ...activation.prohibited])].sort();
|
|
4431
|
+
return names.map((name) => {
|
|
4432
|
+
if (activation.prohibited.has(name)) {
|
|
4433
|
+
return { name, assignment: "prohibited" };
|
|
4434
|
+
}
|
|
4435
|
+
const preference = pnpFeaturePreference(pnp, name);
|
|
4436
|
+
const xmlLang = typeof preference?.["xmlLang"] === "string" ? preference["xmlLang"] : undefined;
|
|
4437
|
+
let value;
|
|
4438
|
+
if (name === "additional-testing-time") {
|
|
4439
|
+
const time = pnp.additionalTestingTime;
|
|
4440
|
+
value = time?.unlimited === true ? "unlimited" : time?.timeMultiplier !== undefined ? String(time.timeMultiplier) : time?.fixedMinutes !== undefined ? String(time.fixedMinutes) : undefined;
|
|
4441
|
+
}
|
|
4442
|
+
return {
|
|
4443
|
+
name,
|
|
4444
|
+
assignment: "assigned",
|
|
4445
|
+
...value !== undefined ? { value } : {},
|
|
4446
|
+
...xmlLang !== undefined ? { xmlLang } : {}
|
|
4447
|
+
};
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
function valueViews(value) {
|
|
4451
|
+
if (value === null || value === undefined || value === "") {
|
|
4452
|
+
return [];
|
|
4453
|
+
}
|
|
4454
|
+
if (Array.isArray(value)) {
|
|
4455
|
+
return value.filter((member) => member !== null && member !== "").map((member) => ({ value: String(member) }));
|
|
4456
|
+
}
|
|
4457
|
+
if (typeof value === "object") {
|
|
4458
|
+
return Object.entries(value).filter(([, member]) => member !== null && member !== "").map(([fieldIdentifier, member]) => ({ fieldIdentifier, value: String(member) }));
|
|
4459
|
+
}
|
|
4460
|
+
return [{ value: String(value) }];
|
|
4461
|
+
}
|
|
4462
|
+
function inferBaseType2(value, identifier) {
|
|
4463
|
+
const sample = Array.isArray(value) ? value[0] : value;
|
|
4464
|
+
if (typeof sample === "number") {
|
|
4465
|
+
return "float";
|
|
4466
|
+
}
|
|
4467
|
+
if (typeof sample === "boolean") {
|
|
4468
|
+
return "boolean";
|
|
4469
|
+
}
|
|
4470
|
+
if (identifier === "completionStatus" || identifier === "completion_status") {
|
|
4471
|
+
return "identifier";
|
|
4472
|
+
}
|
|
4473
|
+
return typeof sample === "string" ? "string" : undefined;
|
|
4474
|
+
}
|
|
4475
|
+
function outcomeVariablesOf(outcomes, declarations) {
|
|
4476
|
+
return Object.entries(outcomes).map(([identifier, value]) => {
|
|
4477
|
+
const declaration = declarations?.find((entry) => entry.identifier === identifier);
|
|
4478
|
+
const cardinality = declaration?.cardinality ?? (Array.isArray(value) ? "multiple" : "single");
|
|
4479
|
+
const baseType = declaration?.baseType ?? inferBaseType2(value, identifier);
|
|
4480
|
+
return {
|
|
4481
|
+
identifier,
|
|
4482
|
+
cardinality,
|
|
4483
|
+
...baseType !== undefined && cardinality !== "record" ? { baseType } : {},
|
|
4484
|
+
values: valueViews(value)
|
|
4485
|
+
};
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
function durationVariable(identifier, seconds) {
|
|
4489
|
+
return {
|
|
4490
|
+
identifier,
|
|
4491
|
+
cardinality: "single",
|
|
4492
|
+
baseType: "duration",
|
|
4493
|
+
candidateResponse: { values: [{ value: String(seconds) }] }
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
function numAttemptsVariable(count) {
|
|
4497
|
+
return {
|
|
4498
|
+
identifier: "numAttempts",
|
|
4499
|
+
cardinality: "single",
|
|
4500
|
+
baseType: "integer",
|
|
4501
|
+
candidateResponse: { values: [{ value: String(count) }] }
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4504
|
+
function responseVariablesOf(responses, details) {
|
|
4505
|
+
return Object.entries(responses ?? {}).map(([identifier, value]) => {
|
|
4506
|
+
const declaration = details?.responseDeclarations?.find((entry) => entry.identifier === identifier);
|
|
4507
|
+
const cardinality = declaration?.cardinality ?? (Array.isArray(value) ? "multiple" : "single");
|
|
4508
|
+
const baseType = declaration?.baseType;
|
|
4509
|
+
const correct = details?.correctResponses?.[identifier];
|
|
4510
|
+
const correctValues = correct === undefined ? [] : valueViews(correct);
|
|
4511
|
+
return {
|
|
4512
|
+
identifier,
|
|
4513
|
+
cardinality,
|
|
4514
|
+
...baseType !== undefined && cardinality !== "record" ? { baseType } : {},
|
|
4515
|
+
candidateResponse: { values: valueViews(value) },
|
|
4516
|
+
...correctValues.length > 0 ? { correctResponse: { values: correctValues } } : {}
|
|
4517
|
+
};
|
|
4518
|
+
});
|
|
4519
|
+
}
|
|
4520
|
+
function buildAssessmentResult(input) {
|
|
4521
|
+
const { test, plan, state } = input;
|
|
4522
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
4523
|
+
const timing = state.timing;
|
|
4524
|
+
const scopeDurations = timing ? [
|
|
4525
|
+
durationVariable("duration", timing.testSeconds),
|
|
4526
|
+
...plan.parts.filter((part) => timing.partSeconds[part.identifier] !== undefined).map((part) => durationVariable(`${part.identifier}.duration`, timing.partSeconds[part.identifier])),
|
|
4527
|
+
...Object.keys(plan.sections).filter((identifier) => timing.sectionSeconds[identifier] !== undefined).map((identifier) => durationVariable(`${identifier}.duration`, timing.sectionSeconds[identifier]))
|
|
4528
|
+
] : [];
|
|
4529
|
+
const testOutcomes = outcomeVariablesOf(state.testOutcomes, test.outcomeDeclarations);
|
|
4530
|
+
const supports = pnpSupports(input.pnp);
|
|
4531
|
+
const testResult = {
|
|
4532
|
+
identifier: test.identifier,
|
|
4533
|
+
datestamp: iso(nowMs),
|
|
4534
|
+
...scopeDurations.length > 0 ? { responseVariables: scopeDurations } : {},
|
|
4535
|
+
...testOutcomes.length > 0 ? { outcomeVariables: testOutcomes } : {},
|
|
4536
|
+
...supports.length > 0 ? { supports } : {}
|
|
4537
|
+
};
|
|
4538
|
+
const itemResults = [];
|
|
4539
|
+
let sequenceIndex = 0;
|
|
4540
|
+
for (const part of plan.parts) {
|
|
4541
|
+
for (const item of part.items) {
|
|
4542
|
+
sequenceIndex += 1;
|
|
4543
|
+
const details = input.itemDetails?.(item) ?? null;
|
|
4544
|
+
const entries = [];
|
|
4545
|
+
(state.attemptHistory?.[item.key] ?? []).forEach((attempt, index) => {
|
|
4546
|
+
const outcomeVariables = outcomeVariablesOf(attempt.outcomes, details?.outcomeDeclarations);
|
|
4547
|
+
entries.push({
|
|
4548
|
+
identifier: item.key,
|
|
4549
|
+
sequenceIndex,
|
|
4550
|
+
datestamp: iso(attempt.atMs),
|
|
4551
|
+
sessionStatus: "final",
|
|
4552
|
+
responseVariables: [
|
|
4553
|
+
numAttemptsVariable(index + 1),
|
|
4554
|
+
...attempt.durationSeconds !== undefined ? [durationVariable("duration", attempt.durationSeconds)] : [],
|
|
4555
|
+
...responseVariablesOf(attempt.responses, details)
|
|
4556
|
+
],
|
|
4557
|
+
...outcomeVariables.length > 0 ? { outcomeVariables } : {}
|
|
4558
|
+
});
|
|
4559
|
+
});
|
|
4560
|
+
const pending = state.pendingItemResults?.[item.key];
|
|
4561
|
+
if (pending) {
|
|
4562
|
+
entries.push({
|
|
4563
|
+
identifier: item.key,
|
|
4564
|
+
sequenceIndex,
|
|
4565
|
+
datestamp: iso(pending.submittedAtMs ?? nowMs),
|
|
4566
|
+
sessionStatus: "pendingResponseProcessing",
|
|
4567
|
+
responseVariables: [
|
|
4568
|
+
numAttemptsVariable(1),
|
|
4569
|
+
...pending.durationSeconds !== undefined ? [durationVariable("duration", pending.durationSeconds)] : [],
|
|
4570
|
+
...responseVariablesOf(pending.responses, details)
|
|
4571
|
+
]
|
|
4572
|
+
});
|
|
4573
|
+
}
|
|
4574
|
+
if (entries.length === 0) {
|
|
4575
|
+
const itemSeconds = timing?.itemSeconds[item.key];
|
|
4576
|
+
entries.push({
|
|
4577
|
+
identifier: item.key,
|
|
4578
|
+
sequenceIndex,
|
|
4579
|
+
datestamp: iso(nowMs),
|
|
4580
|
+
sessionStatus: "initial",
|
|
4581
|
+
responseVariables: [
|
|
4582
|
+
numAttemptsVariable(0),
|
|
4583
|
+
...itemSeconds !== undefined ? [durationVariable("duration", itemSeconds)] : []
|
|
4584
|
+
]
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
const comment = state.itemComments?.[item.key];
|
|
4588
|
+
if (comment !== undefined) {
|
|
4589
|
+
entries[entries.length - 1] = { ...entries[entries.length - 1], candidateComment: comment };
|
|
4590
|
+
}
|
|
4591
|
+
itemResults.push(...entries);
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
return {
|
|
4595
|
+
assessmentResult: {
|
|
4596
|
+
context: input.context ?? {},
|
|
4597
|
+
testResult,
|
|
4598
|
+
...itemResults.length > 0 ? { itemResults } : {}
|
|
4599
|
+
}
|
|
4600
|
+
};
|
|
4601
|
+
}
|
|
4602
|
+
// src/test/session-store.ts
|
|
4603
|
+
function collectCorrectResponseTargets(rules, into) {
|
|
4604
|
+
for (const rule of rules ?? []) {
|
|
4605
|
+
if (rule.kind === "setCorrectResponse" && rule.identifier !== undefined) {
|
|
4606
|
+
into.add(rule.identifier);
|
|
4607
|
+
}
|
|
4608
|
+
for (const branch of [rule.templateIf, ...rule.templateElseIfs ?? []]) {
|
|
4609
|
+
if (branch) {
|
|
4610
|
+
collectCorrectResponseTargets(branch.rules, into);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
if (rule.templateElse) {
|
|
4614
|
+
collectCorrectResponseTargets(rule.templateElse.rules, into);
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
function scorableIdentifiers(view) {
|
|
4619
|
+
const templated = new Set;
|
|
4620
|
+
collectCorrectResponseTargets(view.templateProcessing?.rules, templated);
|
|
4621
|
+
return new Set(view.responseDeclarations.filter((declaration) => declaration.correctResponse !== undefined || declaration.mapping !== undefined || declaration.areaMapping !== undefined || templated.has(declaration.identifier)).map((declaration) => declaration.identifier));
|
|
4622
|
+
}
|
|
4623
|
+
function hasResponse(value) {
|
|
4624
|
+
if (value === null || value === undefined || value === "") {
|
|
4625
|
+
return false;
|
|
4626
|
+
}
|
|
4627
|
+
if (isResponseRecord(value)) {
|
|
4628
|
+
return Object.values(value).some((member) => member !== null && member !== "");
|
|
4629
|
+
}
|
|
4630
|
+
return !Array.isArray(value) || value.length > 0;
|
|
4631
|
+
}
|
|
4632
|
+
function resultFlags(attempt, scorable) {
|
|
4633
|
+
const relevant = attempt.scores.filter((score) => scorable.has(score.identifier));
|
|
4634
|
+
return {
|
|
4635
|
+
...relevant.length > 0 ? { correct: relevant.every((score) => score.correct) } : {},
|
|
4636
|
+
responded: Object.values(attempt.responses).some(hasResponse)
|
|
4637
|
+
};
|
|
4638
|
+
}
|
|
4639
|
+
function deriveItemSeed(seed, itemKey) {
|
|
4640
|
+
let hash = (2166136261 ^ seed) >>> 0;
|
|
4641
|
+
for (let index = 0;index < itemKey.length; index += 1) {
|
|
4642
|
+
hash = Math.imul(hash ^ itemKey.charCodeAt(index), 16777619) >>> 0;
|
|
4643
|
+
}
|
|
4644
|
+
return hash;
|
|
4645
|
+
}
|
|
4646
|
+
function createTestSessionStore(controller, options) {
|
|
4647
|
+
const listeners = new Set;
|
|
4648
|
+
const planItemsByKey = new Map;
|
|
4649
|
+
for (const part of controller.plan.parts) {
|
|
4650
|
+
for (const item of part.items) {
|
|
4651
|
+
planItemsByKey.set(item.key, item);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
const itemViews = new Map;
|
|
4655
|
+
const itemStores = new Map;
|
|
4656
|
+
const forwardedAttempts = new Map;
|
|
4657
|
+
let state = options.initialState ?? controller.start();
|
|
4658
|
+
let snapshot = buildSnapshot();
|
|
4659
|
+
function buildSnapshot() {
|
|
4660
|
+
const currentItem = controller.currentItem(state);
|
|
4661
|
+
return {
|
|
4662
|
+
state,
|
|
4663
|
+
currentItem,
|
|
4664
|
+
currentItemView: currentItem === null ? null : itemView(currentItem.key),
|
|
4665
|
+
visibleFeedbacks: controller.visibleTestFeedbacks(state)
|
|
4666
|
+
};
|
|
4667
|
+
}
|
|
4668
|
+
function activeKeyOf(sessionState) {
|
|
4669
|
+
return sessionState.status === "in-progress" ? sessionState.currentItemKey : null;
|
|
4670
|
+
}
|
|
4671
|
+
function emit(next) {
|
|
4672
|
+
const previousActive = activeKeyOf(state);
|
|
4673
|
+
state = next;
|
|
4674
|
+
snapshot = buildSnapshot();
|
|
4675
|
+
const nextActive = activeKeyOf(next);
|
|
4676
|
+
if (previousActive !== nextActive) {
|
|
4677
|
+
if (previousActive !== null) {
|
|
4678
|
+
itemStores.get(previousActive)?.suspend();
|
|
4679
|
+
}
|
|
4680
|
+
if (nextActive !== null) {
|
|
4681
|
+
itemStores.get(nextActive)?.resume();
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
for (const listener of listeners) {
|
|
4685
|
+
listener();
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
function itemView(itemKey) {
|
|
4689
|
+
if (!itemViews.has(itemKey)) {
|
|
4690
|
+
const planItem = planItemsByKey.get(itemKey);
|
|
4691
|
+
itemViews.set(itemKey, planItem ? options.resolveItem(planItem.ref) : null);
|
|
4692
|
+
}
|
|
4693
|
+
return itemViews.get(itemKey) ?? null;
|
|
4694
|
+
}
|
|
4695
|
+
function itemStore(itemKey) {
|
|
4696
|
+
if (itemStores.has(itemKey)) {
|
|
4697
|
+
return itemStores.get(itemKey) ?? null;
|
|
4698
|
+
}
|
|
4699
|
+
const view = itemView(itemKey);
|
|
4700
|
+
const planItem = planItemsByKey.get(itemKey);
|
|
4701
|
+
if (!view || !planItem) {
|
|
4702
|
+
itemStores.set(itemKey, null);
|
|
4703
|
+
return null;
|
|
4704
|
+
}
|
|
4705
|
+
const individual = controller.plan.parts.some((part) => part.identifier === planItem.partIdentifier && part.submissionMode === "individual");
|
|
4706
|
+
const store = createAttemptStore(view.responseDeclarations, {}, {
|
|
4707
|
+
constraints: collectInteractionConstraints(view.itemBody.content),
|
|
4708
|
+
validateResponses: planItem.sessionControl.validateResponses && individual,
|
|
4709
|
+
outcomeDeclarations: view.outcomeDeclarations,
|
|
4710
|
+
responseProcessing: view.responseProcessing,
|
|
4711
|
+
templateDeclarations: view.templateDeclarations,
|
|
4712
|
+
templateProcessing: view.templateProcessing,
|
|
4713
|
+
adaptive: view.adaptive,
|
|
4714
|
+
seed: deriveItemSeed(options.seed, itemKey),
|
|
4715
|
+
normalization: options.normalization,
|
|
4716
|
+
customOperators: options.customOperators,
|
|
4717
|
+
templateDefaultValues: state.templateDefaultValues?.[itemKey],
|
|
4718
|
+
now: options.now
|
|
4719
|
+
});
|
|
4720
|
+
if (activeKeyOf(state) !== itemKey) {
|
|
4721
|
+
store.suspend();
|
|
4722
|
+
}
|
|
4723
|
+
const scorable = scorableIdentifiers(view);
|
|
4724
|
+
store.subscribe(() => {
|
|
4725
|
+
const attempt = store.getSnapshot();
|
|
4726
|
+
if (attempt.submitted && forwardedAttempts.get(itemKey) !== attempt) {
|
|
4727
|
+
forwardedAttempts.set(itemKey, attempt);
|
|
4728
|
+
const next = controller.submitItem(state, itemKey, {
|
|
4729
|
+
outcomes: attempt.outcomes,
|
|
4730
|
+
...resultFlags(attempt, scorable),
|
|
4731
|
+
...view.adaptive === true ? { adaptive: true } : {},
|
|
4732
|
+
...attempt.durationSeconds !== null ? { durationSeconds: attempt.durationSeconds } : {},
|
|
4733
|
+
valid: attempt.responseViolations.length === 0,
|
|
4734
|
+
responses: attempt.responses
|
|
4735
|
+
});
|
|
4736
|
+
if (next !== state) {
|
|
4737
|
+
emit(next);
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
});
|
|
4741
|
+
itemStores.set(itemKey, store);
|
|
4742
|
+
return store;
|
|
4743
|
+
}
|
|
4744
|
+
return {
|
|
4745
|
+
controller,
|
|
4746
|
+
subscribe: (listener) => {
|
|
4747
|
+
listeners.add(listener);
|
|
4748
|
+
return () => listeners.delete(listener);
|
|
4749
|
+
},
|
|
4750
|
+
getSnapshot: () => snapshot,
|
|
4751
|
+
itemStore,
|
|
4752
|
+
itemView,
|
|
4753
|
+
next: () => emit(controller.next(state)),
|
|
4754
|
+
canMoveTo: (itemKey) => controller.canMoveTo(state, itemKey),
|
|
4755
|
+
moveTo: (itemKey) => emit(controller.moveTo(state, itemKey)),
|
|
4756
|
+
end: () => emit(controller.end(state)),
|
|
4757
|
+
tick: () => emit(controller.tick(state)),
|
|
4758
|
+
review: (itemKey) => emit(controller.review(state, itemKey)),
|
|
4759
|
+
setItemComment: (itemKey, comment) => emit(controller.setItemComment(state, itemKey, comment)),
|
|
4760
|
+
suspend: () => emit(controller.suspend(state)),
|
|
4761
|
+
resume: () => emit(controller.resume(state)),
|
|
4762
|
+
assessmentResult: (resultOptions) => buildAssessmentResult({
|
|
4763
|
+
test: controller.test,
|
|
4764
|
+
plan: controller.plan,
|
|
4765
|
+
state,
|
|
4766
|
+
...resultOptions?.context !== undefined ? { context: resultOptions.context } : {},
|
|
4767
|
+
nowMs: resultOptions?.nowMs ?? (options.now ?? Date.now)(),
|
|
4768
|
+
...options.pnp !== undefined ? { pnp: options.pnp } : {},
|
|
4769
|
+
itemDetails: (item) => {
|
|
4770
|
+
const view = itemView(item.key);
|
|
4771
|
+
if (!view) {
|
|
4772
|
+
return null;
|
|
4773
|
+
}
|
|
4774
|
+
return {
|
|
4775
|
+
responseDeclarations: view.responseDeclarations,
|
|
4776
|
+
...view.outcomeDeclarations !== undefined ? { outcomeDeclarations: view.outcomeDeclarations } : {},
|
|
4777
|
+
correctResponses: itemStore(item.key)?.getSnapshot().correctResponses ?? {}
|
|
4778
|
+
};
|
|
4779
|
+
}
|
|
4780
|
+
})
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
export {
|
|
4784
|
+
stimulusContentFromNormalized,
|
|
4785
|
+
scoreResponse,
|
|
4786
|
+
resolveTemplate,
|
|
4787
|
+
reportItemCapability,
|
|
4788
|
+
referenceInteractionKinds,
|
|
4789
|
+
mulberry32,
|
|
4790
|
+
matchCorrect,
|
|
4791
|
+
mapResponsePoint,
|
|
4792
|
+
mapResponse,
|
|
4793
|
+
foldString,
|
|
4794
|
+
executeTemplateProcessing,
|
|
4795
|
+
executeResponseProcessing,
|
|
4796
|
+
effectiveItemScore,
|
|
4797
|
+
createTestController,
|
|
4798
|
+
createAttemptStore,
|
|
4799
|
+
collectTemplateIssues,
|
|
4800
|
+
collectRpIssues,
|
|
4801
|
+
assessmentTestViewFromNormalized,
|
|
4802
|
+
assessmentItemViewFromNormalized,
|
|
4803
|
+
applyCorrectResponseOverrides
|
|
4804
|
+
};
|