@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
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
import { UTCDate } from "@date-fns/utc";
|
|
2
|
+
import { addDays } from "date-fns/addDays";
|
|
3
|
+
import { addMonths } from "date-fns/addMonths";
|
|
4
|
+
import { addYears } from "date-fns/addYears";
|
|
5
|
+
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
|
|
6
|
+
import { differenceInMonths } from "date-fns/differenceInMonths";
|
|
7
|
+
import { differenceInYears } from "date-fns/differenceInYears";
|
|
8
|
+
import { format } from "date-fns/format";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
//#region src/calendar-date.ts
|
|
11
|
+
const CALENDAR_DATE_UNITS = [
|
|
12
|
+
"years",
|
|
13
|
+
"months",
|
|
14
|
+
"days"
|
|
15
|
+
];
|
|
16
|
+
const isCalendarDateUnit = (value) => CALENDAR_DATE_UNITS.includes(value);
|
|
17
|
+
const pad = (n, width = 2) => String(n).padStart(width, "0");
|
|
18
|
+
const daysInMonth = (year, month) => month === 2 ? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28 : [
|
|
19
|
+
4,
|
|
20
|
+
6,
|
|
21
|
+
9,
|
|
22
|
+
11
|
|
23
|
+
].includes(month) ? 30 : 31;
|
|
24
|
+
function isCalendarDate(value) {
|
|
25
|
+
if (typeof value !== "string" || !/^\d{4}(?:-\d{2}(?:-\d{2})?)?$/.test(value)) return false;
|
|
26
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
27
|
+
return year >= 1 && year <= 9999 && (month === void 0 || month >= 1 && month <= 12 && (day === void 0 || day >= 1 && day <= daysInMonth(year, month)));
|
|
28
|
+
}
|
|
29
|
+
/** A calendar date with day precision, as used for the session's current date. */
|
|
30
|
+
const calendarDaySchema = z.string().refine((value) => isCalendarDate(value) && value.length === 10, "Expected a YYYY-MM-DD date");
|
|
31
|
+
function calendarDatePrecision(value) {
|
|
32
|
+
if (!isCalendarDate(value)) throw new Error(`Invalid calendar date: ${value}`);
|
|
33
|
+
return value.length === 4 ? "year" : value.length === 7 ? "year-month" : "year-month-day";
|
|
34
|
+
}
|
|
35
|
+
function calendarDateFromLocalDate(date, precision = "year-month-day") {
|
|
36
|
+
const value = [
|
|
37
|
+
pad(date.getFullYear(), 4),
|
|
38
|
+
pad(date.getMonth() + 1),
|
|
39
|
+
pad(date.getDate())
|
|
40
|
+
].slice(0, precision === "year" ? 1 : precision === "year-month" ? 2 : 3).join("-");
|
|
41
|
+
if (!isCalendarDate(value)) throw new Error("Invalid calendar date");
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
/** Adapter for calendar widgets only. Never serialize this Date as an instant. */
|
|
45
|
+
function calendarDateToLocalDate(value) {
|
|
46
|
+
calendarDatePrecision(value);
|
|
47
|
+
const [year, month = 1, day = 1] = value.split("-").map(Number);
|
|
48
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
49
|
+
date.setFullYear(year, month - 1, day);
|
|
50
|
+
date.setHours(12, 0, 0, 0);
|
|
51
|
+
return date;
|
|
52
|
+
}
|
|
53
|
+
function calendarDateBounds(value) {
|
|
54
|
+
calendarDatePrecision(value);
|
|
55
|
+
const [year, month] = value.split("-").map(Number);
|
|
56
|
+
if (value.length === 4) return [`${value}-01-01`, `${value}-12-31`];
|
|
57
|
+
if (value.length === 7) return [`${value}-01`, `${value}-${pad(daysInMonth(year, month))}`];
|
|
58
|
+
return [value, value];
|
|
59
|
+
}
|
|
60
|
+
/** Ordering requires equal precision; callers must explicitly choose interval semantics otherwise. */
|
|
61
|
+
function compareCalendarDates(a, b) {
|
|
62
|
+
if (!isCalendarDate(a) || !isCalendarDate(b) || a.length !== b.length) return void 0;
|
|
63
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
64
|
+
}
|
|
65
|
+
function utcDate(value) {
|
|
66
|
+
const [year, month = 1, day = 1] = value.split("-").map(Number);
|
|
67
|
+
const date = new UTCDate(0);
|
|
68
|
+
date.setFullYear(year, month - 1, day);
|
|
69
|
+
return date;
|
|
70
|
+
}
|
|
71
|
+
function fromUtcDate(date, precision) {
|
|
72
|
+
const result = `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`.slice(0, precision === "year" ? 4 : precision === "year-month" ? 7 : 10);
|
|
73
|
+
return isCalendarDate(result) ? result : void 0;
|
|
74
|
+
}
|
|
75
|
+
const unitLength = {
|
|
76
|
+
years: 4,
|
|
77
|
+
months: 7,
|
|
78
|
+
days: 10
|
|
79
|
+
};
|
|
80
|
+
function addCalendarDate(value, amount, unit) {
|
|
81
|
+
if (!isCalendarDate(value) || !Number.isSafeInteger(amount)) return void 0;
|
|
82
|
+
if (amount === 0) return value;
|
|
83
|
+
if (value.length < unitLength[unit]) return void 0;
|
|
84
|
+
return fromUtcDate((unit === "days" ? addDays : unit === "months" ? addMonths : addYears)(utcDate(value), amount), calendarDatePrecision(value));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Signed number of completed units in a - b. Mixed precisions are compared at the coarser
|
|
88
|
+
* one; a unit finer than that precision is unavailable.
|
|
89
|
+
*/
|
|
90
|
+
function differenceCalendarDates(a, b, unit) {
|
|
91
|
+
if (!isCalendarDate(a) || !isCalendarDate(b)) return void 0;
|
|
92
|
+
const length = Math.min(a.length, b.length);
|
|
93
|
+
if (length < unitLength[unit]) return void 0;
|
|
94
|
+
const later = utcDate(a.slice(0, length)), earlier = utcDate(b.slice(0, length));
|
|
95
|
+
return unit === "days" ? differenceInCalendarDays(later, earlier) : unit === "months" ? differenceInMonths(later, earlier) : differenceInYears(later, earlier);
|
|
96
|
+
}
|
|
97
|
+
/** Partial dates retain their precision even if a display pattern requests absent components. */
|
|
98
|
+
function formatCalendarDate(value, pattern) {
|
|
99
|
+
calendarDatePrecision(value);
|
|
100
|
+
if (value.length !== 10) return value;
|
|
101
|
+
return format(utcDate(value), pattern);
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/expressions/tree.ts
|
|
105
|
+
/** Fail an exhaustive node or function switch if a new variant is not handled. */
|
|
106
|
+
function unreachable(value) {
|
|
107
|
+
throw new Error(`Unsupported expression node: ${JSON.stringify(value)}`);
|
|
108
|
+
}
|
|
109
|
+
/** Convert unfinished editor operands to serializable null gaps, including sparse array indexes. */
|
|
110
|
+
function expressionArguments(args) {
|
|
111
|
+
return Array.from(args, (arg) => arg ?? null);
|
|
112
|
+
}
|
|
113
|
+
/** Read an operand, treating a missing or null argument as undefined. */
|
|
114
|
+
function operand(args, index) {
|
|
115
|
+
return args?.[index] ?? void 0;
|
|
116
|
+
}
|
|
117
|
+
/** Dispatch one node to its typed handler; use transformExpression to recurse into children. */
|
|
118
|
+
function visitExpression(node, handlers, path = []) {
|
|
119
|
+
switch (node.type) {
|
|
120
|
+
case "const": return handlers.const(node, path);
|
|
121
|
+
case "responseVariable": return handlers.responseVariable(node, path);
|
|
122
|
+
case "ownerState": return handlers.ownerState(node, path);
|
|
123
|
+
case "contextVariable": return handlers.contextVariable(node, path);
|
|
124
|
+
case "function": return handlers.function(node, path);
|
|
125
|
+
default: return unreachable(node);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Rewrite every node after its children. The path points to the node in the original tree;
|
|
130
|
+
* argument positions and null editor gaps are preserved for diagnostics and remapping.
|
|
131
|
+
*/
|
|
132
|
+
function transformExpression(node, handlers, path = []) {
|
|
133
|
+
const args = (items) => Array.from(items, (_, index) => {
|
|
134
|
+
const child = operand(items, index);
|
|
135
|
+
return child ? transformExpression(child, handlers, [
|
|
136
|
+
...path,
|
|
137
|
+
"arguments",
|
|
138
|
+
index
|
|
139
|
+
]) : null;
|
|
140
|
+
});
|
|
141
|
+
switch (node.type) {
|
|
142
|
+
case "const":
|
|
143
|
+
case "responseVariable":
|
|
144
|
+
case "ownerState": return visitExpression(node, handlers, path);
|
|
145
|
+
case "function": return visitExpression({
|
|
146
|
+
...node,
|
|
147
|
+
arguments: args(node.arguments)
|
|
148
|
+
}, handlers, path);
|
|
149
|
+
case "contextVariable": return visitExpression({
|
|
150
|
+
...node,
|
|
151
|
+
...node.key ? { key: transformExpression(node.key, handlers, [...path, "key"]) } : {},
|
|
152
|
+
...node.arguments ? { arguments: args(node.arguments) } : {}
|
|
153
|
+
}, handlers, path);
|
|
154
|
+
default: return unreachable(node);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/expressions/references.ts
|
|
159
|
+
/** Collect response and owner state reads so the graph can validate and order dependencies. */
|
|
160
|
+
function collectExpressionDependencies(node) {
|
|
161
|
+
const references = [];
|
|
162
|
+
transformExpression(node, {
|
|
163
|
+
const: (value) => value,
|
|
164
|
+
contextVariable: (value) => value,
|
|
165
|
+
function: (value) => value,
|
|
166
|
+
responseVariable: (value, path) => {
|
|
167
|
+
references.push({
|
|
168
|
+
kind: "answer",
|
|
169
|
+
...value.variableRef,
|
|
170
|
+
path
|
|
171
|
+
});
|
|
172
|
+
return value;
|
|
173
|
+
},
|
|
174
|
+
ownerState: (value, path) => {
|
|
175
|
+
references.push({
|
|
176
|
+
kind: "state",
|
|
177
|
+
state: value.state,
|
|
178
|
+
targetRef: value.targetRef,
|
|
179
|
+
path
|
|
180
|
+
});
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
return references;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Rewrite slot IDs, owner IDs, and constant reference values when survey content is copied.
|
|
188
|
+
* IDs absent from the map are left as they are.
|
|
189
|
+
*/
|
|
190
|
+
function remapExpression(node, ids) {
|
|
191
|
+
const id = (value) => ids.get(value) ?? value;
|
|
192
|
+
return transformExpression(node, {
|
|
193
|
+
const: (value) => {
|
|
194
|
+
if (value.value?.type === "reference") return {
|
|
195
|
+
...value,
|
|
196
|
+
value: {
|
|
197
|
+
...value.value,
|
|
198
|
+
value: id(value.value.value)
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
if (value.value?.type === "reference[]") return {
|
|
202
|
+
...value,
|
|
203
|
+
value: {
|
|
204
|
+
...value.value,
|
|
205
|
+
value: value.value.value.map(id)
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
return value;
|
|
209
|
+
},
|
|
210
|
+
contextVariable: (value) => value,
|
|
211
|
+
function: (value) => value,
|
|
212
|
+
responseVariable: (value) => ({
|
|
213
|
+
...value,
|
|
214
|
+
variableRef: {
|
|
215
|
+
...value.variableRef,
|
|
216
|
+
slotId: id(value.variableRef.slotId)
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
ownerState: (value) => ({
|
|
220
|
+
...value,
|
|
221
|
+
targetRef: { ownerId: id(value.targetRef.ownerId) }
|
|
222
|
+
})
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/survey/responses/value-schema.ts
|
|
227
|
+
const precision = z.enum(["int", "float"]).optional();
|
|
228
|
+
const number = z.number().finite();
|
|
229
|
+
const date = z.string().refine(isCalendarDate, "Expected a calendar year, month, or date");
|
|
230
|
+
const unit = z.enum([
|
|
231
|
+
"seconds",
|
|
232
|
+
"minutes",
|
|
233
|
+
"hours",
|
|
234
|
+
"days",
|
|
235
|
+
"weeks",
|
|
236
|
+
"months",
|
|
237
|
+
"years"
|
|
238
|
+
]);
|
|
239
|
+
/** The same JSON value grammar is used by expressions, retained answers, and submissions. */
|
|
240
|
+
const responseValueSchema = z.discriminatedUnion("type", [
|
|
241
|
+
z.strictObject({
|
|
242
|
+
type: z.literal("string"),
|
|
243
|
+
value: z.string()
|
|
244
|
+
}),
|
|
245
|
+
z.strictObject({
|
|
246
|
+
type: z.literal("reference"),
|
|
247
|
+
value: z.string().min(1)
|
|
248
|
+
}),
|
|
249
|
+
z.strictObject({
|
|
250
|
+
type: z.literal("number"),
|
|
251
|
+
value: number,
|
|
252
|
+
precision
|
|
253
|
+
}),
|
|
254
|
+
z.strictObject({
|
|
255
|
+
type: z.literal("boolean"),
|
|
256
|
+
value: z.boolean()
|
|
257
|
+
}),
|
|
258
|
+
z.strictObject({
|
|
259
|
+
type: z.literal("date"),
|
|
260
|
+
value: date
|
|
261
|
+
}),
|
|
262
|
+
z.strictObject({
|
|
263
|
+
type: z.literal("duration"),
|
|
264
|
+
value: number,
|
|
265
|
+
unit,
|
|
266
|
+
precision
|
|
267
|
+
}),
|
|
268
|
+
z.strictObject({
|
|
269
|
+
type: z.literal("string[]"),
|
|
270
|
+
value: z.array(z.string())
|
|
271
|
+
}),
|
|
272
|
+
z.strictObject({
|
|
273
|
+
type: z.literal("reference[]"),
|
|
274
|
+
value: z.array(z.string().min(1))
|
|
275
|
+
}),
|
|
276
|
+
z.strictObject({
|
|
277
|
+
type: z.literal("number[]"),
|
|
278
|
+
value: z.array(number),
|
|
279
|
+
precision
|
|
280
|
+
}),
|
|
281
|
+
z.strictObject({
|
|
282
|
+
type: z.literal("date[]"),
|
|
283
|
+
value: z.array(date)
|
|
284
|
+
}),
|
|
285
|
+
z.strictObject({
|
|
286
|
+
type: z.literal("duration[]"),
|
|
287
|
+
value: z.array(number),
|
|
288
|
+
unit,
|
|
289
|
+
precision
|
|
290
|
+
})
|
|
291
|
+
]);
|
|
292
|
+
const valueTypeSchema = z.enum([
|
|
293
|
+
"string",
|
|
294
|
+
"reference",
|
|
295
|
+
"number",
|
|
296
|
+
"boolean",
|
|
297
|
+
"date",
|
|
298
|
+
"duration",
|
|
299
|
+
"string[]",
|
|
300
|
+
"reference[]",
|
|
301
|
+
"number[]",
|
|
302
|
+
"date[]",
|
|
303
|
+
"duration[]"
|
|
304
|
+
]);
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/expressions/schema.ts
|
|
307
|
+
/** Serialized expression nodes and their Zod validation contract. Other expression modules operate on these nodes. */
|
|
308
|
+
/** Optional authoring metadata carried by a node without affecting evaluation. */
|
|
309
|
+
const expressionEditorConfigSchema = z.strictObject({
|
|
310
|
+
usedTemplate: z.string().optional(),
|
|
311
|
+
label: z.string().optional(),
|
|
312
|
+
description: z.string().optional()
|
|
313
|
+
});
|
|
314
|
+
/** Function names accepted in serialized expression trees. */
|
|
315
|
+
const functionNameSchema = z.enum([
|
|
316
|
+
"and",
|
|
317
|
+
"or",
|
|
318
|
+
"not",
|
|
319
|
+
"list_contains",
|
|
320
|
+
"list_count",
|
|
321
|
+
"if",
|
|
322
|
+
"eq",
|
|
323
|
+
"gt",
|
|
324
|
+
"gte",
|
|
325
|
+
"lt",
|
|
326
|
+
"lte",
|
|
327
|
+
"in_range",
|
|
328
|
+
"sum",
|
|
329
|
+
"min",
|
|
330
|
+
"max",
|
|
331
|
+
"str_eq",
|
|
332
|
+
"date_eq",
|
|
333
|
+
"date_gt",
|
|
334
|
+
"date_gte",
|
|
335
|
+
"date_lt",
|
|
336
|
+
"date_lte",
|
|
337
|
+
"date_add_days",
|
|
338
|
+
"date_add_months",
|
|
339
|
+
"date_add_years",
|
|
340
|
+
"date_diff",
|
|
341
|
+
"date_min",
|
|
342
|
+
"date_max"
|
|
343
|
+
]);
|
|
344
|
+
const editorConfig = expressionEditorConfigSchema.optional();
|
|
345
|
+
const constantSchema = z.strictObject({
|
|
346
|
+
type: z.literal("const"),
|
|
347
|
+
value: responseValueSchema.optional(),
|
|
348
|
+
editorConfig
|
|
349
|
+
});
|
|
350
|
+
const answerSchema = z.strictObject({
|
|
351
|
+
type: z.literal("responseVariable"),
|
|
352
|
+
variableRef: z.strictObject({
|
|
353
|
+
slotId: z.string().min(1),
|
|
354
|
+
method: z.enum(["get", "isDefined"])
|
|
355
|
+
}),
|
|
356
|
+
editorConfig
|
|
357
|
+
});
|
|
358
|
+
const ownerStateSchema = z.strictObject({
|
|
359
|
+
type: z.literal("ownerState"),
|
|
360
|
+
state: z.literal("disabled"),
|
|
361
|
+
targetRef: z.strictObject({ ownerId: z.string().min(1) }),
|
|
362
|
+
editorConfig
|
|
363
|
+
});
|
|
364
|
+
const contextSchema = z.strictObject({
|
|
365
|
+
type: z.literal("contextVariable"),
|
|
366
|
+
contextType: z.enum([
|
|
367
|
+
"locale",
|
|
368
|
+
"currentDate",
|
|
369
|
+
"participantFlag",
|
|
370
|
+
"customValue"
|
|
371
|
+
]),
|
|
372
|
+
get key() {
|
|
373
|
+
return expressionSchema.optional();
|
|
374
|
+
},
|
|
375
|
+
get arguments() {
|
|
376
|
+
return z.array(expressionSchema.nullable()).optional();
|
|
377
|
+
},
|
|
378
|
+
asType: valueTypeSchema.optional(),
|
|
379
|
+
editorConfig
|
|
380
|
+
});
|
|
381
|
+
const functionSchema = z.strictObject({
|
|
382
|
+
type: z.literal("function"),
|
|
383
|
+
functionName: functionNameSchema,
|
|
384
|
+
get arguments() {
|
|
385
|
+
return z.array(expressionSchema.nullable());
|
|
386
|
+
},
|
|
387
|
+
editorConfig
|
|
388
|
+
});
|
|
389
|
+
/** Parse a complete recursive expression tree, including owner state reads. */
|
|
390
|
+
const expressionSchema = z.discriminatedUnion("type", [
|
|
391
|
+
constantSchema,
|
|
392
|
+
answerSchema,
|
|
393
|
+
contextSchema,
|
|
394
|
+
functionSchema,
|
|
395
|
+
ownerStateSchema
|
|
396
|
+
]);
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/schema/annotations.ts
|
|
399
|
+
const annotations = z.registry();
|
|
400
|
+
/** Clone before annotating: shared primitive schemas must not acquire another field's meaning. */
|
|
401
|
+
function annotated(schema, annotation) {
|
|
402
|
+
const result = schema.clone();
|
|
403
|
+
const base = result;
|
|
404
|
+
annotations.add(base, annotation);
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
function schemaAnnotation(schema) {
|
|
408
|
+
return annotations.get(schema);
|
|
409
|
+
}
|
|
410
|
+
const ownedId = (identity, slotAlias = false) => annotated(z.string().min(1), {
|
|
411
|
+
kind: "identity",
|
|
412
|
+
identity,
|
|
413
|
+
...slotAlias ? { slotAlias } : {}
|
|
414
|
+
});
|
|
415
|
+
const referenceId = (target, external = false) => annotated(z.string().min(1), {
|
|
416
|
+
kind: "reference",
|
|
417
|
+
target,
|
|
418
|
+
...external ? { external } : {}
|
|
419
|
+
});
|
|
420
|
+
const expressionField = (expectedReturnType) => annotated(expressionSchema, {
|
|
421
|
+
kind: "expression",
|
|
422
|
+
...expectedReturnType ? { expectedReturnType } : {}
|
|
423
|
+
});
|
|
424
|
+
/**
|
|
425
|
+
* A literal, or `{ expression, fallback? }` whose fallback applies when the expression has no
|
|
426
|
+
* value. The annotation lets tools tell a bound from other config that also holds an expression.
|
|
427
|
+
*/
|
|
428
|
+
const boundField = (value, expectedReturnType) => annotated(z.union([value, z.strictObject({
|
|
429
|
+
expression: expressionField(expectedReturnType),
|
|
430
|
+
fallback: value.optional()
|
|
431
|
+
})]), { kind: "bound" });
|
|
432
|
+
const contentOwner = (schema, roles) => annotated(schema, {
|
|
433
|
+
kind: "owner",
|
|
434
|
+
roles
|
|
435
|
+
});
|
|
436
|
+
/** Object extension changes schema identity, so carry the toolkit metadata explicitly. */
|
|
437
|
+
function extendObject(schema, extension) {
|
|
438
|
+
const result = schema.extend(extension);
|
|
439
|
+
const annotation = annotations.get(schema);
|
|
440
|
+
return annotation ? annotated(result, annotation) : result;
|
|
441
|
+
}
|
|
442
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
443
|
+
const unionDispatch = /* @__PURE__ */ new WeakMap();
|
|
444
|
+
/** Disjoint literal discriminants select a branch without reparsing its recursive subtree. */
|
|
445
|
+
function discriminatedVariant(schema, value) {
|
|
446
|
+
let dispatch = unionDispatch.get(schema);
|
|
447
|
+
if (dispatch === void 0) {
|
|
448
|
+
dispatch = null;
|
|
449
|
+
const objects = schema.options.filter((option) => option instanceof z.ZodObject);
|
|
450
|
+
if (objects.length === schema.options.length && objects.length > 0) for (const key of Object.keys(objects[0].shape)) {
|
|
451
|
+
const variants = /* @__PURE__ */ new Map();
|
|
452
|
+
if (objects.every((option) => {
|
|
453
|
+
const discriminator = option.shape[key];
|
|
454
|
+
if (!(discriminator instanceof z.ZodLiteral)) return false;
|
|
455
|
+
for (const literal of discriminator.values) {
|
|
456
|
+
if (variants.has(literal)) return false;
|
|
457
|
+
variants.set(literal, option);
|
|
458
|
+
}
|
|
459
|
+
return true;
|
|
460
|
+
})) {
|
|
461
|
+
dispatch = {
|
|
462
|
+
key,
|
|
463
|
+
variants
|
|
464
|
+
};
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
unionDispatch.set(schema, dispatch);
|
|
469
|
+
}
|
|
470
|
+
if (!dispatch) return void 0;
|
|
471
|
+
const branch = record(value) ? dispatch.variants.get(value[dispatch.key]) : void 0;
|
|
472
|
+
if (!branch) throw new Error(`Unknown '${dispatch.key}' schema variant`);
|
|
473
|
+
return branch;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* A schema-guided walk of validated data. Values and record keys share one traversal.
|
|
477
|
+
* Expressions are opaque here: their canonical visitor owns the recursive AST grammar.
|
|
478
|
+
*/
|
|
479
|
+
function walkAnnotated(schema, input, transform) {
|
|
480
|
+
const walk = (current, value, path, key = false) => {
|
|
481
|
+
const annotation = annotations.get(current);
|
|
482
|
+
if (annotation) {
|
|
483
|
+
value = transform({
|
|
484
|
+
schema: current,
|
|
485
|
+
value,
|
|
486
|
+
path,
|
|
487
|
+
key,
|
|
488
|
+
annotation
|
|
489
|
+
});
|
|
490
|
+
if (annotation.kind === "expression" || annotation.kind === "responseValue") return value;
|
|
491
|
+
}
|
|
492
|
+
if (value === void 0 || value === null) return value;
|
|
493
|
+
if (current instanceof z.ZodOptional || current instanceof z.ZodExactOptional || current instanceof z.ZodNullable || current instanceof z.ZodReadonly || current instanceof z.ZodNonOptional || current instanceof z.ZodDefault || current instanceof z.ZodPrefault || current instanceof z.ZodCatch) return walk(current.unwrap(), value, path, key);
|
|
494
|
+
if (current instanceof z.ZodLazy) return walk(current.unwrap(), value, path, key);
|
|
495
|
+
if (current instanceof z.ZodObject) {
|
|
496
|
+
if (!record(value)) throw new Error(`Expected object at ${JSON.stringify(path)}`);
|
|
497
|
+
return Object.fromEntries(Object.entries(value).map(([name, child]) => {
|
|
498
|
+
const childSchema = Object.hasOwn(current.shape, name) ? current.shape[name] : current._zod.def.catchall;
|
|
499
|
+
return [name, childSchema ? walk(childSchema, child, [...path, name]) : child];
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
if (current instanceof z.ZodArray) {
|
|
503
|
+
if (!Array.isArray(value)) throw new Error(`Expected array at ${JSON.stringify(path)}`);
|
|
504
|
+
return value.map((child, index) => walk(current.element, child, [...path, index]));
|
|
505
|
+
}
|
|
506
|
+
if (current instanceof z.ZodTuple) {
|
|
507
|
+
if (!Array.isArray(value)) throw new Error(`Expected tuple at ${JSON.stringify(path)}`);
|
|
508
|
+
return value.map((child, index) => {
|
|
509
|
+
const childSchema = current._zod.def.items[index] ?? current._zod.def.rest;
|
|
510
|
+
if (!childSchema) throw new Error(`Unexpected tuple member at ${JSON.stringify(path)}`);
|
|
511
|
+
return walk(childSchema, child, [...path, index]);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (current instanceof z.ZodRecord) {
|
|
515
|
+
if (!record(value)) throw new Error(`Expected record at ${JSON.stringify(path)}`);
|
|
516
|
+
const entries = Object.entries(value).map(([name, child]) => {
|
|
517
|
+
const nextKey = walk(current.keyType, name, [...path, name], true);
|
|
518
|
+
if (typeof nextKey !== "string") throw new Error("A record key must remain a string");
|
|
519
|
+
return [nextKey, walk(current.valueType, child, [...path, name])];
|
|
520
|
+
});
|
|
521
|
+
if (new Set(entries.map(([name]) => name)).size !== entries.length) throw new Error("Remapping collides record keys");
|
|
522
|
+
return Object.fromEntries(entries);
|
|
523
|
+
}
|
|
524
|
+
if (current instanceof z.ZodUnion) {
|
|
525
|
+
const variant = discriminatedVariant(current, value);
|
|
526
|
+
if (variant) return walk(variant, value, path, key);
|
|
527
|
+
for (const candidate of current.options) if (candidate instanceof z.ZodType && candidate.safeParse(value).success) return walk(candidate, value, path, key);
|
|
528
|
+
throw new Error(`No schema variant at ${JSON.stringify(path)}`);
|
|
529
|
+
}
|
|
530
|
+
switch (current._zod.def.type) {
|
|
531
|
+
case "string":
|
|
532
|
+
case "number":
|
|
533
|
+
case "boolean":
|
|
534
|
+
case "bigint":
|
|
535
|
+
case "symbol":
|
|
536
|
+
case "undefined":
|
|
537
|
+
case "null":
|
|
538
|
+
case "any":
|
|
539
|
+
case "unknown":
|
|
540
|
+
case "never":
|
|
541
|
+
case "void":
|
|
542
|
+
case "date":
|
|
543
|
+
case "nan":
|
|
544
|
+
case "enum":
|
|
545
|
+
case "literal":
|
|
546
|
+
case "template_literal":
|
|
547
|
+
case "custom":
|
|
548
|
+
case "file": return value;
|
|
549
|
+
default: throw new Error(`Unsupported schema container '${current._zod.def.type}' at ${JSON.stringify(path)}`);
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
return walk(schema, input, []);
|
|
553
|
+
}
|
|
554
|
+
function transformAnnotated(schema, input, transform) {
|
|
555
|
+
return schema.parse(walkAnnotated(schema, input, transform));
|
|
556
|
+
}
|
|
557
|
+
function inspectAnnotated(schema, input) {
|
|
558
|
+
const visits = [];
|
|
559
|
+
walkAnnotated(schema, input, (visit) => {
|
|
560
|
+
visits.push(visit);
|
|
561
|
+
return visit.value;
|
|
562
|
+
});
|
|
563
|
+
return visits;
|
|
564
|
+
}
|
|
565
|
+
function remapAnnotated(schema, input, ids) {
|
|
566
|
+
const remap = (id) => ids.get(id) ?? id;
|
|
567
|
+
return transformAnnotated(schema, input, ({ value, annotation }) => {
|
|
568
|
+
if (annotation.kind === "identity" || annotation.kind === "reference" && !annotation.external) {
|
|
569
|
+
if (typeof value !== "string") throw new Error("An identity must be a string");
|
|
570
|
+
return remap(value);
|
|
571
|
+
}
|
|
572
|
+
if (annotation.kind === "expression") return remapExpression(expressionSchema.parse(value), ids);
|
|
573
|
+
if (annotation.kind === "responseValue" && record(value)) {
|
|
574
|
+
if (value.type === "reference" && typeof value.value === "string") return {
|
|
575
|
+
...value,
|
|
576
|
+
value: remap(value.value)
|
|
577
|
+
};
|
|
578
|
+
if (value.type === "reference[]" && Array.isArray(value.value)) return {
|
|
579
|
+
...value,
|
|
580
|
+
value: value.value.map((entry) => typeof entry === "string" ? remap(entry) : entry)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return value;
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
function annotatedReferences(visits) {
|
|
587
|
+
return visits.flatMap((visit) => {
|
|
588
|
+
if (visit.annotation.kind === "reference" && !visit.annotation.external && typeof visit.value === "string") return [{
|
|
589
|
+
target: visit.annotation.target,
|
|
590
|
+
id: visit.value,
|
|
591
|
+
path: visit.path
|
|
592
|
+
}];
|
|
593
|
+
if (visit.annotation.kind === "expression") return collectExpressionDependencies(expressionSchema.parse(visit.value)).map((ref) => ({
|
|
594
|
+
target: ref.kind === "answer" ? "slot" : "state",
|
|
595
|
+
id: ref.kind === "answer" ? ref.slotId : ref.targetRef.ownerId,
|
|
596
|
+
path: [...visit.path, ...ref.path]
|
|
597
|
+
}));
|
|
598
|
+
return [];
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
/** Declared content owners in a validated subtree, including nested custom bodies. */
|
|
602
|
+
function contentOwnerIds(schema, input) {
|
|
603
|
+
return new Set(inspectAnnotated(schema, input).flatMap((visit) => visit.annotation.kind === "owner" && record(visit.value) && typeof visit.value.id === "string" ? [visit.value.id] : []));
|
|
604
|
+
}
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/schema/string-record.ts
|
|
607
|
+
/**
|
|
608
|
+
* JSON dictionaries use opaque string keys, including `__proto__`. Zod's record
|
|
609
|
+
* parser deliberately drops that key. Extend its record type at one boundary so
|
|
610
|
+
* schema introspection, annotations, cloning and JSON Schema still see a record.
|
|
611
|
+
* Define the exceptional property as data; never invoke Object.prototype's setter.
|
|
612
|
+
* On Zod upgrades, run string-record.test.ts (clone/check/async/JSON Schema canaries).
|
|
613
|
+
*/
|
|
614
|
+
function stringRecord(keyType, valueType) {
|
|
615
|
+
return new (z.core.$constructor("StringRecord", (schema, def) => {
|
|
616
|
+
if (def.keyType._zod.def.checks?.some((check) => check._zod.def.check === "overwrite")) throw new Error("Dictionary key schemas must not rewrite opaque keys");
|
|
617
|
+
z.ZodRecord.init(schema, def);
|
|
618
|
+
const parse = schema._zod.parse;
|
|
619
|
+
schema._zod.parse = (payload, context) => {
|
|
620
|
+
const input = payload.value;
|
|
621
|
+
const parsed = parse(payload, context);
|
|
622
|
+
if (!z.core.util.isPlainObject(input) || !Object.prototype.propertyIsEnumerable.call(input, "__proto__")) return parsed;
|
|
623
|
+
const finish = (result) => {
|
|
624
|
+
const key = def.keyType._zod.run({
|
|
625
|
+
value: "__proto__",
|
|
626
|
+
issues: []
|
|
627
|
+
}, context);
|
|
628
|
+
if (key instanceof Promise) throw new Error("Async dictionary keys are unsupported");
|
|
629
|
+
if (key.issues.length) {
|
|
630
|
+
result.issues.push({
|
|
631
|
+
code: "invalid_key",
|
|
632
|
+
origin: "record",
|
|
633
|
+
issues: key.issues.map((issue) => z.core.util.finalizeIssue(issue, context, z.config())),
|
|
634
|
+
input: "__proto__",
|
|
635
|
+
path: ["__proto__"],
|
|
636
|
+
inst: schema
|
|
637
|
+
});
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
if (typeof key.value !== "string") throw new Error("Dictionary keys must remain strings");
|
|
641
|
+
const outputKey = key.value;
|
|
642
|
+
const value = def.valueType._zod.run({
|
|
643
|
+
value: input.__proto__,
|
|
644
|
+
issues: []
|
|
645
|
+
}, context);
|
|
646
|
+
const assign = (child) => {
|
|
647
|
+
result.issues.push(...z.core.util.prefixIssues("__proto__", child.issues));
|
|
648
|
+
Object.defineProperty(result.value, outputKey, {
|
|
649
|
+
value: child.value,
|
|
650
|
+
enumerable: true,
|
|
651
|
+
writable: true,
|
|
652
|
+
configurable: true
|
|
653
|
+
});
|
|
654
|
+
return result;
|
|
655
|
+
};
|
|
656
|
+
return value instanceof Promise ? value.then(assign) : assign(value);
|
|
657
|
+
};
|
|
658
|
+
return parsed instanceof Promise ? parsed.then(finish) : finish(parsed);
|
|
659
|
+
};
|
|
660
|
+
}))({
|
|
661
|
+
type: "record",
|
|
662
|
+
keyType,
|
|
663
|
+
valueType
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
/** JSON dictionary lookups must not fall through to Object.prototype. */
|
|
667
|
+
function ownValue(record, key) {
|
|
668
|
+
return record && Object.hasOwn(record, key) ? record[key] : void 0;
|
|
669
|
+
}
|
|
670
|
+
function setOwnValue(record, key, value) {
|
|
671
|
+
Object.defineProperty(record, key, {
|
|
672
|
+
value,
|
|
673
|
+
enumerable: true,
|
|
674
|
+
writable: true,
|
|
675
|
+
configurable: true
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/survey/responses/value-types.ts
|
|
680
|
+
const ValueType = {
|
|
681
|
+
string: "string",
|
|
682
|
+
duration: "duration",
|
|
683
|
+
reference: "reference",
|
|
684
|
+
number: "number",
|
|
685
|
+
boolean: "boolean",
|
|
686
|
+
date: "date",
|
|
687
|
+
stringArray: "string[]",
|
|
688
|
+
durationArray: "duration[]",
|
|
689
|
+
referenceArray: "reference[]",
|
|
690
|
+
numberArray: "number[]",
|
|
691
|
+
dateArray: "date[]"
|
|
692
|
+
};
|
|
693
|
+
const DurationUnits = {
|
|
694
|
+
seconds: "seconds",
|
|
695
|
+
minutes: "minutes",
|
|
696
|
+
hours: "hours",
|
|
697
|
+
days: "days",
|
|
698
|
+
weeks: "weeks",
|
|
699
|
+
months: "months",
|
|
700
|
+
years: "years"
|
|
701
|
+
};
|
|
702
|
+
const NumberPrecision = {
|
|
703
|
+
int: "int",
|
|
704
|
+
float: "float"
|
|
705
|
+
};
|
|
706
|
+
/** Use the canonical codec at every boundary, including custom registry declarations. */
|
|
707
|
+
function isResponseValue(value) {
|
|
708
|
+
return responseValueSchema.safeParse(value).success;
|
|
709
|
+
}
|
|
710
|
+
function assertResponseValue(value, path) {
|
|
711
|
+
if (!isResponseValue(value)) throw new Error(`Invalid response value at '${path}'`);
|
|
712
|
+
}
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region src/survey/composition/content-schema.ts
|
|
715
|
+
const jsonValueSchema = z.lazy(() => z.union([
|
|
716
|
+
z.string(),
|
|
717
|
+
z.number(),
|
|
718
|
+
z.boolean(),
|
|
719
|
+
z.null(),
|
|
720
|
+
z.array(jsonValueSchema),
|
|
721
|
+
stringRecord(z.string(), jsonValueSchema)
|
|
722
|
+
]));
|
|
723
|
+
const contentAttributesSchema = stringRecord(z.string(), jsonValueSchema);
|
|
724
|
+
const richTextStyleSchema = z.strictObject({
|
|
725
|
+
weight: z.enum(["strong", "normal"]).optional(),
|
|
726
|
+
italic: z.enum(["italic", "normal"]).optional(),
|
|
727
|
+
underline: z.enum(["underline", "none"]).optional(),
|
|
728
|
+
color: z.enum([
|
|
729
|
+
"primary",
|
|
730
|
+
"accent",
|
|
731
|
+
"default"
|
|
732
|
+
]).optional()
|
|
733
|
+
});
|
|
734
|
+
const richTextTextInlineSchema = z.strictObject({
|
|
735
|
+
type: z.literal("text"),
|
|
736
|
+
text: z.string(),
|
|
737
|
+
style: richTextStyleSchema.optional(),
|
|
738
|
+
attrs: contentAttributesSchema.optional()
|
|
739
|
+
});
|
|
740
|
+
const richTextTemplateInlineSchema = z.strictObject({
|
|
741
|
+
type: z.literal("template"),
|
|
742
|
+
key: z.string().min(1),
|
|
743
|
+
style: richTextStyleSchema.optional(),
|
|
744
|
+
attrs: contentAttributesSchema.optional()
|
|
745
|
+
});
|
|
746
|
+
const richTextLineBreakInlineSchema = z.strictObject({
|
|
747
|
+
type: z.literal("lineBreak"),
|
|
748
|
+
attrs: contentAttributesSchema.optional()
|
|
749
|
+
});
|
|
750
|
+
const richTextLinkChildInlineSchema = z.discriminatedUnion("type", [
|
|
751
|
+
richTextTextInlineSchema,
|
|
752
|
+
richTextTemplateInlineSchema,
|
|
753
|
+
richTextLineBreakInlineSchema
|
|
754
|
+
]);
|
|
755
|
+
const richTextLinkInlineSchema = z.strictObject({
|
|
756
|
+
type: z.literal("link"),
|
|
757
|
+
href: z.string().min(1),
|
|
758
|
+
target: z.string().nullable().optional(),
|
|
759
|
+
rel: z.string().nullable().optional(),
|
|
760
|
+
title: z.string().nullable().optional(),
|
|
761
|
+
attrs: contentAttributesSchema.optional(),
|
|
762
|
+
children: z.array(richTextLinkChildInlineSchema)
|
|
763
|
+
});
|
|
764
|
+
const richTextInlineExtensionSchema = z.strictObject({
|
|
765
|
+
type: z.literal("inlineExtension"),
|
|
766
|
+
name: z.string().min(1),
|
|
767
|
+
attrs: contentAttributesSchema.optional()
|
|
768
|
+
});
|
|
769
|
+
const richTextInlineSchema = z.discriminatedUnion("type", [
|
|
770
|
+
richTextTextInlineSchema,
|
|
771
|
+
richTextTemplateInlineSchema,
|
|
772
|
+
richTextLinkInlineSchema,
|
|
773
|
+
richTextLineBreakInlineSchema,
|
|
774
|
+
richTextInlineExtensionSchema
|
|
775
|
+
]);
|
|
776
|
+
const textAlignmentSchema = z.enum([
|
|
777
|
+
"left",
|
|
778
|
+
"center",
|
|
779
|
+
"right",
|
|
780
|
+
"justify"
|
|
781
|
+
]);
|
|
782
|
+
const richTextParagraphBlockSchema = z.strictObject({
|
|
783
|
+
type: z.literal("paragraph"),
|
|
784
|
+
children: z.array(richTextInlineSchema),
|
|
785
|
+
alignment: textAlignmentSchema.optional(),
|
|
786
|
+
attrs: contentAttributesSchema.optional()
|
|
787
|
+
});
|
|
788
|
+
const richTextHeadingBlockSchema = z.strictObject({
|
|
789
|
+
type: z.literal("heading"),
|
|
790
|
+
level: z.union([
|
|
791
|
+
z.literal(1),
|
|
792
|
+
z.literal(2),
|
|
793
|
+
z.literal(3),
|
|
794
|
+
z.literal(4),
|
|
795
|
+
z.literal(5),
|
|
796
|
+
z.literal(6)
|
|
797
|
+
]),
|
|
798
|
+
children: z.array(richTextInlineSchema),
|
|
799
|
+
alignment: textAlignmentSchema.optional(),
|
|
800
|
+
attrs: contentAttributesSchema.optional()
|
|
801
|
+
});
|
|
802
|
+
const richTextListItemBlockSchema = z.strictObject({
|
|
803
|
+
type: z.literal("listItem"),
|
|
804
|
+
children: z.array(richTextParagraphBlockSchema),
|
|
805
|
+
attrs: contentAttributesSchema.optional()
|
|
806
|
+
});
|
|
807
|
+
const richTextBulletListBlockSchema = z.strictObject({
|
|
808
|
+
type: z.literal("bulletList"),
|
|
809
|
+
items: z.array(richTextListItemBlockSchema),
|
|
810
|
+
attrs: contentAttributesSchema.optional()
|
|
811
|
+
});
|
|
812
|
+
const richTextImageDimensionSchema = z.union([z.strictObject({
|
|
813
|
+
unit: z.literal("px"),
|
|
814
|
+
value: z.number().finite().nonnegative()
|
|
815
|
+
}), z.strictObject({
|
|
816
|
+
unit: z.literal("%"),
|
|
817
|
+
value: z.number().finite().nonnegative()
|
|
818
|
+
})]);
|
|
819
|
+
const richTextImageSourceSchema = z.discriminatedUnion("type", [z.strictObject({
|
|
820
|
+
type: z.literal("asset"),
|
|
821
|
+
assetId: referenceId("asset")
|
|
822
|
+
}), z.strictObject({
|
|
823
|
+
type: z.literal("external"),
|
|
824
|
+
url: z.string()
|
|
825
|
+
})]);
|
|
826
|
+
const richTextImageBlockSchema = z.strictObject({
|
|
827
|
+
type: z.literal("image"),
|
|
828
|
+
source: richTextImageSourceSchema,
|
|
829
|
+
alt: z.string().optional(),
|
|
830
|
+
caption: z.array(richTextParagraphBlockSchema).optional(),
|
|
831
|
+
size: z.strictObject({
|
|
832
|
+
width: richTextImageDimensionSchema.optional(),
|
|
833
|
+
height: richTextImageDimensionSchema.optional(),
|
|
834
|
+
maxWidth: richTextImageDimensionSchema.optional(),
|
|
835
|
+
maxHeight: richTextImageDimensionSchema.optional()
|
|
836
|
+
}).optional(),
|
|
837
|
+
alignment: textAlignmentSchema.optional(),
|
|
838
|
+
attrs: contentAttributesSchema.optional()
|
|
839
|
+
});
|
|
840
|
+
const richTextInfoBoxBlockSchema = z.strictObject({
|
|
841
|
+
type: z.literal("infoBox"),
|
|
842
|
+
tone: z.enum(["info", "warning"]).optional(),
|
|
843
|
+
title: z.array(richTextInlineSchema).optional(),
|
|
844
|
+
collapsible: z.boolean().optional(),
|
|
845
|
+
defaultOpen: z.boolean().optional(),
|
|
846
|
+
children: z.array(richTextParagraphBlockSchema),
|
|
847
|
+
attrs: contentAttributesSchema.optional()
|
|
848
|
+
});
|
|
849
|
+
const richTextSeparatorBlockSchema = z.strictObject({
|
|
850
|
+
type: z.literal("separator"),
|
|
851
|
+
attrs: contentAttributesSchema.optional()
|
|
852
|
+
});
|
|
853
|
+
const richTextBlockExtensionSchema = z.strictObject({
|
|
854
|
+
type: z.literal("blockExtension"),
|
|
855
|
+
name: z.string().min(1),
|
|
856
|
+
attrs: contentAttributesSchema.optional()
|
|
857
|
+
});
|
|
858
|
+
const richTextBlockSchema = z.discriminatedUnion("type", [
|
|
859
|
+
richTextParagraphBlockSchema,
|
|
860
|
+
richTextHeadingBlockSchema,
|
|
861
|
+
richTextBulletListBlockSchema,
|
|
862
|
+
richTextImageBlockSchema,
|
|
863
|
+
richTextInfoBoxBlockSchema,
|
|
864
|
+
richTextSeparatorBlockSchema,
|
|
865
|
+
richTextBlockExtensionSchema
|
|
866
|
+
]);
|
|
867
|
+
/** Survey roles use two document levels; general rich editors can represent all HTML levels. */
|
|
868
|
+
const surveyContentSchema = z.discriminatedUnion("type", [
|
|
869
|
+
z.strictObject({
|
|
870
|
+
type: z.literal("md"),
|
|
871
|
+
content: z.string()
|
|
872
|
+
}),
|
|
873
|
+
z.strictObject({
|
|
874
|
+
type: z.literal("plain"),
|
|
875
|
+
content: z.string()
|
|
876
|
+
}),
|
|
877
|
+
z.strictObject({
|
|
878
|
+
type: z.literal("richText"),
|
|
879
|
+
version: z.literal(1),
|
|
880
|
+
doc: z.strictObject({
|
|
881
|
+
type: z.literal("doc"),
|
|
882
|
+
blocks: z.array(richTextBlockSchema)
|
|
883
|
+
})
|
|
884
|
+
})
|
|
885
|
+
]).superRefine((content, context) => {
|
|
886
|
+
if (content.type !== "richText") return;
|
|
887
|
+
content.doc.blocks.forEach((block, index) => {
|
|
888
|
+
if (block.type === "heading" && block.level > 2) context.addIssue({
|
|
889
|
+
code: "custom",
|
|
890
|
+
path: [
|
|
891
|
+
"doc",
|
|
892
|
+
"blocks",
|
|
893
|
+
index,
|
|
894
|
+
"level"
|
|
895
|
+
],
|
|
896
|
+
message: "Survey headings use level 1 or 2"
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/survey/composition/schema.ts
|
|
902
|
+
const localizedContent = surveyContentSchema;
|
|
903
|
+
const CURRENT_SURVEY_SCHEMA_VERSION = 3;
|
|
904
|
+
const CURRENT_SURVEY_SCHEMA = "https://github.com/case-framework/case-survey-toolkit/packages/survey-core/schemas/survey-schema.json";
|
|
905
|
+
const conditionsShape = {
|
|
906
|
+
visibility: expressionField().optional(),
|
|
907
|
+
disabled: expressionField().optional()
|
|
908
|
+
};
|
|
909
|
+
const layoutPolicySchema = z.discriminatedUnion("mode", [z.strictObject({
|
|
910
|
+
mode: z.literal("automatic"),
|
|
911
|
+
maxColumns: z.number().int().min(1).max(12)
|
|
912
|
+
}), z.strictObject({
|
|
913
|
+
mode: z.literal("advanced"),
|
|
914
|
+
columns: z.strictObject({
|
|
915
|
+
base: z.number().int().min(1).max(12),
|
|
916
|
+
xs: z.number().int().min(1).max(12),
|
|
917
|
+
sm: z.number().int().min(1).max(12),
|
|
918
|
+
md: z.number().int().min(1).max(12),
|
|
919
|
+
xl: z.number().int().min(1).max(12)
|
|
920
|
+
})
|
|
921
|
+
})]);
|
|
922
|
+
const slotTransformSchema = z.strictObject({
|
|
923
|
+
mode: z.string(),
|
|
924
|
+
include: z.boolean().optional(),
|
|
925
|
+
categorical: z.enum([
|
|
926
|
+
"codes",
|
|
927
|
+
"labels",
|
|
928
|
+
"ids"
|
|
929
|
+
]).optional(),
|
|
930
|
+
multiple: z.enum([
|
|
931
|
+
"json",
|
|
932
|
+
"delimited",
|
|
933
|
+
"indicators"
|
|
934
|
+
]).optional(),
|
|
935
|
+
delimiter: z.string().optional(),
|
|
936
|
+
locale: z.string().optional(),
|
|
937
|
+
optionCodes: stringRecord(referenceId("option"), z.string()).optional(),
|
|
938
|
+
optionLabels: stringRecord(referenceId("option"), z.string()).optional(),
|
|
939
|
+
booleanValues: z.strictObject({
|
|
940
|
+
true: z.string(),
|
|
941
|
+
false: z.string()
|
|
942
|
+
}).optional(),
|
|
943
|
+
precision: z.number().int().nonnegative().optional(),
|
|
944
|
+
durationUnit: z.enum([
|
|
945
|
+
"seconds",
|
|
946
|
+
"minutes",
|
|
947
|
+
"hours",
|
|
948
|
+
"days",
|
|
949
|
+
"weeks",
|
|
950
|
+
"months",
|
|
951
|
+
"years"
|
|
952
|
+
]).optional(),
|
|
953
|
+
dateFormat: z.strictObject({ pattern: z.string() }).optional(),
|
|
954
|
+
maskText: z.string().optional()
|
|
955
|
+
});
|
|
956
|
+
const slotPrefillSchema = z.strictObject({
|
|
957
|
+
id: ownedId("prefill"),
|
|
958
|
+
when: expressionField().optional(),
|
|
959
|
+
apply: z.literal("ifEmpty").optional(),
|
|
960
|
+
source: z.discriminatedUnion("type", [
|
|
961
|
+
z.strictObject({
|
|
962
|
+
type: z.literal("static"),
|
|
963
|
+
value: annotated(responseValueSchema, { kind: "responseValue" })
|
|
964
|
+
}),
|
|
965
|
+
z.strictObject({
|
|
966
|
+
type: z.literal("expression"),
|
|
967
|
+
expression: expressionField()
|
|
968
|
+
}),
|
|
969
|
+
z.strictObject({
|
|
970
|
+
type: z.literal("templateValue"),
|
|
971
|
+
key: z.string().min(1)
|
|
972
|
+
}),
|
|
973
|
+
z.strictObject({
|
|
974
|
+
type: z.literal("previousResponse"),
|
|
975
|
+
ref: z.strictObject({
|
|
976
|
+
surveyKey: z.string().optional(),
|
|
977
|
+
slotId: referenceId("slot", true)
|
|
978
|
+
}),
|
|
979
|
+
ifUnsupported: z.literal("skip").optional()
|
|
980
|
+
})
|
|
981
|
+
])
|
|
982
|
+
});
|
|
983
|
+
const elementShape = {
|
|
984
|
+
id: ownedId("element"),
|
|
985
|
+
kind: z.literal("element"),
|
|
986
|
+
...conditionsShape,
|
|
987
|
+
placement: z.strictObject({
|
|
988
|
+
width: z.enum(["auto", "full"]).optional(),
|
|
989
|
+
minWidthRem: z.number().positive().optional()
|
|
990
|
+
}).optional(),
|
|
991
|
+
exportSettings: stringRecord(referenceId("slot"), slotTransformSchema).optional(),
|
|
992
|
+
prefills: stringRecord(referenceId("slot"), z.array(slotPrefillSchema)).optional()
|
|
993
|
+
};
|
|
994
|
+
const validationRuleSchema = contentOwner(z.strictObject({
|
|
995
|
+
id: ownedId("rule"),
|
|
996
|
+
targets: z.array(referenceId("slot")).min(1),
|
|
997
|
+
expression: expressionField(),
|
|
998
|
+
when: expressionField().optional(),
|
|
999
|
+
phase: z.enum(["page", "submit"]).optional()
|
|
1000
|
+
}), [{
|
|
1001
|
+
role: "message",
|
|
1002
|
+
policy: "inline"
|
|
1003
|
+
}]);
|
|
1004
|
+
function createCompositionSchema(element) {
|
|
1005
|
+
const body = annotated(z.strictObject({ blocks: z.array(z.discriminatedUnion("kind", [z.strictObject({
|
|
1006
|
+
id: ownedId("layout"),
|
|
1007
|
+
kind: z.literal("layout"),
|
|
1008
|
+
layout: layoutPolicySchema,
|
|
1009
|
+
elements: z.array(element)
|
|
1010
|
+
}), contentOwner(z.strictObject({
|
|
1011
|
+
id: ownedId("section"),
|
|
1012
|
+
kind: z.literal("section"),
|
|
1013
|
+
...conditionsShape,
|
|
1014
|
+
get body() {
|
|
1015
|
+
return body;
|
|
1016
|
+
}
|
|
1017
|
+
}), [{
|
|
1018
|
+
role: "title",
|
|
1019
|
+
policy: "inline",
|
|
1020
|
+
recommended: true
|
|
1021
|
+
}, {
|
|
1022
|
+
role: "description",
|
|
1023
|
+
policy: "block"
|
|
1024
|
+
}])])) }), {
|
|
1025
|
+
kind: "body",
|
|
1026
|
+
followUp: false
|
|
1027
|
+
});
|
|
1028
|
+
return body;
|
|
1029
|
+
}
|
|
1030
|
+
const followUpBody = (body) => annotated(body, {
|
|
1031
|
+
kind: "body",
|
|
1032
|
+
followUp: true
|
|
1033
|
+
});
|
|
1034
|
+
const metadata = stringRecord(z.string(), z.string()).optional();
|
|
1035
|
+
const itemEditorMetadataSchema = z.strictObject({
|
|
1036
|
+
createdAt: z.string().optional(),
|
|
1037
|
+
updatedAt: z.string().optional()
|
|
1038
|
+
});
|
|
1039
|
+
const editorMetadata = itemEditorMetadataSchema.optional();
|
|
1040
|
+
const itemShape = {
|
|
1041
|
+
id: ownedId("item"),
|
|
1042
|
+
metadata,
|
|
1043
|
+
editorMetadata,
|
|
1044
|
+
...conditionsShape
|
|
1045
|
+
};
|
|
1046
|
+
const groupSchema = contentOwner(z.strictObject({
|
|
1047
|
+
...itemShape,
|
|
1048
|
+
itemType: z.literal("group"),
|
|
1049
|
+
config: z.strictObject({
|
|
1050
|
+
isRoot: z.boolean().optional(),
|
|
1051
|
+
items: z.array(referenceId("item")),
|
|
1052
|
+
shuffleItems: z.boolean().optional()
|
|
1053
|
+
})
|
|
1054
|
+
}), []);
|
|
1055
|
+
const pageBreakSchema = contentOwner(z.strictObject({
|
|
1056
|
+
...itemShape,
|
|
1057
|
+
itemType: z.literal("page-break"),
|
|
1058
|
+
config: z.strictObject({})
|
|
1059
|
+
}), []);
|
|
1060
|
+
function createContentItemSchema(body) {
|
|
1061
|
+
return contentOwner(z.strictObject({
|
|
1062
|
+
...itemShape,
|
|
1063
|
+
itemType: z.literal("content"),
|
|
1064
|
+
config: z.strictObject({
|
|
1065
|
+
body,
|
|
1066
|
+
validationRules: z.array(validationRuleSchema).optional()
|
|
1067
|
+
})
|
|
1068
|
+
}), [
|
|
1069
|
+
{
|
|
1070
|
+
role: "title",
|
|
1071
|
+
policy: "inline"
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
role: "subtitle",
|
|
1075
|
+
policy: "block"
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
role: "footer",
|
|
1079
|
+
policy: "block"
|
|
1080
|
+
}
|
|
1081
|
+
]);
|
|
1082
|
+
}
|
|
1083
|
+
function createFlowItemSchema(body) {
|
|
1084
|
+
return z.discriminatedUnion("itemType", [
|
|
1085
|
+
createContentItemSchema(body),
|
|
1086
|
+
groupSchema,
|
|
1087
|
+
pageBreakSchema
|
|
1088
|
+
]);
|
|
1089
|
+
}
|
|
1090
|
+
const surveyLocaleContentSchema = z.strictObject({
|
|
1091
|
+
surveyCardContent: z.strictObject({
|
|
1092
|
+
name: localizedContent.optional(),
|
|
1093
|
+
description: localizedContent.optional(),
|
|
1094
|
+
typicalDuration: localizedContent.optional()
|
|
1095
|
+
}).optional(),
|
|
1096
|
+
navigationContent: z.strictObject({
|
|
1097
|
+
previousButtonText: localizedContent.optional(),
|
|
1098
|
+
nextButtonText: localizedContent.optional(),
|
|
1099
|
+
submitButtonText: localizedContent.optional(),
|
|
1100
|
+
pageIndicatorText: localizedContent.optional(),
|
|
1101
|
+
submitBoxMessage: localizedContent.optional()
|
|
1102
|
+
}).optional(),
|
|
1103
|
+
validationMessages: z.strictObject({ invalidResponse: localizedContent.optional() }).optional(),
|
|
1104
|
+
content: stringRecord(referenceId("owner"), stringRecord(z.string(), localizedContent))
|
|
1105
|
+
});
|
|
1106
|
+
const templateValueSchema = z.discriminatedUnion("type", [z.strictObject({
|
|
1107
|
+
type: z.literal("default"),
|
|
1108
|
+
returnType: valueTypeSchema,
|
|
1109
|
+
expression: expressionField().optional()
|
|
1110
|
+
}), z.strictObject({
|
|
1111
|
+
type: z.literal("formatDate"),
|
|
1112
|
+
returnType: z.literal("string"),
|
|
1113
|
+
expression: expressionField().optional(),
|
|
1114
|
+
dateFormat: z.string()
|
|
1115
|
+
})]);
|
|
1116
|
+
const surveyShape = {
|
|
1117
|
+
$schema: z.literal(CURRENT_SURVEY_SCHEMA),
|
|
1118
|
+
schemaVersion: z.literal(3),
|
|
1119
|
+
metadata,
|
|
1120
|
+
/** The source language; exports and hosts fall back to it. Must be a key of `translations`. */
|
|
1121
|
+
defaultLocale: z.string().min(1).optional(),
|
|
1122
|
+
translations: stringRecord(z.string().min(1), surveyLocaleContentSchema).optional(),
|
|
1123
|
+
maxItemsPerPage: z.strictObject({
|
|
1124
|
+
large: z.number().int().positive(),
|
|
1125
|
+
small: z.number().int().positive()
|
|
1126
|
+
}).optional(),
|
|
1127
|
+
assets: stringRecord(ownedId("asset"), z.strictObject({
|
|
1128
|
+
kind: z.literal("image"),
|
|
1129
|
+
source: z.strictObject({
|
|
1130
|
+
type: z.literal("embedded"),
|
|
1131
|
+
mediaType: z.string(),
|
|
1132
|
+
encoding: z.literal("base64"),
|
|
1133
|
+
data: z.string()
|
|
1134
|
+
}),
|
|
1135
|
+
metadata: z.strictObject({
|
|
1136
|
+
name: z.string().optional(),
|
|
1137
|
+
filename: z.string().optional(),
|
|
1138
|
+
alt: z.string().optional(),
|
|
1139
|
+
width: z.number().positive().optional(),
|
|
1140
|
+
height: z.number().positive().optional()
|
|
1141
|
+
}).optional()
|
|
1142
|
+
})).optional(),
|
|
1143
|
+
templateValues: stringRecord(z.string(), templateValueSchema).optional()
|
|
1144
|
+
};
|
|
1145
|
+
function createSurveySchema(body) {
|
|
1146
|
+
return z.strictObject({
|
|
1147
|
+
...surveyShape,
|
|
1148
|
+
surveyItems: z.array(createFlowItemSchema(body)).min(1)
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region src/survey/utils/content.ts
|
|
1153
|
+
const ContentType = {
|
|
1154
|
+
richText: "richText",
|
|
1155
|
+
plain: "plain",
|
|
1156
|
+
md: "md"
|
|
1157
|
+
};
|
|
1158
|
+
function hasRenderableRichTextBlock(block) {
|
|
1159
|
+
switch (block.type) {
|
|
1160
|
+
case "paragraph": return block.children.length > 0;
|
|
1161
|
+
case "heading": return block.children.length > 0;
|
|
1162
|
+
case "bulletList": return block.items.some((item) => item.children.some((paragraph) => paragraph.children.length > 0));
|
|
1163
|
+
case "image": return true;
|
|
1164
|
+
case "infoBox": return true;
|
|
1165
|
+
case "separator": return true;
|
|
1166
|
+
case "blockExtension": return true;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function hasRenderableRichTextContent(content) {
|
|
1170
|
+
return content.doc.blocks.some((block) => hasRenderableRichTextBlock(block));
|
|
1171
|
+
}
|
|
1172
|
+
function createRichTextContent(text = "") {
|
|
1173
|
+
return {
|
|
1174
|
+
type: ContentType.richText,
|
|
1175
|
+
version: 1,
|
|
1176
|
+
doc: {
|
|
1177
|
+
type: "doc",
|
|
1178
|
+
blocks: [{
|
|
1179
|
+
type: "paragraph",
|
|
1180
|
+
children: text.length > 0 ? text.split("\n").flatMap((line, index) => {
|
|
1181
|
+
const parts = [];
|
|
1182
|
+
if (index > 0) parts.push({ type: "lineBreak" });
|
|
1183
|
+
if (line.length > 0) parts.push({
|
|
1184
|
+
type: "text",
|
|
1185
|
+
text: line
|
|
1186
|
+
});
|
|
1187
|
+
return parts;
|
|
1188
|
+
}) : []
|
|
1189
|
+
}]
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function getPlainTextFromLinkChildren(children) {
|
|
1194
|
+
return children.map((inline) => {
|
|
1195
|
+
switch (inline.type) {
|
|
1196
|
+
case "text": return inline.text;
|
|
1197
|
+
case "template": return `{${inline.key}}`;
|
|
1198
|
+
case "lineBreak": return "\n";
|
|
1199
|
+
}
|
|
1200
|
+
}).join("");
|
|
1201
|
+
}
|
|
1202
|
+
function getPlainTextFromInlineNodes(inlines) {
|
|
1203
|
+
return inlines.map((inline) => {
|
|
1204
|
+
switch (inline.type) {
|
|
1205
|
+
case "text": return inline.text;
|
|
1206
|
+
case "template": return `{${inline.key}}`;
|
|
1207
|
+
case "link": return getPlainTextFromLinkChildren(inline.children);
|
|
1208
|
+
case "lineBreak": return "\n";
|
|
1209
|
+
case "inlineExtension": return "";
|
|
1210
|
+
}
|
|
1211
|
+
}).join("");
|
|
1212
|
+
}
|
|
1213
|
+
function getPlainTextFromParagraphs(paragraphs) {
|
|
1214
|
+
return paragraphs.map((paragraph) => getPlainTextFromInlineNodes(paragraph.children)).join("\n");
|
|
1215
|
+
}
|
|
1216
|
+
function getPlainTextFromBlock(block) {
|
|
1217
|
+
switch (block.type) {
|
|
1218
|
+
case "paragraph": return getPlainTextFromInlineNodes(block.children);
|
|
1219
|
+
case "heading": return getPlainTextFromInlineNodes(block.children);
|
|
1220
|
+
case "bulletList": return block.items.map((item) => getPlainTextFromParagraphs(item.children)).join("\n");
|
|
1221
|
+
case "image": {
|
|
1222
|
+
const captionText = block.caption ? getPlainTextFromParagraphs(block.caption) : "";
|
|
1223
|
+
return [block.alt ?? "", captionText].filter(Boolean).join("\n");
|
|
1224
|
+
}
|
|
1225
|
+
case "infoBox": return [block.title ? getPlainTextFromInlineNodes(block.title) : "", getPlainTextFromParagraphs(block.children)].filter(Boolean).join("\n");
|
|
1226
|
+
case "separator": return "";
|
|
1227
|
+
case "blockExtension": return "";
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function getPlainTextFromRichTextContent(content) {
|
|
1231
|
+
return content.doc.blocks.map((block) => getPlainTextFromBlock(block)).filter((text) => text.length > 0).join("\n").trim();
|
|
1232
|
+
}
|
|
1233
|
+
function getContentPlainText(content) {
|
|
1234
|
+
if (!content) return "";
|
|
1235
|
+
if (content.type !== ContentType.richText) return content.content ?? "";
|
|
1236
|
+
return getPlainTextFromRichTextContent(content);
|
|
1237
|
+
}
|
|
1238
|
+
function isContentEmpty(content) {
|
|
1239
|
+
if (!content) return true;
|
|
1240
|
+
if (content.type !== ContentType.richText) return content.content.trim().length === 0;
|
|
1241
|
+
return !hasRenderableRichTextContent(content);
|
|
1242
|
+
}
|
|
1243
|
+
function getAssetUsagesFromContent(content) {
|
|
1244
|
+
if (!content || content.type !== ContentType.richText) return [];
|
|
1245
|
+
const usages = [];
|
|
1246
|
+
content.doc.blocks.forEach((block, blockIndex) => {
|
|
1247
|
+
if (block.type === "image" && block.source.type === "asset") usages.push({
|
|
1248
|
+
assetId: block.source.assetId,
|
|
1249
|
+
blockIndex
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
return usages;
|
|
1253
|
+
}
|
|
1254
|
+
//#endregion
|
|
1255
|
+
//#region src/survey/utils/localization.ts
|
|
1256
|
+
const SURVEY_LOCALIZATION_SURFACES = [
|
|
1257
|
+
"surveyCardContent",
|
|
1258
|
+
"navigationContent",
|
|
1259
|
+
"validationMessages"
|
|
1260
|
+
];
|
|
1261
|
+
const SURVEY_LOCALIZATION_FIELDS = {
|
|
1262
|
+
surveyCardContent: {
|
|
1263
|
+
name: true,
|
|
1264
|
+
description: true,
|
|
1265
|
+
typicalDuration: true
|
|
1266
|
+
},
|
|
1267
|
+
navigationContent: {
|
|
1268
|
+
previousButtonText: true,
|
|
1269
|
+
nextButtonText: true,
|
|
1270
|
+
submitButtonText: true,
|
|
1271
|
+
pageIndicatorText: true,
|
|
1272
|
+
submitBoxMessage: true
|
|
1273
|
+
},
|
|
1274
|
+
validationMessages: { invalidResponse: true }
|
|
1275
|
+
};
|
|
1276
|
+
const hasLocalizationText = (content) => Boolean(content && getContentPlainText(content).trim());
|
|
1277
|
+
const getLocalizationEntryKey = (ref) => ref.scope === "content" ? JSON.stringify([
|
|
1278
|
+
ref.scope,
|
|
1279
|
+
ref.ownerId,
|
|
1280
|
+
ref.role
|
|
1281
|
+
]) : JSON.stringify([
|
|
1282
|
+
ref.scope,
|
|
1283
|
+
ref.surface,
|
|
1284
|
+
ref.contentKey
|
|
1285
|
+
]);
|
|
1286
|
+
const getLocalizationCellKey = (ref, locale) => JSON.stringify([locale, getLocalizationEntryKey(ref)]);
|
|
1287
|
+
/** Accepts parsed draft data too, so disconnected items remain discoverable once. */
|
|
1288
|
+
function getLocalizationItems(survey, itemIds) {
|
|
1289
|
+
const byId = new Map(survey.surveyItems.map((item) => [item.id, item]));
|
|
1290
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1291
|
+
const ordered = [];
|
|
1292
|
+
const visit = (id) => {
|
|
1293
|
+
if (visited.has(id)) return;
|
|
1294
|
+
visited.add(id);
|
|
1295
|
+
const item = byId.get(id);
|
|
1296
|
+
if (!item) return;
|
|
1297
|
+
ordered.push(item);
|
|
1298
|
+
if (item.itemType === "group") item.config.items.forEach(visit);
|
|
1299
|
+
};
|
|
1300
|
+
if (itemIds) itemIds.forEach((id) => {
|
|
1301
|
+
if (!byId.has(id)) throw new Error(`Unknown localization item '${id}'`);
|
|
1302
|
+
visit(id);
|
|
1303
|
+
});
|
|
1304
|
+
else {
|
|
1305
|
+
survey.surveyItems.filter((item) => item.itemType === "group" && item.config.isRoot).forEach((item) => visit(item.id));
|
|
1306
|
+
survey.surveyItems.forEach((item) => visit(item.id));
|
|
1307
|
+
}
|
|
1308
|
+
return ordered;
|
|
1309
|
+
}
|
|
1310
|
+
function validateLocalizationRef(survey, ref) {
|
|
1311
|
+
if (ref.scope === "content") {
|
|
1312
|
+
if (!survey.owners.get(ref.ownerId)?.roles.some((entry) => entry.role === ref.role)) throw new Error(`Unavailable localized content '${ref.ownerId}/${ref.role}'`);
|
|
1313
|
+
} else if (!Object.hasOwn(SURVEY_LOCALIZATION_FIELDS[ref.surface], ref.contentKey)) throw new Error(`Unknown survey content field '${ref.surface}/${ref.contentKey}'`);
|
|
1314
|
+
}
|
|
1315
|
+
function readLocalizationContent(survey, ref, locale) {
|
|
1316
|
+
validateLocalizationRef(survey, ref);
|
|
1317
|
+
const translation = ownValue(survey.data.translations, locale);
|
|
1318
|
+
if (ref.scope === "content") return ownValue(ownValue(translation?.content, ref.ownerId), ref.role);
|
|
1319
|
+
const values = translation?.[ref.surface];
|
|
1320
|
+
return ownValue(values, ref.contentKey);
|
|
1321
|
+
}
|
|
1322
|
+
function listLocalizationEntries(survey, options = {}) {
|
|
1323
|
+
const locales = Object.keys(survey.data.translations ?? {}), entries = [];
|
|
1324
|
+
const add = (ref) => {
|
|
1325
|
+
const contents = Object.fromEntries(locales.map((locale) => [locale, readLocalizationContent(survey, ref, locale)]));
|
|
1326
|
+
const includeEmpty = ref.scope === "survey" && options.includeEmptySurveyFields && !options.sourceLocale;
|
|
1327
|
+
if (options.sourceLocale ? !hasLocalizationText(ownValue(contents, options.sourceLocale)) : !includeEmpty && !Object.values(contents).some(hasLocalizationText)) return;
|
|
1328
|
+
entries.push({
|
|
1329
|
+
key: getLocalizationEntryKey(ref),
|
|
1330
|
+
ref,
|
|
1331
|
+
contents
|
|
1332
|
+
});
|
|
1333
|
+
};
|
|
1334
|
+
if (!options.itemIds) for (const surface of SURVEY_LOCALIZATION_SURFACES) for (const contentKey of Object.keys(SURVEY_LOCALIZATION_FIELDS[surface])) add({
|
|
1335
|
+
scope: "survey",
|
|
1336
|
+
surface,
|
|
1337
|
+
contentKey
|
|
1338
|
+
});
|
|
1339
|
+
for (const item of getLocalizationItems(survey.data, options.itemIds)) for (const owner of survey.owners.values()) if (owner.itemId === item.id) for (const role of owner.roles) add({
|
|
1340
|
+
scope: "content",
|
|
1341
|
+
ownerId: owner.id,
|
|
1342
|
+
role: role.role
|
|
1343
|
+
});
|
|
1344
|
+
return entries;
|
|
1345
|
+
}
|
|
1346
|
+
const isEmptySurveyLocalizationEntry = (entry) => entry.ref.scope === "survey" && !Object.values(entry.contents).some(hasLocalizationText);
|
|
1347
|
+
function getLocalizationCoverage(entries, locale) {
|
|
1348
|
+
const required = entries.filter((entry) => !isEmptySurveyLocalizationEntry(entry));
|
|
1349
|
+
const present = required.filter((entry) => hasLocalizationText(ownValue(entry.contents, locale))).length;
|
|
1350
|
+
return {
|
|
1351
|
+
total: required.length,
|
|
1352
|
+
present,
|
|
1353
|
+
missing: required.length - present
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
/** Fallback is a participant concern only; inventory and authoring readiness use exact locales. */
|
|
1357
|
+
function resolveParticipantContent(survey, ownerId, role, locales) {
|
|
1358
|
+
for (const locale of locales) {
|
|
1359
|
+
const content = readLocalizationContent(survey, {
|
|
1360
|
+
scope: "content",
|
|
1361
|
+
ownerId,
|
|
1362
|
+
role
|
|
1363
|
+
}, locale);
|
|
1364
|
+
if (content && (hasLocalizationText(content) || content.type === "richText" && content.doc.blocks.some((block) => block.type !== "paragraph" && block.type !== "heading" && block.type !== "bulletList" && hasRenderableRichTextBlock(block)))) return {
|
|
1365
|
+
locale,
|
|
1366
|
+
content
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
//#endregion
|
|
1371
|
+
export { inspectAnnotated as $, slotPrefillSchema as A, ValueType as B, createCompositionSchema as C, calendarDaySchema as Ct, followUpBody as D, isCalendarDate as Dt, elementShape as E, formatCalendarDate as Et, richTextImageDimensionSchema as F, stringRecord as G, isResponseValue as H, richTextImageSourceSchema as I, boundField as J, annotated as K, surveyContentSchema as L, templateValueSchema as M, validationRuleSchema as N, itemEditorMetadataSchema as O, isCalendarDateUnit as Ot, jsonValueSchema as P, extendObject as Q, DurationUnits as R, conditionsShape as S, calendarDateToLocalDate as St, createSurveySchema as T, differenceCalendarDates as Tt, ownValue as U, assertResponseValue as V, setOwnValue as W, contentOwnerIds as X, contentOwner as Y, expressionField as Z, hasRenderableRichTextBlock as _, CALENDAR_DATE_UNITS as _t, getLocalizationItems as a, expressionEditorConfigSchema as at, CURRENT_SURVEY_SCHEMA as b, calendarDateFromLocalDate as bt, listLocalizationEntries as c, responseValueSchema as ct, validateLocalizationRef as d, remapExpression as dt, ownedId as et, ContentType as f, expressionArguments as ft, getPlainTextFromRichTextContent as g, visitExpression as gt, getContentPlainText as h, unreachable as ht, getLocalizationEntryKey as i, transformAnnotated as it, slotTransformSchema as j, layoutPolicySchema as k, readLocalizationContent as l, valueTypeSchema as lt, getAssetUsagesFromContent as m, transformExpression as mt, getLocalizationCellKey as n, remapAnnotated as nt, hasLocalizationText as o, expressionSchema as ot, createRichTextContent as p, operand as pt, annotatedReferences as q, getLocalizationCoverage as r, schemaAnnotation as rt, isEmptySurveyLocalizationEntry as s, functionNameSchema as st, SURVEY_LOCALIZATION_SURFACES as t, referenceId as tt, resolveParticipantContent as u, collectExpressionDependencies as ut, hasRenderableRichTextContent as v, addCalendarDate as vt, createFlowItemSchema as w, compareCalendarDates as wt, CURRENT_SURVEY_SCHEMA_VERSION as x, calendarDatePrecision as xt, isContentEmpty as y, calendarDateBounds as yt, NumberPrecision as z };
|
|
1372
|
+
|
|
1373
|
+
//# sourceMappingURL=localization-1lzNpyFX.mjs.map
|