@case-framework/survey-core 0.7.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.
@@ -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