@case-framework/survey-core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -244
- package/build/answer-types-DH0gv0Ea.d.mts +20 -0
- package/build/answer-types-DH0gv0Ea.d.mts.map +1 -0
- package/build/answer-types-Dtm6MqZQ.mjs +19 -0
- package/build/answer-types-Dtm6MqZQ.mjs.map +1 -0
- package/build/editor/colors.d.mts +11 -2
- package/build/editor/colors.d.mts.map +1 -0
- package/build/editor.d.mts +72913 -266
- package/build/editor.d.mts.map +1 -1
- package/build/editor.mjs +1268 -1001
- package/build/editor.mjs.map +1 -1
- package/build/index.d.mts +2 -3
- package/build/index.mjs +1620 -824
- package/build/index.mjs.map +1 -1
- package/build/keyed-object-D1yh_W5v.mjs +45 -0
- package/build/keyed-object-D1yh_W5v.mjs.map +1 -0
- package/build/localization-1lzNpyFX.mjs +1373 -0
- package/build/localization-1lzNpyFX.mjs.map +1 -0
- package/build/package.json +9 -3
- package/build/projection-DrLPFroE.mjs +2344 -0
- package/build/projection-DrLPFroE.mjs.map +1 -0
- package/build/registry-BOXcyWfX.d.mts +54292 -0
- package/build/registry-BOXcyWfX.d.mts.map +1 -0
- package/build/schema.d.mts +3 -0
- package/build/schema.mjs +4 -0
- package/package.json +9 -3
- package/build/index-SDbZ5WQM.d.mts +0 -2208
- package/build/index-SDbZ5WQM.d.mts.map +0 -1
- package/build/item-colors-DgoJeSCQ.d.mts +0 -12
- package/build/item-colors-DgoJeSCQ.d.mts.map +0 -1
- package/build/projection-Bl6qCinx.mjs +0 -3930
- package/build/projection-Bl6qCinx.mjs.map +0 -1
|
@@ -1,3930 +0,0 @@
|
|
|
1
|
-
import { format } from "date-fns/format";
|
|
2
|
-
//#region src/calendar-date.ts
|
|
3
|
-
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
4
|
-
const daysInMonth = (year, month) => month === 2 ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28 : [
|
|
5
|
-
4,
|
|
6
|
-
6,
|
|
7
|
-
9,
|
|
8
|
-
11
|
|
9
|
-
].includes(month) ? 30 : 31;
|
|
10
|
-
function isCalendarDate(value) {
|
|
11
|
-
if (typeof value !== "string" || !/^\d{4}(?:-\d{2}(?:-\d{2})?)?$/.test(value)) return false;
|
|
12
|
-
const [year, month, day] = value.split("-").map(Number);
|
|
13
|
-
return year >= 1 && year <= 9999 && (month === void 0 || month >= 1 && month <= 12 && (day === void 0 || day >= 1 && day <= daysInMonth(year, month)));
|
|
14
|
-
}
|
|
15
|
-
function calendarDatePrecision(value) {
|
|
16
|
-
if (!isCalendarDate(value)) throw new Error(`Invalid calendar date: ${value}`);
|
|
17
|
-
return value.length === 4 ? "year" : value.length === 7 ? "year-month" : "year-month-day";
|
|
18
|
-
}
|
|
19
|
-
function calendarDateFromLocalDate(date, precision = "year-month-day") {
|
|
20
|
-
const value = [
|
|
21
|
-
pad(date.getFullYear(), 4),
|
|
22
|
-
pad(date.getMonth() + 1),
|
|
23
|
-
pad(date.getDate())
|
|
24
|
-
].slice(0, precision === "year" ? 1 : precision === "year-month" ? 2 : 3).join("-");
|
|
25
|
-
if (!isCalendarDate(value)) throw new Error("Invalid calendar date");
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
|
-
/** Adapter for calendar widgets only. Never serialize this Date as an instant. */
|
|
29
|
-
function calendarDateToLocalDate(value) {
|
|
30
|
-
calendarDatePrecision(value);
|
|
31
|
-
const [year, month = 1, day = 1] = value.split("-").map(Number);
|
|
32
|
-
const date = /* @__PURE__ */ new Date(0);
|
|
33
|
-
date.setFullYear(year, month - 1, day);
|
|
34
|
-
date.setHours(12, 0, 0, 0);
|
|
35
|
-
return date;
|
|
36
|
-
}
|
|
37
|
-
function calendarDateBounds(value) {
|
|
38
|
-
calendarDatePrecision(value);
|
|
39
|
-
const [year, month] = value.split("-").map(Number);
|
|
40
|
-
if (value.length === 4) return [`${value}-01-01`, `${value}-12-31`];
|
|
41
|
-
if (value.length === 7) return [`${value}-01`, `${value}-${pad(daysInMonth(year, month))}`];
|
|
42
|
-
return [value, value];
|
|
43
|
-
}
|
|
44
|
-
/** Ordering requires equal precision; callers must explicitly choose interval semantics otherwise. */
|
|
45
|
-
function compareCalendarDates(a, b) {
|
|
46
|
-
if (!isCalendarDate(a) || !isCalendarDate(b) || a.length !== b.length) return void 0;
|
|
47
|
-
return a === b ? 0 : a < b ? -1 : 1;
|
|
48
|
-
}
|
|
49
|
-
function utcDate(value) {
|
|
50
|
-
const [year, month = 1, day = 1] = value.split("-").map(Number);
|
|
51
|
-
const date = /* @__PURE__ */ new Date(0);
|
|
52
|
-
date.setUTCFullYear(year, month - 1, day);
|
|
53
|
-
return date;
|
|
54
|
-
}
|
|
55
|
-
function addCalendarDate(value, amount, unit) {
|
|
56
|
-
if (!isCalendarDate(value) || !Number.isSafeInteger(amount)) return void 0;
|
|
57
|
-
if (amount === 0) return value;
|
|
58
|
-
if (unit === "days" && value.length !== 10 || unit === "months" && value.length < 7) return void 0;
|
|
59
|
-
const [year, month = 1, day = 1] = value.split("-").map(Number);
|
|
60
|
-
let result;
|
|
61
|
-
if (unit === "days") {
|
|
62
|
-
const date = utcDate(value);
|
|
63
|
-
date.setUTCDate(date.getUTCDate() + amount);
|
|
64
|
-
result = `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
|
65
|
-
} else {
|
|
66
|
-
const index = (year - 1) * 12 + month - 1 + amount * (unit === "years" ? 12 : 1);
|
|
67
|
-
const nextYear = Math.floor(index / 12) + 1;
|
|
68
|
-
const nextMonth = (index % 12 + 12) % 12 + 1;
|
|
69
|
-
result = `${pad(nextYear, 4)}-${pad(nextMonth)}-${pad(Math.min(day, daysInMonth(nextYear, nextMonth)))}`.slice(0, value.length);
|
|
70
|
-
}
|
|
71
|
-
return isCalendarDate(result) ? result : void 0;
|
|
72
|
-
}
|
|
73
|
-
/** Signed difference a - b in the value's precision unit (days, months, or years). */
|
|
74
|
-
function differenceCalendarDates(a, b) {
|
|
75
|
-
if (compareCalendarDates(a, b) === void 0) return void 0;
|
|
76
|
-
if (a.length === 10) return (utcDate(a).getTime() - utcDate(b).getTime()) / 864e5;
|
|
77
|
-
const [ay, am = 1] = a.split("-").map(Number);
|
|
78
|
-
const [by, bm = 1] = b.split("-").map(Number);
|
|
79
|
-
return a.length === 4 ? ay - by : (ay - by) * 12 + am - bm;
|
|
80
|
-
}
|
|
81
|
-
var CalendarFormattingDate = class extends Date {
|
|
82
|
-
getTimezoneOffset() {
|
|
83
|
-
return 0;
|
|
84
|
-
}
|
|
85
|
-
getFullYear() {
|
|
86
|
-
return this.getUTCFullYear();
|
|
87
|
-
}
|
|
88
|
-
getMonth() {
|
|
89
|
-
return this.getUTCMonth();
|
|
90
|
-
}
|
|
91
|
-
getDate() {
|
|
92
|
-
return this.getUTCDate();
|
|
93
|
-
}
|
|
94
|
-
getDay() {
|
|
95
|
-
return this.getUTCDay();
|
|
96
|
-
}
|
|
97
|
-
getHours() {
|
|
98
|
-
return this.getUTCHours();
|
|
99
|
-
}
|
|
100
|
-
getMinutes() {
|
|
101
|
-
return this.getUTCMinutes();
|
|
102
|
-
}
|
|
103
|
-
getSeconds() {
|
|
104
|
-
return this.getUTCSeconds();
|
|
105
|
-
}
|
|
106
|
-
getMilliseconds() {
|
|
107
|
-
return this.getUTCMilliseconds();
|
|
108
|
-
}
|
|
109
|
-
setFullYear(...args) {
|
|
110
|
-
return this.setUTCFullYear(...args);
|
|
111
|
-
}
|
|
112
|
-
setMonth(...args) {
|
|
113
|
-
return this.setUTCMonth(...args);
|
|
114
|
-
}
|
|
115
|
-
setDate(...args) {
|
|
116
|
-
return this.setUTCDate(...args);
|
|
117
|
-
}
|
|
118
|
-
setHours(...args) {
|
|
119
|
-
return this.setUTCHours(...args);
|
|
120
|
-
}
|
|
121
|
-
setMinutes(...args) {
|
|
122
|
-
return this.setUTCMinutes(...args);
|
|
123
|
-
}
|
|
124
|
-
setSeconds(...args) {
|
|
125
|
-
return this.setUTCSeconds(...args);
|
|
126
|
-
}
|
|
127
|
-
setMilliseconds(...args) {
|
|
128
|
-
return this.setUTCMilliseconds(...args);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
/** Partial dates retain their precision even if a display pattern requests absent components. */
|
|
132
|
-
function formatCalendarDate(value, pattern) {
|
|
133
|
-
calendarDatePrecision(value);
|
|
134
|
-
if (value.length !== 10) return value;
|
|
135
|
-
return format(new CalendarFormattingDate(utcDate(value).getTime()), pattern);
|
|
136
|
-
}
|
|
137
|
-
//#endregion
|
|
138
|
-
//#region src/survey/responses/value-types.ts
|
|
139
|
-
const ValueType = {
|
|
140
|
-
string: "string",
|
|
141
|
-
duration: "duration",
|
|
142
|
-
reference: "reference",
|
|
143
|
-
number: "number",
|
|
144
|
-
boolean: "boolean",
|
|
145
|
-
date: "date",
|
|
146
|
-
stringArray: "string[]",
|
|
147
|
-
durationArray: "duration[]",
|
|
148
|
-
referenceArray: "reference[]",
|
|
149
|
-
numberArray: "number[]",
|
|
150
|
-
dateArray: "date[]"
|
|
151
|
-
};
|
|
152
|
-
const DurationUnits = {
|
|
153
|
-
seconds: "seconds",
|
|
154
|
-
minutes: "minutes",
|
|
155
|
-
hours: "hours",
|
|
156
|
-
days: "days",
|
|
157
|
-
weeks: "weeks",
|
|
158
|
-
months: "months",
|
|
159
|
-
years: "years"
|
|
160
|
-
};
|
|
161
|
-
const NumberPrecision = {
|
|
162
|
-
int: "int",
|
|
163
|
-
float: "float"
|
|
164
|
-
};
|
|
165
|
-
function isPlainObject(value) {
|
|
166
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
167
|
-
}
|
|
168
|
-
function isFiniteNumber(value) {
|
|
169
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
170
|
-
}
|
|
171
|
-
function isStringArray(value) {
|
|
172
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
173
|
-
}
|
|
174
|
-
function isNumberArray(value) {
|
|
175
|
-
return Array.isArray(value) && value.every((item) => isFiniteNumber(item));
|
|
176
|
-
}
|
|
177
|
-
function isOptionalNumberPrecision(value) {
|
|
178
|
-
return value === void 0 || value === NumberPrecision.int || value === NumberPrecision.float;
|
|
179
|
-
}
|
|
180
|
-
function isDurationUnit(value) {
|
|
181
|
-
return typeof value === "string" && Object.values(DurationUnits).includes(value);
|
|
182
|
-
}
|
|
183
|
-
function isResponseValue(value) {
|
|
184
|
-
if (!isPlainObject(value) || typeof value.type !== "string") return false;
|
|
185
|
-
switch (value.type) {
|
|
186
|
-
case ValueType.string:
|
|
187
|
-
case ValueType.reference: return typeof value.value === "string";
|
|
188
|
-
case ValueType.number: return isFiniteNumber(value.value) && isOptionalNumberPrecision(value.precision);
|
|
189
|
-
case ValueType.boolean: return typeof value.value === "boolean";
|
|
190
|
-
case ValueType.date: return isCalendarDate(value.value);
|
|
191
|
-
case ValueType.duration: return isFiniteNumber(value.value) && isDurationUnit(value.unit) && isOptionalNumberPrecision(value.precision);
|
|
192
|
-
case ValueType.stringArray:
|
|
193
|
-
case ValueType.referenceArray: return isStringArray(value.value);
|
|
194
|
-
case ValueType.dateArray: return Array.isArray(value.value) && value.value.every(isCalendarDate);
|
|
195
|
-
case ValueType.numberArray: return isNumberArray(value.value) && (value.type !== ValueType.numberArray || isOptionalNumberPrecision(value.precision));
|
|
196
|
-
case ValueType.durationArray: return isNumberArray(value.value) && isDurationUnit(value.unit) && isOptionalNumberPrecision(value.precision);
|
|
197
|
-
default: return false;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function assertResponseValue(value, path) {
|
|
201
|
-
if (!isResponseValue(value)) throw new Error(`Invalid response value at '${path}'`);
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region src/survey/responses/item-response.ts
|
|
205
|
-
/**
|
|
206
|
-
* SurveyItemResponse to store the response of a survey item.
|
|
207
|
-
*/
|
|
208
|
-
var SurveyItemResponse = class SurveyItemResponse {
|
|
209
|
-
itemId;
|
|
210
|
-
meta;
|
|
211
|
-
response;
|
|
212
|
-
itemType;
|
|
213
|
-
constructor(itemDef, response) {
|
|
214
|
-
this.itemId = itemDef.itemId;
|
|
215
|
-
this.response = response;
|
|
216
|
-
this.itemType = itemDef.itemType;
|
|
217
|
-
}
|
|
218
|
-
serialize() {
|
|
219
|
-
return {
|
|
220
|
-
itemId: this.itemId,
|
|
221
|
-
itemType: this.itemType,
|
|
222
|
-
meta: this.meta,
|
|
223
|
-
response: this.response?.serialize()
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
static deserialize(json) {
|
|
227
|
-
if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid survey item response payload");
|
|
228
|
-
if (typeof json.itemId !== "string" || json.itemId.trim().length === 0) throw new Error("Survey item response is missing a valid itemId");
|
|
229
|
-
if (typeof json.itemType !== "string" || json.itemType.trim().length === 0) throw new Error("Survey item response is missing a valid itemType");
|
|
230
|
-
const itemDef = {
|
|
231
|
-
itemId: json.itemId,
|
|
232
|
-
itemType: json.itemType
|
|
233
|
-
};
|
|
234
|
-
const response = json.response ? ResponseItem.deserialize(json.response) : void 0;
|
|
235
|
-
const newResponse = new SurveyItemResponse(itemDef, response);
|
|
236
|
-
newResponse.meta = json.meta;
|
|
237
|
-
return newResponse;
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
var ResponseItem = class ResponseItem {
|
|
241
|
-
_slots;
|
|
242
|
-
constructor(initialValues) {
|
|
243
|
-
this._slots = new Map(initialValues ?? []);
|
|
244
|
-
}
|
|
245
|
-
get(slotId) {
|
|
246
|
-
return this._slots.get(slotId);
|
|
247
|
-
}
|
|
248
|
-
setSlotValue(slotId, slotResponse) {
|
|
249
|
-
if (slotResponse) this._slots.set(slotId, slotResponse);
|
|
250
|
-
else this._slots.delete(slotId);
|
|
251
|
-
}
|
|
252
|
-
serialize() {
|
|
253
|
-
if (this._slots.size === 0) return;
|
|
254
|
-
return { slots: Object.fromEntries(this._slots.entries()) };
|
|
255
|
-
}
|
|
256
|
-
isEmpty() {
|
|
257
|
-
return this._slots.size === 0;
|
|
258
|
-
}
|
|
259
|
-
clone() {
|
|
260
|
-
return new ResponseItem(Array.from(this._slots.entries()));
|
|
261
|
-
}
|
|
262
|
-
static deserialize(json) {
|
|
263
|
-
if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid response item payload");
|
|
264
|
-
const { slots } = json;
|
|
265
|
-
if (!slots || typeof slots !== "object" || Array.isArray(slots)) throw new Error("Invalid response slots payload");
|
|
266
|
-
const slotValues = Object.entries(slots).map(([slotId, slotResponse]) => {
|
|
267
|
-
if (slotId.trim().length === 0) throw new Error("Response slot id cannot be empty");
|
|
268
|
-
assertResponseValue(slotResponse, `response.slots.${slotId}`);
|
|
269
|
-
return [slotId, slotResponse];
|
|
270
|
-
});
|
|
271
|
-
return new ResponseItem(slotValues);
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
//#endregion
|
|
275
|
-
//#region src/survey/responses/survey-response.ts
|
|
276
|
-
const SURVEY_RESPONSE_SCHEMA_VERSION = 2;
|
|
277
|
-
/** Publication and schema association belong to the host, never to an answer address. */
|
|
278
|
-
var SurveyResponse = class SurveyResponse {
|
|
279
|
-
key;
|
|
280
|
-
participantId;
|
|
281
|
-
submittedAt;
|
|
282
|
-
answers = Object.create(null);
|
|
283
|
-
itemMetadata = Object.create(null);
|
|
284
|
-
events = [];
|
|
285
|
-
context;
|
|
286
|
-
evaluationContext;
|
|
287
|
-
constructor(key) {
|
|
288
|
-
this.key = key;
|
|
289
|
-
}
|
|
290
|
-
/** Collect an engine item view into a flat submission, rejecting ambiguous ownership. */
|
|
291
|
-
addItemResponse(item) {
|
|
292
|
-
const slots = item.response?.serialize()?.slots ?? {};
|
|
293
|
-
for (const slotId of Object.keys(slots)) if (Object.hasOwn(this.answers, slotId)) throw new Error(`Duplicate submission slot: ${slotId}`);
|
|
294
|
-
Object.assign(this.answers, structuredClone(slots));
|
|
295
|
-
if (item.meta) this.itemMetadata[item.itemId] = structuredClone(item.meta);
|
|
296
|
-
}
|
|
297
|
-
serialize() {
|
|
298
|
-
return structuredClone({
|
|
299
|
-
schemaVersion: 2,
|
|
300
|
-
key: this.key,
|
|
301
|
-
participantId: this.participantId,
|
|
302
|
-
submittedAt: this.submittedAt,
|
|
303
|
-
answers: this.answers,
|
|
304
|
-
itemMetadata: this.itemMetadata,
|
|
305
|
-
events: this.events,
|
|
306
|
-
context: this.context,
|
|
307
|
-
evaluationContext: this.evaluationContext
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
static deserialize(json) {
|
|
311
|
-
if (!json || typeof json !== "object" || Array.isArray(json)) throw new Error("Invalid survey response payload");
|
|
312
|
-
if (json.schemaVersion !== 2) throw new Error(`Unsupported survey response schema version: ${String(json.schemaVersion)}`);
|
|
313
|
-
if (typeof json.key !== "string" || !json.key.trim()) throw new Error("Survey response is missing a valid key");
|
|
314
|
-
const readValues = (value, name) => {
|
|
315
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid ${name} object`);
|
|
316
|
-
const values = Object.create(null);
|
|
317
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
318
|
-
if (!key.trim()) throw new Error(`Empty ${name} key`);
|
|
319
|
-
assertResponseValue(entry, `${name}.${key}`);
|
|
320
|
-
values[key] = structuredClone(entry);
|
|
321
|
-
}
|
|
322
|
-
return values;
|
|
323
|
-
};
|
|
324
|
-
const response = new SurveyResponse(json.key);
|
|
325
|
-
response.answers = readValues(json.answers, "answers");
|
|
326
|
-
response.participantId = json.participantId;
|
|
327
|
-
response.submittedAt = json.submittedAt;
|
|
328
|
-
if (json.itemMetadata !== void 0 && (!json.itemMetadata || typeof json.itemMetadata !== "object" || Array.isArray(json.itemMetadata))) throw new Error("Invalid itemMetadata object");
|
|
329
|
-
response.itemMetadata = structuredClone(json.itemMetadata ?? {});
|
|
330
|
-
if (!Array.isArray(json.events)) throw new Error("Invalid response events");
|
|
331
|
-
response.events = structuredClone(json.events);
|
|
332
|
-
if (json.context !== void 0) response.context = readValues(json.context, "context");
|
|
333
|
-
if (json.evaluationContext !== void 0) {
|
|
334
|
-
const context = json.evaluationContext;
|
|
335
|
-
if (!context || typeof context !== "object" || typeof context.locale !== "string" || !context.locale || context.participantFlags !== void 0 && (!context.participantFlags || typeof context.participantFlags !== "object" || Array.isArray(context.participantFlags) || Object.values(context.participantFlags).some((value) => typeof value !== "string"))) throw new Error("Invalid response evaluation context");
|
|
336
|
-
response.evaluationContext = structuredClone(context);
|
|
337
|
-
}
|
|
338
|
-
return response;
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
//#endregion
|
|
342
|
-
//#region src/survey/responses/utils.ts
|
|
343
|
-
const initValueForType = (returnType) => {
|
|
344
|
-
switch (returnType) {
|
|
345
|
-
case ValueType.string: return {
|
|
346
|
-
type: ValueType.string,
|
|
347
|
-
value: ""
|
|
348
|
-
};
|
|
349
|
-
case ValueType.number: return {
|
|
350
|
-
type: ValueType.number,
|
|
351
|
-
value: 0
|
|
352
|
-
};
|
|
353
|
-
case ValueType.boolean: return {
|
|
354
|
-
type: ValueType.boolean,
|
|
355
|
-
value: false
|
|
356
|
-
};
|
|
357
|
-
case ValueType.date: return {
|
|
358
|
-
type: ValueType.date,
|
|
359
|
-
value: calendarDateFromLocalDate(/* @__PURE__ */ new Date())
|
|
360
|
-
};
|
|
361
|
-
case ValueType.stringArray: return {
|
|
362
|
-
type: ValueType.stringArray,
|
|
363
|
-
value: []
|
|
364
|
-
};
|
|
365
|
-
case ValueType.numberArray: return {
|
|
366
|
-
type: ValueType.numberArray,
|
|
367
|
-
value: []
|
|
368
|
-
};
|
|
369
|
-
case ValueType.dateArray: return {
|
|
370
|
-
type: ValueType.dateArray,
|
|
371
|
-
value: []
|
|
372
|
-
};
|
|
373
|
-
case ValueType.duration: return {
|
|
374
|
-
type: ValueType.duration,
|
|
375
|
-
value: 0,
|
|
376
|
-
unit: DurationUnits.seconds
|
|
377
|
-
};
|
|
378
|
-
case ValueType.reference: return {
|
|
379
|
-
type: ValueType.reference,
|
|
380
|
-
value: ""
|
|
381
|
-
};
|
|
382
|
-
case ValueType.durationArray: return {
|
|
383
|
-
type: ValueType.durationArray,
|
|
384
|
-
value: [],
|
|
385
|
-
unit: DurationUnits.seconds
|
|
386
|
-
};
|
|
387
|
-
case ValueType.referenceArray: return {
|
|
388
|
-
type: ValueType.referenceArray,
|
|
389
|
-
value: []
|
|
390
|
-
};
|
|
391
|
-
default: throw new Error("Invalid return type: " + returnType);
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
//#endregion
|
|
395
|
-
//#region src/survey/responses/value-domain.ts
|
|
396
|
-
/** Finite domains describe individual selectable values, including for array slots. */
|
|
397
|
-
function responseValueElements(value) {
|
|
398
|
-
if (!Array.isArray(value.value)) return [value];
|
|
399
|
-
const type = value.type.slice(0, -2);
|
|
400
|
-
return value.value.map((entry) => ({
|
|
401
|
-
...value,
|
|
402
|
-
type,
|
|
403
|
-
value: entry
|
|
404
|
-
}));
|
|
405
|
-
}
|
|
406
|
-
function responseValuesEqual(left, right) {
|
|
407
|
-
return left.type === right.type && JSON.stringify(left.value) === JSON.stringify(right.value) && (!("unit" in left) || "unit" in right && left.unit === right.unit);
|
|
408
|
-
}
|
|
409
|
-
/** No coercion: an export code or label is never a categorical option identity. */
|
|
410
|
-
function invalidResponseDomainValues(value, allowedValues) {
|
|
411
|
-
const domain = allowedValues.flatMap(responseValueElements);
|
|
412
|
-
return responseValueElements(value).filter((entry) => !domain.some((allowed) => responseValuesEqual(entry, allowed)));
|
|
413
|
-
}
|
|
414
|
-
function validateResponseSlotValue(slot, value) {
|
|
415
|
-
if (!isResponseValue(value)) return [`Slot "${slot.slotId}" has an invalid typed value.`];
|
|
416
|
-
if (value.type !== slot.valueType) return [`Slot "${slot.slotId}" requires ${slot.valueType}, received ${value.type}.`];
|
|
417
|
-
if (!slot.allowedValues) return [];
|
|
418
|
-
return invalidResponseDomainValues(value, slot.allowedValues).map((entry) => `Slot "${slot.slotId}" contains invalid value ${JSON.stringify(entry)}. Allowed values: ${JSON.stringify(slot.allowedValues)}.`);
|
|
419
|
-
}
|
|
420
|
-
//#endregion
|
|
421
|
-
//#region src/survey/responses/variable-naming.ts
|
|
422
|
-
const RESPONSE_VARIABLE_NAME_MAX_LENGTH = 32;
|
|
423
|
-
const isValidResponseVariableName = (name) => name.length <= 32 && /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/.test(name);
|
|
424
|
-
/** Generate an initial suggestion; explicit rename operations validate instead of coercing. */
|
|
425
|
-
function suggestResponseVariableName(text, fallback = "response") {
|
|
426
|
-
const normalized = text.replace(/([a-z0-9])([A-Z])/g, "$1_$2").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
427
|
-
return ((/^[a-z]/.test(normalized) ? normalized : `${fallback}_${normalized}`) || fallback).slice(0, 32).replace(/_+$/g, "");
|
|
428
|
-
}
|
|
429
|
-
function allocateResponseVariableName(base, used, maxLength = 32) {
|
|
430
|
-
if (!isValidResponseVariableName(base)) throw new Error(`Invalid response variable name "${base}"; use lowercase snake_case, starting with a letter, at most 32 characters`);
|
|
431
|
-
if (base.length <= maxLength && !used.has(base)) return base;
|
|
432
|
-
const indexed = /^(.*)_([1-9][0-9]*)$/.exec(base);
|
|
433
|
-
const stemBase = indexed ? indexed[1] : base;
|
|
434
|
-
const start = indexed ? BigInt(indexed[2]) + 1n : 2n;
|
|
435
|
-
for (let n = start;; n++) {
|
|
436
|
-
const suffix = `_${n}`;
|
|
437
|
-
const stem = stemBase.slice(0, Math.max(0, maxLength - suffix.length)).replace(/_+$/g, "");
|
|
438
|
-
if (!stem) throw new Error("The variable naming suffix leaves no room for a name");
|
|
439
|
-
const name = `${stem}${suffix}`;
|
|
440
|
-
if (!used.has(name)) return name;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
//#endregion
|
|
444
|
-
//#region src/survey/responses/slot-registry.ts
|
|
445
|
-
/** A detached, derived index. Ambiguous identities never resolve to an arbitrary owner. */
|
|
446
|
-
var ResponseSlotRegistry = class {
|
|
447
|
-
slots = /* @__PURE__ */ new Map();
|
|
448
|
-
diagnostics = [];
|
|
449
|
-
constructor(declarations) {
|
|
450
|
-
const seen = /* @__PURE__ */ new Set();
|
|
451
|
-
for (const declaration of declarations) for (const definition of declaration.slots) {
|
|
452
|
-
const slot = structuredClone({
|
|
453
|
-
...definition,
|
|
454
|
-
itemId: declaration.itemId,
|
|
455
|
-
primitiveId: definition.primitiveId ?? declaration.itemId
|
|
456
|
-
});
|
|
457
|
-
if (!slot.slotId.trim() || !slot.primitiveId.trim() || !slot.itemId.trim() || !Object.values(ValueType).includes(slot.valueType)) {
|
|
458
|
-
this.diagnostics.push({
|
|
459
|
-
code: "invalid-slot",
|
|
460
|
-
slotId: slot.slotId,
|
|
461
|
-
itemId: slot.itemId,
|
|
462
|
-
message: "Response slots require nonempty identities and a supported value type."
|
|
463
|
-
});
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
if (seen.has(slot.slotId)) {
|
|
467
|
-
this.slots.delete(slot.slotId);
|
|
468
|
-
this.diagnostics.push({
|
|
469
|
-
code: "duplicate-slot-id",
|
|
470
|
-
slotId: slot.slotId,
|
|
471
|
-
itemId: slot.itemId,
|
|
472
|
-
message: `Slot "${slot.slotId}" is declared more than once. Allocate unique persistent identities before using this schema.`
|
|
473
|
-
});
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
seen.add(slot.slotId);
|
|
477
|
-
this.slots.set(slot.slotId, slot);
|
|
478
|
-
const elementType = slot.valueType.endsWith("[]") ? slot.valueType.slice(0, -2) : slot.valueType;
|
|
479
|
-
if (slot.allowedValues?.some((value) => !isResponseValue(value) || responseValueElements(value).some((entry) => entry.type !== elementType))) this.diagnostics.push({
|
|
480
|
-
code: "invalid-domain",
|
|
481
|
-
slotId: slot.slotId,
|
|
482
|
-
itemId: slot.itemId,
|
|
483
|
-
message: `The allowed values for slot "${slot.slotId}" must have type ${elementType}.`
|
|
484
|
-
});
|
|
485
|
-
if (slot.referenceValueScores && (slot.valueType !== ValueType.reference && slot.valueType !== ValueType.referenceArray || Object.entries(slot.referenceValueScores).some(([id, score]) => !Number.isFinite(score) || slot.allowedValues && !slot.allowedValues.some((value) => value.type === ValueType.reference && value.value === id)))) this.diagnostics.push({
|
|
486
|
-
code: "invalid-domain",
|
|
487
|
-
slotId: slot.slotId,
|
|
488
|
-
itemId: slot.itemId,
|
|
489
|
-
message: `Scores for slot "${slot.slotId}" must map allowed option IDs to finite numbers.`
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
const names = /* @__PURE__ */ new Map();
|
|
493
|
-
for (const slot of this.slots.values()) {
|
|
494
|
-
if (typeof slot.variableName !== "string" || !isValidResponseVariableName(slot.variableName)) this.diagnostics.push({
|
|
495
|
-
code: "invalid-variable-name",
|
|
496
|
-
slotId: slot.slotId,
|
|
497
|
-
itemId: slot.itemId,
|
|
498
|
-
message: `Variable "${slot.variableName}" must use lowercase snake_case, start with a letter and contain at most 32 characters.`
|
|
499
|
-
});
|
|
500
|
-
const previous = names.get(slot.variableName);
|
|
501
|
-
if (previous) this.diagnostics.push({
|
|
502
|
-
code: "duplicate-variable-name",
|
|
503
|
-
slotId: slot.slotId,
|
|
504
|
-
itemId: slot.itemId,
|
|
505
|
-
message: `Variable name "${slot.variableName}" is already used by slot "${previous}".`
|
|
506
|
-
});
|
|
507
|
-
names.set(slot.variableName, slot.slotId);
|
|
508
|
-
}
|
|
509
|
-
for (const slot of this.slots.values()) for (const dependency of slot.computed?.sourceSlotIds ?? []) if (!this.slots.has(dependency)) this.diagnostics.push({
|
|
510
|
-
code: "unknown-dependency",
|
|
511
|
-
slotId: slot.slotId,
|
|
512
|
-
itemId: slot.itemId,
|
|
513
|
-
message: `Computed slot "${slot.slotId}" depends on unknown or ambiguous slot "${dependency}".`
|
|
514
|
-
});
|
|
515
|
-
const active = /* @__PURE__ */ new Set();
|
|
516
|
-
const visited = /* @__PURE__ */ new Set();
|
|
517
|
-
const visit = (id) => {
|
|
518
|
-
if (active.has(id)) {
|
|
519
|
-
this.diagnostics.push({
|
|
520
|
-
code: "computed-cycle",
|
|
521
|
-
slotId: id,
|
|
522
|
-
message: `Computed slot "${id}" participates in a dependency cycle.`
|
|
523
|
-
});
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
if (visited.has(id)) return;
|
|
527
|
-
active.add(id);
|
|
528
|
-
for (const dependency of this.slots.get(id)?.computed?.sourceSlotIds ?? []) visit(dependency);
|
|
529
|
-
active.delete(id);
|
|
530
|
-
visited.add(id);
|
|
531
|
-
};
|
|
532
|
-
for (const id of this.slots.keys()) visit(id);
|
|
533
|
-
}
|
|
534
|
-
get(slotId) {
|
|
535
|
-
const slot = this.slots.get(slotId);
|
|
536
|
-
return slot && structuredClone(slot);
|
|
537
|
-
}
|
|
538
|
-
get definitions() {
|
|
539
|
-
return structuredClone([...this.slots.values()]);
|
|
540
|
-
}
|
|
541
|
-
assertValid() {
|
|
542
|
-
if (this.diagnostics.length) throw new Error(this.diagnostics.map((entry) => entry.message).join("\n"));
|
|
543
|
-
}
|
|
544
|
-
validateAnswers(answers) {
|
|
545
|
-
const diagnostics = [...this.diagnostics];
|
|
546
|
-
for (const [slotId, value] of Object.entries(answers)) {
|
|
547
|
-
const slot = this.slots.get(slotId);
|
|
548
|
-
if (!slot) {
|
|
549
|
-
diagnostics.push({
|
|
550
|
-
code: "unknown-slot",
|
|
551
|
-
slotId,
|
|
552
|
-
message: `Answer references unknown or ambiguous slot "${slotId}".`
|
|
553
|
-
});
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
const issues = slot.computed ? [`Computed slot "${slotId}" cannot contain a stored answer.`] : validateResponseSlotValue(slot, value);
|
|
557
|
-
for (const message of issues) diagnostics.push({
|
|
558
|
-
code: "invalid-answer",
|
|
559
|
-
slotId,
|
|
560
|
-
itemId: slot.itemId,
|
|
561
|
-
message
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
return diagnostics;
|
|
565
|
-
}
|
|
566
|
-
};
|
|
567
|
-
//#endregion
|
|
568
|
-
//#region src/survey/utils/value-reference.ts
|
|
569
|
-
let ValueReferenceMethod = /* @__PURE__ */ function(ValueReferenceMethod) {
|
|
570
|
-
ValueReferenceMethod["get"] = "get";
|
|
571
|
-
ValueReferenceMethod["isDefined"] = "isDefined";
|
|
572
|
-
return ValueReferenceMethod;
|
|
573
|
-
}({});
|
|
574
|
-
/** Slot identity is opaque. String keys are an internal JSON-tuple lookup codec. */
|
|
575
|
-
var ValueReference = class ValueReference {
|
|
576
|
-
slotId;
|
|
577
|
-
name;
|
|
578
|
-
constructor(reference) {
|
|
579
|
-
let value = reference;
|
|
580
|
-
if (typeof reference === "string") {
|
|
581
|
-
const tuple = JSON.parse(reference);
|
|
582
|
-
if (!Array.isArray(tuple) || tuple.length !== 2) throw new Error("Invalid response-reference lookup key");
|
|
583
|
-
value = {
|
|
584
|
-
slotId: tuple[0],
|
|
585
|
-
method: tuple[1]
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
const parsed = value;
|
|
589
|
-
if (!parsed || typeof parsed.slotId !== "string" || !parsed.slotId.trim() || parsed.method !== "get" && parsed.method !== "isDefined") throw new Error("Response references require a slotId and get or isDefined method");
|
|
590
|
-
this.slotId = parsed.slotId;
|
|
591
|
-
this.name = parsed.method;
|
|
592
|
-
}
|
|
593
|
-
serialize() {
|
|
594
|
-
return {
|
|
595
|
-
slotId: this.slotId,
|
|
596
|
-
method: this.name
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
toString() {
|
|
600
|
-
return JSON.stringify([this.slotId, this.name]);
|
|
601
|
-
}
|
|
602
|
-
static fromParts(slotId, method) {
|
|
603
|
-
return new ValueReference({
|
|
604
|
-
slotId,
|
|
605
|
-
method
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
let ReferenceUsageType = /* @__PURE__ */ function(ReferenceUsageType) {
|
|
610
|
-
ReferenceUsageType["displayConditions"] = "displayConditions";
|
|
611
|
-
ReferenceUsageType["templateValues"] = "templateValues";
|
|
612
|
-
ReferenceUsageType["validations"] = "validations";
|
|
613
|
-
ReferenceUsageType["disabledConditions"] = "disabledConditions";
|
|
614
|
-
ReferenceUsageType["prefills"] = "prefills";
|
|
615
|
-
return ReferenceUsageType;
|
|
616
|
-
}({});
|
|
617
|
-
//#endregion
|
|
618
|
-
//#region src/expressions/expression.ts
|
|
619
|
-
const ExpressionType = {
|
|
620
|
-
Const: "const",
|
|
621
|
-
ResponseVariable: "responseVariable",
|
|
622
|
-
ContextVariable: "contextVariable",
|
|
623
|
-
Function: "function"
|
|
624
|
-
};
|
|
625
|
-
const ContextVariableType = {
|
|
626
|
-
Locale: "locale",
|
|
627
|
-
ParticipantFlag: "participantFlag",
|
|
628
|
-
CustomValue: "customValue",
|
|
629
|
-
CustomExpression: "customExpression"
|
|
630
|
-
};
|
|
631
|
-
/**
|
|
632
|
-
* Base class for all expressions.
|
|
633
|
-
*/
|
|
634
|
-
var Expression = class Expression {
|
|
635
|
-
type;
|
|
636
|
-
editorConfig;
|
|
637
|
-
constructor(type, editorConfig) {
|
|
638
|
-
this.type = type;
|
|
639
|
-
this.editorConfig = editorConfig;
|
|
640
|
-
}
|
|
641
|
-
static deserialize(json) {
|
|
642
|
-
if (!json) return;
|
|
643
|
-
switch (json.type) {
|
|
644
|
-
case ExpressionType.Const: return ConstExpression.deserialize(json);
|
|
645
|
-
case ExpressionType.ResponseVariable: return ResponseVariableExpression.deserialize(json);
|
|
646
|
-
case ExpressionType.ContextVariable: return ContextVariableExpression.deserialize(json);
|
|
647
|
-
case ExpressionType.Function: return FunctionExpression.deserialize(json);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
clone() {
|
|
651
|
-
return Expression.deserialize(this.serialize()) ?? (() => {
|
|
652
|
-
throw new Error("Failed to clone expression");
|
|
653
|
-
})();
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
var ConstExpression = class ConstExpression extends Expression {
|
|
657
|
-
value;
|
|
658
|
-
constructor(value, editorConfig) {
|
|
659
|
-
super(ExpressionType.Const, editorConfig);
|
|
660
|
-
if (value?.type === "date" || value?.type === "date[]") assertResponseValue(value, "expression.value");
|
|
661
|
-
this.value = value;
|
|
662
|
-
this.type = ExpressionType.Const;
|
|
663
|
-
}
|
|
664
|
-
static deserialize(json) {
|
|
665
|
-
if (json.type !== ExpressionType.Const) throw new Error("Invalid expression type: " + json.type);
|
|
666
|
-
return new ConstExpression(json.value, json.editorConfig);
|
|
667
|
-
}
|
|
668
|
-
get responseVariableRefs() {
|
|
669
|
-
return [];
|
|
670
|
-
}
|
|
671
|
-
serialize() {
|
|
672
|
-
return {
|
|
673
|
-
type: this.type,
|
|
674
|
-
value: this.value,
|
|
675
|
-
editorConfig: this.editorConfig
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
updateItemKeyReferences(_oldItemKey, _newItemKey) {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
var ResponseVariableExpression = class ResponseVariableExpression extends Expression {
|
|
683
|
-
variableRef;
|
|
684
|
-
constructor(variableRef, editorConfig) {
|
|
685
|
-
super(ExpressionType.ResponseVariable, editorConfig);
|
|
686
|
-
this.variableRef = new ValueReference(variableRef).toString();
|
|
687
|
-
this.type = ExpressionType.ResponseVariable;
|
|
688
|
-
}
|
|
689
|
-
static deserialize(json) {
|
|
690
|
-
if (json.type !== ExpressionType.ResponseVariable) throw new Error("Invalid expression type: " + json.type);
|
|
691
|
-
if (typeof json.variableRef !== "object") throw new Error("Response expressions require a structured slot reference");
|
|
692
|
-
return new ResponseVariableExpression(json.variableRef, json.editorConfig);
|
|
693
|
-
}
|
|
694
|
-
get responseVariableRefs() {
|
|
695
|
-
return [new ValueReference(this.variableRef)];
|
|
696
|
-
}
|
|
697
|
-
get responseVariableRef() {
|
|
698
|
-
return new ValueReference(this.variableRef);
|
|
699
|
-
}
|
|
700
|
-
serialize() {
|
|
701
|
-
return {
|
|
702
|
-
type: this.type,
|
|
703
|
-
variableRef: this.responseVariableRef.serialize(),
|
|
704
|
-
editorConfig: this.editorConfig
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
};
|
|
708
|
-
var ContextVariableExpression = class ContextVariableExpression extends Expression {
|
|
709
|
-
contextType;
|
|
710
|
-
key;
|
|
711
|
-
arguments;
|
|
712
|
-
asType;
|
|
713
|
-
constructor(contextType, key, args, asType, editorConfig) {
|
|
714
|
-
super(ExpressionType.ContextVariable, editorConfig);
|
|
715
|
-
this.type = ExpressionType.ContextVariable;
|
|
716
|
-
this.contextType = contextType;
|
|
717
|
-
this.key = key;
|
|
718
|
-
this.arguments = args;
|
|
719
|
-
this.asType = asType;
|
|
720
|
-
}
|
|
721
|
-
static deserialize(json) {
|
|
722
|
-
if (json.type !== ExpressionType.ContextVariable) throw new Error("Invalid expression type: " + json.type);
|
|
723
|
-
return new ContextVariableExpression(json.contextType, Expression.deserialize(json.key), json.arguments?.map((arg) => Expression.deserialize(arg)), json.asType, json.editorConfig);
|
|
724
|
-
}
|
|
725
|
-
get responseVariableRefs() {
|
|
726
|
-
return [this.key, ...this.arguments ?? []].flatMap((arg) => arg?.responseVariableRefs ?? []);
|
|
727
|
-
}
|
|
728
|
-
serialize() {
|
|
729
|
-
return {
|
|
730
|
-
type: this.type,
|
|
731
|
-
contextType: this.contextType,
|
|
732
|
-
key: this.key?.serialize(),
|
|
733
|
-
arguments: this.arguments?.map((arg) => arg?.serialize()),
|
|
734
|
-
asType: this.asType,
|
|
735
|
-
editorConfig: this.editorConfig
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
let FunctionExpressionNames = /* @__PURE__ */ function(FunctionExpressionNames) {
|
|
740
|
-
FunctionExpressionNames["and"] = "and";
|
|
741
|
-
FunctionExpressionNames["or"] = "or";
|
|
742
|
-
FunctionExpressionNames["not"] = "not";
|
|
743
|
-
FunctionExpressionNames["list_contains"] = "list_contains";
|
|
744
|
-
FunctionExpressionNames["eq"] = "eq";
|
|
745
|
-
FunctionExpressionNames["gt"] = "gt";
|
|
746
|
-
FunctionExpressionNames["gte"] = "gte";
|
|
747
|
-
FunctionExpressionNames["lt"] = "lt";
|
|
748
|
-
FunctionExpressionNames["lte"] = "lte";
|
|
749
|
-
FunctionExpressionNames["in_range"] = "in_range";
|
|
750
|
-
FunctionExpressionNames["sum"] = "sum";
|
|
751
|
-
FunctionExpressionNames["min"] = "min";
|
|
752
|
-
FunctionExpressionNames["max"] = "max";
|
|
753
|
-
FunctionExpressionNames["str_eq"] = "str_eq";
|
|
754
|
-
FunctionExpressionNames["date_eq"] = "date_eq";
|
|
755
|
-
FunctionExpressionNames["date_gt"] = "date_gt";
|
|
756
|
-
FunctionExpressionNames["date_gte"] = "date_gte";
|
|
757
|
-
FunctionExpressionNames["date_lt"] = "date_lt";
|
|
758
|
-
FunctionExpressionNames["date_lte"] = "date_lte";
|
|
759
|
-
FunctionExpressionNames["date_add_days"] = "date_add_days";
|
|
760
|
-
FunctionExpressionNames["date_add_months"] = "date_add_months";
|
|
761
|
-
FunctionExpressionNames["date_add_years"] = "date_add_years";
|
|
762
|
-
FunctionExpressionNames["date_diff"] = "date_diff";
|
|
763
|
-
FunctionExpressionNames["date_min"] = "date_min";
|
|
764
|
-
FunctionExpressionNames["date_max"] = "date_max";
|
|
765
|
-
return FunctionExpressionNames;
|
|
766
|
-
}({});
|
|
767
|
-
var FunctionExpression = class FunctionExpression extends Expression {
|
|
768
|
-
functionName;
|
|
769
|
-
arguments;
|
|
770
|
-
constructor(functionName, args, editorConfig) {
|
|
771
|
-
super(ExpressionType.Function);
|
|
772
|
-
this.type = ExpressionType.Function;
|
|
773
|
-
this.functionName = functionName;
|
|
774
|
-
this.arguments = args;
|
|
775
|
-
this.editorConfig = editorConfig;
|
|
776
|
-
}
|
|
777
|
-
static deserialize(json) {
|
|
778
|
-
if (json.type !== ExpressionType.Function) throw new Error("Invalid expression type: " + json.type);
|
|
779
|
-
const functionName = json.functionName;
|
|
780
|
-
if (!Object.values(FunctionExpressionNames).includes(functionName)) throw new Error("Invalid function name: " + functionName);
|
|
781
|
-
const expr = new FunctionExpression(functionName, json.arguments.map((arg) => Expression.deserialize(arg)));
|
|
782
|
-
expr.editorConfig = json.editorConfig;
|
|
783
|
-
return expr;
|
|
784
|
-
}
|
|
785
|
-
get responseVariableRefs() {
|
|
786
|
-
const refStrings = this.arguments.flatMap((arg) => arg?.responseVariableRefs).filter((ref) => ref !== void 0).map((ref) => ref.toString());
|
|
787
|
-
return [...new Set(refStrings)].map((ref) => new ValueReference(ref));
|
|
788
|
-
}
|
|
789
|
-
serialize() {
|
|
790
|
-
return {
|
|
791
|
-
type: this.type,
|
|
792
|
-
functionName: this.functionName,
|
|
793
|
-
arguments: this.arguments.map((arg) => arg?.serialize()),
|
|
794
|
-
editorConfig: this.editorConfig
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
//#endregion
|
|
799
|
-
//#region src/expressions/expression-evaluator.ts
|
|
800
|
-
var ExpressionEvaluator = class {
|
|
801
|
-
context;
|
|
802
|
-
constructor(context) {
|
|
803
|
-
this.context = context;
|
|
804
|
-
}
|
|
805
|
-
eval(expression) {
|
|
806
|
-
if (expression === void 0) return;
|
|
807
|
-
switch (expression.type) {
|
|
808
|
-
case ExpressionType.Const: return this.evaluateConst(expression);
|
|
809
|
-
case ExpressionType.Function: return this.evaluateFunction(expression);
|
|
810
|
-
case ExpressionType.ResponseVariable: return this.evaluateResponseVariable(expression);
|
|
811
|
-
case ExpressionType.ContextVariable: return this.evaluateContextVariable(expression);
|
|
812
|
-
default: throw new Error(`Unsupported expression type: ${expression.type}`);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
setContext(context) {
|
|
816
|
-
this.context = context;
|
|
817
|
-
}
|
|
818
|
-
evaluateConst(expression) {
|
|
819
|
-
return expression.value;
|
|
820
|
-
}
|
|
821
|
-
resolveStoredValue(slotId) {
|
|
822
|
-
if (this.context?.resolveResponseValue) return this.context.resolveResponseValue(slotId);
|
|
823
|
-
const values = Object.values(this.context?.responses ?? {}).map((item) => item.response?.get(slotId)).filter((value) => value !== void 0);
|
|
824
|
-
if (values.length > 1) throw new Error(`Ambiguous response slot "${slotId}"`);
|
|
825
|
-
return values[0];
|
|
826
|
-
}
|
|
827
|
-
evaluateResponseVariable(expression) {
|
|
828
|
-
const varRef = expression.responseVariableRef;
|
|
829
|
-
switch (varRef.name) {
|
|
830
|
-
case "get": return this.context?.resolveComputedValue?.(varRef.slotId) ?? this.resolveStoredValue(varRef.slotId);
|
|
831
|
-
case "isDefined": return {
|
|
832
|
-
type: ValueType.boolean,
|
|
833
|
-
value: (this.context?.resolveComputedValue?.(varRef.slotId) ?? this.resolveStoredValue(varRef.slotId)) !== void 0
|
|
834
|
-
};
|
|
835
|
-
default: throw new Error(`Unsupported value reference method: ${varRef.name}`);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
evaluateFunction(expression) {
|
|
839
|
-
switch (expression.functionName) {
|
|
840
|
-
case "and": return this.evaluateAnd(expression);
|
|
841
|
-
case "or": return this.evaluateOr(expression);
|
|
842
|
-
case "not": return this.evaluateNot(expression);
|
|
843
|
-
case "date_eq":
|
|
844
|
-
case "date_gt":
|
|
845
|
-
case "date_gte":
|
|
846
|
-
case "date_lt":
|
|
847
|
-
case "date_lte":
|
|
848
|
-
case "date_add_days":
|
|
849
|
-
case "date_add_months":
|
|
850
|
-
case "date_add_years":
|
|
851
|
-
case "date_diff":
|
|
852
|
-
case "date_min":
|
|
853
|
-
case "date_max": return this.evaluateDateFunction(expression);
|
|
854
|
-
case "str_eq": return this.evaluateStrEq(expression);
|
|
855
|
-
case "eq": return this.evaluateEq(expression);
|
|
856
|
-
case "gt": return this.evaluateGt(expression);
|
|
857
|
-
case "gte": return this.evaluateGte(expression);
|
|
858
|
-
case "lt": return this.evaluateLt(expression);
|
|
859
|
-
case "lte": return this.evaluateLte(expression);
|
|
860
|
-
case "in_range": return this.evaluateInRange(expression);
|
|
861
|
-
case "sum": return this.evaluateSum(expression);
|
|
862
|
-
case "min": return this.evaluateMin(expression);
|
|
863
|
-
case "max": return this.evaluateMax(expression);
|
|
864
|
-
case "list_contains": return this.evaluateListContains(expression);
|
|
865
|
-
default: throw new Error(`Unsupported function: ${expression.functionName}`);
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
evaluateContextVariable(expression) {
|
|
869
|
-
if (!this.context?.surveyContext) return;
|
|
870
|
-
const surveyContext = this.context.surveyContext;
|
|
871
|
-
switch (expression.contextType) {
|
|
872
|
-
case ContextVariableType.Locale: return {
|
|
873
|
-
type: ValueType.string,
|
|
874
|
-
value: surveyContext.locale
|
|
875
|
-
};
|
|
876
|
-
case ContextVariableType.ParticipantFlag: {
|
|
877
|
-
if (!expression.key) return;
|
|
878
|
-
const flagKey = this.eval(expression.key);
|
|
879
|
-
if (flagKey?.type !== ValueType.string || flagKey.value === void 0) return;
|
|
880
|
-
const flagValue = surveyContext.participantFlags?.[flagKey.value];
|
|
881
|
-
switch (expression.asType) {
|
|
882
|
-
case ValueType.boolean: return {
|
|
883
|
-
type: ValueType.boolean,
|
|
884
|
-
value: flagValue !== void 0
|
|
885
|
-
};
|
|
886
|
-
case ValueType.string:
|
|
887
|
-
if (flagValue === void 0) return;
|
|
888
|
-
return {
|
|
889
|
-
type: ValueType.string,
|
|
890
|
-
value: flagValue
|
|
891
|
-
};
|
|
892
|
-
case ValueType.number: {
|
|
893
|
-
if (flagValue === void 0) return;
|
|
894
|
-
const numValue = parseFloat(flagValue);
|
|
895
|
-
return isNaN(numValue) ? void 0 : {
|
|
896
|
-
type: ValueType.number,
|
|
897
|
-
value: numValue
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
case ValueType.date:
|
|
901
|
-
if (flagValue === void 0) return;
|
|
902
|
-
return isCalendarDate(flagValue) ? {
|
|
903
|
-
type: ValueType.date,
|
|
904
|
-
value: flagValue
|
|
905
|
-
} : void 0;
|
|
906
|
-
default:
|
|
907
|
-
if (flagValue === void 0) return;
|
|
908
|
-
return {
|
|
909
|
-
type: ValueType.string,
|
|
910
|
-
value: flagValue
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
case ContextVariableType.CustomValue: {
|
|
915
|
-
if (!expression.key) return;
|
|
916
|
-
const customKey = this.eval(expression.key);
|
|
917
|
-
if (customKey?.type !== ValueType.string || customKey.value === void 0) return;
|
|
918
|
-
const customValue = surveyContext.customValues?.[customKey.value];
|
|
919
|
-
if (customValue === void 0) return;
|
|
920
|
-
if (expression.asType !== void 0) {
|
|
921
|
-
if (customValue?.type !== expression.asType) throw new Error(`Custom value type does not match expected type, got ${customValue?.type} and expected ${expression.asType}`);
|
|
922
|
-
}
|
|
923
|
-
return customValue;
|
|
924
|
-
}
|
|
925
|
-
case ContextVariableType.CustomExpression: {
|
|
926
|
-
if (!expression.key) return;
|
|
927
|
-
const expressionKey = this.eval(expression.key);
|
|
928
|
-
if (expressionKey?.type !== ValueType.string || expressionKey.value === void 0) return;
|
|
929
|
-
const customExpression = surveyContext.customExpressions?.[expressionKey.value];
|
|
930
|
-
if (typeof customExpression !== ExpressionType.Function || customExpression === void 0) return;
|
|
931
|
-
try {
|
|
932
|
-
const args = expression.arguments;
|
|
933
|
-
const result = customExpression(args);
|
|
934
|
-
if (expression.asType !== void 0) {
|
|
935
|
-
if (result?.type !== expression.asType) throw new Error(`Custom expression result type does not match expected type, got ${result?.type} and expected ${expression.asType}`);
|
|
936
|
-
}
|
|
937
|
-
return result;
|
|
938
|
-
} catch (error) {
|
|
939
|
-
throw new Error(`Error evaluating custom expression: ${error}`);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
evaluateDateFunction(expression) {
|
|
945
|
-
const name = expression.functionName;
|
|
946
|
-
const aggregate = name === "date_min" || name === "date_max";
|
|
947
|
-
if (aggregate ? expression.arguments.length < 1 : expression.arguments.length !== 2) throw new Error(`${name} expects ${aggregate ? "at least 1 argument" : "2 arguments"}`);
|
|
948
|
-
const args = expression.arguments.map((arg) => this.eval(arg));
|
|
949
|
-
if (args.some((arg) => arg === void 0)) return void 0;
|
|
950
|
-
const first = args[0];
|
|
951
|
-
if (first?.type !== ValueType.date || !isCalendarDate(first.value)) return void 0;
|
|
952
|
-
if (name.startsWith("date_add_")) {
|
|
953
|
-
const amount = args[1];
|
|
954
|
-
if (amount?.type !== ValueType.number) return void 0;
|
|
955
|
-
const unit = name === "date_add_days" ? "days" : name === "date_add_months" ? "months" : "years";
|
|
956
|
-
const value = addCalendarDate(first.value, amount.value, unit);
|
|
957
|
-
return value === void 0 ? void 0 : {
|
|
958
|
-
type: ValueType.date,
|
|
959
|
-
value
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
const dates = [];
|
|
963
|
-
for (const arg of args) {
|
|
964
|
-
if (arg?.type !== ValueType.date || !isCalendarDate(arg.value)) return void 0;
|
|
965
|
-
dates.push(arg.value);
|
|
966
|
-
}
|
|
967
|
-
if (name === "date_eq") return {
|
|
968
|
-
type: ValueType.boolean,
|
|
969
|
-
value: dates[0] === dates[1]
|
|
970
|
-
};
|
|
971
|
-
if (dates.some((date) => date.length !== first.value.length)) return void 0;
|
|
972
|
-
if (aggregate) return {
|
|
973
|
-
type: ValueType.date,
|
|
974
|
-
value: dates.reduce((a, b) => name === "date_min" ? a < b ? a : b : a > b ? a : b)
|
|
975
|
-
};
|
|
976
|
-
if (name === "date_diff") return {
|
|
977
|
-
type: ValueType.number,
|
|
978
|
-
value: differenceCalendarDates(dates[0], dates[1])
|
|
979
|
-
};
|
|
980
|
-
const comparison = compareCalendarDates(dates[0], dates[1]);
|
|
981
|
-
return {
|
|
982
|
-
type: ValueType.boolean,
|
|
983
|
-
value: name === "date_gt" ? comparison > 0 : name === "date_gte" ? comparison >= 0 : name === "date_lt" ? comparison < 0 : comparison <= 0
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
evaluateAnd(expression) {
|
|
987
|
-
return {
|
|
988
|
-
type: ValueType.boolean,
|
|
989
|
-
value: expression.arguments.every((arg) => this.eval(arg)?.value === true)
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
evaluateOr(expression) {
|
|
993
|
-
return {
|
|
994
|
-
type: ValueType.boolean,
|
|
995
|
-
value: expression.arguments.some((arg) => this.eval(arg)?.value === true)
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
evaluateNot(expression) {
|
|
999
|
-
if (expression.arguments.length !== 1 || expression.arguments[0] === void 0) throw new Error(`Not function expects 1 argument, got ${expression.arguments.length}`);
|
|
1000
|
-
const resolvedValue = this.eval(expression.arguments[0]);
|
|
1001
|
-
if (resolvedValue?.type !== ValueType.boolean) throw new Error(`Not function expects a boolean argument, got ${resolvedValue?.type}`);
|
|
1002
|
-
if (resolvedValue === void 0) return {
|
|
1003
|
-
type: ValueType.boolean,
|
|
1004
|
-
value: false
|
|
1005
|
-
};
|
|
1006
|
-
return {
|
|
1007
|
-
type: ValueType.boolean,
|
|
1008
|
-
value: !resolvedValue.value
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
evaluateListContains(expression) {
|
|
1012
|
-
if (expression.arguments.length !== 2) throw new Error(`List contains function expects 2 arguments, got ${expression.arguments.length}`);
|
|
1013
|
-
const resolvedList = this.eval(expression.arguments[0]);
|
|
1014
|
-
const resolvedItem = this.eval(expression.arguments[1]);
|
|
1015
|
-
if (resolvedList === void 0 || resolvedItem === void 0) return {
|
|
1016
|
-
type: ValueType.boolean,
|
|
1017
|
-
value: false
|
|
1018
|
-
};
|
|
1019
|
-
const allowedListTypes = [ValueType.stringArray, ValueType.referenceArray];
|
|
1020
|
-
const allowedItemTypes = [ValueType.string, ValueType.reference];
|
|
1021
|
-
const resolvedListType = resolvedList.type;
|
|
1022
|
-
const resolvedItemType = resolvedItem.type;
|
|
1023
|
-
if (!allowedListTypes.includes(resolvedListType) || !allowedItemTypes.includes(resolvedItemType)) throw new Error(`List contains function expects a string array list and a string item, got ${resolvedList?.type} and ${resolvedItem?.type}`);
|
|
1024
|
-
const list = resolvedList.value;
|
|
1025
|
-
const item = resolvedItem.value;
|
|
1026
|
-
return {
|
|
1027
|
-
type: ValueType.boolean,
|
|
1028
|
-
value: list.includes(item)
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
evaluateStrEq(expression) {
|
|
1032
|
-
if (expression.arguments.length !== 2) throw new Error(`String equals function expects 2 arguments, got ${expression.arguments.length}`);
|
|
1033
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
1034
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
1035
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
1036
|
-
type: ValueType.boolean,
|
|
1037
|
-
value: false
|
|
1038
|
-
};
|
|
1039
|
-
if (resolvedA?.type !== ValueType.string || resolvedB?.type !== ValueType.string) throw new Error(`String equals function expects strings for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
1040
|
-
return {
|
|
1041
|
-
type: ValueType.boolean,
|
|
1042
|
-
value: resolvedA.value === resolvedB.value
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
evaluateEq(expression) {
|
|
1046
|
-
if (expression.arguments.length !== 2) throw new Error(`Equals function expects 2 arguments, got ${expression.arguments.length}`);
|
|
1047
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
1048
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
1049
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
1050
|
-
type: ValueType.boolean,
|
|
1051
|
-
value: false
|
|
1052
|
-
};
|
|
1053
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Equals function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
1054
|
-
return {
|
|
1055
|
-
type: ValueType.boolean,
|
|
1056
|
-
value: resolvedA.value === resolvedB.value
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
evaluateGt(expression) {
|
|
1060
|
-
if (expression.arguments.length !== 2) throw new Error(`Greater than function expects 2 arguments, got ${expression.arguments.length}`);
|
|
1061
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
1062
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
1063
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
1064
|
-
type: ValueType.boolean,
|
|
1065
|
-
value: false
|
|
1066
|
-
};
|
|
1067
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Greater than function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
1068
|
-
return {
|
|
1069
|
-
type: ValueType.boolean,
|
|
1070
|
-
value: resolvedA.value > resolvedB.value
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
evaluateGte(expression) {
|
|
1074
|
-
if (expression.arguments.length !== 2) throw new Error(`Greater than or equal to function expects 2 arguments, got ${expression.arguments.length}`);
|
|
1075
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
1076
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
1077
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
1078
|
-
type: ValueType.boolean,
|
|
1079
|
-
value: false
|
|
1080
|
-
};
|
|
1081
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Greater than or equal to function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
1082
|
-
return {
|
|
1083
|
-
type: ValueType.boolean,
|
|
1084
|
-
value: resolvedA.value >= resolvedB.value
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
evaluateLt(expression) {
|
|
1088
|
-
if (expression.arguments.length !== 2) throw new Error(`Less than function expects 2 arguments, got ${expression.arguments.length}`);
|
|
1089
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
1090
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
1091
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
1092
|
-
type: ValueType.boolean,
|
|
1093
|
-
value: false
|
|
1094
|
-
};
|
|
1095
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Less than function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
1096
|
-
return {
|
|
1097
|
-
type: ValueType.boolean,
|
|
1098
|
-
value: resolvedA.value < resolvedB.value
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
evaluateLte(expression) {
|
|
1102
|
-
if (expression.arguments.length !== 2) throw new Error(`Less than or equal to function expects 2 arguments, got ${expression.arguments.length}`);
|
|
1103
|
-
const resolvedA = this.eval(expression.arguments[0]);
|
|
1104
|
-
const resolvedB = this.eval(expression.arguments[1]);
|
|
1105
|
-
if (resolvedA === void 0 || resolvedB === void 0) return {
|
|
1106
|
-
type: ValueType.boolean,
|
|
1107
|
-
value: false
|
|
1108
|
-
};
|
|
1109
|
-
if (resolvedA?.type !== resolvedB?.type) throw new Error(`Less than or equal to function expects the same type for all arguments, got ${resolvedA?.type} and ${resolvedB?.type}`);
|
|
1110
|
-
return {
|
|
1111
|
-
type: ValueType.boolean,
|
|
1112
|
-
value: resolvedA.value <= resolvedB.value
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
evaluateInRange(expression) {
|
|
1116
|
-
if (expression.arguments.length !== 4) throw new Error(`In range function expects 4 arguments, got ${expression.arguments.length}`);
|
|
1117
|
-
const resolvedValue = this.eval(expression.arguments[0]);
|
|
1118
|
-
const resolvedMin = this.eval(expression.arguments[1]);
|
|
1119
|
-
const resolvedMax = this.eval(expression.arguments[2]);
|
|
1120
|
-
const resolvedInclusive = this.eval(expression.arguments[3])?.value === true;
|
|
1121
|
-
if (resolvedValue === void 0 || resolvedMin === void 0 || resolvedMax === void 0) return {
|
|
1122
|
-
type: ValueType.boolean,
|
|
1123
|
-
value: false
|
|
1124
|
-
};
|
|
1125
|
-
if (resolvedValue?.type !== ValueType.number || resolvedMin?.type !== ValueType.number || resolvedMax?.type !== ValueType.number) throw new Error(`In range function expects numbers for all arguments, got ${resolvedValue?.type} and ${resolvedMin?.type} and ${resolvedMax?.type}`);
|
|
1126
|
-
if (resolvedInclusive) return {
|
|
1127
|
-
type: ValueType.boolean,
|
|
1128
|
-
value: resolvedValue.value >= resolvedMin.value && resolvedValue.value <= resolvedMax.value
|
|
1129
|
-
};
|
|
1130
|
-
return {
|
|
1131
|
-
type: ValueType.boolean,
|
|
1132
|
-
value: resolvedValue.value > resolvedMin.value && resolvedValue.value < resolvedMax.value
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
evaluateSum(expression) {
|
|
1136
|
-
if (expression.arguments.length < 1) throw new Error(`Sum function expects at least 1 argument, got ${expression.arguments.length}`);
|
|
1137
|
-
return expression.arguments.reduce((sum, arg) => {
|
|
1138
|
-
if (arg === void 0) return sum;
|
|
1139
|
-
const resolvedValue = this.eval(arg);
|
|
1140
|
-
if (resolvedValue === void 0) return sum;
|
|
1141
|
-
if (resolvedValue?.type === ValueType.number) return {
|
|
1142
|
-
type: ValueType.number,
|
|
1143
|
-
value: sum.value + resolvedValue.value
|
|
1144
|
-
};
|
|
1145
|
-
throw new Error(`Sum function expects all arguments to be numbers, got ${resolvedValue?.type}`);
|
|
1146
|
-
}, {
|
|
1147
|
-
type: ValueType.number,
|
|
1148
|
-
value: 0
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1151
|
-
evaluateMin(expression) {
|
|
1152
|
-
if (expression.arguments.length < 1) throw new Error(`Min function expects at least 1 argument, got ${expression.arguments.length}`);
|
|
1153
|
-
return expression.arguments.reduce((min, arg) => {
|
|
1154
|
-
if (arg === void 0) return min;
|
|
1155
|
-
const resolvedValue = this.eval(arg);
|
|
1156
|
-
if (resolvedValue === void 0) return min;
|
|
1157
|
-
if (resolvedValue?.type === ValueType.number) return {
|
|
1158
|
-
type: ValueType.number,
|
|
1159
|
-
value: Math.min(min.value, resolvedValue.value)
|
|
1160
|
-
};
|
|
1161
|
-
throw new Error(`Min function expects all arguments to be numbers, got ${typeof resolvedValue}`);
|
|
1162
|
-
}, {
|
|
1163
|
-
type: ValueType.number,
|
|
1164
|
-
value: Infinity
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
evaluateMax(expression) {
|
|
1168
|
-
if (expression.arguments.length < 1) throw new Error(`Max function expects at least 1 argument, got ${expression.arguments.length}`);
|
|
1169
|
-
return expression.arguments.reduce((max, arg) => {
|
|
1170
|
-
if (arg === void 0) return max;
|
|
1171
|
-
const resolvedValue = this.eval(arg);
|
|
1172
|
-
if (resolvedValue === void 0) return max;
|
|
1173
|
-
if (resolvedValue?.type === ValueType.number) return {
|
|
1174
|
-
type: ValueType.number,
|
|
1175
|
-
value: Math.max(max.value, resolvedValue.value)
|
|
1176
|
-
};
|
|
1177
|
-
throw new Error(`Max function expects all arguments to be numbers, got ${typeof resolvedValue}`);
|
|
1178
|
-
}, {
|
|
1179
|
-
type: ValueType.number,
|
|
1180
|
-
value: -Infinity
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
};
|
|
1184
|
-
//#endregion
|
|
1185
|
-
//#region src/expressions/template-value.ts
|
|
1186
|
-
let TemplateDefTypes = /* @__PURE__ */ function(TemplateDefTypes) {
|
|
1187
|
-
TemplateDefTypes["Default"] = "default";
|
|
1188
|
-
TemplateDefTypes["Date2String"] = "date2string";
|
|
1189
|
-
return TemplateDefTypes;
|
|
1190
|
-
}({});
|
|
1191
|
-
const serializeTemplateValue = (templateValue) => {
|
|
1192
|
-
const json = {
|
|
1193
|
-
type: templateValue.type,
|
|
1194
|
-
returnType: templateValue.returnType,
|
|
1195
|
-
expression: templateValue.expression?.serialize()
|
|
1196
|
-
};
|
|
1197
|
-
if (templateValue.type === "date2string") json.dateFormat = templateValue.dateFormat;
|
|
1198
|
-
return json;
|
|
1199
|
-
};
|
|
1200
|
-
const serializeTemplateValues = (templateValues) => {
|
|
1201
|
-
const json = {};
|
|
1202
|
-
for (const [key, value] of templateValues.entries()) json[key] = serializeTemplateValue(value);
|
|
1203
|
-
return json;
|
|
1204
|
-
};
|
|
1205
|
-
const deserializeTemplateValue = (json) => {
|
|
1206
|
-
return {
|
|
1207
|
-
type: json.type,
|
|
1208
|
-
expression: json.expression ? Expression.deserialize(json.expression) : void 0,
|
|
1209
|
-
returnType: json.returnType,
|
|
1210
|
-
dateFormat: json.dateFormat
|
|
1211
|
-
};
|
|
1212
|
-
};
|
|
1213
|
-
const deserializeTemplateValues = (json) => {
|
|
1214
|
-
return new Map(Object.entries(json).map(([key, value]) => [key, deserializeTemplateValue(value)]));
|
|
1215
|
-
};
|
|
1216
|
-
//#endregion
|
|
1217
|
-
//#region src/expressions/date-functions.ts
|
|
1218
|
-
/** Shared signatures used by authoring, reference diagnostics and assistant validation. */
|
|
1219
|
-
const DATE_FUNCTION_SIGNATURES = {
|
|
1220
|
-
date_eq: {
|
|
1221
|
-
args: [ValueType.date, ValueType.date],
|
|
1222
|
-
returns: ValueType.boolean
|
|
1223
|
-
},
|
|
1224
|
-
date_gt: {
|
|
1225
|
-
args: [ValueType.date, ValueType.date],
|
|
1226
|
-
returns: ValueType.boolean
|
|
1227
|
-
},
|
|
1228
|
-
date_gte: {
|
|
1229
|
-
args: [ValueType.date, ValueType.date],
|
|
1230
|
-
returns: ValueType.boolean
|
|
1231
|
-
},
|
|
1232
|
-
date_lt: {
|
|
1233
|
-
args: [ValueType.date, ValueType.date],
|
|
1234
|
-
returns: ValueType.boolean
|
|
1235
|
-
},
|
|
1236
|
-
date_lte: {
|
|
1237
|
-
args: [ValueType.date, ValueType.date],
|
|
1238
|
-
returns: ValueType.boolean
|
|
1239
|
-
},
|
|
1240
|
-
date_add_days: {
|
|
1241
|
-
args: [ValueType.date, ValueType.number],
|
|
1242
|
-
returns: ValueType.date
|
|
1243
|
-
},
|
|
1244
|
-
date_add_months: {
|
|
1245
|
-
args: [ValueType.date, ValueType.number],
|
|
1246
|
-
returns: ValueType.date
|
|
1247
|
-
},
|
|
1248
|
-
date_add_years: {
|
|
1249
|
-
args: [ValueType.date, ValueType.number],
|
|
1250
|
-
returns: ValueType.date
|
|
1251
|
-
},
|
|
1252
|
-
date_diff: {
|
|
1253
|
-
args: [ValueType.date, ValueType.date],
|
|
1254
|
-
returns: ValueType.number
|
|
1255
|
-
},
|
|
1256
|
-
date_min: {
|
|
1257
|
-
args: [ValueType.date],
|
|
1258
|
-
variadic: true,
|
|
1259
|
-
returns: ValueType.date
|
|
1260
|
-
},
|
|
1261
|
-
date_max: {
|
|
1262
|
-
args: [ValueType.date],
|
|
1263
|
-
variadic: true,
|
|
1264
|
-
returns: ValueType.date
|
|
1265
|
-
}
|
|
1266
|
-
};
|
|
1267
|
-
//#endregion
|
|
1268
|
-
//#region src/expressions/reference-integrity.ts
|
|
1269
|
-
const ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY = {
|
|
1270
|
-
component: false,
|
|
1271
|
-
item: true,
|
|
1272
|
-
survey: false
|
|
1273
|
-
};
|
|
1274
|
-
const ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY = {
|
|
1275
|
-
"display-condition": true,
|
|
1276
|
-
"disabled-condition": false,
|
|
1277
|
-
validation: false,
|
|
1278
|
-
"prefill-condition": false,
|
|
1279
|
-
"prefill-source": false,
|
|
1280
|
-
"template-value": false
|
|
1281
|
-
};
|
|
1282
|
-
/**
|
|
1283
|
-
* Display-condition cycles prevent active-response evaluation, even with prefills.
|
|
1284
|
-
* This item-level analysis is complemented by ActiveResponsePlan's full runtime graph,
|
|
1285
|
-
* which also includes inherited visibility, component activation and computed dependencies.
|
|
1286
|
-
*/
|
|
1287
|
-
const SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY = {
|
|
1288
|
-
"invalid-expression": {
|
|
1289
|
-
category: "reference-integrity",
|
|
1290
|
-
disposition: "blocking"
|
|
1291
|
-
},
|
|
1292
|
-
"unknown-response-ref": {
|
|
1293
|
-
category: "reference-integrity",
|
|
1294
|
-
disposition: "blocking"
|
|
1295
|
-
},
|
|
1296
|
-
"expression-reference-type-mismatch": {
|
|
1297
|
-
category: "reference-integrity",
|
|
1298
|
-
disposition: "blocking"
|
|
1299
|
-
},
|
|
1300
|
-
"unknown-reference-value": {
|
|
1301
|
-
category: "reference-integrity",
|
|
1302
|
-
disposition: "blocking"
|
|
1303
|
-
},
|
|
1304
|
-
"display-condition-cycle": {
|
|
1305
|
-
category: "display-dependency",
|
|
1306
|
-
disposition: "blocking"
|
|
1307
|
-
}
|
|
1308
|
-
};
|
|
1309
|
-
const createExpressionReferenceDiagnostic = (diagnostic) => ({
|
|
1310
|
-
...diagnostic,
|
|
1311
|
-
...SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY[diagnostic.code]
|
|
1312
|
-
});
|
|
1313
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1314
|
-
const isJsonExpression = (value) => isRecord(value) && typeof value.type === "string";
|
|
1315
|
-
/** Discover references even when a sibling expression is malformed and cannot deserialize. */
|
|
1316
|
-
function collectExpressionResponseReferences(expression) {
|
|
1317
|
-
const refs = /* @__PURE__ */ new Map();
|
|
1318
|
-
const visit = (node) => {
|
|
1319
|
-
if (!isRecord(node)) return;
|
|
1320
|
-
if (node.type === ExpressionType.ResponseVariable && isRecord(node.variableRef)) try {
|
|
1321
|
-
const reference = new ValueReference(node.variableRef);
|
|
1322
|
-
refs.set(reference.toString(), reference);
|
|
1323
|
-
} catch {}
|
|
1324
|
-
if (node.type === ExpressionType.ContextVariable) visit(node.key);
|
|
1325
|
-
if (node.type === ExpressionType.Function || node.type === ExpressionType.ContextVariable) {
|
|
1326
|
-
if (Array.isArray(node.arguments)) node.arguments.forEach(visit);
|
|
1327
|
-
}
|
|
1328
|
-
};
|
|
1329
|
-
visit(expression);
|
|
1330
|
-
return [...refs.values()];
|
|
1331
|
-
}
|
|
1332
|
-
/** Shared by integrity diagnostics and deletion guards for config-owned bounds. */
|
|
1333
|
-
const collectConfigValueExpressions = (config) => {
|
|
1334
|
-
const locations = [];
|
|
1335
|
-
const visit = (value, path) => {
|
|
1336
|
-
if (!value || typeof value !== "object") return;
|
|
1337
|
-
for (const [key, child] of Object.entries(value)) {
|
|
1338
|
-
const location = `${path}.${key}`;
|
|
1339
|
-
if (key === "valueExpression") {
|
|
1340
|
-
if (isJsonExpression(child)) locations.push({
|
|
1341
|
-
expression: child,
|
|
1342
|
-
location
|
|
1343
|
-
});
|
|
1344
|
-
} else visit(child, location);
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
visit(config, "config");
|
|
1348
|
-
return locations;
|
|
1349
|
-
};
|
|
1350
|
-
const collectItemExpressionLocations = (item) => {
|
|
1351
|
-
const locations = [];
|
|
1352
|
-
const add = (expression, location, ownerScope, surface) => {
|
|
1353
|
-
if (!isJsonExpression(expression)) return;
|
|
1354
|
-
locations.push({
|
|
1355
|
-
expression,
|
|
1356
|
-
location,
|
|
1357
|
-
ownerItemId: item.id,
|
|
1358
|
-
ownerItemKey: item.metadata?.itemLabel ?? "",
|
|
1359
|
-
ownerScope,
|
|
1360
|
-
surface
|
|
1361
|
-
});
|
|
1362
|
-
};
|
|
1363
|
-
add(item.displayConditions?.root, "displayConditions.root", "item", "display-condition");
|
|
1364
|
-
for (const [componentId, expression] of Object.entries(item.displayConditions?.components ?? {})) add(expression, `displayConditions.components.${componentId}`, "component", "display-condition");
|
|
1365
|
-
for (const [componentId, expression] of Object.entries(item.disabledConditions?.components ?? {})) add(expression, `disabledConditions.components.${componentId}`, "component", "disabled-condition");
|
|
1366
|
-
for (const [validationKey, expression] of Object.entries(item.validations ?? {})) add(expression, `validations.${validationKey}`, "item", "validation");
|
|
1367
|
-
for (const [prefillIndex, prefill] of (item.prefills ?? []).entries()) {
|
|
1368
|
-
const prefillId = prefill.id || String(prefillIndex);
|
|
1369
|
-
add(prefill.when, `prefills.${prefillId}.when`, "item", "prefill-condition");
|
|
1370
|
-
if (prefill.source.type === "expression") add(prefill.source.expression, `prefills.${prefillId}.source.expression`, "item", "prefill-source");
|
|
1371
|
-
}
|
|
1372
|
-
for (const { expression, location } of collectConfigValueExpressions(item.config)) add(expression, location, "item", "validation");
|
|
1373
|
-
return locations;
|
|
1374
|
-
};
|
|
1375
|
-
const collectSurveyExpressionLocations = (survey) => [...survey.surveyItems.flatMap(collectItemExpressionLocations), ...Object.entries(survey.templateValues ?? {}).flatMap(([templateKey, templateValue]) => templateValue.expression ? [{
|
|
1376
|
-
expression: templateValue.expression,
|
|
1377
|
-
location: `templateValues.${templateKey}.expression`,
|
|
1378
|
-
ownerScope: "survey",
|
|
1379
|
-
surface: "template-value"
|
|
1380
|
-
}] : [])];
|
|
1381
|
-
const candidateRefs = (unknownRef, availableRefs) => {
|
|
1382
|
-
let parsed;
|
|
1383
|
-
try {
|
|
1384
|
-
parsed = new ValueReference(unknownRef);
|
|
1385
|
-
} catch {
|
|
1386
|
-
return availableRefs.slice(0, 5);
|
|
1387
|
-
}
|
|
1388
|
-
const sameItemAndMethod = availableRefs.filter((candidate) => {
|
|
1389
|
-
try {
|
|
1390
|
-
return new ValueReference(candidate).name === parsed?.name;
|
|
1391
|
-
} catch {
|
|
1392
|
-
return false;
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
|
-
return (sameItemAndMethod.length > 0 ? sameItemAndMethod : availableRefs).slice(0, 5);
|
|
1396
|
-
};
|
|
1397
|
-
const findCyclicItemDisplayDependencyEdges = ({ edges, itemIds }) => {
|
|
1398
|
-
const adjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
|
|
1399
|
-
const reverseAdjacency = new Map(itemIds.map((itemId) => [itemId, /* @__PURE__ */ new Set()]));
|
|
1400
|
-
for (const edge of edges) {
|
|
1401
|
-
adjacency.get(edge.fromItemId)?.add(edge.toItemId);
|
|
1402
|
-
reverseAdjacency.get(edge.toItemId)?.add(edge.fromItemId);
|
|
1403
|
-
}
|
|
1404
|
-
const finishOrder = [];
|
|
1405
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1406
|
-
for (const startItemId of itemIds) {
|
|
1407
|
-
if (visited.has(startItemId)) continue;
|
|
1408
|
-
const stack = [{
|
|
1409
|
-
expanded: false,
|
|
1410
|
-
itemId: startItemId
|
|
1411
|
-
}];
|
|
1412
|
-
while (stack.length > 0) {
|
|
1413
|
-
const current = stack.pop();
|
|
1414
|
-
if (!current) break;
|
|
1415
|
-
if (current.expanded) {
|
|
1416
|
-
finishOrder.push(current.itemId);
|
|
1417
|
-
continue;
|
|
1418
|
-
}
|
|
1419
|
-
if (visited.has(current.itemId)) continue;
|
|
1420
|
-
visited.add(current.itemId);
|
|
1421
|
-
stack.push({
|
|
1422
|
-
expanded: true,
|
|
1423
|
-
itemId: current.itemId
|
|
1424
|
-
});
|
|
1425
|
-
const dependencies = [...adjacency.get(current.itemId) ?? []];
|
|
1426
|
-
for (let index = dependencies.length - 1; index >= 0; index -= 1) {
|
|
1427
|
-
const dependencyId = dependencies[index];
|
|
1428
|
-
if (visited.has(dependencyId)) continue;
|
|
1429
|
-
stack.push({
|
|
1430
|
-
expanded: false,
|
|
1431
|
-
itemId: dependencyId
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
const componentByItemId = /* @__PURE__ */ new Map();
|
|
1437
|
-
const componentMembers = /* @__PURE__ */ new Map();
|
|
1438
|
-
for (const startItemId of finishOrder.reverse()) {
|
|
1439
|
-
if (componentByItemId.has(startItemId)) continue;
|
|
1440
|
-
const componentId = componentMembers.size;
|
|
1441
|
-
const members = [];
|
|
1442
|
-
const stack = [startItemId];
|
|
1443
|
-
componentByItemId.set(startItemId, componentId);
|
|
1444
|
-
while (stack.length > 0) {
|
|
1445
|
-
const itemId = stack.pop();
|
|
1446
|
-
if (!itemId) break;
|
|
1447
|
-
members.push(itemId);
|
|
1448
|
-
for (const dependencyOwnerId of reverseAdjacency.get(itemId) ?? []) {
|
|
1449
|
-
if (componentByItemId.has(dependencyOwnerId)) continue;
|
|
1450
|
-
componentByItemId.set(dependencyOwnerId, componentId);
|
|
1451
|
-
stack.push(dependencyOwnerId);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
componentMembers.set(componentId, members.sort());
|
|
1455
|
-
}
|
|
1456
|
-
return edges.flatMap((edge) => {
|
|
1457
|
-
const componentId = componentByItemId.get(edge.fromItemId);
|
|
1458
|
-
if (componentId === void 0 || componentId !== componentByItemId.get(edge.toItemId)) return [];
|
|
1459
|
-
const cycleItemIds = componentMembers.get(componentId) ?? [];
|
|
1460
|
-
if (edge.fromItemId !== edge.toItemId && cycleItemIds.length < 2) return [];
|
|
1461
|
-
return [{
|
|
1462
|
-
edge,
|
|
1463
|
-
cycleItemIds
|
|
1464
|
-
}];
|
|
1465
|
-
});
|
|
1466
|
-
};
|
|
1467
|
-
const inferExpressionType = (expression, slots) => {
|
|
1468
|
-
if (!expression) return void 0;
|
|
1469
|
-
if (expression.type === ExpressionType.Const) return expression.value?.type;
|
|
1470
|
-
if (expression.type === ExpressionType.ResponseVariable) return slots.get(new ValueReference(expression.variableRef).toString())?.valueType;
|
|
1471
|
-
if (expression.type === ExpressionType.ContextVariable) return expression.contextType === "locale" ? ValueType.string : expression.asType;
|
|
1472
|
-
switch (expression.functionName) {
|
|
1473
|
-
case "and":
|
|
1474
|
-
case "or":
|
|
1475
|
-
case "not":
|
|
1476
|
-
case "eq":
|
|
1477
|
-
case "gt":
|
|
1478
|
-
case "gte":
|
|
1479
|
-
case "lt":
|
|
1480
|
-
case "lte":
|
|
1481
|
-
case "in_range":
|
|
1482
|
-
case "list_contains":
|
|
1483
|
-
case "str_eq":
|
|
1484
|
-
case "date_eq":
|
|
1485
|
-
case "date_gt":
|
|
1486
|
-
case "date_gte":
|
|
1487
|
-
case "date_lt":
|
|
1488
|
-
case "date_lte": return ValueType.boolean;
|
|
1489
|
-
case "date_add_days":
|
|
1490
|
-
case "date_add_months":
|
|
1491
|
-
case "date_add_years":
|
|
1492
|
-
case "date_min":
|
|
1493
|
-
case "date_max": return ValueType.date;
|
|
1494
|
-
case "date_diff": return ValueType.number;
|
|
1495
|
-
case "sum":
|
|
1496
|
-
case "min":
|
|
1497
|
-
case "max": return ValueType.number;
|
|
1498
|
-
}
|
|
1499
|
-
};
|
|
1500
|
-
const itemTypeForList = (type) => {
|
|
1501
|
-
switch (type) {
|
|
1502
|
-
case ValueType.stringArray: return ValueType.string;
|
|
1503
|
-
case ValueType.referenceArray: return ValueType.reference;
|
|
1504
|
-
case ValueType.numberArray: return ValueType.number;
|
|
1505
|
-
case ValueType.dateArray: return ValueType.date;
|
|
1506
|
-
case ValueType.durationArray: return ValueType.duration;
|
|
1507
|
-
default: return;
|
|
1508
|
-
}
|
|
1509
|
-
};
|
|
1510
|
-
const inspectFunctionReferenceCompatibility = ({ diagnostics, expression, location, slots }) => {
|
|
1511
|
-
if (expression.type === ExpressionType.ContextVariable) {
|
|
1512
|
-
for (const argument of [expression.key, ...expression.arguments ?? []]) if (argument) inspectFunctionReferenceCompatibility({
|
|
1513
|
-
diagnostics,
|
|
1514
|
-
expression: argument,
|
|
1515
|
-
location,
|
|
1516
|
-
slots
|
|
1517
|
-
});
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
if (expression.type !== ExpressionType.Function) return;
|
|
1521
|
-
expression.arguments.forEach((argument) => {
|
|
1522
|
-
if (argument) inspectFunctionReferenceCompatibility({
|
|
1523
|
-
diagnostics,
|
|
1524
|
-
expression: argument,
|
|
1525
|
-
location,
|
|
1526
|
-
slots
|
|
1527
|
-
});
|
|
1528
|
-
});
|
|
1529
|
-
const signature = DATE_FUNCTION_SIGNATURES[expression.functionName];
|
|
1530
|
-
if (signature) {
|
|
1531
|
-
const invalidArity = signature.variadic ? expression.arguments.length < 1 : expression.arguments.length !== signature.args.length;
|
|
1532
|
-
const invalidType = expression.arguments.some((arg, i) => {
|
|
1533
|
-
const actual = inferExpressionType(arg, slots);
|
|
1534
|
-
return actual && actual !== signature.args[signature.variadic ? 0 : i];
|
|
1535
|
-
});
|
|
1536
|
-
if (invalidArity || invalidType) diagnostics.push({
|
|
1537
|
-
code: "expression-reference-type-mismatch",
|
|
1538
|
-
message: `${location.location} has invalid arguments for ${expression.functionName}.`,
|
|
1539
|
-
location: location.location,
|
|
1540
|
-
ownerItemId: location.ownerItemId,
|
|
1541
|
-
ownerItemKey: location.ownerItemKey
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
const [left, right] = expression.arguments;
|
|
1545
|
-
const leftType = inferExpressionType(left, slots);
|
|
1546
|
-
const rightType = inferExpressionType(right, slots);
|
|
1547
|
-
if (expression.functionName === "list_contains" ? Boolean(leftType && rightType && itemTypeForList(leftType) !== rightType) : expression.functionName === "eq" ? Boolean(leftType && rightType && leftType !== rightType) : false) diagnostics.push({
|
|
1548
|
-
code: "expression-reference-type-mismatch",
|
|
1549
|
-
message: `${location.location} compares incompatible values (${leftType} and ${rightType}).`,
|
|
1550
|
-
location: location.location,
|
|
1551
|
-
ownerItemId: location.ownerItemId,
|
|
1552
|
-
ownerItemKey: location.ownerItemKey
|
|
1553
|
-
});
|
|
1554
|
-
if (expression.functionName !== "eq" && expression.functionName !== "str_eq" && expression.functionName !== "list_contains") return;
|
|
1555
|
-
const pairs = [[left, right], [right, left]];
|
|
1556
|
-
for (const [variable, constant] of pairs) {
|
|
1557
|
-
if (variable?.type !== ExpressionType.ResponseVariable || constant?.type !== ExpressionType.Const || !constant.value) continue;
|
|
1558
|
-
const slot = slots.get(new ValueReference(variable.variableRef).toString());
|
|
1559
|
-
const domain = slot?.allowedValues ?? slot?.allowedReferenceValues?.map((value) => ({
|
|
1560
|
-
type: ValueType.reference,
|
|
1561
|
-
value
|
|
1562
|
-
}));
|
|
1563
|
-
if (!domain) continue;
|
|
1564
|
-
for (const invalid of invalidResponseDomainValues(constant.value, domain)) {
|
|
1565
|
-
const referenceValue = typeof invalid.value === "string" ? invalid.value : JSON.stringify(invalid.value);
|
|
1566
|
-
diagnostics.push({
|
|
1567
|
-
code: "unknown-reference-value",
|
|
1568
|
-
message: `${location.location} compares slot "${variable.variableRef.slotId}" with invalid value ${JSON.stringify(invalid)}. Allowed values: ${JSON.stringify(domain)}.`,
|
|
1569
|
-
location: location.location,
|
|
1570
|
-
ownerItemId: location.ownerItemId,
|
|
1571
|
-
ownerItemKey: location.ownerItemKey,
|
|
1572
|
-
reference: new ValueReference(variable.variableRef).toString(),
|
|
1573
|
-
referenceValue,
|
|
1574
|
-
candidates: domain.map((value) => String(value.value))
|
|
1575
|
-
});
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
};
|
|
1579
|
-
const analyzeSurveyExpressionReferences = ({ responseSlots, survey }) => {
|
|
1580
|
-
const slots = new Map(responseSlots.map((slot) => [slot.ref, slot]));
|
|
1581
|
-
const availableRefs = responseSlots.map((slot) => slot.ref);
|
|
1582
|
-
const diagnostics = [];
|
|
1583
|
-
const surveyItemIds = survey.surveyItems.map((item) => item.id);
|
|
1584
|
-
const surveyItemIdSet = new Set(surveyItemIds);
|
|
1585
|
-
const itemDisplayDependencyEdges = [];
|
|
1586
|
-
for (const location of collectSurveyExpressionLocations(survey)) {
|
|
1587
|
-
let parsed;
|
|
1588
|
-
try {
|
|
1589
|
-
parsed = Expression.deserialize(location.expression);
|
|
1590
|
-
} catch (error) {
|
|
1591
|
-
diagnostics.push({
|
|
1592
|
-
code: "invalid-expression",
|
|
1593
|
-
message: `${location.location} contains invalid expression JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
1594
|
-
location: location.location,
|
|
1595
|
-
ownerItemId: location.ownerItemId,
|
|
1596
|
-
ownerItemKey: location.ownerItemKey
|
|
1597
|
-
});
|
|
1598
|
-
continue;
|
|
1599
|
-
}
|
|
1600
|
-
for (const ref of parsed?.responseVariableRefs ?? []) {
|
|
1601
|
-
const reference = ref.toString();
|
|
1602
|
-
if (location.ownerItemId && ITEM_DISPLAY_DEPENDENCY_OWNER_SCOPE_POLICY[location.ownerScope] && ITEM_DISPLAY_DEPENDENCY_SURFACE_POLICY[location.surface] && surveyItemIdSet.has(slots.get(reference)?.itemId ?? "")) itemDisplayDependencyEdges.push({
|
|
1603
|
-
fromItemId: location.ownerItemId,
|
|
1604
|
-
location,
|
|
1605
|
-
reference,
|
|
1606
|
-
toItemId: slots.get(reference).itemId
|
|
1607
|
-
});
|
|
1608
|
-
if (!slots.has(reference)) diagnostics.push({
|
|
1609
|
-
code: "unknown-response-ref",
|
|
1610
|
-
message: `${location.location} references unknown response slot "${reference}".`,
|
|
1611
|
-
location: location.location,
|
|
1612
|
-
ownerItemId: location.ownerItemId,
|
|
1613
|
-
ownerItemKey: location.ownerItemKey,
|
|
1614
|
-
reference,
|
|
1615
|
-
candidates: candidateRefs(reference, availableRefs)
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
if (location.location.endsWith(".valueExpression")) {
|
|
1619
|
-
const actual = inferExpressionType(location.expression, slots);
|
|
1620
|
-
if (actual && actual !== ValueType.date) diagnostics.push({
|
|
1621
|
-
code: "expression-reference-type-mismatch",
|
|
1622
|
-
message: `${location.location} must return a date.`,
|
|
1623
|
-
location: location.location,
|
|
1624
|
-
ownerItemId: location.ownerItemId,
|
|
1625
|
-
ownerItemKey: location.ownerItemKey
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
inspectFunctionReferenceCompatibility({
|
|
1629
|
-
diagnostics,
|
|
1630
|
-
expression: location.expression,
|
|
1631
|
-
location,
|
|
1632
|
-
slots
|
|
1633
|
-
});
|
|
1634
|
-
}
|
|
1635
|
-
for (const { cycleItemIds, edge } of findCyclicItemDisplayDependencyEdges({
|
|
1636
|
-
edges: itemDisplayDependencyEdges,
|
|
1637
|
-
itemIds: surveyItemIds
|
|
1638
|
-
})) {
|
|
1639
|
-
const isSelfReference = edge.fromItemId === edge.toItemId;
|
|
1640
|
-
diagnostics.push({
|
|
1641
|
-
code: "display-condition-cycle",
|
|
1642
|
-
message: isSelfReference ? `${edge.location.location} for item "${edge.fromItemId}" references its own response "${edge.reference}", creating a circular display dependency.` : `${edge.location.location} for item "${edge.fromItemId}" references item "${edge.toItemId}" within a circular display dependency among ${cycleItemIds.map((itemId) => `"${itemId}"`).join(", ")}.`,
|
|
1643
|
-
location: edge.location.location,
|
|
1644
|
-
ownerItemId: edge.location.ownerItemId,
|
|
1645
|
-
ownerItemKey: edge.location.ownerItemKey,
|
|
1646
|
-
reference: edge.reference
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
return diagnostics.map(createExpressionReferenceDiagnostic);
|
|
1650
|
-
};
|
|
1651
|
-
const expressionReferenceDiagnosticKey = (diagnostic) => [
|
|
1652
|
-
diagnostic.code,
|
|
1653
|
-
diagnostic.ownerItemId ?? "survey",
|
|
1654
|
-
diagnostic.location,
|
|
1655
|
-
diagnostic.reference ?? "",
|
|
1656
|
-
diagnostic.referenceValue ?? ""
|
|
1657
|
-
].join("\0");
|
|
1658
|
-
const findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories = ({ after, before }) => {
|
|
1659
|
-
const existing = new Set(before.map(expressionReferenceDiagnosticKey));
|
|
1660
|
-
return after.filter((diagnostic) => !existing.has(expressionReferenceDiagnosticKey(diagnostic)));
|
|
1661
|
-
};
|
|
1662
|
-
const isBlockingSurveyExpressionReferenceDiagnostic = (diagnostic) => {
|
|
1663
|
-
switch (diagnostic.disposition) {
|
|
1664
|
-
case "blocking": return true;
|
|
1665
|
-
case "advisory": return false;
|
|
1666
|
-
default: return true;
|
|
1667
|
-
}
|
|
1668
|
-
};
|
|
1669
|
-
const findIntroducedBlockingExpressionReferenceDiagnostics = ({ after, before }) => findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories({
|
|
1670
|
-
after,
|
|
1671
|
-
before
|
|
1672
|
-
}).filter(isBlockingSurveyExpressionReferenceDiagnostic);
|
|
1673
|
-
const replaceResponseVariableReferences = (value, replacements, optionReplacements = {}) => {
|
|
1674
|
-
let changed = false;
|
|
1675
|
-
const visit = (current) => {
|
|
1676
|
-
if (Array.isArray(current)) return current.map(visit);
|
|
1677
|
-
if (!isRecord(current)) return current;
|
|
1678
|
-
const next = Object.fromEntries(Object.entries(current).map(([key, entry]) => [key, visit(entry)]));
|
|
1679
|
-
if (next.type === ExpressionType.ResponseVariable && isRecord(next.variableRef) && typeof next.variableRef.slotId === "string" && replacements[next.variableRef.slotId]) {
|
|
1680
|
-
next.variableRef.slotId = replacements[next.variableRef.slotId];
|
|
1681
|
-
changed = true;
|
|
1682
|
-
}
|
|
1683
|
-
if (next.type === ExpressionType.Const && isRecord(next.value)) {
|
|
1684
|
-
const remap = (id) => {
|
|
1685
|
-
if (!Object.hasOwn(optionReplacements, id)) return id;
|
|
1686
|
-
changed = true;
|
|
1687
|
-
return optionReplacements[id];
|
|
1688
|
-
};
|
|
1689
|
-
if (next.value.type === ValueType.reference && typeof next.value.value === "string") next.value.value = remap(next.value.value);
|
|
1690
|
-
if (next.value.type === ValueType.referenceArray && Array.isArray(next.value.value)) next.value.value = next.value.value.map((id) => typeof id === "string" ? remap(id) : id);
|
|
1691
|
-
}
|
|
1692
|
-
return next;
|
|
1693
|
-
};
|
|
1694
|
-
const rewritten = visit(value);
|
|
1695
|
-
return {
|
|
1696
|
-
changed,
|
|
1697
|
-
value: rewritten
|
|
1698
|
-
};
|
|
1699
|
-
};
|
|
1700
|
-
const responseValueReferenceIds = (values) => values?.flatMap((value) => value.type === ValueType.reference ? [value.value] : value.type === ValueType.referenceArray ? value.value : []);
|
|
1701
|
-
//#endregion
|
|
1702
|
-
//#region src/expressions/editors/expression-editor.ts
|
|
1703
|
-
var ExpressionEditor = class {
|
|
1704
|
-
returnType;
|
|
1705
|
-
_editorConfig;
|
|
1706
|
-
get editorConfig() {
|
|
1707
|
-
return this._editorConfig;
|
|
1708
|
-
}
|
|
1709
|
-
withEditorConfig(editorConfig) {
|
|
1710
|
-
this._editorConfig = editorConfig;
|
|
1711
|
-
return this;
|
|
1712
|
-
}
|
|
1713
|
-
};
|
|
1714
|
-
var ConstStringArrayEditor = class extends ExpressionEditor {
|
|
1715
|
-
returnType = ValueType.stringArray;
|
|
1716
|
-
_values;
|
|
1717
|
-
constructor(values, editorConfig) {
|
|
1718
|
-
super();
|
|
1719
|
-
this._values = values;
|
|
1720
|
-
this._editorConfig = editorConfig;
|
|
1721
|
-
}
|
|
1722
|
-
get values() {
|
|
1723
|
-
return this._values;
|
|
1724
|
-
}
|
|
1725
|
-
set values(values) {
|
|
1726
|
-
this._values = values;
|
|
1727
|
-
}
|
|
1728
|
-
getExpression() {
|
|
1729
|
-
return new ConstExpression({
|
|
1730
|
-
type: ValueType.stringArray,
|
|
1731
|
-
value: this._values
|
|
1732
|
-
}, this._editorConfig);
|
|
1733
|
-
}
|
|
1734
|
-
};
|
|
1735
|
-
var ConstStringEditor = class extends ExpressionEditor {
|
|
1736
|
-
returnType = ValueType.string;
|
|
1737
|
-
_value;
|
|
1738
|
-
constructor(value, editorConfig) {
|
|
1739
|
-
super();
|
|
1740
|
-
this._value = value;
|
|
1741
|
-
this._editorConfig = editorConfig;
|
|
1742
|
-
}
|
|
1743
|
-
get value() {
|
|
1744
|
-
return this._value;
|
|
1745
|
-
}
|
|
1746
|
-
set value(value) {
|
|
1747
|
-
this._value = value;
|
|
1748
|
-
}
|
|
1749
|
-
getExpression() {
|
|
1750
|
-
return new ConstExpression({
|
|
1751
|
-
type: ValueType.string,
|
|
1752
|
-
value: this._value
|
|
1753
|
-
}, this._editorConfig);
|
|
1754
|
-
}
|
|
1755
|
-
};
|
|
1756
|
-
var ConstNumberEditor = class extends ExpressionEditor {
|
|
1757
|
-
returnType = ValueType.number;
|
|
1758
|
-
_value;
|
|
1759
|
-
constructor(value, editorConfig) {
|
|
1760
|
-
super();
|
|
1761
|
-
this._value = value;
|
|
1762
|
-
this._editorConfig = editorConfig;
|
|
1763
|
-
}
|
|
1764
|
-
get value() {
|
|
1765
|
-
return this._value;
|
|
1766
|
-
}
|
|
1767
|
-
set value(value) {
|
|
1768
|
-
this._value = value;
|
|
1769
|
-
}
|
|
1770
|
-
getExpression() {
|
|
1771
|
-
return new ConstExpression({
|
|
1772
|
-
type: ValueType.number,
|
|
1773
|
-
value: this._value
|
|
1774
|
-
}, this._editorConfig);
|
|
1775
|
-
}
|
|
1776
|
-
};
|
|
1777
|
-
var ConstBooleanEditor = class extends ExpressionEditor {
|
|
1778
|
-
returnType = ValueType.boolean;
|
|
1779
|
-
_value;
|
|
1780
|
-
constructor(value, editorConfig) {
|
|
1781
|
-
super();
|
|
1782
|
-
this._value = value;
|
|
1783
|
-
this._editorConfig = editorConfig;
|
|
1784
|
-
}
|
|
1785
|
-
get value() {
|
|
1786
|
-
return this._value;
|
|
1787
|
-
}
|
|
1788
|
-
set value(value) {
|
|
1789
|
-
this._value = value;
|
|
1790
|
-
}
|
|
1791
|
-
getExpression() {
|
|
1792
|
-
return new ConstExpression({
|
|
1793
|
-
type: ValueType.boolean,
|
|
1794
|
-
value: this._value
|
|
1795
|
-
}, this._editorConfig);
|
|
1796
|
-
}
|
|
1797
|
-
};
|
|
1798
|
-
var ConstDateEditor = class extends ExpressionEditor {
|
|
1799
|
-
returnType = ValueType.date;
|
|
1800
|
-
_value;
|
|
1801
|
-
constructor(value, editorConfig) {
|
|
1802
|
-
super();
|
|
1803
|
-
this._value = value;
|
|
1804
|
-
this._editorConfig = editorConfig;
|
|
1805
|
-
}
|
|
1806
|
-
get value() {
|
|
1807
|
-
return this._value;
|
|
1808
|
-
}
|
|
1809
|
-
set value(value) {
|
|
1810
|
-
this._value = value;
|
|
1811
|
-
}
|
|
1812
|
-
getExpression() {
|
|
1813
|
-
const value = this._value;
|
|
1814
|
-
return new ConstExpression({
|
|
1815
|
-
type: ValueType.date,
|
|
1816
|
-
value
|
|
1817
|
-
}, this._editorConfig);
|
|
1818
|
-
}
|
|
1819
|
-
};
|
|
1820
|
-
var ConstNumberArrayEditor = class extends ExpressionEditor {
|
|
1821
|
-
returnType = ValueType.numberArray;
|
|
1822
|
-
_values;
|
|
1823
|
-
constructor(values, editorConfig) {
|
|
1824
|
-
super();
|
|
1825
|
-
this._values = values;
|
|
1826
|
-
this._editorConfig = editorConfig;
|
|
1827
|
-
}
|
|
1828
|
-
get values() {
|
|
1829
|
-
return this._values;
|
|
1830
|
-
}
|
|
1831
|
-
set values(values) {
|
|
1832
|
-
this._values = values;
|
|
1833
|
-
}
|
|
1834
|
-
getExpression() {
|
|
1835
|
-
return new ConstExpression({
|
|
1836
|
-
type: ValueType.numberArray,
|
|
1837
|
-
value: this._values
|
|
1838
|
-
}, this._editorConfig);
|
|
1839
|
-
}
|
|
1840
|
-
};
|
|
1841
|
-
var ConstDateArrayEditor = class extends ExpressionEditor {
|
|
1842
|
-
returnType = ValueType.dateArray;
|
|
1843
|
-
_values;
|
|
1844
|
-
constructor(values, editorConfig) {
|
|
1845
|
-
super();
|
|
1846
|
-
this._values = values;
|
|
1847
|
-
this._editorConfig = editorConfig;
|
|
1848
|
-
}
|
|
1849
|
-
get values() {
|
|
1850
|
-
return this._values;
|
|
1851
|
-
}
|
|
1852
|
-
set values(values) {
|
|
1853
|
-
this._values = values;
|
|
1854
|
-
}
|
|
1855
|
-
getExpression() {
|
|
1856
|
-
const value = this._values;
|
|
1857
|
-
return new ConstExpression({
|
|
1858
|
-
type: ValueType.dateArray,
|
|
1859
|
-
value
|
|
1860
|
-
}, this._editorConfig);
|
|
1861
|
-
}
|
|
1862
|
-
};
|
|
1863
|
-
var ResponseVariableEditor = class extends ExpressionEditor {
|
|
1864
|
-
_variableName;
|
|
1865
|
-
_variableRef;
|
|
1866
|
-
constructor(variableName, variableType, editorConfig) {
|
|
1867
|
-
super();
|
|
1868
|
-
this._variableName = new ValueReference(variableName).toString();
|
|
1869
|
-
this._variableRef = new ValueReference(variableName);
|
|
1870
|
-
this._editorConfig = editorConfig;
|
|
1871
|
-
this.returnType = variableType;
|
|
1872
|
-
}
|
|
1873
|
-
get variableName() {
|
|
1874
|
-
return this._variableName;
|
|
1875
|
-
}
|
|
1876
|
-
get variableRef() {
|
|
1877
|
-
return this._variableRef;
|
|
1878
|
-
}
|
|
1879
|
-
getExpression() {
|
|
1880
|
-
return new ResponseVariableExpression(this._variableName, this._editorConfig);
|
|
1881
|
-
}
|
|
1882
|
-
};
|
|
1883
|
-
var ContextVariableEditor = class extends ExpressionEditor {
|
|
1884
|
-
_contextType;
|
|
1885
|
-
_key;
|
|
1886
|
-
_args;
|
|
1887
|
-
_asType;
|
|
1888
|
-
constructor(contextType, key, args, asType, editorConfig) {
|
|
1889
|
-
super();
|
|
1890
|
-
this._contextType = contextType;
|
|
1891
|
-
this._key = key;
|
|
1892
|
-
this._args = args;
|
|
1893
|
-
this._asType = asType;
|
|
1894
|
-
this._editorConfig = editorConfig;
|
|
1895
|
-
}
|
|
1896
|
-
getExpression() {
|
|
1897
|
-
return new ContextVariableExpression(this._contextType, this._key?.getExpression(), this._args?.map((arg) => arg.getExpression()), this._asType, this._editorConfig);
|
|
1898
|
-
}
|
|
1899
|
-
};
|
|
1900
|
-
var CtxLocaleEditor = class extends ContextVariableEditor {
|
|
1901
|
-
returnType = ValueType.string;
|
|
1902
|
-
constructor(editorConfig) {
|
|
1903
|
-
super(ContextVariableType.Locale, void 0, void 0, ValueType.string, editorConfig);
|
|
1904
|
-
}
|
|
1905
|
-
};
|
|
1906
|
-
var CtxPFlagIsDefinedEditor = class extends ContextVariableEditor {
|
|
1907
|
-
returnType = ValueType.boolean;
|
|
1908
|
-
constructor(key, editorConfig) {
|
|
1909
|
-
super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.boolean, editorConfig);
|
|
1910
|
-
}
|
|
1911
|
-
};
|
|
1912
|
-
var CtxPFlagStringEditor = class extends ContextVariableEditor {
|
|
1913
|
-
returnType = ValueType.string;
|
|
1914
|
-
constructor(key, editorConfig) {
|
|
1915
|
-
super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.string, editorConfig);
|
|
1916
|
-
}
|
|
1917
|
-
};
|
|
1918
|
-
var CtxPFlagNumEditor = class extends ContextVariableEditor {
|
|
1919
|
-
returnType = ValueType.number;
|
|
1920
|
-
constructor(key, editorConfig) {
|
|
1921
|
-
super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.number, editorConfig);
|
|
1922
|
-
}
|
|
1923
|
-
};
|
|
1924
|
-
var CtxPFlagDateEditor = class extends ContextVariableEditor {
|
|
1925
|
-
returnType = ValueType.date;
|
|
1926
|
-
constructor(key, editorConfig) {
|
|
1927
|
-
super(ContextVariableType.ParticipantFlag, key, void 0, ValueType.date, editorConfig);
|
|
1928
|
-
}
|
|
1929
|
-
};
|
|
1930
|
-
var CtxCustomValueEditor = class extends ContextVariableEditor {
|
|
1931
|
-
constructor(key, asType, editorConfig) {
|
|
1932
|
-
super(ContextVariableType.CustomValue, key, void 0, asType, editorConfig);
|
|
1933
|
-
this.returnType = asType;
|
|
1934
|
-
}
|
|
1935
|
-
};
|
|
1936
|
-
var CtxCustomExpressionEditor = class extends ContextVariableEditor {
|
|
1937
|
-
constructor(key, args, asType, editorConfig) {
|
|
1938
|
-
super(ContextVariableType.CustomExpression, key, args, asType, editorConfig);
|
|
1939
|
-
this.returnType = asType;
|
|
1940
|
-
}
|
|
1941
|
-
};
|
|
1942
|
-
var GroupExpressionEditor = class extends ExpressionEditor {
|
|
1943
|
-
_args;
|
|
1944
|
-
constructor(args, editorConfig) {
|
|
1945
|
-
super();
|
|
1946
|
-
this._args = args;
|
|
1947
|
-
this._editorConfig = editorConfig;
|
|
1948
|
-
}
|
|
1949
|
-
get args() {
|
|
1950
|
-
return this._args;
|
|
1951
|
-
}
|
|
1952
|
-
addArg(arg, position) {
|
|
1953
|
-
if (position === void 0) this._args.push(arg);
|
|
1954
|
-
else this._args.splice(position, 0, arg);
|
|
1955
|
-
}
|
|
1956
|
-
removeArg(position) {
|
|
1957
|
-
this._args.splice(position, 1);
|
|
1958
|
-
}
|
|
1959
|
-
replaceArg(position, arg) {
|
|
1960
|
-
this._args[position] = arg;
|
|
1961
|
-
}
|
|
1962
|
-
swapArgs(activeIndex, overIndex) {
|
|
1963
|
-
const newOrder = [...this._args];
|
|
1964
|
-
newOrder.splice(activeIndex, 1);
|
|
1965
|
-
newOrder.splice(overIndex, 0, this._args[activeIndex]);
|
|
1966
|
-
this._args = newOrder;
|
|
1967
|
-
}
|
|
1968
|
-
};
|
|
1969
|
-
var AndExpressionEditor = class extends GroupExpressionEditor {
|
|
1970
|
-
returnType = ValueType.boolean;
|
|
1971
|
-
constructor(args, editorConfig) {
|
|
1972
|
-
if (args.some((arg) => arg.returnType !== ValueType.boolean)) throw new Error("And expression editor must have boolean arguments");
|
|
1973
|
-
super(args, editorConfig);
|
|
1974
|
-
}
|
|
1975
|
-
getExpression() {
|
|
1976
|
-
return new FunctionExpression("and", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
1977
|
-
}
|
|
1978
|
-
};
|
|
1979
|
-
var OrExpressionEditor = class extends GroupExpressionEditor {
|
|
1980
|
-
returnType = ValueType.boolean;
|
|
1981
|
-
constructor(args, editorConfig) {
|
|
1982
|
-
if (args.some((arg) => arg.returnType !== ValueType.boolean)) throw new Error("Or expression editor must have boolean arguments");
|
|
1983
|
-
super(args, editorConfig);
|
|
1984
|
-
}
|
|
1985
|
-
getExpression() {
|
|
1986
|
-
return new FunctionExpression("or", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
1987
|
-
}
|
|
1988
|
-
};
|
|
1989
|
-
var StrListContainsExpressionEditor = class extends ExpressionEditor {
|
|
1990
|
-
returnType = ValueType.boolean;
|
|
1991
|
-
_list;
|
|
1992
|
-
_item;
|
|
1993
|
-
constructor(list, item, editorConfig) {
|
|
1994
|
-
super();
|
|
1995
|
-
if (list.returnType !== ValueType.stringArray) throw new Error("List contains expression editor must have a string array list");
|
|
1996
|
-
if (item.returnType !== ValueType.string) throw new Error("List contains expression editor must have a string item");
|
|
1997
|
-
this._list = list;
|
|
1998
|
-
this._item = item;
|
|
1999
|
-
this._editorConfig = editorConfig;
|
|
2000
|
-
}
|
|
2001
|
-
get list() {
|
|
2002
|
-
return this._list;
|
|
2003
|
-
}
|
|
2004
|
-
get item() {
|
|
2005
|
-
return this._item;
|
|
2006
|
-
}
|
|
2007
|
-
set list(list) {
|
|
2008
|
-
this._list = list;
|
|
2009
|
-
}
|
|
2010
|
-
set item(item) {
|
|
2011
|
-
this._item = item;
|
|
2012
|
-
}
|
|
2013
|
-
getExpression() {
|
|
2014
|
-
return new FunctionExpression("list_contains", [this._list?.getExpression(), this._item?.getExpression()], this._editorConfig);
|
|
2015
|
-
}
|
|
2016
|
-
};
|
|
2017
|
-
var StrEqExpressionEditor = class extends ExpressionEditor {
|
|
2018
|
-
returnType = ValueType.boolean;
|
|
2019
|
-
_a;
|
|
2020
|
-
_b;
|
|
2021
|
-
constructor(a, b, editorConfig) {
|
|
2022
|
-
super();
|
|
2023
|
-
this._a = a;
|
|
2024
|
-
this._b = b;
|
|
2025
|
-
this._editorConfig = editorConfig;
|
|
2026
|
-
}
|
|
2027
|
-
get a() {
|
|
2028
|
-
return this._a;
|
|
2029
|
-
}
|
|
2030
|
-
get b() {
|
|
2031
|
-
return this._b;
|
|
2032
|
-
}
|
|
2033
|
-
set a(a) {
|
|
2034
|
-
this._a = a;
|
|
2035
|
-
}
|
|
2036
|
-
set b(b) {
|
|
2037
|
-
this._b = b;
|
|
2038
|
-
}
|
|
2039
|
-
getExpression() {
|
|
2040
|
-
return new FunctionExpression("str_eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
2041
|
-
}
|
|
2042
|
-
};
|
|
2043
|
-
var EqExpressionEditor = class extends ExpressionEditor {
|
|
2044
|
-
returnType = ValueType.boolean;
|
|
2045
|
-
_a;
|
|
2046
|
-
_b;
|
|
2047
|
-
constructor(a, b, editorConfig) {
|
|
2048
|
-
super();
|
|
2049
|
-
this._a = a;
|
|
2050
|
-
this._b = b;
|
|
2051
|
-
this._editorConfig = editorConfig;
|
|
2052
|
-
}
|
|
2053
|
-
get a() {
|
|
2054
|
-
return this._a;
|
|
2055
|
-
}
|
|
2056
|
-
get b() {
|
|
2057
|
-
return this._b;
|
|
2058
|
-
}
|
|
2059
|
-
set a(a) {
|
|
2060
|
-
this._a = a;
|
|
2061
|
-
}
|
|
2062
|
-
set b(b) {
|
|
2063
|
-
this._b = b;
|
|
2064
|
-
}
|
|
2065
|
-
getExpression() {
|
|
2066
|
-
return new FunctionExpression("eq", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
2067
|
-
}
|
|
2068
|
-
};
|
|
2069
|
-
var GtExpressionEditor = class extends ExpressionEditor {
|
|
2070
|
-
returnType = ValueType.boolean;
|
|
2071
|
-
_a;
|
|
2072
|
-
_b;
|
|
2073
|
-
constructor(a, b, editorConfig) {
|
|
2074
|
-
super();
|
|
2075
|
-
this._a = a;
|
|
2076
|
-
this._b = b;
|
|
2077
|
-
this._editorConfig = editorConfig;
|
|
2078
|
-
}
|
|
2079
|
-
get a() {
|
|
2080
|
-
return this._a;
|
|
2081
|
-
}
|
|
2082
|
-
get b() {
|
|
2083
|
-
return this._b;
|
|
2084
|
-
}
|
|
2085
|
-
set a(a) {
|
|
2086
|
-
this._a = a;
|
|
2087
|
-
}
|
|
2088
|
-
set b(b) {
|
|
2089
|
-
this._b = b;
|
|
2090
|
-
}
|
|
2091
|
-
getExpression() {
|
|
2092
|
-
return new FunctionExpression("gt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
2093
|
-
}
|
|
2094
|
-
};
|
|
2095
|
-
var GteExpressionEditor = class extends ExpressionEditor {
|
|
2096
|
-
returnType = ValueType.boolean;
|
|
2097
|
-
_a;
|
|
2098
|
-
_b;
|
|
2099
|
-
constructor(a, b, editorConfig) {
|
|
2100
|
-
super();
|
|
2101
|
-
this._a = a;
|
|
2102
|
-
this._b = b;
|
|
2103
|
-
this._editorConfig = editorConfig;
|
|
2104
|
-
}
|
|
2105
|
-
get a() {
|
|
2106
|
-
return this._a;
|
|
2107
|
-
}
|
|
2108
|
-
get b() {
|
|
2109
|
-
return this._b;
|
|
2110
|
-
}
|
|
2111
|
-
set a(a) {
|
|
2112
|
-
this._a = a;
|
|
2113
|
-
}
|
|
2114
|
-
set b(b) {
|
|
2115
|
-
this._b = b;
|
|
2116
|
-
}
|
|
2117
|
-
getExpression() {
|
|
2118
|
-
return new FunctionExpression("gte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
2119
|
-
}
|
|
2120
|
-
};
|
|
2121
|
-
var LtExpressionEditor = class extends ExpressionEditor {
|
|
2122
|
-
returnType = ValueType.boolean;
|
|
2123
|
-
_a;
|
|
2124
|
-
_b;
|
|
2125
|
-
constructor(a, b, editorConfig) {
|
|
2126
|
-
super();
|
|
2127
|
-
this._a = a;
|
|
2128
|
-
this._b = b;
|
|
2129
|
-
this._editorConfig = editorConfig;
|
|
2130
|
-
}
|
|
2131
|
-
get a() {
|
|
2132
|
-
return this._a;
|
|
2133
|
-
}
|
|
2134
|
-
get b() {
|
|
2135
|
-
return this._b;
|
|
2136
|
-
}
|
|
2137
|
-
set a(a) {
|
|
2138
|
-
this._a = a;
|
|
2139
|
-
}
|
|
2140
|
-
set b(b) {
|
|
2141
|
-
this._b = b;
|
|
2142
|
-
}
|
|
2143
|
-
getExpression() {
|
|
2144
|
-
return new FunctionExpression("lt", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
2145
|
-
}
|
|
2146
|
-
};
|
|
2147
|
-
var LteExpressionEditor = class extends ExpressionEditor {
|
|
2148
|
-
returnType = ValueType.boolean;
|
|
2149
|
-
_a;
|
|
2150
|
-
_b;
|
|
2151
|
-
constructor(a, b, editorConfig) {
|
|
2152
|
-
super();
|
|
2153
|
-
this._a = a;
|
|
2154
|
-
this._b = b;
|
|
2155
|
-
this._editorConfig = editorConfig;
|
|
2156
|
-
}
|
|
2157
|
-
get a() {
|
|
2158
|
-
return this._a;
|
|
2159
|
-
}
|
|
2160
|
-
get b() {
|
|
2161
|
-
return this._b;
|
|
2162
|
-
}
|
|
2163
|
-
set b(b) {
|
|
2164
|
-
this._b = b;
|
|
2165
|
-
}
|
|
2166
|
-
set a(a) {
|
|
2167
|
-
this._a = a;
|
|
2168
|
-
}
|
|
2169
|
-
getExpression() {
|
|
2170
|
-
return new FunctionExpression("lte", [this._a?.getExpression(), this._b?.getExpression()], this._editorConfig);
|
|
2171
|
-
}
|
|
2172
|
-
};
|
|
2173
|
-
var InRangeExpressionEditor = class extends ExpressionEditor {
|
|
2174
|
-
returnType = ValueType.boolean;
|
|
2175
|
-
value;
|
|
2176
|
-
min;
|
|
2177
|
-
max;
|
|
2178
|
-
inclusive;
|
|
2179
|
-
constructor(value, min, max, inclusive, editorConfig) {
|
|
2180
|
-
super();
|
|
2181
|
-
this.value = value;
|
|
2182
|
-
this.min = min;
|
|
2183
|
-
this.max = max;
|
|
2184
|
-
this.inclusive = inclusive;
|
|
2185
|
-
this._editorConfig = editorConfig;
|
|
2186
|
-
}
|
|
2187
|
-
getExpression() {
|
|
2188
|
-
return new FunctionExpression("in_range", [
|
|
2189
|
-
this.value?.getExpression(),
|
|
2190
|
-
this.min?.getExpression(),
|
|
2191
|
-
this.max?.getExpression(),
|
|
2192
|
-
this.inclusive?.getExpression()
|
|
2193
|
-
], this._editorConfig);
|
|
2194
|
-
}
|
|
2195
|
-
};
|
|
2196
|
-
var SumExpressionEditor = class extends GroupExpressionEditor {
|
|
2197
|
-
returnType = ValueType.number;
|
|
2198
|
-
constructor(args, editorConfig) {
|
|
2199
|
-
super(args, editorConfig);
|
|
2200
|
-
}
|
|
2201
|
-
getExpression() {
|
|
2202
|
-
return new FunctionExpression("sum", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
2203
|
-
}
|
|
2204
|
-
};
|
|
2205
|
-
var MinExpressionEditor = class extends GroupExpressionEditor {
|
|
2206
|
-
returnType = ValueType.number;
|
|
2207
|
-
constructor(args, editorConfig) {
|
|
2208
|
-
super(args, editorConfig);
|
|
2209
|
-
}
|
|
2210
|
-
getExpression() {
|
|
2211
|
-
return new FunctionExpression("min", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
2212
|
-
}
|
|
2213
|
-
};
|
|
2214
|
-
var MaxExpressionEditor = class extends GroupExpressionEditor {
|
|
2215
|
-
returnType = ValueType.number;
|
|
2216
|
-
constructor(args, editorConfig) {
|
|
2217
|
-
super(args, editorConfig);
|
|
2218
|
-
}
|
|
2219
|
-
getExpression() {
|
|
2220
|
-
return new FunctionExpression("max", this.args.map((arg) => arg.getExpression()), this._editorConfig);
|
|
2221
|
-
}
|
|
2222
|
-
};
|
|
2223
|
-
//#endregion
|
|
2224
|
-
//#region src/expressions/editors/expression-editor-generators.ts
|
|
2225
|
-
const const_string_array = (...values) => {
|
|
2226
|
-
return new ConstStringArrayEditor(values);
|
|
2227
|
-
};
|
|
2228
|
-
const const_string = (value) => {
|
|
2229
|
-
return new ConstStringEditor(value);
|
|
2230
|
-
};
|
|
2231
|
-
const const_number_array = (...values) => {
|
|
2232
|
-
return new ConstNumberArrayEditor(values);
|
|
2233
|
-
};
|
|
2234
|
-
const const_number = (value) => {
|
|
2235
|
-
return new ConstNumberEditor(value);
|
|
2236
|
-
};
|
|
2237
|
-
const const_boolean = (value) => {
|
|
2238
|
-
return new ConstBooleanEditor(value);
|
|
2239
|
-
};
|
|
2240
|
-
const const_date = (value) => {
|
|
2241
|
-
return new ConstDateEditor(value);
|
|
2242
|
-
};
|
|
2243
|
-
const const_date_array = (...values) => {
|
|
2244
|
-
return new ConstDateArrayEditor(values);
|
|
2245
|
-
};
|
|
2246
|
-
const response_string = (valueRef) => {
|
|
2247
|
-
return new ResponseVariableEditor(valueRef, ValueType.string);
|
|
2248
|
-
};
|
|
2249
|
-
const response_string_array = (valueRef) => {
|
|
2250
|
-
return new ResponseVariableEditor(valueRef, ValueType.stringArray);
|
|
2251
|
-
};
|
|
2252
|
-
const response_number = (valueRef) => {
|
|
2253
|
-
return new ResponseVariableEditor(valueRef, ValueType.number);
|
|
2254
|
-
};
|
|
2255
|
-
const response_boolean = (valueRef) => {
|
|
2256
|
-
return new ResponseVariableEditor(valueRef, ValueType.boolean);
|
|
2257
|
-
};
|
|
2258
|
-
const response_date = (valueRef) => {
|
|
2259
|
-
return new ResponseVariableEditor(valueRef, ValueType.date);
|
|
2260
|
-
};
|
|
2261
|
-
const response_number_array = (valueRef) => {
|
|
2262
|
-
return new ResponseVariableEditor(valueRef, ValueType.numberArray);
|
|
2263
|
-
};
|
|
2264
|
-
const response_date_array = (valueRef) => {
|
|
2265
|
-
return new ResponseVariableEditor(valueRef, ValueType.dateArray);
|
|
2266
|
-
};
|
|
2267
|
-
const ctx_locale = () => {
|
|
2268
|
-
return new CtxLocaleEditor();
|
|
2269
|
-
};
|
|
2270
|
-
const ctx_pflag_is_defined = (key) => {
|
|
2271
|
-
return new CtxPFlagIsDefinedEditor(key);
|
|
2272
|
-
};
|
|
2273
|
-
const ctx_pflag_string = (key) => {
|
|
2274
|
-
return new CtxPFlagStringEditor(key);
|
|
2275
|
-
};
|
|
2276
|
-
const ctx_pflag_num = (key) => {
|
|
2277
|
-
return new CtxPFlagNumEditor(key);
|
|
2278
|
-
};
|
|
2279
|
-
const ctx_pflag_date = (key) => {
|
|
2280
|
-
return new CtxPFlagDateEditor(key);
|
|
2281
|
-
};
|
|
2282
|
-
const ctx_custom_value = (key, dType) => {
|
|
2283
|
-
return new CtxCustomValueEditor(key, dType);
|
|
2284
|
-
};
|
|
2285
|
-
const ctx_custom_expression = (key, args, returnType) => {
|
|
2286
|
-
return new CtxCustomExpressionEditor(key, args, returnType);
|
|
2287
|
-
};
|
|
2288
|
-
const and = (...args) => {
|
|
2289
|
-
return new AndExpressionEditor(args);
|
|
2290
|
-
};
|
|
2291
|
-
const or = (...args) => {
|
|
2292
|
-
return new OrExpressionEditor(args);
|
|
2293
|
-
};
|
|
2294
|
-
const str_list_contains = (list, item) => {
|
|
2295
|
-
return new StrListContainsExpressionEditor(list, item);
|
|
2296
|
-
};
|
|
2297
|
-
const str_eq = (a, b) => {
|
|
2298
|
-
return new StrEqExpressionEditor(a, b);
|
|
2299
|
-
};
|
|
2300
|
-
const eq = (a, b) => {
|
|
2301
|
-
return new EqExpressionEditor(a, b);
|
|
2302
|
-
};
|
|
2303
|
-
const gt = (a, b) => {
|
|
2304
|
-
return new GtExpressionEditor(a, b);
|
|
2305
|
-
};
|
|
2306
|
-
const gte = (a, b) => {
|
|
2307
|
-
return new GteExpressionEditor(a, b);
|
|
2308
|
-
};
|
|
2309
|
-
const lt = (a, b) => {
|
|
2310
|
-
return new LtExpressionEditor(a, b);
|
|
2311
|
-
};
|
|
2312
|
-
const lte = (a, b) => {
|
|
2313
|
-
return new LteExpressionEditor(a, b);
|
|
2314
|
-
};
|
|
2315
|
-
const in_range = (value, min, max, inclusive) => {
|
|
2316
|
-
return new InRangeExpressionEditor(value, min, max, inclusive);
|
|
2317
|
-
};
|
|
2318
|
-
const sum = (...args) => {
|
|
2319
|
-
return new SumExpressionEditor(args);
|
|
2320
|
-
};
|
|
2321
|
-
const min = (...args) => {
|
|
2322
|
-
return new MinExpressionEditor(args);
|
|
2323
|
-
};
|
|
2324
|
-
const max = (...args) => {
|
|
2325
|
-
return new MaxExpressionEditor(args);
|
|
2326
|
-
};
|
|
2327
|
-
var CalendarFunctionEditor = class extends ExpressionEditor {
|
|
2328
|
-
name;
|
|
2329
|
-
args;
|
|
2330
|
-
returnType;
|
|
2331
|
-
constructor(name, args, returnType) {
|
|
2332
|
-
super();
|
|
2333
|
-
this.name = name;
|
|
2334
|
-
this.args = args;
|
|
2335
|
-
this.returnType = returnType;
|
|
2336
|
-
}
|
|
2337
|
-
getExpression() {
|
|
2338
|
-
return new FunctionExpression(this.name, this.args.map((arg) => arg.getExpression()));
|
|
2339
|
-
}
|
|
2340
|
-
};
|
|
2341
|
-
const date_eq = (a, b) => new CalendarFunctionEditor("date_eq", [a, b], ValueType.boolean);
|
|
2342
|
-
const date_gt = (a, b) => new CalendarFunctionEditor("date_gt", [a, b], ValueType.boolean);
|
|
2343
|
-
const date_gte = (a, b) => new CalendarFunctionEditor("date_gte", [a, b], ValueType.boolean);
|
|
2344
|
-
const date_lt = (a, b) => new CalendarFunctionEditor("date_lt", [a, b], ValueType.boolean);
|
|
2345
|
-
const date_lte = (a, b) => new CalendarFunctionEditor("date_lte", [a, b], ValueType.boolean);
|
|
2346
|
-
const date_add_days = (a, b) => new CalendarFunctionEditor("date_add_days", [a, b], ValueType.date);
|
|
2347
|
-
const date_add_months = (a, b) => new CalendarFunctionEditor("date_add_months", [a, b], ValueType.date);
|
|
2348
|
-
const date_add_years = (a, b) => new CalendarFunctionEditor("date_add_years", [a, b], ValueType.date);
|
|
2349
|
-
const date_diff = (a, b) => new CalendarFunctionEditor("date_diff", [a, b], ValueType.number);
|
|
2350
|
-
const date_min = (...args) => new CalendarFunctionEditor("date_min", args, ValueType.date);
|
|
2351
|
-
const date_max = (...args) => new CalendarFunctionEditor("date_max", args, ValueType.date);
|
|
2352
|
-
//#endregion
|
|
2353
|
-
//#region src/survey/items/types.ts
|
|
2354
|
-
let ReservedSurveyItemTypes = /* @__PURE__ */ function(ReservedSurveyItemTypes) {
|
|
2355
|
-
ReservedSurveyItemTypes["Group"] = "group";
|
|
2356
|
-
ReservedSurveyItemTypes["PageBreak"] = "page-break";
|
|
2357
|
-
return ReservedSurveyItemTypes;
|
|
2358
|
-
}({});
|
|
2359
|
-
//#endregion
|
|
2360
|
-
//#region src/utils.ts
|
|
2361
|
-
function hashString(seed) {
|
|
2362
|
-
let hash = 2166136261;
|
|
2363
|
-
for (let i = 0; i < seed.length; i++) {
|
|
2364
|
-
hash ^= seed.charCodeAt(i);
|
|
2365
|
-
hash = Math.imul(hash, 16777619);
|
|
2366
|
-
}
|
|
2367
|
-
return hash >>> 0;
|
|
2368
|
-
}
|
|
2369
|
-
function createSeededRandom(seed) {
|
|
2370
|
-
let state = hashString(seed);
|
|
2371
|
-
return () => {
|
|
2372
|
-
state = state + 1831565813 >>> 0;
|
|
2373
|
-
let next = state;
|
|
2374
|
-
next = Math.imul(next ^ next >>> 15, next | 1);
|
|
2375
|
-
next ^= next + Math.imul(next ^ next >>> 7, next | 61);
|
|
2376
|
-
return ((next ^ next >>> 14) >>> 0) / 4294967296;
|
|
2377
|
-
};
|
|
2378
|
-
}
|
|
2379
|
-
function shuffleArray(values, random = Math.random) {
|
|
2380
|
-
const shuffledValues = [...values];
|
|
2381
|
-
for (let i = shuffledValues.length - 1; i > 0; i--) {
|
|
2382
|
-
const j = Math.floor(random() * (i + 1));
|
|
2383
|
-
[shuffledValues[i], shuffledValues[j]] = [shuffledValues[j], shuffledValues[i]];
|
|
2384
|
-
}
|
|
2385
|
-
return shuffledValues;
|
|
2386
|
-
}
|
|
2387
|
-
/**
|
|
2388
|
-
* Shuffles an array of indices using the Fisher-Yates shuffle algorithm
|
|
2389
|
-
* @param length - The length of the array to create indices for
|
|
2390
|
-
* @returns A shuffled array of indices from 0 to length-1
|
|
2391
|
-
*/
|
|
2392
|
-
function shuffleIndices(length, random = Math.random) {
|
|
2393
|
-
return shuffleArray(Array.from({ length }, (_, i) => i), random);
|
|
2394
|
-
}
|
|
2395
|
-
function structuredCloneMethod(obj) {
|
|
2396
|
-
if (obj === void 0) return obj;
|
|
2397
|
-
if (typeof structuredClone !== "undefined") return structuredClone(obj);
|
|
2398
|
-
return JSON.parse(JSON.stringify(obj));
|
|
2399
|
-
}
|
|
2400
|
-
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
2401
|
-
function generateId() {
|
|
2402
|
-
const length = 10;
|
|
2403
|
-
const timestamp = Date.now();
|
|
2404
|
-
let encodedTimestamp = "";
|
|
2405
|
-
let t = timestamp;
|
|
2406
|
-
const base = 62;
|
|
2407
|
-
while (t > 0) {
|
|
2408
|
-
encodedTimestamp = ALPHABET.charAt(t % base) + encodedTimestamp;
|
|
2409
|
-
t = Math.floor(t / base);
|
|
2410
|
-
}
|
|
2411
|
-
let randomPart = "";
|
|
2412
|
-
for (let i = 0; i < length; i++) randomPart += ALPHABET.charAt(Math.floor(Math.random() * base));
|
|
2413
|
-
return randomPart + encodedTimestamp;
|
|
2414
|
-
}
|
|
2415
|
-
/** Alphanumeric chars without ambiguous ones (0/O, 1/I) for human-readable coding keys. */
|
|
2416
|
-
const CODING_KEY_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
2417
|
-
/**
|
|
2418
|
-
* Generates a short random coding key suitable for item keys in exports/codebooks.
|
|
2419
|
-
* @param length - Key length (default 4)
|
|
2420
|
-
* @returns A random string of the specified length.
|
|
2421
|
-
*/
|
|
2422
|
-
function generateCodingKey(length = 4) {
|
|
2423
|
-
const base = 32;
|
|
2424
|
-
let key = "";
|
|
2425
|
-
for (let i = 0; i < length; i++) key += CODING_KEY_ALPHABET.charAt(Math.floor(Math.random() * base));
|
|
2426
|
-
return key;
|
|
2427
|
-
}
|
|
2428
|
-
/** Resolve a sibling key using the editor's stable numeric suffix convention. */
|
|
2429
|
-
function resolveUniqueItemKey(key, siblingKeys) {
|
|
2430
|
-
let candidate = key;
|
|
2431
|
-
let suffix = 1;
|
|
2432
|
-
while (siblingKeys.has(candidate)) candidate = `${key}_${suffix++}`;
|
|
2433
|
-
return candidate;
|
|
2434
|
-
}
|
|
2435
|
-
//#endregion
|
|
2436
|
-
//#region src/survey/items/utils.ts
|
|
2437
|
-
const displayConditionsFromJson = (json) => {
|
|
2438
|
-
return {
|
|
2439
|
-
root: json.root ? Expression.deserialize(json.root) : void 0,
|
|
2440
|
-
components: json.components ? Object.fromEntries(Object.entries(json.components).map(([key, value]) => [key, Expression.deserialize(value)])) : void 0
|
|
2441
|
-
};
|
|
2442
|
-
};
|
|
2443
|
-
const disabledConditionsFromJson = (json) => {
|
|
2444
|
-
return { components: json.components ? Object.fromEntries(Object.entries(json.components).map(([key, value]) => [key, Expression.deserialize(value)])) : void 0 };
|
|
2445
|
-
};
|
|
2446
|
-
//#endregion
|
|
2447
|
-
//#region src/survey/items/prefill.ts
|
|
2448
|
-
const SurveyItemPrefillApplyMode = { ifEmpty: "ifEmpty" };
|
|
2449
|
-
const SurveyItemPrefillTargetType = {
|
|
2450
|
-
itemResponse: "itemResponse",
|
|
2451
|
-
field: "field",
|
|
2452
|
-
embeddedField: "embeddedField"
|
|
2453
|
-
};
|
|
2454
|
-
function deserializeSurveyItemPrefill(json) {
|
|
2455
|
-
switch (json.source.type) {
|
|
2456
|
-
case "static": return {
|
|
2457
|
-
id: json.id,
|
|
2458
|
-
target: json.target,
|
|
2459
|
-
when: Expression.deserialize(json.when),
|
|
2460
|
-
apply: json.apply,
|
|
2461
|
-
source: {
|
|
2462
|
-
type: "static",
|
|
2463
|
-
value: json.source.value
|
|
2464
|
-
}
|
|
2465
|
-
};
|
|
2466
|
-
case "expression": return {
|
|
2467
|
-
id: json.id,
|
|
2468
|
-
target: json.target,
|
|
2469
|
-
when: Expression.deserialize(json.when),
|
|
2470
|
-
apply: json.apply,
|
|
2471
|
-
source: {
|
|
2472
|
-
type: "expression",
|
|
2473
|
-
expression: Expression.deserialize(json.source.expression) ?? (() => {
|
|
2474
|
-
throw new Error(`Prefill '${json.id}' is missing a valid expression source`);
|
|
2475
|
-
})()
|
|
2476
|
-
}
|
|
2477
|
-
};
|
|
2478
|
-
case "templateValue": return {
|
|
2479
|
-
id: json.id,
|
|
2480
|
-
target: json.target,
|
|
2481
|
-
when: Expression.deserialize(json.when),
|
|
2482
|
-
apply: json.apply,
|
|
2483
|
-
source: {
|
|
2484
|
-
type: "templateValue",
|
|
2485
|
-
key: json.source.key
|
|
2486
|
-
}
|
|
2487
|
-
};
|
|
2488
|
-
case "previousResponse": return {
|
|
2489
|
-
id: json.id,
|
|
2490
|
-
target: json.target,
|
|
2491
|
-
when: Expression.deserialize(json.when),
|
|
2492
|
-
apply: json.apply,
|
|
2493
|
-
source: {
|
|
2494
|
-
type: "previousResponse",
|
|
2495
|
-
ref: json.source.ref,
|
|
2496
|
-
ifUnsupported: json.source.ifUnsupported
|
|
2497
|
-
}
|
|
2498
|
-
};
|
|
2499
|
-
default: throw new Error(`Unsupported prefill source type: ${json.source.type}`);
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
function serializeSurveyItemPrefill(prefill) {
|
|
2503
|
-
switch (prefill.source.type) {
|
|
2504
|
-
case "static": return {
|
|
2505
|
-
id: prefill.id,
|
|
2506
|
-
target: prefill.target,
|
|
2507
|
-
when: prefill.when?.serialize(),
|
|
2508
|
-
apply: prefill.apply,
|
|
2509
|
-
source: {
|
|
2510
|
-
type: "static",
|
|
2511
|
-
value: prefill.source.value
|
|
2512
|
-
}
|
|
2513
|
-
};
|
|
2514
|
-
case "expression": return {
|
|
2515
|
-
id: prefill.id,
|
|
2516
|
-
target: prefill.target,
|
|
2517
|
-
when: prefill.when?.serialize(),
|
|
2518
|
-
apply: prefill.apply,
|
|
2519
|
-
source: {
|
|
2520
|
-
type: "expression",
|
|
2521
|
-
expression: prefill.source.expression.serialize() ?? (() => {
|
|
2522
|
-
throw new Error(`Prefill '${prefill.id}' cannot serialize an empty expression source`);
|
|
2523
|
-
})()
|
|
2524
|
-
}
|
|
2525
|
-
};
|
|
2526
|
-
case "templateValue": return {
|
|
2527
|
-
id: prefill.id,
|
|
2528
|
-
target: prefill.target,
|
|
2529
|
-
when: prefill.when?.serialize(),
|
|
2530
|
-
apply: prefill.apply,
|
|
2531
|
-
source: {
|
|
2532
|
-
type: "templateValue",
|
|
2533
|
-
key: prefill.source.key
|
|
2534
|
-
}
|
|
2535
|
-
};
|
|
2536
|
-
case "previousResponse": return {
|
|
2537
|
-
id: prefill.id,
|
|
2538
|
-
target: prefill.target,
|
|
2539
|
-
when: prefill.when?.serialize(),
|
|
2540
|
-
apply: prefill.apply,
|
|
2541
|
-
source: {
|
|
2542
|
-
type: "previousResponse",
|
|
2543
|
-
ref: prefill.source.ref,
|
|
2544
|
-
ifUnsupported: prefill.source.ifUnsupported
|
|
2545
|
-
}
|
|
2546
|
-
};
|
|
2547
|
-
default: throw new Error(`Unsupported prefill source type: ${prefill.source.type}`);
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
function prefillTargetsEqual(left, right) {
|
|
2551
|
-
if (left.type !== right.type) return false;
|
|
2552
|
-
switch (left.type) {
|
|
2553
|
-
case SurveyItemPrefillTargetType.itemResponse: return true;
|
|
2554
|
-
case SurveyItemPrefillTargetType.field: return left.fieldId === right.fieldId;
|
|
2555
|
-
case SurveyItemPrefillTargetType.embeddedField: return left.optionId === right.optionId && left.fieldId === right.fieldId;
|
|
2556
|
-
default: return false;
|
|
2557
|
-
}
|
|
2558
|
-
}
|
|
2559
|
-
//#endregion
|
|
2560
|
-
//#region src/survey/items/survey-item.ts
|
|
2561
|
-
/**
|
|
2562
|
-
* Base class for survey item handlers.
|
|
2563
|
-
* Each handler holds type, id, key, and config (properly typed) and implements core methods.
|
|
2564
|
-
* Subclass for each item type; the core works with SurveyItemHandler only—no casting needed.
|
|
2565
|
-
*/
|
|
2566
|
-
var SurveyItemCore = class {
|
|
2567
|
-
id;
|
|
2568
|
-
config;
|
|
2569
|
-
_rawItem;
|
|
2570
|
-
displayConditions;
|
|
2571
|
-
disabledConditions;
|
|
2572
|
-
validations;
|
|
2573
|
-
prefills;
|
|
2574
|
-
constructor(rawItem) {
|
|
2575
|
-
this._rawItem = rawItem;
|
|
2576
|
-
this.updateExpressions();
|
|
2577
|
-
this.id = this._rawItem.id;
|
|
2578
|
-
this.config = this.parseConfig(this._rawItem.config);
|
|
2579
|
-
}
|
|
2580
|
-
/** Primitive authors can identify meaningful headings without exposing option/decorative text. */
|
|
2581
|
-
getOutlineContentKeys() {
|
|
2582
|
-
return ["title", "prompt"];
|
|
2583
|
-
}
|
|
2584
|
-
getCompositionTypes() {
|
|
2585
|
-
return [this.type];
|
|
2586
|
-
}
|
|
2587
|
-
/**
|
|
2588
|
-
* Validate/canonicalize updates, initial responses, and configured prefills before evaluation.
|
|
2589
|
-
* Prefills are applied one slot at a time, so multi-slot items must allow partial responses.
|
|
2590
|
-
*/
|
|
2591
|
-
normalizeResponse(response) {
|
|
2592
|
-
return { response };
|
|
2593
|
-
}
|
|
2594
|
-
/** Opt-in component ownership for copy/paste. Existing item formats remain unchanged. */
|
|
2595
|
-
getOwnedComponentIds() {
|
|
2596
|
-
return [];
|
|
2597
|
-
}
|
|
2598
|
-
/** Slots omitted from submissions when their component condition is false; session answers are retained. */
|
|
2599
|
-
getConditionalResponseSlots() {
|
|
2600
|
-
return [];
|
|
2601
|
-
}
|
|
2602
|
-
/** Dependencies used when projecting a stored answer (for example visible choice options). */
|
|
2603
|
-
getActiveResponseDependencies(_slotId) {
|
|
2604
|
-
return {
|
|
2605
|
-
slotIds: [],
|
|
2606
|
-
componentIds: []
|
|
2607
|
-
};
|
|
2608
|
-
}
|
|
2609
|
-
/** Project retained data without mutating it. Dependencies must be declared above. */
|
|
2610
|
-
getActiveResponseValue(_slotId, value, _isVisible, _getAnswer) {
|
|
2611
|
-
return value;
|
|
2612
|
-
}
|
|
2613
|
-
/** Source slots used by a computed reference, for component visibility cycle diagnostics. */
|
|
2614
|
-
getComputedResponseDependencies(slotId) {
|
|
2615
|
-
return this.getResponseSlotDefinitions().find((slot) => slot.slotId === slotId)?.computed?.sourceSlotIds ?? [];
|
|
2616
|
-
}
|
|
2617
|
-
/** Computed references are read-only, evaluated on demand, never persisted/exported as answers. */
|
|
2618
|
-
getComputedResponseValue(_slotId, _response, _isVisible) {}
|
|
2619
|
-
getResponseValidationValues(_response, _isVisible, _evaluate) {
|
|
2620
|
-
return {};
|
|
2621
|
-
}
|
|
2622
|
-
/** Runtime-only checks; authoring may retain incomplete drafts. */
|
|
2623
|
-
getExecutionConfigurationIssues() {
|
|
2624
|
-
return [];
|
|
2625
|
-
}
|
|
2626
|
-
getAvailableResponseValueSlots(byType) {
|
|
2627
|
-
const valueRefs = {};
|
|
2628
|
-
for (const slot of this.getResponseSlotDefinitions()) {
|
|
2629
|
-
valueRefs[JSON.stringify([slot.slotId, "get"])] = slot.valueType;
|
|
2630
|
-
valueRefs[JSON.stringify([slot.slotId, "isDefined"])] = ValueType.boolean;
|
|
2631
|
-
}
|
|
2632
|
-
if (!byType) return valueRefs;
|
|
2633
|
-
return Object.fromEntries(Object.entries(valueRefs).filter(([, valueType]) => valueType === byType));
|
|
2634
|
-
}
|
|
2635
|
-
get rawItem() {
|
|
2636
|
-
return {
|
|
2637
|
-
...this._rawItem,
|
|
2638
|
-
config: this.config
|
|
2639
|
-
};
|
|
2640
|
-
}
|
|
2641
|
-
get metadata() {
|
|
2642
|
-
return this._rawItem.metadata ?? {};
|
|
2643
|
-
}
|
|
2644
|
-
set metadata(metadata) {
|
|
2645
|
-
this.updateRawItem({
|
|
2646
|
-
...this._rawItem,
|
|
2647
|
-
metadata
|
|
2648
|
-
});
|
|
2649
|
-
}
|
|
2650
|
-
get editorMetadata() {
|
|
2651
|
-
return this._rawItem.editorMetadata ?? {};
|
|
2652
|
-
}
|
|
2653
|
-
setCreatedAt(timestamp) {
|
|
2654
|
-
this.updateRawItem({
|
|
2655
|
-
...this._rawItem,
|
|
2656
|
-
editorMetadata: {
|
|
2657
|
-
...this._rawItem.editorMetadata,
|
|
2658
|
-
createdAt: timestamp
|
|
2659
|
-
}
|
|
2660
|
-
});
|
|
2661
|
-
}
|
|
2662
|
-
setModifiedAt(timestamp) {
|
|
2663
|
-
this.updateRawItem({
|
|
2664
|
-
...this._rawItem,
|
|
2665
|
-
editorMetadata: {
|
|
2666
|
-
...this._rawItem.editorMetadata,
|
|
2667
|
-
createdAt: this._rawItem.editorMetadata?.createdAt ?? timestamp,
|
|
2668
|
-
updatedAt: timestamp
|
|
2669
|
-
}
|
|
2670
|
-
});
|
|
2671
|
-
}
|
|
2672
|
-
updateRawItem(rawItem) {
|
|
2673
|
-
if (rawItem.id !== this.id) throw new Error("Cannot update raw item with a different id");
|
|
2674
|
-
this._rawItem = rawItem;
|
|
2675
|
-
this.updateExpressions();
|
|
2676
|
-
this.config = this.parseConfig(rawItem.config);
|
|
2677
|
-
}
|
|
2678
|
-
updateExpressions() {
|
|
2679
|
-
this.displayConditions = this._rawItem.displayConditions ? displayConditionsFromJson(this._rawItem.displayConditions) : void 0;
|
|
2680
|
-
this.disabledConditions = this._rawItem.disabledConditions ? disabledConditionsFromJson(this._rawItem.disabledConditions) : void 0;
|
|
2681
|
-
this.validations = this._rawItem.validations ? Object.fromEntries(Object.entries(this._rawItem.validations).map(([key, value]) => [key, Expression.deserialize(value)])) : void 0;
|
|
2682
|
-
this.prefills = this._rawItem.prefills?.map((prefill) => deserializeSurveyItemPrefill(prefill));
|
|
2683
|
-
}
|
|
2684
|
-
getReferenceUsages() {
|
|
2685
|
-
const usages = [];
|
|
2686
|
-
if (this.displayConditions) {
|
|
2687
|
-
for (const ref of this.displayConditions.root?.responseVariableRefs || []) usages.push({
|
|
2688
|
-
itemId: this.id,
|
|
2689
|
-
usageType: "displayConditions",
|
|
2690
|
-
valueReference: ref
|
|
2691
|
-
});
|
|
2692
|
-
for (const [componentKey, expression] of Object.entries(this.displayConditions.components || {})) for (const ref of expression?.responseVariableRefs || []) usages.push({
|
|
2693
|
-
itemId: this.id,
|
|
2694
|
-
fullComponentKey: componentKey,
|
|
2695
|
-
usageType: "displayConditions",
|
|
2696
|
-
valueReference: ref
|
|
2697
|
-
});
|
|
2698
|
-
}
|
|
2699
|
-
if (this.disabledConditions) for (const [componentKey, expression] of Object.entries(this.disabledConditions.components || {})) for (const ref of expression?.responseVariableRefs || []) usages.push({
|
|
2700
|
-
itemId: this.id,
|
|
2701
|
-
fullComponentKey: componentKey,
|
|
2702
|
-
usageType: "disabledConditions",
|
|
2703
|
-
valueReference: ref
|
|
2704
|
-
});
|
|
2705
|
-
if (this.validations) for (const [validationKey, expression] of Object.entries(this.validations)) for (const ref of expression?.responseVariableRefs || []) usages.push({
|
|
2706
|
-
itemId: this.id,
|
|
2707
|
-
fullComponentKey: validationKey,
|
|
2708
|
-
usageType: "validations",
|
|
2709
|
-
valueReference: ref
|
|
2710
|
-
});
|
|
2711
|
-
if (this.prefills) for (const prefill of this.prefills) {
|
|
2712
|
-
for (const ref of prefill.when?.responseVariableRefs || []) usages.push({
|
|
2713
|
-
itemId: this.id,
|
|
2714
|
-
fullComponentKey: prefill.id,
|
|
2715
|
-
usageType: "prefills",
|
|
2716
|
-
valueReference: ref
|
|
2717
|
-
});
|
|
2718
|
-
if (prefill.source.type === "expression") for (const ref of prefill.source.expression.responseVariableRefs || []) usages.push({
|
|
2719
|
-
itemId: this.id,
|
|
2720
|
-
fullComponentKey: prefill.id,
|
|
2721
|
-
usageType: "prefills",
|
|
2722
|
-
valueReference: ref
|
|
2723
|
-
});
|
|
2724
|
-
}
|
|
2725
|
-
for (const { expression, location } of collectConfigValueExpressions(this.config)) for (const ref of collectExpressionResponseReferences(expression)) usages.push({
|
|
2726
|
-
itemId: this.id,
|
|
2727
|
-
fullComponentKey: location,
|
|
2728
|
-
usageType: "validations",
|
|
2729
|
-
valueReference: ref
|
|
2730
|
-
});
|
|
2731
|
-
return usages;
|
|
2732
|
-
}
|
|
2733
|
-
resolvePrefillTarget(target) {
|
|
2734
|
-
const slotDefinitions = this.getResponseSlotDefinitions();
|
|
2735
|
-
const explicitMatch = slotDefinitions.find((slot) => slot.prefillTarget !== void 0 && prefillTargetsEqual(slot.prefillTarget, target));
|
|
2736
|
-
if (explicitMatch) return explicitMatch;
|
|
2737
|
-
if (target.type === SurveyItemPrefillTargetType.itemResponse && slotDefinitions.length === 1) return slotDefinitions[0];
|
|
2738
|
-
}
|
|
2739
|
-
normalizePrefillValue(_prefill, targetSlot, value) {
|
|
2740
|
-
return value.type === targetSlot.valueType ? value : void 0;
|
|
2741
|
-
}
|
|
2742
|
-
};
|
|
2743
|
-
//#endregion
|
|
2744
|
-
//#region src/survey/item-display-name.ts
|
|
2745
|
-
function getCompositionLabel(types, locale) {
|
|
2746
|
-
const language = locale.split("-")[0];
|
|
2747
|
-
const labels = {
|
|
2748
|
-
en: {
|
|
2749
|
-
choiceItem: "Choice",
|
|
2750
|
-
formItem: "Form",
|
|
2751
|
-
formMatrix: "Form matrix",
|
|
2752
|
-
choiceMatrix: "Choice matrix",
|
|
2753
|
-
consentItem: "Consent",
|
|
2754
|
-
infoItem: "Information",
|
|
2755
|
-
group: "Group",
|
|
2756
|
-
"page-break": "Page break",
|
|
2757
|
-
empty: "Empty item"
|
|
2758
|
-
},
|
|
2759
|
-
nl: {
|
|
2760
|
-
choiceItem: "Keuze",
|
|
2761
|
-
formItem: "Formulier",
|
|
2762
|
-
formMatrix: "Formuliermatrix",
|
|
2763
|
-
choiceMatrix: "Keuzematrix",
|
|
2764
|
-
consentItem: "Toestemming",
|
|
2765
|
-
infoItem: "Informatie",
|
|
2766
|
-
group: "Groep",
|
|
2767
|
-
"page-break": "Pagina-einde",
|
|
2768
|
-
empty: "Leeg item"
|
|
2769
|
-
},
|
|
2770
|
-
de: {
|
|
2771
|
-
choiceItem: "Auswahl",
|
|
2772
|
-
formItem: "Formular",
|
|
2773
|
-
formMatrix: "Formularmatrix",
|
|
2774
|
-
choiceMatrix: "Auswahlmatrix",
|
|
2775
|
-
consentItem: "Einwilligung",
|
|
2776
|
-
infoItem: "Information",
|
|
2777
|
-
group: "Gruppe",
|
|
2778
|
-
"page-break": "Seitenumbruch",
|
|
2779
|
-
empty: "Leeres Element"
|
|
2780
|
-
}
|
|
2781
|
-
};
|
|
2782
|
-
const dictionary = labels[language] ?? labels.en;
|
|
2783
|
-
return [...new Set(types)].map((type) => dictionary[type]).filter(Boolean).join(" + ") || dictionary.empty;
|
|
2784
|
-
}
|
|
2785
|
-
//#endregion
|
|
2786
|
-
//#region src/survey/utils/group-utils.ts
|
|
2787
|
-
/**
|
|
2788
|
-
* Shared utilities for array operations.
|
|
2789
|
-
*/
|
|
2790
|
-
/**
|
|
2791
|
-
* Swap two elements in an array by index.
|
|
2792
|
-
* @returns A new array with the elements swapped
|
|
2793
|
-
*/
|
|
2794
|
-
function swapArrayElements(arr, fromIndex, toIndex) {
|
|
2795
|
-
const result = [...arr];
|
|
2796
|
-
[result[fromIndex], result[toIndex]] = [result[toIndex], result[fromIndex]];
|
|
2797
|
-
return result;
|
|
2798
|
-
}
|
|
2799
|
-
/**
|
|
2800
|
-
* Move an element from one index to another.
|
|
2801
|
-
* @returns A new array with the element moved
|
|
2802
|
-
*/
|
|
2803
|
-
function moveArrayElement(arr, fromIndex, toIndex) {
|
|
2804
|
-
const result = [...arr];
|
|
2805
|
-
const [moved] = result.splice(fromIndex, 1);
|
|
2806
|
-
result.splice(toIndex, 0, moved);
|
|
2807
|
-
return result;
|
|
2808
|
-
}
|
|
2809
|
-
//#endregion
|
|
2810
|
-
//#region src/survey/registry/built-in-items.ts
|
|
2811
|
-
var GroupItemCore = class extends SurveyItemCore {
|
|
2812
|
-
type = "group";
|
|
2813
|
-
parseConfig(rawConfig) {
|
|
2814
|
-
const cfg = rawConfig ?? {};
|
|
2815
|
-
return {
|
|
2816
|
-
items: cfg.items,
|
|
2817
|
-
shuffleItems: cfg.shuffleItems,
|
|
2818
|
-
isRoot: cfg.isRoot
|
|
2819
|
-
};
|
|
2820
|
-
}
|
|
2821
|
-
serializeConfig() {
|
|
2822
|
-
return this.config;
|
|
2823
|
-
}
|
|
2824
|
-
isInteractive() {
|
|
2825
|
-
return false;
|
|
2826
|
-
}
|
|
2827
|
-
getResponseSlotDefinitions() {
|
|
2828
|
-
return [];
|
|
2829
|
-
}
|
|
2830
|
-
isRoot() {
|
|
2831
|
-
return this.config.isRoot ?? false;
|
|
2832
|
-
}
|
|
2833
|
-
get items() {
|
|
2834
|
-
return this.config.items ?? [];
|
|
2835
|
-
}
|
|
2836
|
-
/**
|
|
2837
|
-
* Add a child item id to this group at the given index.
|
|
2838
|
-
* @param itemId The id of the item to add to this group.
|
|
2839
|
-
* @param index The index at which to add the item. If not provided, the item is added to the end of the group.
|
|
2840
|
-
*/
|
|
2841
|
-
addChild(itemId, index) {
|
|
2842
|
-
const items = this.config.items ??= [];
|
|
2843
|
-
const insertIndex = index !== void 0 ? Math.min(index, items.length) : items.length;
|
|
2844
|
-
items.splice(insertIndex, 0, itemId);
|
|
2845
|
-
this.updateRawItem({
|
|
2846
|
-
...this.rawItem,
|
|
2847
|
-
config: { ...this.config }
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
/**
|
|
2851
|
-
* Remove a child item id from this group.
|
|
2852
|
-
* @param itemId The id of the item to remove from this group.
|
|
2853
|
-
*/
|
|
2854
|
-
removeChild(itemId) {
|
|
2855
|
-
const items = this.config.items ??= [];
|
|
2856
|
-
const index = items.indexOf(itemId);
|
|
2857
|
-
if (index !== -1) {
|
|
2858
|
-
items.splice(index, 1);
|
|
2859
|
-
this.updateRawItem({
|
|
2860
|
-
...this.rawItem,
|
|
2861
|
-
config: { ...this.config }
|
|
2862
|
-
});
|
|
2863
|
-
}
|
|
2864
|
-
}
|
|
2865
|
-
/**
|
|
2866
|
-
* Check if an item is a direct child of this group.
|
|
2867
|
-
*/
|
|
2868
|
-
hasChild(itemId) {
|
|
2869
|
-
return this.items.includes(itemId);
|
|
2870
|
-
}
|
|
2871
|
-
/** Whether items in this group should be shuffled. */
|
|
2872
|
-
get shuffleItems() {
|
|
2873
|
-
return this.config.shuffleItems ?? false;
|
|
2874
|
-
}
|
|
2875
|
-
set shuffleItems(value) {
|
|
2876
|
-
this.config.shuffleItems = value;
|
|
2877
|
-
this.updateRawItem({
|
|
2878
|
-
...this.rawItem,
|
|
2879
|
-
config: { ...this.config }
|
|
2880
|
-
});
|
|
2881
|
-
}
|
|
2882
|
-
/**
|
|
2883
|
-
* Get the ordered list of child item ids.
|
|
2884
|
-
*/
|
|
2885
|
-
getChildrenIds() {
|
|
2886
|
-
return [...this.items];
|
|
2887
|
-
}
|
|
2888
|
-
/**
|
|
2889
|
-
* Swap two items by their positions in the group.
|
|
2890
|
-
* @throws Error if indices are out of bounds
|
|
2891
|
-
*/
|
|
2892
|
-
swapItemsByIndex(from, to) {
|
|
2893
|
-
const items = this.items;
|
|
2894
|
-
if (from < 0 || from >= items.length || to < 0 || to >= items.length) throw new Error(`Index out of bounds. Valid range is 0-${items.length - 1}`);
|
|
2895
|
-
if (from === to) return;
|
|
2896
|
-
this.config.items = swapArrayElements(items, from, to);
|
|
2897
|
-
this.updateRawItem({
|
|
2898
|
-
...this.rawItem,
|
|
2899
|
-
config: { ...this.config }
|
|
2900
|
-
});
|
|
2901
|
-
}
|
|
2902
|
-
/**
|
|
2903
|
-
* Swap two items by their ids.
|
|
2904
|
-
* @throws Error if either id is not found in this group
|
|
2905
|
-
*/
|
|
2906
|
-
swapItemsById(id, withId) {
|
|
2907
|
-
const items = this.items;
|
|
2908
|
-
const index1 = items.indexOf(id);
|
|
2909
|
-
const index2 = items.indexOf(withId);
|
|
2910
|
-
if (index1 === -1) throw new Error(`Item '${id}' not found in group '${this.metadata?.itemLabel ?? ""}'`);
|
|
2911
|
-
if (index2 === -1) throw new Error(`Item '${withId}' not found in group '${this.metadata?.itemLabel ?? ""}'`);
|
|
2912
|
-
this.swapItemsByIndex(index1, index2);
|
|
2913
|
-
}
|
|
2914
|
-
/**
|
|
2915
|
-
* Move an item by id to a specific index.
|
|
2916
|
-
* @throws Error if the item is not found or index is out of bounds
|
|
2917
|
-
*/
|
|
2918
|
-
moveItemToIndex(id, index) {
|
|
2919
|
-
const items = this.items;
|
|
2920
|
-
const fromIndex = items.indexOf(id);
|
|
2921
|
-
if (fromIndex === -1) throw new Error(`Item '${id}' not found in group '${this.metadata?.itemLabel ?? ""}'`);
|
|
2922
|
-
if (index < 0 || index >= items.length) throw new Error(`Index out of bounds. Valid range is 0-${items.length - 1}`);
|
|
2923
|
-
if (fromIndex === index) return;
|
|
2924
|
-
this.config.items = moveArrayElement(items, fromIndex, index);
|
|
2925
|
-
this.updateRawItem({
|
|
2926
|
-
...this.rawItem,
|
|
2927
|
-
config: { ...this.config }
|
|
2928
|
-
});
|
|
2929
|
-
}
|
|
2930
|
-
};
|
|
2931
|
-
var PageBreakItemCore = class extends SurveyItemCore {
|
|
2932
|
-
type = "page-break";
|
|
2933
|
-
parseConfig(_rawConfig) {
|
|
2934
|
-
return {};
|
|
2935
|
-
}
|
|
2936
|
-
serializeConfig() {}
|
|
2937
|
-
isInteractive() {
|
|
2938
|
-
return false;
|
|
2939
|
-
}
|
|
2940
|
-
getResponseSlotDefinitions() {
|
|
2941
|
-
return [];
|
|
2942
|
-
}
|
|
2943
|
-
};
|
|
2944
|
-
const builtInItemCoreRegistry = {
|
|
2945
|
-
["group"]: GroupItemCore,
|
|
2946
|
-
["page-break"]: PageBreakItemCore
|
|
2947
|
-
};
|
|
2948
|
-
const isBuiltInItemType = (itemType) => itemType in builtInItemCoreRegistry;
|
|
2949
|
-
//#endregion
|
|
2950
|
-
//#region src/survey/registry/item-registry.ts
|
|
2951
|
-
/** Convert constructor registry to explicit definitions for higher-layer composition. */
|
|
2952
|
-
function toItemTypeDefinitionRegistry(registry) {
|
|
2953
|
-
return Object.fromEntries(Object.entries(registry).map(([itemType, constructor]) => [itemType, {
|
|
2954
|
-
itemType,
|
|
2955
|
-
constructor
|
|
2956
|
-
}]));
|
|
2957
|
-
}
|
|
2958
|
-
/**
|
|
2959
|
-
* Creates a handler instance for a raw survey item.
|
|
2960
|
-
* Uses built-in registry for group/page-break, otherwise the provided plugin registry.
|
|
2961
|
-
*/
|
|
2962
|
-
function createItemCore(rawItem, pluginRegistry) {
|
|
2963
|
-
if (isBuiltInItemType(rawItem.itemType)) {
|
|
2964
|
-
const HandlerClass = builtInItemCoreRegistry[rawItem.itemType];
|
|
2965
|
-
return new HandlerClass(rawItem);
|
|
2966
|
-
}
|
|
2967
|
-
if (pluginRegistry && rawItem.itemType in pluginRegistry) {
|
|
2968
|
-
const HandlerClass = pluginRegistry[rawItem.itemType];
|
|
2969
|
-
return new HandlerClass(rawItem);
|
|
2970
|
-
}
|
|
2971
|
-
throw new Error(`Unknown item type: ${rawItem.itemType}`);
|
|
2972
|
-
}
|
|
2973
|
-
/**
|
|
2974
|
-
* Merge built-in handlers with plugin registry for a complete registry.
|
|
2975
|
-
*/
|
|
2976
|
-
function createFullRegistry(pluginRegistry) {
|
|
2977
|
-
return {
|
|
2978
|
-
...builtInItemCoreRegistry,
|
|
2979
|
-
...pluginRegistry
|
|
2980
|
-
};
|
|
2981
|
-
}
|
|
2982
|
-
/** Build full registry as explicit definitions for editor/ui composition. */
|
|
2983
|
-
function createItemTypeDefinitionRegistry(pluginRegistry) {
|
|
2984
|
-
return toItemTypeDefinitionRegistry(createFullRegistry(pluginRegistry));
|
|
2985
|
-
}
|
|
2986
|
-
//#endregion
|
|
2987
|
-
//#region src/survey/survey-file-schema.ts
|
|
2988
|
-
const CURRENT_SURVEY_SCHEMA_VERSION = 2;
|
|
2989
|
-
const CURRENT_SURVEY_SCHEMA = "https://github.com/case-framework/case-survey-toolkit/packages/survey-core/schemas/survey-schema.json";
|
|
2990
|
-
//#endregion
|
|
2991
|
-
//#region src/survey/utils/translations.ts
|
|
2992
|
-
const validateLocale = (locale) => {
|
|
2993
|
-
if (locale.trim() === "") throw new Error("Locale cannot be empty");
|
|
2994
|
-
};
|
|
2995
|
-
var SurveyItemTranslations = class SurveyItemTranslations {
|
|
2996
|
-
_translations;
|
|
2997
|
-
constructor() {
|
|
2998
|
-
this._translations = {};
|
|
2999
|
-
}
|
|
3000
|
-
setContent(locale, contentKey, content) {
|
|
3001
|
-
validateLocale(locale);
|
|
3002
|
-
if (!this._translations?.[locale]) {
|
|
3003
|
-
if (!content) return;
|
|
3004
|
-
this._translations[locale] = {};
|
|
3005
|
-
}
|
|
3006
|
-
if (!content) delete this._translations[locale][contentKey];
|
|
3007
|
-
else this._translations[locale][contentKey] = content;
|
|
3008
|
-
}
|
|
3009
|
-
setAllForLocale(locale, content) {
|
|
3010
|
-
validateLocale(locale);
|
|
3011
|
-
if (!this._translations?.[locale]) {
|
|
3012
|
-
if (!content) return;
|
|
3013
|
-
this._translations[locale] = {};
|
|
3014
|
-
}
|
|
3015
|
-
this._translations[locale] = content || {};
|
|
3016
|
-
}
|
|
3017
|
-
get locales() {
|
|
3018
|
-
return Object.keys(this._translations || {});
|
|
3019
|
-
}
|
|
3020
|
-
getAllForLocale(locale) {
|
|
3021
|
-
return this._translations?.[locale];
|
|
3022
|
-
}
|
|
3023
|
-
getContent(locale, contentKey, fallbackLocale) {
|
|
3024
|
-
const content = this._translations?.[locale]?.[contentKey];
|
|
3025
|
-
if (content) return content;
|
|
3026
|
-
if (fallbackLocale) return this._translations?.[fallbackLocale]?.[contentKey];
|
|
3027
|
-
}
|
|
3028
|
-
/**
|
|
3029
|
-
* Create a deep clone that can be safely mutated before persisting with
|
|
3030
|
-
* `setItemTranslations` or `updateItemTranslations`.
|
|
3031
|
-
*/
|
|
3032
|
-
clone() {
|
|
3033
|
-
const cloned = new SurveyItemTranslations();
|
|
3034
|
-
for (const locale of this.locales) {
|
|
3035
|
-
const localeContent = this.getAllForLocale(locale);
|
|
3036
|
-
if (!localeContent) continue;
|
|
3037
|
-
cloned.setAllForLocale(locale, structuredCloneMethod(localeContent));
|
|
3038
|
-
}
|
|
3039
|
-
return cloned;
|
|
3040
|
-
}
|
|
3041
|
-
/**
|
|
3042
|
-
* Remove a single content key from every locale in this translation set.
|
|
3043
|
-
*/
|
|
3044
|
-
removeContentKey(contentKey) {
|
|
3045
|
-
for (const locale of this.locales) delete this._translations?.[locale]?.[contentKey];
|
|
3046
|
-
}
|
|
3047
|
-
/**
|
|
3048
|
-
* Remove every content key that is exactly the prefix or nested beneath it.
|
|
3049
|
-
*/
|
|
3050
|
-
removeContentKeysWithPrefix(prefix) {
|
|
3051
|
-
for (const locale of this.locales) {
|
|
3052
|
-
const localeContent = this._translations?.[locale];
|
|
3053
|
-
if (!localeContent) continue;
|
|
3054
|
-
for (const contentKey of Object.keys(localeContent)) if (contentKey === prefix || contentKey.startsWith(prefix + ".")) delete localeContent[contentKey];
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3057
|
-
/**
|
|
3058
|
-
* Remove empty locales in place.
|
|
3059
|
-
* Returns `undefined` when no translations remain so callers can pass the
|
|
3060
|
-
* result directly into `setItemTranslations` / `updateItemTranslations`.
|
|
3061
|
-
*/
|
|
3062
|
-
compact() {
|
|
3063
|
-
for (const locale of this.locales) {
|
|
3064
|
-
const localeContent = this._translations?.[locale];
|
|
3065
|
-
if (!localeContent) continue;
|
|
3066
|
-
if (Object.keys(localeContent).length === 0) delete this._translations?.[locale];
|
|
3067
|
-
}
|
|
3068
|
-
return this.locales.length > 0 ? this : void 0;
|
|
3069
|
-
}
|
|
3070
|
-
};
|
|
3071
|
-
var SurveyTranslations = class SurveyTranslations {
|
|
3072
|
-
_translations;
|
|
3073
|
-
constructor(translations) {
|
|
3074
|
-
this._translations = structuredCloneMethod(translations || {});
|
|
3075
|
-
}
|
|
3076
|
-
/** Cheap revision fork. Every writer replaces its path before changing it. */
|
|
3077
|
-
fork() {
|
|
3078
|
-
const next = new SurveyTranslations();
|
|
3079
|
-
next._translations = this._translations;
|
|
3080
|
-
return next;
|
|
3081
|
-
}
|
|
3082
|
-
serialize() {
|
|
3083
|
-
return this.locales.length ? structuredCloneMethod(this._translations) : void 0;
|
|
3084
|
-
}
|
|
3085
|
-
get locales() {
|
|
3086
|
-
return Object.keys(this._translations);
|
|
3087
|
-
}
|
|
3088
|
-
replaceLocale(locale, value) {
|
|
3089
|
-
this._translations = {
|
|
3090
|
-
...this._translations,
|
|
3091
|
-
[locale]: value
|
|
3092
|
-
};
|
|
3093
|
-
}
|
|
3094
|
-
replaceItem(locale, itemId, fields, keepEmpty = false) {
|
|
3095
|
-
const current = this._translations[locale] ?? {};
|
|
3096
|
-
const items = { ...current.itemTranslations };
|
|
3097
|
-
if (fields && (keepEmpty || Object.keys(fields).length)) items[itemId] = fields;
|
|
3098
|
-
else delete items[itemId];
|
|
3099
|
-
this.replaceLocale(locale, {
|
|
3100
|
-
...current,
|
|
3101
|
-
itemTranslations: items
|
|
3102
|
-
});
|
|
3103
|
-
}
|
|
3104
|
-
addLocale(locale) {
|
|
3105
|
-
validateLocale(locale);
|
|
3106
|
-
if (this._translations[locale]) return false;
|
|
3107
|
-
this.replaceLocale(locale, {});
|
|
3108
|
-
return true;
|
|
3109
|
-
}
|
|
3110
|
-
removeLocale(locale) {
|
|
3111
|
-
validateLocale(locale);
|
|
3112
|
-
const next = { ...this._translations };
|
|
3113
|
-
delete next[locale];
|
|
3114
|
-
this._translations = next;
|
|
3115
|
-
}
|
|
3116
|
-
renameLocale(oldLocale, newLocale) {
|
|
3117
|
-
validateLocale(oldLocale);
|
|
3118
|
-
validateLocale(newLocale);
|
|
3119
|
-
if (this._translations[oldLocale]) {
|
|
3120
|
-
this.replaceLocale(newLocale, this._translations[oldLocale]);
|
|
3121
|
-
this.removeLocale(oldLocale);
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
cloneLocaleAs(locale, newLocale) {
|
|
3125
|
-
validateLocale(locale);
|
|
3126
|
-
validateLocale(newLocale);
|
|
3127
|
-
if (this._translations[locale]) this.replaceLocale(newLocale, this._translations[locale]);
|
|
3128
|
-
}
|
|
3129
|
-
get surveyCardContent() {
|
|
3130
|
-
const result = {};
|
|
3131
|
-
for (const locale of this.locales) {
|
|
3132
|
-
const content = this._translations[locale].surveyCardContent;
|
|
3133
|
-
if (content) result[locale] = structuredCloneMethod(content);
|
|
3134
|
-
}
|
|
3135
|
-
return result;
|
|
3136
|
-
}
|
|
3137
|
-
get navigationContent() {
|
|
3138
|
-
const result = {};
|
|
3139
|
-
for (const locale of this.locales) {
|
|
3140
|
-
const content = this._translations[locale].navigationContent;
|
|
3141
|
-
if (content) result[locale] = structuredCloneMethod(content);
|
|
3142
|
-
}
|
|
3143
|
-
return result;
|
|
3144
|
-
}
|
|
3145
|
-
get validationMessages() {
|
|
3146
|
-
const result = {};
|
|
3147
|
-
for (const locale of this.locales) {
|
|
3148
|
-
const content = this._translations[locale].validationMessages;
|
|
3149
|
-
if (content) result[locale] = structuredCloneMethod(content);
|
|
3150
|
-
}
|
|
3151
|
-
return result;
|
|
3152
|
-
}
|
|
3153
|
-
setSurveyCardContent(locale, content) {
|
|
3154
|
-
validateLocale(locale);
|
|
3155
|
-
if (!content && !this._translations[locale]) return;
|
|
3156
|
-
this.replaceLocale(locale, {
|
|
3157
|
-
...this._translations[locale],
|
|
3158
|
-
surveyCardContent: structuredCloneMethod(content)
|
|
3159
|
-
});
|
|
3160
|
-
}
|
|
3161
|
-
setNavigationContent(locale, content) {
|
|
3162
|
-
validateLocale(locale);
|
|
3163
|
-
if (!content && !this._translations[locale]) return;
|
|
3164
|
-
this.replaceLocale(locale, {
|
|
3165
|
-
...this._translations[locale],
|
|
3166
|
-
navigationContent: structuredCloneMethod(content)
|
|
3167
|
-
});
|
|
3168
|
-
}
|
|
3169
|
-
setValidationMessages(locale, content) {
|
|
3170
|
-
validateLocale(locale);
|
|
3171
|
-
if (!content && !this._translations[locale]) return;
|
|
3172
|
-
this.replaceLocale(locale, {
|
|
3173
|
-
...this._translations[locale],
|
|
3174
|
-
validationMessages: structuredCloneMethod(content)
|
|
3175
|
-
});
|
|
3176
|
-
}
|
|
3177
|
-
getItemTranslationContent(itemId, locale, contentKey, fallbackLocale) {
|
|
3178
|
-
return structuredCloneMethod(this._translations[locale]?.itemTranslations?.[itemId]?.[contentKey] ?? (fallbackLocale ? this._translations[fallbackLocale]?.itemTranslations?.[itemId]?.[contentKey] : void 0));
|
|
3179
|
-
}
|
|
3180
|
-
setItemTranslationContent(itemId, locale, contentKey, content) {
|
|
3181
|
-
validateLocale(locale);
|
|
3182
|
-
const current = this._translations[locale]?.itemTranslations?.[itemId];
|
|
3183
|
-
if (JSON.stringify(current?.[contentKey]) === JSON.stringify(content)) return false;
|
|
3184
|
-
const fields = { ...current };
|
|
3185
|
-
if (content === void 0) delete fields[contentKey];
|
|
3186
|
-
else fields[contentKey] = structuredCloneMethod(content);
|
|
3187
|
-
this.replaceItem(locale, itemId, fields);
|
|
3188
|
-
return true;
|
|
3189
|
-
}
|
|
3190
|
-
getItemTranslations(itemId) {
|
|
3191
|
-
const result = new SurveyItemTranslations();
|
|
3192
|
-
for (const locale of this.locales) result.setAllForLocale(locale, structuredCloneMethod(this._translations[locale].itemTranslations?.[itemId]));
|
|
3193
|
-
return result.locales.length ? result : void 0;
|
|
3194
|
-
}
|
|
3195
|
-
setItemTranslations(itemId, content) {
|
|
3196
|
-
const normalized = content?.clone().compact();
|
|
3197
|
-
for (const locale of /* @__PURE__ */ new Set([...this.locales, ...normalized?.locales ?? []])) {
|
|
3198
|
-
validateLocale(locale);
|
|
3199
|
-
const fields = normalized?.getAllForLocale(locale);
|
|
3200
|
-
if (fields || this._translations[locale]?.itemTranslations?.[itemId]) this.replaceItem(locale, itemId, fields);
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
onComponentKeyChanged(itemId, oldKey, newKey) {
|
|
3204
|
-
for (const locale of this.locales) {
|
|
3205
|
-
const current = this._translations[locale].itemTranslations?.[itemId];
|
|
3206
|
-
if (!current) continue;
|
|
3207
|
-
const fields = { ...current };
|
|
3208
|
-
for (const key of Object.keys(current)) if (key === oldKey || key.startsWith(oldKey + ".")) {
|
|
3209
|
-
fields[key.replace(oldKey, newKey)] = current[key];
|
|
3210
|
-
delete fields[key];
|
|
3211
|
-
}
|
|
3212
|
-
this.replaceItem(locale, itemId, fields, true);
|
|
3213
|
-
}
|
|
3214
|
-
}
|
|
3215
|
-
onComponentDeleted(itemId, componentKey) {
|
|
3216
|
-
for (const locale of this.locales) {
|
|
3217
|
-
const current = this._translations[locale].itemTranslations?.[itemId];
|
|
3218
|
-
if (!current) continue;
|
|
3219
|
-
const fields = { ...current };
|
|
3220
|
-
for (const key of Object.keys(current)) if (key === componentKey || key.startsWith(componentKey + ".")) delete fields[key];
|
|
3221
|
-
this.replaceItem(locale, itemId, fields, true);
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
|
-
onItemDeleted(itemId) {
|
|
3225
|
-
for (const locale of this.locales) if (this._translations[locale].itemTranslations?.[itemId]) this.replaceItem(locale, itemId);
|
|
3226
|
-
}
|
|
3227
|
-
};
|
|
3228
|
-
//#endregion
|
|
3229
|
-
//#region src/survey/utils/content.ts
|
|
3230
|
-
let ContentType = /* @__PURE__ */ function(ContentType) {
|
|
3231
|
-
ContentType["richText"] = "richText";
|
|
3232
|
-
ContentType["plain"] = "plain";
|
|
3233
|
-
ContentType["md"] = "md";
|
|
3234
|
-
return ContentType;
|
|
3235
|
-
}({});
|
|
3236
|
-
function hasRenderableRichTextBlock(block) {
|
|
3237
|
-
switch (block.type) {
|
|
3238
|
-
case "paragraph": return block.children.length > 0;
|
|
3239
|
-
case "heading": return block.children.length > 0;
|
|
3240
|
-
case "bulletList": return block.items.some((item) => item.children.some((paragraph) => paragraph.children.length > 0));
|
|
3241
|
-
case "image": return true;
|
|
3242
|
-
case "infoBox": return true;
|
|
3243
|
-
case "separator": return true;
|
|
3244
|
-
case "blockExtension": return true;
|
|
3245
|
-
}
|
|
3246
|
-
}
|
|
3247
|
-
function hasRenderableRichTextContent(content) {
|
|
3248
|
-
return content.doc.blocks.some((block) => hasRenderableRichTextBlock(block));
|
|
3249
|
-
}
|
|
3250
|
-
function createRichTextContent(text = "") {
|
|
3251
|
-
return {
|
|
3252
|
-
type: "richText",
|
|
3253
|
-
version: 1,
|
|
3254
|
-
doc: {
|
|
3255
|
-
type: "doc",
|
|
3256
|
-
blocks: [{
|
|
3257
|
-
type: "paragraph",
|
|
3258
|
-
children: text.length > 0 ? text.split("\n").flatMap((line, index) => {
|
|
3259
|
-
const parts = [];
|
|
3260
|
-
if (index > 0) parts.push({ type: "lineBreak" });
|
|
3261
|
-
if (line.length > 0) parts.push({
|
|
3262
|
-
type: "text",
|
|
3263
|
-
text: line
|
|
3264
|
-
});
|
|
3265
|
-
return parts;
|
|
3266
|
-
}) : []
|
|
3267
|
-
}]
|
|
3268
|
-
}
|
|
3269
|
-
};
|
|
3270
|
-
}
|
|
3271
|
-
function getPlainTextFromLinkChildren(children) {
|
|
3272
|
-
return children.map((inline) => {
|
|
3273
|
-
switch (inline.type) {
|
|
3274
|
-
case "text": return inline.text;
|
|
3275
|
-
case "template": return `{${inline.key}}`;
|
|
3276
|
-
case "lineBreak": return "\n";
|
|
3277
|
-
}
|
|
3278
|
-
}).join("");
|
|
3279
|
-
}
|
|
3280
|
-
function getPlainTextFromInlineNodes(inlines) {
|
|
3281
|
-
return inlines.map((inline) => {
|
|
3282
|
-
switch (inline.type) {
|
|
3283
|
-
case "text": return inline.text;
|
|
3284
|
-
case "template": return `{${inline.key}}`;
|
|
3285
|
-
case "link": return getPlainTextFromLinkChildren(inline.children);
|
|
3286
|
-
case "lineBreak": return "\n";
|
|
3287
|
-
case "inlineExtension": return "";
|
|
3288
|
-
}
|
|
3289
|
-
}).join("");
|
|
3290
|
-
}
|
|
3291
|
-
function getPlainTextFromParagraphs(paragraphs) {
|
|
3292
|
-
return paragraphs.map((paragraph) => getPlainTextFromInlineNodes(paragraph.children)).join("\n");
|
|
3293
|
-
}
|
|
3294
|
-
function getPlainTextFromBlock(block) {
|
|
3295
|
-
switch (block.type) {
|
|
3296
|
-
case "paragraph": return getPlainTextFromInlineNodes(block.children);
|
|
3297
|
-
case "heading": return getPlainTextFromInlineNodes(block.children);
|
|
3298
|
-
case "bulletList": return block.items.map((item) => getPlainTextFromParagraphs(item.children)).join("\n");
|
|
3299
|
-
case "image": {
|
|
3300
|
-
const captionText = block.caption ? getPlainTextFromParagraphs(block.caption) : "";
|
|
3301
|
-
return [block.alt ?? "", captionText].filter(Boolean).join("\n");
|
|
3302
|
-
}
|
|
3303
|
-
case "infoBox": return [block.title ? getPlainTextFromInlineNodes(block.title) : "", getPlainTextFromParagraphs(block.children)].filter(Boolean).join("\n");
|
|
3304
|
-
case "separator": return "";
|
|
3305
|
-
case "blockExtension": return "";
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
function getPlainTextFromRichTextContent(content) {
|
|
3309
|
-
return content.doc.blocks.map((block) => getPlainTextFromBlock(block)).filter((text) => text.length > 0).join("\n").trim();
|
|
3310
|
-
}
|
|
3311
|
-
function getContentPlainText(content) {
|
|
3312
|
-
if (!content) return "";
|
|
3313
|
-
if (content.type !== "richText") return content.content ?? "";
|
|
3314
|
-
return getPlainTextFromRichTextContent(content);
|
|
3315
|
-
}
|
|
3316
|
-
function isContentEmpty(content) {
|
|
3317
|
-
if (!content) return true;
|
|
3318
|
-
if (content.type !== "richText") return content.content.trim().length === 0;
|
|
3319
|
-
return !hasRenderableRichTextContent(content);
|
|
3320
|
-
}
|
|
3321
|
-
function getAssetUsagesFromContent(content) {
|
|
3322
|
-
if (!content || content.type !== "richText") return [];
|
|
3323
|
-
const usages = [];
|
|
3324
|
-
content.doc.blocks.forEach((block, blockIndex) => {
|
|
3325
|
-
if (block.type === "image" && block.source.type === "asset") usages.push({
|
|
3326
|
-
assetId: block.source.assetId,
|
|
3327
|
-
blockIndex
|
|
3328
|
-
});
|
|
3329
|
-
});
|
|
3330
|
-
return usages;
|
|
3331
|
-
}
|
|
3332
|
-
//#endregion
|
|
3333
|
-
//#region src/survey/survey.ts
|
|
3334
|
-
var Survey = class Survey {
|
|
3335
|
-
pluginRegistry;
|
|
3336
|
-
maxItemsPerPage;
|
|
3337
|
-
metadata;
|
|
3338
|
-
surveyItems = /* @__PURE__ */ new Map();
|
|
3339
|
-
_templateValues = /* @__PURE__ */ new Map();
|
|
3340
|
-
_assets = /* @__PURE__ */ new Map();
|
|
3341
|
-
_translations;
|
|
3342
|
-
constructor(pluginRegistry) {
|
|
3343
|
-
this.pluginRegistry = pluginRegistry;
|
|
3344
|
-
this._translations = new SurveyTranslations();
|
|
3345
|
-
}
|
|
3346
|
-
/**
|
|
3347
|
-
* Derive a read revision from an already detached view. Editable core instances
|
|
3348
|
-
* are never reused: the changed item is reconstructed from owned raw data.
|
|
3349
|
-
* Other detached items are unchanged by translation publication.
|
|
3350
|
-
*/
|
|
3351
|
-
withItemTranslationContent(rawItem, locale, contentKey, content) {
|
|
3352
|
-
const next = new Survey(this.pluginRegistry);
|
|
3353
|
-
next.surveyItems = new Map(this.surveyItems);
|
|
3354
|
-
next.surveyItems.set(rawItem.id, next.createItemFromRaw(structuredCloneMethod(rawItem)));
|
|
3355
|
-
next._translations = this.translations.fork();
|
|
3356
|
-
next._translations.setItemTranslationContent(rawItem.id, locale, contentKey, content);
|
|
3357
|
-
next._assets = this._assets;
|
|
3358
|
-
next._templateValues = this._templateValues;
|
|
3359
|
-
next.metadata = this.metadata;
|
|
3360
|
-
next.maxItemsPerPage = this.maxItemsPerPage;
|
|
3361
|
-
return next;
|
|
3362
|
-
}
|
|
3363
|
-
/** Plugin registry used when parsing items. Exposed for editors that re-parse without their own registry. */
|
|
3364
|
-
getPluginRegistry() {
|
|
3365
|
-
return this.pluginRegistry;
|
|
3366
|
-
}
|
|
3367
|
-
/**
|
|
3368
|
-
* Create a survey item from raw JSON data.
|
|
3369
|
-
* Uses the survey's plugin registry when available.
|
|
3370
|
-
*/
|
|
3371
|
-
createItemFromRaw(rawItem) {
|
|
3372
|
-
return createItemCore(rawItem, this.pluginRegistry);
|
|
3373
|
-
}
|
|
3374
|
-
/**
|
|
3375
|
-
* Create a minimal blank survey with a root group and one empty page.
|
|
3376
|
-
*/
|
|
3377
|
-
static createBlankSurvey(pluginRegistry, surveyKey) {
|
|
3378
|
-
const rawSurvey = {
|
|
3379
|
-
$schema: CURRENT_SURVEY_SCHEMA,
|
|
3380
|
-
schemaVersion: 2,
|
|
3381
|
-
metadata: { surveyKey: surveyKey ?? "survey" },
|
|
3382
|
-
surveyItems: [{
|
|
3383
|
-
id: generateId(),
|
|
3384
|
-
itemType: "group",
|
|
3385
|
-
config: {
|
|
3386
|
-
isRoot: true,
|
|
3387
|
-
items: [],
|
|
3388
|
-
shuffleItems: false
|
|
3389
|
-
}
|
|
3390
|
-
}]
|
|
3391
|
-
};
|
|
3392
|
-
return Survey.fromJson(rawSurvey, pluginRegistry);
|
|
3393
|
-
}
|
|
3394
|
-
static fromJson(json, pluginRegistry) {
|
|
3395
|
-
const survey = new Survey(pluginRegistry);
|
|
3396
|
-
const rawSurvey = json;
|
|
3397
|
-
if (rawSurvey.$schema !== "https://github.com/case-framework/case-survey-toolkit/packages/survey-core/schemas/survey-schema.json") throw new Error(`Unsupported survey schema: ${rawSurvey.$schema}`);
|
|
3398
|
-
if (rawSurvey.schemaVersion !== 2) throw new Error(`Unsupported survey format version: ${String(rawSurvey.schemaVersion)}. Recreate this draft using the current schema.`);
|
|
3399
|
-
survey.surveyItems = /* @__PURE__ */ new Map();
|
|
3400
|
-
if (!rawSurvey.surveyItems || rawSurvey.surveyItems.length === 0) throw new Error("surveyItems is required");
|
|
3401
|
-
rawSurvey.surveyItems.forEach((item) => {
|
|
3402
|
-
if (survey.surveyItems.has(item.id)) throw new Error(`Duplicate survey item identity: ${item.id}`);
|
|
3403
|
-
const surveyItem = createItemCore(item, pluginRegistry);
|
|
3404
|
-
survey.surveyItems.set(surveyItem.id, surveyItem);
|
|
3405
|
-
});
|
|
3406
|
-
if (rawSurvey.templateValues) survey._templateValues = deserializeTemplateValues(rawSurvey.templateValues);
|
|
3407
|
-
if (rawSurvey.assets) survey._assets = new Map(Object.entries(rawSurvey.assets));
|
|
3408
|
-
survey._translations = new SurveyTranslations(rawSurvey.translations);
|
|
3409
|
-
if (rawSurvey.maxItemsPerPage) survey.maxItemsPerPage = rawSurvey.maxItemsPerPage;
|
|
3410
|
-
if (rawSurvey.metadata) survey.metadata = rawSurvey.metadata;
|
|
3411
|
-
return survey;
|
|
3412
|
-
}
|
|
3413
|
-
serialize() {
|
|
3414
|
-
const json = {
|
|
3415
|
-
$schema: CURRENT_SURVEY_SCHEMA,
|
|
3416
|
-
schemaVersion: 2,
|
|
3417
|
-
surveyItems: Array.from(this.surveyItems.values()).map((item) => item.rawItem)
|
|
3418
|
-
};
|
|
3419
|
-
json.translations = this._translations?.serialize();
|
|
3420
|
-
if (this._templateValues) json.templateValues = serializeTemplateValues(this._templateValues);
|
|
3421
|
-
if (this._assets && this._assets.size > 0) json.assets = Object.fromEntries(this._assets.entries());
|
|
3422
|
-
if (this.maxItemsPerPage) json.maxItemsPerPage = this.maxItemsPerPage;
|
|
3423
|
-
if (this.metadata) json.metadata = this.metadata;
|
|
3424
|
-
return json;
|
|
3425
|
-
}
|
|
3426
|
-
get surveyKey() {
|
|
3427
|
-
return this.metadata?.surveyKey;
|
|
3428
|
-
}
|
|
3429
|
-
resolveItemEditorName(itemId, locale = this.locales[0] ?? "en") {
|
|
3430
|
-
const item = this.surveyItems.get(itemId);
|
|
3431
|
-
if (!item) return getCompositionLabel([], locale);
|
|
3432
|
-
const explicit = item.metadata.itemLabel?.trim();
|
|
3433
|
-
if (explicit) return explicit;
|
|
3434
|
-
const translations = this.getItemTranslations(itemId);
|
|
3435
|
-
for (const key of item.getOutlineContentKeys()) {
|
|
3436
|
-
const content = translations?.getContent(locale, key, this.locales[0]);
|
|
3437
|
-
if (content) {
|
|
3438
|
-
const summary = getContentPlainText(content).replace(/\s+/g, " ").trim();
|
|
3439
|
-
if (summary) return summary;
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
return getCompositionLabel(item.getCompositionTypes(), locale);
|
|
3443
|
-
}
|
|
3444
|
-
get locales() {
|
|
3445
|
-
return this._translations?.locales || [];
|
|
3446
|
-
}
|
|
3447
|
-
get rootItem() {
|
|
3448
|
-
return Array.from(this.surveyItems.values()).find((item) => item instanceof GroupItemCore && item.isRoot);
|
|
3449
|
-
}
|
|
3450
|
-
get itemOutline() {
|
|
3451
|
-
return [...this.surveyItems.keys()].map((id) => this.getItemDisplayPath(id));
|
|
3452
|
-
}
|
|
3453
|
-
getItemPath(itemId) {
|
|
3454
|
-
const item = this.surveyItems.get(itemId);
|
|
3455
|
-
if (!item) throw new Error(`Item ${itemId} not found`);
|
|
3456
|
-
if (item instanceof GroupItemCore && item.isRoot()) return [];
|
|
3457
|
-
const parentItem = this.getParentItem(itemId);
|
|
3458
|
-
if (!parentItem) throw new Error(`Parent item for ${itemId} not found`);
|
|
3459
|
-
return [...this.getItemPath(parentItem.id), parentItem.id];
|
|
3460
|
-
}
|
|
3461
|
-
getItemDisplayPath(itemId, locale = this.locales[0] ?? "en") {
|
|
3462
|
-
if (!this.surveyItems.has(itemId)) return;
|
|
3463
|
-
const path = this.getItemPath(itemId);
|
|
3464
|
-
const name = this.resolveItemEditorName(itemId, locale);
|
|
3465
|
-
const namePath = path.map((id) => this.resolveItemEditorName(id, locale));
|
|
3466
|
-
return {
|
|
3467
|
-
itemId,
|
|
3468
|
-
name,
|
|
3469
|
-
path,
|
|
3470
|
-
namePath,
|
|
3471
|
-
breadcrumb: [...namePath, name].join(" / "),
|
|
3472
|
-
parentBreadcrumb: namePath.join(" / ")
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
|
-
getParentItem(itemId) {
|
|
3476
|
-
const item = this.surveyItems.get(itemId);
|
|
3477
|
-
if (!item) throw new Error(`Item ${itemId} not found`);
|
|
3478
|
-
if (item instanceof GroupItemCore && item.isRoot()) return;
|
|
3479
|
-
const parentItem = Array.from(this.surveyItems.values()).find((item) => item instanceof GroupItemCore && item.config.items?.includes(itemId));
|
|
3480
|
-
if (!parentItem) return;
|
|
3481
|
-
return parentItem;
|
|
3482
|
-
}
|
|
3483
|
-
getChildrenItems(parentId) {
|
|
3484
|
-
const parentItem = this.surveyItems.get(parentId);
|
|
3485
|
-
if (!parentItem || !(parentItem instanceof GroupItemCore)) return [];
|
|
3486
|
-
return parentItem.config.items?.map((id) => this.surveyItems.get(id))?.filter((item) => item !== void 0) || [];
|
|
3487
|
-
}
|
|
3488
|
-
getSiblings(itemId) {
|
|
3489
|
-
if (!this.surveyItems.get(itemId)) return [];
|
|
3490
|
-
return this.getParentItem(itemId)?.items?.map((id) => this.surveyItems.get(id))?.filter((item) => item !== void 0) || [];
|
|
3491
|
-
}
|
|
3492
|
-
get translations() {
|
|
3493
|
-
if (!this._translations) this._translations = new SurveyTranslations();
|
|
3494
|
-
return this._translations;
|
|
3495
|
-
}
|
|
3496
|
-
getItemTranslations(id) {
|
|
3497
|
-
if (!this.surveyItems.get(id)) throw new Error(`Item ${id} not found`);
|
|
3498
|
-
return this._translations?.getItemTranslations(id);
|
|
3499
|
-
}
|
|
3500
|
-
getTemplateValue(templateValueKey) {
|
|
3501
|
-
return this._templateValues?.get(templateValueKey);
|
|
3502
|
-
}
|
|
3503
|
-
setTemplateValue(templateValueKey, templateValue) {
|
|
3504
|
-
if (!this._templateValues) this._templateValues = /* @__PURE__ */ new Map();
|
|
3505
|
-
this._templateValues?.set(templateValueKey, templateValue);
|
|
3506
|
-
}
|
|
3507
|
-
deleteTemplateValue(templateValueKey) {
|
|
3508
|
-
this._templateValues?.delete(templateValueKey);
|
|
3509
|
-
}
|
|
3510
|
-
getTemplateValueKeys() {
|
|
3511
|
-
return Array.from(this._templateValues?.keys() || []);
|
|
3512
|
-
}
|
|
3513
|
-
getAsset(assetId) {
|
|
3514
|
-
const asset = this._assets?.get(assetId);
|
|
3515
|
-
return asset ? structuredCloneMethod(asset) : void 0;
|
|
3516
|
-
}
|
|
3517
|
-
setAsset(assetId, asset) {
|
|
3518
|
-
if (!this._assets) this._assets = /* @__PURE__ */ new Map();
|
|
3519
|
-
this._assets.set(assetId, structuredCloneMethod(asset));
|
|
3520
|
-
}
|
|
3521
|
-
deleteAsset(assetId) {
|
|
3522
|
-
this._assets?.delete(assetId);
|
|
3523
|
-
}
|
|
3524
|
-
hasAsset(assetId) {
|
|
3525
|
-
return this._assets?.has(assetId) ?? false;
|
|
3526
|
-
}
|
|
3527
|
-
getAssetIds() {
|
|
3528
|
-
return Array.from(this._assets?.keys() || []);
|
|
3529
|
-
}
|
|
3530
|
-
getAssets() {
|
|
3531
|
-
return new Map(Array.from(this._assets?.entries() || []).map(([assetId, asset]) => [assetId, structuredCloneMethod(asset)]));
|
|
3532
|
-
}
|
|
3533
|
-
getAssetUsages(filterAssetId) {
|
|
3534
|
-
const usages = [];
|
|
3535
|
-
const pushContentUsages = (content, usageScope, contentKey, locale, itemId) => {
|
|
3536
|
-
for (const usage of getAssetUsagesFromContent(content)) {
|
|
3537
|
-
if (filterAssetId && usage.assetId !== filterAssetId) continue;
|
|
3538
|
-
usages.push({
|
|
3539
|
-
assetId: usage.assetId,
|
|
3540
|
-
itemId,
|
|
3541
|
-
locale,
|
|
3542
|
-
contentKey,
|
|
3543
|
-
usageScope,
|
|
3544
|
-
blockIndex: usage.blockIndex
|
|
3545
|
-
});
|
|
3546
|
-
}
|
|
3547
|
-
};
|
|
3548
|
-
const serializedTranslations = this._translations?.serialize();
|
|
3549
|
-
const rootItemId = this.rootItem?.id;
|
|
3550
|
-
if (!serializedTranslations) return usages;
|
|
3551
|
-
for (const [locale, localeTranslations] of Object.entries(serializedTranslations)) {
|
|
3552
|
-
for (const [contentKey, content] of Object.entries(localeTranslations.surveyCardContent || {})) pushContentUsages(content, "surveyCard", contentKey, locale, rootItemId);
|
|
3553
|
-
for (const [contentKey, content] of Object.entries(localeTranslations.navigationContent || {})) pushContentUsages(content, "navigation", contentKey, locale, rootItemId);
|
|
3554
|
-
for (const [contentKey, content] of Object.entries(localeTranslations.validationMessages || {})) pushContentUsages(content, "validationMessages", contentKey, locale, rootItemId);
|
|
3555
|
-
for (const [itemId, itemTranslations] of Object.entries(localeTranslations.itemTranslations || {})) for (const [contentKey, content] of Object.entries(itemTranslations || {})) pushContentUsages(content, "itemTranslation", contentKey, locale, itemId);
|
|
3556
|
-
}
|
|
3557
|
-
return usages;
|
|
3558
|
-
}
|
|
3559
|
-
getAvailableResponseValueSlots(byType) {
|
|
3560
|
-
let valueRefs = {};
|
|
3561
|
-
for (const item of this.surveyItems.values()) valueRefs = {
|
|
3562
|
-
...valueRefs,
|
|
3563
|
-
...item.getAvailableResponseValueSlots(byType)
|
|
3564
|
-
};
|
|
3565
|
-
return valueRefs;
|
|
3566
|
-
}
|
|
3567
|
-
getResponseSlotDefinitions() {
|
|
3568
|
-
return Array.from(this.surveyItems.values()).map((item) => ({
|
|
3569
|
-
itemId: item.id,
|
|
3570
|
-
slots: item.getResponseSlotDefinitions().map((slot) => ({
|
|
3571
|
-
...slot,
|
|
3572
|
-
exportSettings: item.rawItem.responseSettings?.[slot.slotId]
|
|
3573
|
-
}))
|
|
3574
|
-
}));
|
|
3575
|
-
}
|
|
3576
|
-
/** Compile a detached index from item declarations; diagnostics retain incomplete drafts. */
|
|
3577
|
-
getResponseSlotRegistry() {
|
|
3578
|
-
const declarations = this.getResponseSlotDefinitions().map(({ itemId, slots }) => ({
|
|
3579
|
-
itemId,
|
|
3580
|
-
slots: slots.map((slot) => ({
|
|
3581
|
-
...slot,
|
|
3582
|
-
...slot.referenceValueLabelKeys ? { referenceValueLabels: Object.fromEntries(this.locales.map((locale) => [locale, Object.fromEntries(Object.entries(slot.referenceValueLabelKeys).map(([id, key]) => [id, getContentPlainText(this.getItemTranslations(itemId)?.getContent(locale, key, this.locales[0]))]))])) } : {}
|
|
3583
|
-
}))
|
|
3584
|
-
}));
|
|
3585
|
-
const registry = new ResponseSlotRegistry(declarations);
|
|
3586
|
-
for (const { itemId, slots } of declarations) for (const slotId of Object.keys(this.surveyItems.get(itemId).rawItem.responseSettings ?? {})) if (!slots.some((slot) => slot.slotId === slotId)) registry.diagnostics.push({
|
|
3587
|
-
code: "unknown-slot",
|
|
3588
|
-
slotId,
|
|
3589
|
-
itemId,
|
|
3590
|
-
message: `Response settings reference an unknown variable "${slotId}" on item "${itemId}".`
|
|
3591
|
-
});
|
|
3592
|
-
return registry;
|
|
3593
|
-
}
|
|
3594
|
-
/** One typed declaration path for editor, runtime and export integrity checks. */
|
|
3595
|
-
getExpressionResponseSlots() {
|
|
3596
|
-
return this.getResponseSlotRegistry().definitions.flatMap((slot) => ["get", "isDefined"].map((method) => ({
|
|
3597
|
-
ref: JSON.stringify([slot.slotId, method]),
|
|
3598
|
-
itemId: slot.itemId,
|
|
3599
|
-
valueType: method === "get" ? slot.valueType : ValueType.boolean,
|
|
3600
|
-
...method === "get" && slot.allowedValues ? { allowedValues: slot.allowedValues } : {}
|
|
3601
|
-
})));
|
|
3602
|
-
}
|
|
3603
|
-
getExpressionReferenceDiagnostics() {
|
|
3604
|
-
return analyzeSurveyExpressionReferences({
|
|
3605
|
-
survey: this.serialize(),
|
|
3606
|
-
responseSlots: this.getExpressionResponseSlots()
|
|
3607
|
-
});
|
|
3608
|
-
}
|
|
3609
|
-
getBlockingExpressionIssues() {
|
|
3610
|
-
return this.getExpressionReferenceDiagnostics().filter(isBlockingSurveyExpressionReferenceDiagnostic).map((diagnostic) => diagnostic.message);
|
|
3611
|
-
}
|
|
3612
|
-
/**
|
|
3613
|
-
* Get all reference usages for the survey
|
|
3614
|
-
* @param forItemId - optional item id to filter usages for a specific item and its children (if not provided, all usages are returned)
|
|
3615
|
-
* @returns all reference usages for the survey (or for a specific item and its children)
|
|
3616
|
-
*/
|
|
3617
|
-
getReferenceUsages(forItemId) {
|
|
3618
|
-
const usages = [];
|
|
3619
|
-
for (const item of this.surveyItems.values()) {
|
|
3620
|
-
if (forItemId && item.id !== forItemId && !this.isDescendantOf(item.id, forItemId)) continue;
|
|
3621
|
-
usages.push(...item.getReferenceUsages());
|
|
3622
|
-
}
|
|
3623
|
-
if (this._templateValues) for (const [templateValueKey, templateValue] of this._templateValues.entries()) for (const ref of templateValue.expression?.responseVariableRefs || []) usages.push({
|
|
3624
|
-
itemId: templateValueKey,
|
|
3625
|
-
usageType: "templateValues",
|
|
3626
|
-
valueReference: ref
|
|
3627
|
-
});
|
|
3628
|
-
return usages;
|
|
3629
|
-
}
|
|
3630
|
-
isDescendantOf(targetId, ancestorId) {
|
|
3631
|
-
if (targetId === ancestorId) return true;
|
|
3632
|
-
return this.getItemPath(targetId).includes(ancestorId);
|
|
3633
|
-
}
|
|
3634
|
-
};
|
|
3635
|
-
//#endregion
|
|
3636
|
-
//#region src/response-exporter/column-id.ts
|
|
3637
|
-
function encodeExportColumnId(identity) {
|
|
3638
|
-
return JSON.stringify(identity);
|
|
3639
|
-
}
|
|
3640
|
-
function decodeExportColumnId(value) {
|
|
3641
|
-
const tuple = JSON.parse(value);
|
|
3642
|
-
if (!Array.isArray(tuple) || tuple.some((part) => typeof part !== "string") || !(tuple.length === 2 && (tuple[0] === "meta" || tuple[0] === "context") || tuple[0] === "response" && (tuple.length === 3 && tuple[2] === "value" || tuple.length === 4 && tuple[2] === "option"))) throw new Error(`Invalid export column ID: ${value}`);
|
|
3643
|
-
if (JSON.stringify(tuple) !== value) throw new Error("Export column IDs must use the canonical JSON tuple encoding");
|
|
3644
|
-
return tuple;
|
|
3645
|
-
}
|
|
3646
|
-
//#endregion
|
|
3647
|
-
//#region src/response-exporter/types.ts
|
|
3648
|
-
/** Meta column keys in fixed order (common attributes first). */
|
|
3649
|
-
const META_COLUMN_ORDER = [
|
|
3650
|
-
"submittedAt",
|
|
3651
|
-
"participantId",
|
|
3652
|
-
"responseId",
|
|
3653
|
-
"sourceId"
|
|
3654
|
-
];
|
|
3655
|
-
/** Built-in transform modes for slot export. */
|
|
3656
|
-
const SlotTransformMode = {
|
|
3657
|
-
default: "default",
|
|
3658
|
-
mask: "mask",
|
|
3659
|
-
dateFormat: "dateFormat"
|
|
3660
|
-
};
|
|
3661
|
-
const EXPORTER_PROFILE_SCHEMA_VERSION = 1;
|
|
3662
|
-
/** Source of an export column. */
|
|
3663
|
-
const ExportColumnSource = {
|
|
3664
|
-
meta: "meta",
|
|
3665
|
-
context: "context",
|
|
3666
|
-
response: "response"
|
|
3667
|
-
};
|
|
3668
|
-
//#endregion
|
|
3669
|
-
//#region src/response-exporter/slot-formatter.ts
|
|
3670
|
-
/** Default transform config when no profile override. */
|
|
3671
|
-
const DEFAULT_TRANSFORM = { mode: SlotTransformMode.default };
|
|
3672
|
-
/**
|
|
3673
|
-
* Export a single response slot to a string. Pure function for unit testing.
|
|
3674
|
-
* Uses default formatting by ValueType unless overridden by transformConfig.
|
|
3675
|
-
*/
|
|
3676
|
-
function exportSingleResponseSlot(params) {
|
|
3677
|
-
const { value, valueType, transformConfig } = params;
|
|
3678
|
-
if (value === void 0 || value === null) return "";
|
|
3679
|
-
const mode = transformConfig.mode;
|
|
3680
|
-
if (mode === SlotTransformMode.mask) return maskValue(value, transformConfig.maskChar ?? "***");
|
|
3681
|
-
if (mode === SlotTransformMode.dateFormat && (valueType === ValueType.date || valueType === ValueType.dateArray)) return formatDateValue(value, transformConfig.dateFormat?.pattern ?? "yyyy-MM-dd");
|
|
3682
|
-
if (value.type === ValueType.reference || value.type === ValueType.referenceArray) {
|
|
3683
|
-
const keys = transformConfig.categorical === "labels" ? transformConfig.optionLabels : transformConfig.optionCodes ?? params.referenceValueKeys;
|
|
3684
|
-
const exportReference = (id) => {
|
|
3685
|
-
if (transformConfig.categorical === "ids") return id;
|
|
3686
|
-
const key = keys && Object.hasOwn(keys, id) ? keys[id] : void 0;
|
|
3687
|
-
if (typeof key !== "string" || !key.trim()) throw new Error(`Missing categorical export mapping for option "${id}"`);
|
|
3688
|
-
return key.trim();
|
|
3689
|
-
};
|
|
3690
|
-
return value.type === ValueType.reference ? exportReference(value.value) : JSON.stringify(value.value.map(exportReference));
|
|
3691
|
-
}
|
|
3692
|
-
return formatDefault(value);
|
|
3693
|
-
}
|
|
3694
|
-
const BooleanExportValues = {
|
|
3695
|
-
TRUE: "TRUE",
|
|
3696
|
-
FALSE: "FALSE"
|
|
3697
|
-
};
|
|
3698
|
-
function formatDefault(value) {
|
|
3699
|
-
switch (value.type) {
|
|
3700
|
-
case ValueType.string: return String(value.value);
|
|
3701
|
-
case ValueType.reference: return String(value.value);
|
|
3702
|
-
case ValueType.boolean: return value.value ? BooleanExportValues.TRUE : BooleanExportValues.FALSE;
|
|
3703
|
-
case ValueType.number: return value.precision === NumberPrecision.int ? String(Math.floor(value.value)) : String(value.value);
|
|
3704
|
-
case ValueType.date: return value.value;
|
|
3705
|
-
case ValueType.duration: {
|
|
3706
|
-
const d = value;
|
|
3707
|
-
return `${d.precision === NumberPrecision.int ? Math.floor(d.value) : d.value} ${d.unit}`;
|
|
3708
|
-
}
|
|
3709
|
-
case ValueType.stringArray: return value.value.join("; ");
|
|
3710
|
-
case ValueType.referenceArray: return value.value.join("; ");
|
|
3711
|
-
case ValueType.numberArray: {
|
|
3712
|
-
const arr = value.value;
|
|
3713
|
-
const prec = value.precision;
|
|
3714
|
-
const fmt = (n) => prec === NumberPrecision.int ? Math.floor(n) : n;
|
|
3715
|
-
return arr.map(fmt).join("; ");
|
|
3716
|
-
}
|
|
3717
|
-
case ValueType.dateArray: return value.value.join("; ");
|
|
3718
|
-
case ValueType.durationArray: {
|
|
3719
|
-
const d = value;
|
|
3720
|
-
const prec = d.precision === NumberPrecision.int;
|
|
3721
|
-
const fmt = (n) => prec ? Math.floor(n) : n;
|
|
3722
|
-
return d.value.map((v) => `${fmt(v)} ${d.unit}`).join("; ");
|
|
3723
|
-
}
|
|
3724
|
-
default: return String(value.value ?? "");
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
function formatDateValue(value, pattern) {
|
|
3728
|
-
if (value.type === ValueType.date) return formatCalendarDate(value.value, pattern);
|
|
3729
|
-
if (value.type === ValueType.dateArray) return value.value.map((date) => formatCalendarDate(date, pattern)).join("; ");
|
|
3730
|
-
return formatDefault(value);
|
|
3731
|
-
}
|
|
3732
|
-
function maskValue(value, maskChar) {
|
|
3733
|
-
return valueToString(value) ? maskChar : "";
|
|
3734
|
-
}
|
|
3735
|
-
function valueToString(value) {
|
|
3736
|
-
switch (value.type) {
|
|
3737
|
-
case ValueType.string:
|
|
3738
|
-
case ValueType.reference: return String(value.value);
|
|
3739
|
-
case ValueType.stringArray:
|
|
3740
|
-
case ValueType.referenceArray: return value.value.join(" ");
|
|
3741
|
-
default: return String(value.value ?? "");
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
//#endregion
|
|
3745
|
-
//#region src/response-exporter/projection.ts
|
|
3746
|
-
function optionIds(slot) {
|
|
3747
|
-
return [...new Set((slot.allowedValues ?? []).flatMap((value) => value.type === ValueType.reference ? [value.value] : value.type === ValueType.referenceArray ? value.value : []))];
|
|
3748
|
-
}
|
|
3749
|
-
function optionMapping(slot, config) {
|
|
3750
|
-
const ids = optionIds(slot);
|
|
3751
|
-
const encoding = config.categorical ?? "codes";
|
|
3752
|
-
const mapping = encoding === "ids" ? Object.fromEntries(ids.map((id) => [id, id])) : encoding === "labels" ? config.optionLabels ?? slot.referenceValueLabels?.[config.locale ?? "en"] ?? {} : config.optionCodes ?? slot.referenceValueKeys ?? {};
|
|
3753
|
-
const used = /* @__PURE__ */ new Set();
|
|
3754
|
-
for (const id of ids) {
|
|
3755
|
-
if (typeof mapping[id] !== "string" || !mapping[id].trim()) throw new Error(`Variable "${slot.variableName}" has no ${encoding} mapping for option "${id}"`);
|
|
3756
|
-
if (used.has(mapping[id])) throw new Error(`Variable "${slot.variableName}" has ambiguous ${encoding} mapping "${mapping[id]}"`);
|
|
3757
|
-
used.add(mapping[id]);
|
|
3758
|
-
}
|
|
3759
|
-
for (const id of [...Object.keys(config.optionCodes ?? {}), ...Object.keys(config.optionLabels ?? {})]) if (!ids.includes(id)) throw new Error(`Mapping for "${slot.variableName}" contains unknown option "${id}"`);
|
|
3760
|
-
return mapping;
|
|
3761
|
-
}
|
|
3762
|
-
/** Compile every declared role before applying inclusion settings. */
|
|
3763
|
-
function projectSlot(slot, config, allRoles = false) {
|
|
3764
|
-
if (![
|
|
3765
|
-
"default",
|
|
3766
|
-
"mask",
|
|
3767
|
-
"dateFormat"
|
|
3768
|
-
].includes(config.mode)) throw new Error(`Unsupported export transform "${config.mode}"`);
|
|
3769
|
-
if (config.categorical && ![
|
|
3770
|
-
"codes",
|
|
3771
|
-
"labels",
|
|
3772
|
-
"ids"
|
|
3773
|
-
].includes(config.categorical)) throw new Error("Unsupported categorical encoding");
|
|
3774
|
-
if (config.multiple && ![
|
|
3775
|
-
"json",
|
|
3776
|
-
"delimited",
|
|
3777
|
-
"indicators"
|
|
3778
|
-
].includes(config.multiple)) throw new Error("Unsupported multiple-selection encoding");
|
|
3779
|
-
if (config.mode === "dateFormat" && slot.valueType !== "date" && slot.valueType !== "date[]") throw new Error("Date formatting requires a date variable");
|
|
3780
|
-
if (config.mode === "dateFormat") {
|
|
3781
|
-
const pattern = config.dateFormat?.pattern ?? "yyyy-MM-dd";
|
|
3782
|
-
if (!pattern.trim()) throw new Error("Date export pattern cannot be empty");
|
|
3783
|
-
formatCalendarDate("2026-01-15", pattern);
|
|
3784
|
-
}
|
|
3785
|
-
if (config.durationUnit && (!Object.values(DurationUnits).includes(config.durationUnit) || !slot.valueType.startsWith("duration"))) throw new Error("Duration units require a duration variable and a supported unit");
|
|
3786
|
-
if (config.booleanValues && (!config.booleanValues.true || !config.booleanValues.false || config.booleanValues.true === config.booleanValues.false)) throw new Error("Boolean export values must be distinct and nonempty");
|
|
3787
|
-
const categorical = slot.valueType === ValueType.reference || slot.valueType === ValueType.referenceArray;
|
|
3788
|
-
if (config.multiple && slot.valueType !== ValueType.referenceArray) throw new Error(`Multiple-selection encoding requires a reference[] variable: ${slot.variableName}`);
|
|
3789
|
-
if ((config.categorical || config.optionCodes || config.optionLabels) && !categorical) throw new Error(`Categorical encoding requires a categorical variable: ${slot.variableName}`);
|
|
3790
|
-
if (config.booleanValues && slot.valueType !== ValueType.boolean) throw new Error(`Boolean encoding requires a boolean variable: ${slot.variableName}`);
|
|
3791
|
-
if (config.precision !== void 0 && ![
|
|
3792
|
-
"number",
|
|
3793
|
-
"number[]",
|
|
3794
|
-
"duration",
|
|
3795
|
-
"duration[]"
|
|
3796
|
-
].includes(slot.valueType)) throw new Error("Precision requires a numeric or duration variable");
|
|
3797
|
-
if (config.precision !== void 0 && (!Number.isInteger(config.precision) || config.precision < 0 || config.precision > 15)) throw new Error("Precision must be an integer between 0 and 15");
|
|
3798
|
-
if (config.multiple === "delimited" && (!config.delimiter || config.delimiter.includes("\"") || /[\r\n]/.test(config.delimiter))) throw new Error("Delimited selection export requires an explicit delimiter without quotes or line breaks");
|
|
3799
|
-
if (!allRoles && (config.include === false || slot.computed && config.include !== true)) return [];
|
|
3800
|
-
const mapping = categorical && !allRoles ? optionMapping(slot, config) : {};
|
|
3801
|
-
const base = {
|
|
3802
|
-
source: "response",
|
|
3803
|
-
itemId: slot.itemId,
|
|
3804
|
-
slotId: slot.slotId,
|
|
3805
|
-
valueType: slot.valueType
|
|
3806
|
-
};
|
|
3807
|
-
const scalar = {
|
|
3808
|
-
...base,
|
|
3809
|
-
id: encodeExportColumnId([
|
|
3810
|
-
"response",
|
|
3811
|
-
slot.slotId,
|
|
3812
|
-
"value"
|
|
3813
|
-
]),
|
|
3814
|
-
header: slot.variableName
|
|
3815
|
-
};
|
|
3816
|
-
const indicators = () => optionIds(slot).map((optionId) => ({
|
|
3817
|
-
...base,
|
|
3818
|
-
id: encodeExportColumnId([
|
|
3819
|
-
"response",
|
|
3820
|
-
slot.slotId,
|
|
3821
|
-
"option",
|
|
3822
|
-
optionId
|
|
3823
|
-
]),
|
|
3824
|
-
header: `${slot.variableName}_${mapping[optionId] ?? optionId}`,
|
|
3825
|
-
optionId
|
|
3826
|
-
}));
|
|
3827
|
-
return allRoles ? [scalar, ...slot.valueType === ValueType.referenceArray ? indicators() : []] : config.multiple === "indicators" ? indicators() : [scalar];
|
|
3828
|
-
}
|
|
3829
|
-
function formatProjectedValue(slot, column, value, config) {
|
|
3830
|
-
if (value === void 0) return "";
|
|
3831
|
-
if (config.mode === "mask") return config.maskChar ?? "***";
|
|
3832
|
-
if (value.type === ValueType.reference || value.type === ValueType.referenceArray) {
|
|
3833
|
-
const mapping = optionMapping(slot, config);
|
|
3834
|
-
const values = value.type === ValueType.reference ? [value.value] : value.value;
|
|
3835
|
-
if (values.some((id) => !Object.hasOwn(mapping, id))) throw new Error(`Unmapped option in "${slot.variableName}"`);
|
|
3836
|
-
if (column.optionId) return values.includes(column.optionId) ? "1" : "0";
|
|
3837
|
-
const encoded = values.map((id) => mapping[id]);
|
|
3838
|
-
if (value.type === ValueType.reference) return encoded[0];
|
|
3839
|
-
if (config.multiple === "delimited") return encoded.map((entry) => `"${entry.replaceAll("\"", "\"\"")}"`).join(config.delimiter);
|
|
3840
|
-
return JSON.stringify(encoded);
|
|
3841
|
-
}
|
|
3842
|
-
if (value.type === ValueType.boolean) {
|
|
3843
|
-
const values = config.booleanValues ?? {
|
|
3844
|
-
true: "TRUE",
|
|
3845
|
-
false: "FALSE"
|
|
3846
|
-
};
|
|
3847
|
-
if (!values.true || !values.false || values.true === values.false) throw new Error("Boolean export values must be distinct and nonempty");
|
|
3848
|
-
return value.value ? values.true : values.false;
|
|
3849
|
-
}
|
|
3850
|
-
if (value.type === ValueType.duration || value.type === ValueType.durationArray) {
|
|
3851
|
-
const unit = config.durationUnit ?? "seconds";
|
|
3852
|
-
const seconds = {
|
|
3853
|
-
seconds: 1,
|
|
3854
|
-
minutes: 60,
|
|
3855
|
-
hours: 3600,
|
|
3856
|
-
days: 86400,
|
|
3857
|
-
weeks: 604800
|
|
3858
|
-
};
|
|
3859
|
-
if (unit !== value.unit && (!seconds[unit] || !seconds[value.unit])) throw new Error("Calendar months and years cannot be converted to fixed durations");
|
|
3860
|
-
const convert = (number) => {
|
|
3861
|
-
const n = number * (unit === value.unit ? 1 : seconds[value.unit] / seconds[unit]);
|
|
3862
|
-
return config.precision === void 0 ? String(n) : n.toFixed(config.precision);
|
|
3863
|
-
};
|
|
3864
|
-
return Array.isArray(value.value) ? JSON.stringify(value.value.map(convert)) : convert(value.value);
|
|
3865
|
-
}
|
|
3866
|
-
if (value.type === ValueType.number && config.precision !== void 0) return value.value.toFixed(config.precision);
|
|
3867
|
-
if (value.type === ValueType.numberArray && config.precision !== void 0) return `[${value.value.map((number) => number.toFixed(config.precision)).join(",")}]`;
|
|
3868
|
-
if (Array.isArray(value.value) && config.mode !== "dateFormat") return JSON.stringify(value.value);
|
|
3869
|
-
return exportSingleResponseSlot({
|
|
3870
|
-
value,
|
|
3871
|
-
valueType: slot.valueType,
|
|
3872
|
-
transformConfig: config
|
|
3873
|
-
});
|
|
3874
|
-
}
|
|
3875
|
-
/** Collect draft diagnostics without stopping at the first unfinished variable. */
|
|
3876
|
-
function getDefaultProjectionIssues(survey) {
|
|
3877
|
-
const issues = [];
|
|
3878
|
-
const headers = /* @__PURE__ */ new Set([
|
|
3879
|
-
"submittedAt",
|
|
3880
|
-
"participantId",
|
|
3881
|
-
"responseId",
|
|
3882
|
-
"sourceId"
|
|
3883
|
-
]);
|
|
3884
|
-
for (const slot of survey.getResponseSlotRegistry().definitions) try {
|
|
3885
|
-
for (const column of projectSlot(slot, {
|
|
3886
|
-
mode: "default",
|
|
3887
|
-
...slot.exportSettings
|
|
3888
|
-
})) {
|
|
3889
|
-
if (headers.has(column.header)) issues.push(`Duplicate export column header '${column.header}'`);
|
|
3890
|
-
headers.add(column.header);
|
|
3891
|
-
}
|
|
3892
|
-
} catch (error) {
|
|
3893
|
-
issues.push(error instanceof Error ? error.message : String(error));
|
|
3894
|
-
}
|
|
3895
|
-
return issues;
|
|
3896
|
-
}
|
|
3897
|
-
/** Fully resolved output semantics, shared by compatibility checks and codebooks. */
|
|
3898
|
-
function resolvedSlotEncoding(slot, config) {
|
|
3899
|
-
const encoding = {
|
|
3900
|
-
mode: config.mode,
|
|
3901
|
-
include: config.include ?? !slot.computed
|
|
3902
|
-
};
|
|
3903
|
-
if (config.mode === "mask") return {
|
|
3904
|
-
...encoding,
|
|
3905
|
-
maskChar: config.maskChar ?? "***"
|
|
3906
|
-
};
|
|
3907
|
-
if (config.mode === "dateFormat") encoding.dateFormat = { pattern: config.dateFormat?.pattern ?? "yyyy-MM-dd" };
|
|
3908
|
-
if (slot.valueType === "reference" || slot.valueType === "reference[]") {
|
|
3909
|
-
encoding.categorical = config.categorical ?? "codes";
|
|
3910
|
-
if (encoding.categorical === "labels") {
|
|
3911
|
-
encoding.locale = config.locale ?? "en";
|
|
3912
|
-
encoding.optionLabels = optionMapping(slot, config);
|
|
3913
|
-
} else if (encoding.categorical === "codes") encoding.optionCodes = optionMapping(slot, config);
|
|
3914
|
-
if (slot.valueType === "reference[]") {
|
|
3915
|
-
encoding.multiple = config.multiple ?? "json";
|
|
3916
|
-
if (encoding.multiple === "delimited") encoding.delimiter = config.delimiter;
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
if (slot.valueType === "boolean") encoding.booleanValues = config.booleanValues ?? {
|
|
3920
|
-
true: "TRUE",
|
|
3921
|
-
false: "FALSE"
|
|
3922
|
-
};
|
|
3923
|
-
if (slot.valueType.startsWith("duration")) encoding.durationUnit = config.durationUnit ?? "seconds";
|
|
3924
|
-
if (config.precision !== void 0) encoding.precision = config.precision;
|
|
3925
|
-
return structuredClone(encoding);
|
|
3926
|
-
}
|
|
3927
|
-
//#endregion
|
|
3928
|
-
export { const_date as $, responseValuesEqual as $n, CtxPFlagDateEditor as $t, createItemTypeDefinitionRegistry as A, deserializeTemplateValue as An, min as At, deserializeSurveyItemPrefill as B, FunctionExpression as Bn, str_list_contains as Bt, SurveyItemTranslations as C, findIntroducedBlockingExpressionReferenceDiagnostics as Cn, eq as Ct, CURRENT_SURVEY_SCHEMA_VERSION as D, responseValueReferenceIds as Dn, lt as Dt, CURRENT_SURVEY_SCHEMA as E, replaceResponseVariableReferences as En, in_range as Et, isBuiltInItemType as F, ConstExpression as Fn, response_number as Ft, generateId as G, ValueReferenceMethod as Gn, ConstDateEditor as Gt, serializeSurveyItemPrefill as H, ResponseVariableExpression as Hn, AndExpressionEditor as Ht, getCompositionLabel as I, ContextVariableExpression as In, response_number_array as It, shuffleIndices as J, allocateResponseVariableName as Jn, ConstStringArrayEditor as Jt, resolveUniqueItemKey as K, ResponseSlotRegistry as Kn, ConstNumberArrayEditor as Kt, SurveyItemCore as L, ContextVariableType as Ln, response_string as Lt, GroupItemCore as M, serializeTemplateValue as Mn, response_boolean as Mt, PageBreakItemCore as N, serializeTemplateValues as Nn, response_date as Nt, createFullRegistry as O, DATE_FUNCTION_SIGNATURES as On, lte as Ot, builtInItemCoreRegistry as P, ExpressionEvaluator as Pn, response_date_array as Pt, const_boolean as Q, responseValueElements as Qn, CtxLocaleEditor as Qt, SurveyItemPrefillApplyMode as R, Expression as Rn, response_string_array as Rt, isContentEmpty as S, expressionReferenceDiagnosticKey as Sn, date_min as St, validateLocale as T, isBlockingSurveyExpressionReferenceDiagnostic as Tn, gte as Tt, createSeededRandom as U, ReferenceUsageType as Un, ConstBooleanEditor as Ut, prefillTargetsEqual as V, FunctionExpressionNames as Vn, sum as Vt, generateCodingKey as W, ValueReference as Wn, ConstDateArrayEditor as Wt, ReservedSurveyItemTypes as X, suggestResponseVariableName as Xn, CtxCustomExpressionEditor as Xt, structuredCloneMethod as Y, isValidResponseVariableName as Yn, ConstStringEditor as Yt, and as Z, invalidResponseDomainValues as Zn, CtxCustomValueEditor as Zt, getAssetUsagesFromContent as _, SURVEY_EXPRESSION_REFERENCE_DIAGNOSTIC_POLICY as _n, differenceCalendarDates as _r, date_gt as _t, resolvedSlotEncoding as a, GtExpressionEditor as an, SurveyItemResponse as ar, ctx_custom_expression as at, hasRenderableRichTextBlock as b, collectExpressionResponseReferences as bn, date_lte as bt, EXPORTER_PROFILE_SCHEMA_VERSION as c, LtExpressionEditor as cn, ValueType as cr, ctx_pflag_date as ct, SlotTransformMode as d, MinExpressionEditor as dn, addCalendarDate as dr, ctx_pflag_string as dt, CtxPFlagIsDefinedEditor as en, validateResponseSlotValue as er, const_date_array as et, decodeExportColumnId as f, OrExpressionEditor as fn, calendarDateBounds as fr, date_add_days as ft, createRichTextContent as g, SumExpressionEditor as gn, compareCalendarDates as gr, date_eq as gt, ContentType as h, StrListContainsExpressionEditor as hn, calendarDateToLocalDate as hr, date_diff as ht, projectSlot as i, ExpressionEditor as in, ResponseItem as ir, const_string_array as it, toItemTypeDefinitionRegistry as j, deserializeTemplateValues as jn, or as jt, createItemCore as k, TemplateDefTypes as kn, max as kt, ExportColumnSource as l, LteExpressionEditor as ln, assertResponseValue as lr, ctx_pflag_is_defined as lt, Survey as m, StrEqExpressionEditor as mn, calendarDatePrecision as mr, date_add_years as mt, getDefaultProjectionIssues as n, CtxPFlagStringEditor as nn, SURVEY_RESPONSE_SCHEMA_VERSION as nr, const_number_array as nt, DEFAULT_TRANSFORM as o, GteExpressionEditor as on, DurationUnits as or, ctx_custom_value as ot, encodeExportColumnId as p, ResponseVariableEditor as pn, calendarDateFromLocalDate as pr, date_add_months as pt, shuffleArray as q, RESPONSE_VARIABLE_NAME_MAX_LENGTH as qn, ConstNumberEditor as qt, optionMapping as r, EqExpressionEditor as rn, SurveyResponse as rr, const_string as rt, exportSingleResponseSlot as s, InRangeExpressionEditor as sn, NumberPrecision as sr, ctx_locale as st, formatProjectedValue as t, CtxPFlagNumEditor as tn, initValueForType as tr, const_number as tt, META_COLUMN_ORDER as u, MaxExpressionEditor as un, isResponseValue as ur, ctx_pflag_num as ut, getContentPlainText as v, analyzeSurveyExpressionReferences as vn, formatCalendarDate as vr, date_gte as vt, SurveyTranslations as w, findIntroducedExpressionReferenceDiagnosticsIncludingAdvisories as wn, gt as wt, hasRenderableRichTextContent as x, collectSurveyExpressionLocations as xn, date_max as xt, getPlainTextFromRichTextContent as y, collectConfigValueExpressions as yn, isCalendarDate as yr, date_lt as yt, SurveyItemPrefillTargetType as z, ExpressionType as zn, str_eq as zt };
|
|
3929
|
-
|
|
3930
|
-
//# sourceMappingURL=projection-Bl6qCinx.mjs.map
|