@marktiderman/genesis-forms 0.2.0 → 0.3.1
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 +113 -13
- package/conformance/{after-class-back-after-a-jump.scenario.json → event-feedback-back-after-a-jump.scenario.json} +21 -21
- package/conformance/{after-class-contact-yes-completed.scenario.json → event-feedback-contact-yes-completed.scenario.json} +21 -21
- package/conformance/{after-class-contact-yes-requires-name-and-email.scenario.json → event-feedback-contact-yes-requires-name-and-email.scenario.json} +19 -19
- package/conformance/{after-class-happy-path.scenario.json → event-feedback-happy-path.scenario.json} +24 -24
- package/conformance/{after-class-invalid-email-rejected.scenario.json → event-feedback-invalid-email-rejected.scenario.json} +19 -19
- package/conformance/{after-class-low-rating-skips-went-well.scenario.json → event-feedback-low-rating-skips-went-well.scenario.json} +21 -21
- package/conformance/{after-class-server-drops-unknown-and-hidden-answers.scenario.json → event-feedback-server-drops-unknown-and-hidden-answers.scenario.json} +22 -22
- package/conformance/{after-class-server-ignores-a-client-sent-coach.scenario.json → event-feedback-server-ignores-a-client-sent-owner.scenario.json} +24 -24
- package/conformance/{monthly-pulse-default-ending.scenario.json → monthly-check-in-default-ending.scenario.json} +6 -6
- package/conformance/{monthly-pulse-promoter-ending-via-jump.scenario.json → monthly-check-in-promoter-ending-via-jump.scenario.json} +6 -6
- package/dist/destinations.cjs +1235 -0
- package/dist/destinations.cjs.map +1 -0
- package/dist/destinations.d.cts +465 -0
- package/dist/destinations.d.ts +465 -0
- package/dist/destinations.js +1193 -0
- package/dist/destinations.js.map +1 -0
- package/dist/{engine-Coc-gQIB.d.ts → engine-CPLtKVew.d.cts} +2 -1
- package/dist/{engine-Bj8-mMVb.d.cts → engine-DOrPgFDw.d.ts} +2 -1
- package/dist/index.cjs +322 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -5
- package/dist/index.d.ts +102 -5
- package/dist/index.js +311 -25
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +249 -22
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +249 -22
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +253 -22
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/testing.js +253 -22
- package/dist/testing.js.map +1 -1
- package/dist/{validate-BbackAjB.d.cts → types-BBKBQ8sp.d.cts} +2 -29
- package/dist/{validate-BbackAjB.d.ts → types-BBKBQ8sp.d.ts} +2 -29
- package/dist/validate-BXXqi0fq.d.cts +30 -0
- package/dist/validate-BjNwF1UE.d.ts +30 -0
- package/package.json +14 -2
package/dist/testing.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-
|
|
2
|
-
export { d as compactEvents } from './engine-
|
|
3
|
-
import { J as Json,
|
|
1
|
+
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-CPLtKVew.cjs';
|
|
2
|
+
export { d as compactEvents } from './engine-CPLtKVew.cjs';
|
|
3
|
+
import { J as Json, F as FormDefinition, e as FormEvent } from './types-BBKBQ8sp.cjs';
|
|
4
|
+
import './validate-BXXqi0fq.cjs';
|
|
4
5
|
|
|
5
6
|
interface CoverageOptions {
|
|
6
7
|
/** Stop after this many explored states and report `truncated`. Default 5000. */
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-
|
|
2
|
-
export { d as compactEvents } from './engine-
|
|
3
|
-
import { J as Json,
|
|
1
|
+
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-DOrPgFDw.js';
|
|
2
|
+
export { d as compactEvents } from './engine-DOrPgFDw.js';
|
|
3
|
+
import { J as Json, F as FormDefinition, e as FormEvent } from './types-BBKBQ8sp.js';
|
|
4
|
+
import './validate-BjNwF1UE.js';
|
|
4
5
|
|
|
5
6
|
interface CoverageOptions {
|
|
6
7
|
/** Stop after this many explored states and report `truncated`. Default 5000. */
|
package/dist/testing.js
CHANGED
|
@@ -1,3 +1,185 @@
|
|
|
1
|
+
// src/structured.ts
|
|
2
|
+
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3
|
+
function intProp(field, name, fallback, floor = 1) {
|
|
4
|
+
const v = field.props?.[name];
|
|
5
|
+
return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : fallback;
|
|
6
|
+
}
|
|
7
|
+
function optionalIntProp(field, name, floor = 0) {
|
|
8
|
+
const v = field.props?.[name];
|
|
9
|
+
return typeof v === "number" && Number.isInteger(v) && v >= floor ? v : void 0;
|
|
10
|
+
}
|
|
11
|
+
var blank = (v) => typeof v !== "string" || v.trim() === "";
|
|
12
|
+
var blankText = (v) => typeof v === "string" && v.trim() === "";
|
|
13
|
+
var emptyList = (value, isBlank) => value === void 0 || value === null || Array.isArray(value) && value.every(isBlank);
|
|
14
|
+
var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
15
|
+
function listLimits(field) {
|
|
16
|
+
return { min: optionalIntProp(field, "min") ?? 0, max: intProp(field, "max", 10), itemMaxLength: intProp(field, "itemMaxLength", 500) };
|
|
17
|
+
}
|
|
18
|
+
var listItems = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
|
|
19
|
+
var MAX_TABLE_COLUMNS = 6;
|
|
20
|
+
var MAX_TABLE_CELL_LENGTH = 2e3;
|
|
21
|
+
function tableColumns(field) {
|
|
22
|
+
const raw = field.props?.columns;
|
|
23
|
+
if (!Array.isArray(raw)) return [];
|
|
24
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const c of raw) {
|
|
27
|
+
if (!isObject(c) || typeof c.key !== "string" || !c.key || seen.has(c.key)) continue;
|
|
28
|
+
seen.add(c.key);
|
|
29
|
+
const column = { key: c.key, label: typeof c.label === "string" ? c.label : c.key };
|
|
30
|
+
if (c.long === true) column.long = true;
|
|
31
|
+
out.push(column);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function tableLimits(field) {
|
|
36
|
+
return { minRows: optionalIntProp(field, "minRows") ?? 0, maxRows: intProp(field, "maxRows", 3) };
|
|
37
|
+
}
|
|
38
|
+
var filledRows = (value) => Array.isArray(value) ? value.filter((r) => isObject(r) && Object.values(r).some((c) => !blank(c))) : [];
|
|
39
|
+
var MAX_TIMESTAMP_SECONDS = 86400;
|
|
40
|
+
function formatTimestamp(seconds) {
|
|
41
|
+
const total = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
|
|
42
|
+
const h = Math.floor(total / 3600);
|
|
43
|
+
const m = Math.floor(total % 3600 / 60);
|
|
44
|
+
const s = String(total % 60).padStart(2, "0");
|
|
45
|
+
return h ? `${h}:${String(m).padStart(2, "0")}:${s}` : `${m}:${s}`;
|
|
46
|
+
}
|
|
47
|
+
var isTimestamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= MAX_TIMESTAMP_SECONDS;
|
|
48
|
+
var notesWithText = (value) => Array.isArray(value) ? value.filter((n) => isObject(n) && !blank(n.text)) : [];
|
|
49
|
+
var MAX_HIGHLIGHT_NOTE_LENGTH = 1e3;
|
|
50
|
+
var MAX_HIGHLIGHT_QUOTE_LENGTH = 2e3;
|
|
51
|
+
var HIGHLIGHT_NOTE_MESSAGE = "Add a note to each highlight.";
|
|
52
|
+
var sourceText = (field) => typeof field.props?.text === "string" && field.props.text ? field.props.text : void 0;
|
|
53
|
+
var structuredFieldTypes = [
|
|
54
|
+
{
|
|
55
|
+
type: "list",
|
|
56
|
+
label: "List",
|
|
57
|
+
category: "text",
|
|
58
|
+
value: { json: true },
|
|
59
|
+
// logic.ts `contains` on an array is exact item equality, same as multi-choice, so it is offered.
|
|
60
|
+
operators: ["contains", "answered", "empty"],
|
|
61
|
+
a11y: { semantics: "group" },
|
|
62
|
+
isEmpty: (value) => emptyList(value, blankText),
|
|
63
|
+
validate: (value, field) => {
|
|
64
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) return "Enter a list of answers.";
|
|
65
|
+
const { min, max, itemMaxLength } = listLimits(field);
|
|
66
|
+
if (value.some((v) => v.trim().length > itemMaxLength)) return `Keep each item under ${itemMaxLength} characters.`;
|
|
67
|
+
const count = listItems(value).length;
|
|
68
|
+
if (count < min) return `Add at least ${plural(min, "item")}.`;
|
|
69
|
+
if (count > max) return `Add at most ${plural(max, "item")}.`;
|
|
70
|
+
return null;
|
|
71
|
+
},
|
|
72
|
+
normalize: (value) => Array.isArray(value) ? listItems(value).map((v) => v.trim()) : value,
|
|
73
|
+
summarize: (value) => listItems(value).map((v) => v.trim()).join("; "),
|
|
74
|
+
samples: (field) => {
|
|
75
|
+
const { min, max } = listLimits(field);
|
|
76
|
+
return [Array.from({ length: Math.min(Math.max(1, min), max) }, (_, i) => `Item ${i + 1}`)];
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "table",
|
|
81
|
+
label: "Table",
|
|
82
|
+
category: "advanced",
|
|
83
|
+
value: { json: true },
|
|
84
|
+
operators: ["answered", "empty"],
|
|
85
|
+
a11y: { semantics: "group" },
|
|
86
|
+
isEmpty: (value) => emptyList(value, (r) => isObject(r) && Object.values(r).every(blankText)),
|
|
87
|
+
validate: (value, field) => {
|
|
88
|
+
const columns = tableColumns(field);
|
|
89
|
+
if (!columns.length) return "This question has no columns.";
|
|
90
|
+
if (!Array.isArray(value) || !value.every(isObject)) return "Fill in the rows.";
|
|
91
|
+
const keys = new Set(columns.map((c) => c.key));
|
|
92
|
+
for (const row of value) {
|
|
93
|
+
for (const [key, cell] of Object.entries(row)) {
|
|
94
|
+
if (!keys.has(key)) return "An answer names a column this question doesn't have.";
|
|
95
|
+
if (typeof cell !== "string") return "Fill in the rows with text.";
|
|
96
|
+
if (cell.trim().length > MAX_TABLE_CELL_LENGTH) return `Keep each answer under ${MAX_TABLE_CELL_LENGTH} characters.`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const { minRows, maxRows } = tableLimits(field);
|
|
100
|
+
const count = filledRows(value).length;
|
|
101
|
+
if (count < minRows) return `Fill in at least ${plural(minRows, "row")}.`;
|
|
102
|
+
if (count > maxRows) return `Fill in at most ${plural(maxRows, "row")}.`;
|
|
103
|
+
return null;
|
|
104
|
+
},
|
|
105
|
+
normalize: (value) => Array.isArray(value) ? filledRows(value).map((row) => {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [key, cell] of Object.entries(row)) if (typeof cell === "string") out[key] = cell.trim();
|
|
108
|
+
return out;
|
|
109
|
+
}) : value,
|
|
110
|
+
summarize: (value, field) => {
|
|
111
|
+
const columns = tableColumns(field);
|
|
112
|
+
return filledRows(value).map((row) => columns.filter((c) => !blank(row[c.key])).map((c) => `${c.label}: ${row[c.key].trim()}`).join(" \xB7 ")).join(" / ");
|
|
113
|
+
},
|
|
114
|
+
samples: (field) => {
|
|
115
|
+
const columns = tableColumns(field);
|
|
116
|
+
if (!columns.length) return [];
|
|
117
|
+
const { minRows, maxRows } = tableLimits(field);
|
|
118
|
+
const row = Object.fromEntries(columns.map((c) => [c.key, "sample"]));
|
|
119
|
+
return [Array.from({ length: Math.min(Math.max(1, minRows), maxRows) }, () => ({ ...row }))];
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: "media-notes",
|
|
124
|
+
label: "Notes at moments",
|
|
125
|
+
category: "media",
|
|
126
|
+
value: { json: true },
|
|
127
|
+
operators: ["answered", "empty"],
|
|
128
|
+
a11y: { semantics: "group" },
|
|
129
|
+
isEmpty: (value) => emptyList(value, (n) => isObject(n) && blankText(n.text)),
|
|
130
|
+
validate: (value, field) => {
|
|
131
|
+
if (!Array.isArray(value)) return "Add notes.";
|
|
132
|
+
const textMaxLength = intProp(field, "textMaxLength", 1e3);
|
|
133
|
+
for (const note of value) {
|
|
134
|
+
if (!isObject(note) || typeof note.text !== "string") return "Add notes.";
|
|
135
|
+
if (!isTimestamp(note.at)) return "Each note needs a time.";
|
|
136
|
+
if (note.text.trim().length > textMaxLength) return `Keep each note under ${textMaxLength} characters.`;
|
|
137
|
+
}
|
|
138
|
+
const maxNotes = intProp(field, "maxNotes", 20);
|
|
139
|
+
return notesWithText(value).length > maxNotes ? `Add at most ${plural(maxNotes, "note")}.` : null;
|
|
140
|
+
},
|
|
141
|
+
normalize: (value) => Array.isArray(value) ? notesWithText(value).map((n) => ({ at: n.at, text: n.text.trim() })).sort((a, b) => a.at - b.at) : value,
|
|
142
|
+
summarize: (value) => notesWithText(value).map((n) => ({ at: typeof n.at === "number" ? n.at : 0, text: n.text.trim() })).sort((a, b) => a.at - b.at).map((n) => `${formatTimestamp(n.at)} ${n.text}`).join("; "),
|
|
143
|
+
samples: () => [[{ at: 0, text: "sample" }]]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: "text-highlights",
|
|
147
|
+
label: "Highlights",
|
|
148
|
+
category: "media",
|
|
149
|
+
value: { json: true },
|
|
150
|
+
operators: ["answered", "empty"],
|
|
151
|
+
a11y: { semantics: "group" },
|
|
152
|
+
isEmpty: (value) => emptyList(value, () => false),
|
|
153
|
+
validate: (value, field) => {
|
|
154
|
+
if (!Array.isArray(value)) return "Highlight a passage.";
|
|
155
|
+
const maxHighlights = intProp(field, "maxHighlights", 20);
|
|
156
|
+
if (value.length > maxHighlights) return `Add at most ${plural(maxHighlights, "highlight")}.`;
|
|
157
|
+
const text = sourceText(field);
|
|
158
|
+
for (const h of value) {
|
|
159
|
+
if (!isObject(h) || !Number.isInteger(h.start) || !Number.isInteger(h.end) || typeof h.quote !== "string" || typeof h.note !== "string") {
|
|
160
|
+
return "Highlight a passage of the text.";
|
|
161
|
+
}
|
|
162
|
+
const start = h.start;
|
|
163
|
+
const end = h.end;
|
|
164
|
+
if (start < 0 || end <= start || h.quote.length !== end - start) return "Highlight a passage of the text.";
|
|
165
|
+
if (end - start > MAX_HIGHLIGHT_QUOTE_LENGTH) return `Highlight under ${MAX_HIGHLIGHT_QUOTE_LENGTH} characters at a time.`;
|
|
166
|
+
if (text !== void 0 && (end > text.length || text.slice(start, end) !== h.quote)) return "A highlight doesn't match the text.";
|
|
167
|
+
if (h.note.trim() === "") return HIGHLIGHT_NOTE_MESSAGE;
|
|
168
|
+
if (h.note.trim().length > MAX_HIGHLIGHT_NOTE_LENGTH) return `Keep each note under ${MAX_HIGHLIGHT_NOTE_LENGTH} characters.`;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
},
|
|
172
|
+
normalize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => ({ start: h.start, end: h.end, quote: h.quote, note: String(h.note).trim() })).sort((a, b) => a.start - b.start || a.end - b.end) : value,
|
|
173
|
+
summarize: (value) => Array.isArray(value) ? value.filter(isObject).map((h) => `"${String(h.quote)}" \u2014 ${String(h.note).trim()}`).join("; ") : "",
|
|
174
|
+
samples: (field) => {
|
|
175
|
+
const text = sourceText(field);
|
|
176
|
+
if (text === void 0) return [[{ start: 0, end: 6, quote: "sample", note: "sample" }]];
|
|
177
|
+
const end = Math.min(text.length, 20);
|
|
178
|
+
return [[{ start: 0, end, quote: text.slice(0, end), note: "sample" }]];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
|
|
1
183
|
// src/fields.ts
|
|
2
184
|
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
|
|
3
185
|
var globalScope = globalThis;
|
|
@@ -179,7 +361,7 @@ var builtins = [
|
|
|
179
361
|
summarize: (value) => value ? "Yes" : "No"
|
|
180
362
|
}
|
|
181
363
|
];
|
|
182
|
-
for (const manifest of builtins) registerFieldType(manifest);
|
|
364
|
+
for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
|
|
183
365
|
|
|
184
366
|
// src/events.ts
|
|
185
367
|
function isPlainObject(value) {
|
|
@@ -668,6 +850,10 @@ function domainOf(field, extra, maxOptions) {
|
|
|
668
850
|
extra = extra.map((v) => typeof v === "string" ? [v] : v);
|
|
669
851
|
break;
|
|
670
852
|
}
|
|
853
|
+
case "list":
|
|
854
|
+
base = getFieldType(field.type)?.samples?.(field) ?? [];
|
|
855
|
+
extra = extra.map((v) => typeof v === "string" ? [v] : v);
|
|
856
|
+
break;
|
|
671
857
|
case "email":
|
|
672
858
|
base = ["ada@example.com"];
|
|
673
859
|
break;
|
|
@@ -781,7 +967,7 @@ function shown(v) {
|
|
|
781
967
|
text ??= typeof v === "bigint" ? `${v}n` : typeof v;
|
|
782
968
|
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
783
969
|
}
|
|
784
|
-
var
|
|
970
|
+
var isObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
785
971
|
var isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
786
972
|
var LITERAL_TYPE = {
|
|
787
973
|
"short-text": "string",
|
|
@@ -792,7 +978,8 @@ var LITERAL_TYPE = {
|
|
|
792
978
|
nps: "number",
|
|
793
979
|
"single-choice": "string",
|
|
794
980
|
"multi-choice": "string",
|
|
795
|
-
"yes-no": "boolean"
|
|
981
|
+
"yes-no": "boolean",
|
|
982
|
+
list: "string"
|
|
796
983
|
};
|
|
797
984
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
798
985
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
@@ -834,7 +1021,48 @@ function parseForm(input) {
|
|
|
834
1021
|
}
|
|
835
1022
|
return true;
|
|
836
1023
|
};
|
|
837
|
-
|
|
1024
|
+
const optionalInt = (v, path, floor) => {
|
|
1025
|
+
if (v !== void 0 && !(isInt(v) && v >= floor)) issue(path, `Must be a whole number of ${floor} or more.`);
|
|
1026
|
+
};
|
|
1027
|
+
const ordered = (low, high, path, what) => {
|
|
1028
|
+
if (isInt(low) && isInt(high) && low > high) issue(path, `Can't be more than the ${what}.`);
|
|
1029
|
+
};
|
|
1030
|
+
const structuredProps = (type, props, path) => {
|
|
1031
|
+
if (type === "list") {
|
|
1032
|
+
optionalInt(props.min, `${path}.min`, 0);
|
|
1033
|
+
optionalInt(props.max, `${path}.max`, 1);
|
|
1034
|
+
optionalInt(props.itemMaxLength, `${path}.itemMaxLength`, 1);
|
|
1035
|
+
optionalText(props.placeholder, `${path}.placeholder`);
|
|
1036
|
+
ordered(props.min, props.max ?? 10, `${path}.min`, "most items");
|
|
1037
|
+
} else if (type === "table") {
|
|
1038
|
+
const columns = props.columns;
|
|
1039
|
+
if (!Array.isArray(columns) || columns.length < 1 || columns.length > MAX_TABLE_COLUMNS) {
|
|
1040
|
+
issue(`${path}.columns`, `Must be a list of 1 to ${MAX_TABLE_COLUMNS} columns.`);
|
|
1041
|
+
} else {
|
|
1042
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1043
|
+
columns.forEach((c, j) => {
|
|
1044
|
+
const at = `${path}.columns[${j}]`;
|
|
1045
|
+
if (!isObject2(c)) return issue(at, "Each column needs a key and a label.");
|
|
1046
|
+
if (name(c.key, `${at}.key`, "A column key")) {
|
|
1047
|
+
if (seen.has(c.key)) issue(`${at}.key`, `Duplicate column key "${c.key}".`);
|
|
1048
|
+
seen.add(c.key);
|
|
1049
|
+
}
|
|
1050
|
+
text(c.label, `${at}.label`);
|
|
1051
|
+
optionalBoolean(c.long, `${at}.long`);
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
optionalInt(props.minRows, `${path}.minRows`, 0);
|
|
1055
|
+
optionalInt(props.maxRows, `${path}.maxRows`, 1);
|
|
1056
|
+
ordered(props.minRows, props.maxRows ?? 3, `${path}.minRows`, "most rows");
|
|
1057
|
+
} else if (type === "media-notes") {
|
|
1058
|
+
optionalInt(props.maxNotes, `${path}.maxNotes`, 1);
|
|
1059
|
+
optionalInt(props.textMaxLength, `${path}.textMaxLength`, 1);
|
|
1060
|
+
} else if (type === "text-highlights") {
|
|
1061
|
+
optionalText(props.text, `${path}.text`);
|
|
1062
|
+
optionalInt(props.maxHighlights, `${path}.maxHighlights`, 1);
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
if (!isObject2(input)) {
|
|
838
1066
|
issue("", "A form definition must be an object.");
|
|
839
1067
|
return fail();
|
|
840
1068
|
}
|
|
@@ -843,19 +1071,19 @@ function parseForm(input) {
|
|
|
843
1071
|
text(input.title, "title");
|
|
844
1072
|
optionalText(input.description, "description");
|
|
845
1073
|
if (input.layout !== void 0) {
|
|
846
|
-
if (!
|
|
1074
|
+
if (!isObject2(input.layout)) issue("layout", "Must be an object.");
|
|
847
1075
|
else if (input.layout.mode !== "focus" && input.layout.mode !== "classic") {
|
|
848
1076
|
issue("layout.mode", "Must be focus or classic.");
|
|
849
1077
|
}
|
|
850
1078
|
}
|
|
851
1079
|
if (input.settings !== void 0) {
|
|
852
|
-
if (!
|
|
1080
|
+
if (!isObject2(input.settings)) issue("settings", "Must be an object.");
|
|
853
1081
|
else {
|
|
854
1082
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
855
1083
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
856
1084
|
}
|
|
857
1085
|
}
|
|
858
|
-
if (input.meta !== void 0 && (!
|
|
1086
|
+
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
859
1087
|
issue("meta", "Must be a JSON object.");
|
|
860
1088
|
}
|
|
861
1089
|
if (!Array.isArray(input.blocks)) {
|
|
@@ -870,7 +1098,7 @@ function parseForm(input) {
|
|
|
870
1098
|
const choices = /* @__PURE__ */ new Map();
|
|
871
1099
|
input.blocks.forEach((block, i) => {
|
|
872
1100
|
const path = `blocks[${i}]`;
|
|
873
|
-
if (!
|
|
1101
|
+
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
874
1102
|
issue(path, "Every block needs a string id.");
|
|
875
1103
|
return;
|
|
876
1104
|
}
|
|
@@ -891,23 +1119,23 @@ function parseForm(input) {
|
|
|
891
1119
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
892
1120
|
}
|
|
893
1121
|
if (block.props !== void 0) {
|
|
894
|
-
if (!
|
|
1122
|
+
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
895
1123
|
else if (block.props.options !== void 0) {
|
|
896
1124
|
const options = block.props.options;
|
|
897
1125
|
if (!Array.isArray(options)) issue(`${path}.props.options`, "Must be an array.");
|
|
898
1126
|
else {
|
|
899
1127
|
options.forEach((o, j) => {
|
|
900
1128
|
const at = `${path}.props.options[${j}]`;
|
|
901
|
-
if (!
|
|
1129
|
+
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
902
1130
|
else optionalText(o.label, `${at}.label`);
|
|
903
1131
|
});
|
|
904
1132
|
choices.set(
|
|
905
1133
|
block.id,
|
|
906
|
-
new Set(options.flatMap((o) =>
|
|
1134
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
907
1135
|
);
|
|
908
1136
|
}
|
|
909
1137
|
}
|
|
910
|
-
const { max, maxLength: maxLength2 } =
|
|
1138
|
+
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
911
1139
|
if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
|
|
912
1140
|
issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
|
|
913
1141
|
}
|
|
@@ -915,6 +1143,9 @@ function parseForm(input) {
|
|
|
915
1143
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
916
1144
|
}
|
|
917
1145
|
}
|
|
1146
|
+
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
1147
|
+
structuredProps(block.type, block.props ?? {}, `${path}.props`);
|
|
1148
|
+
}
|
|
918
1149
|
if (validKey) {
|
|
919
1150
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
920
1151
|
keys.set(block.key, block.id);
|
|
@@ -933,7 +1164,7 @@ function parseForm(input) {
|
|
|
933
1164
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
934
1165
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
935
1166
|
(Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
|
|
936
|
-
if (!
|
|
1167
|
+
if (!isObject2(h) || typeof h.name !== "string" || !h.name) {
|
|
937
1168
|
issue(`hidden[${i}]`, "Each hidden input needs a name.");
|
|
938
1169
|
return;
|
|
939
1170
|
}
|
|
@@ -954,7 +1185,7 @@ function parseForm(input) {
|
|
|
954
1185
|
if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
|
|
955
1186
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
956
1187
|
const path = `endings[${i}]`;
|
|
957
|
-
if (!
|
|
1188
|
+
if (!isObject2(e)) {
|
|
958
1189
|
issue(path, "Each ending must be an object with an id and a title.");
|
|
959
1190
|
return;
|
|
960
1191
|
}
|
|
@@ -966,7 +1197,7 @@ function parseForm(input) {
|
|
|
966
1197
|
});
|
|
967
1198
|
const checkOperand = (o, path, reads) => {
|
|
968
1199
|
if (o === void 0) return;
|
|
969
|
-
if (!
|
|
1200
|
+
if (!isObject2(o)) return issue(path, "An operand must be an object.");
|
|
970
1201
|
if ("field" in o) {
|
|
971
1202
|
if (typeof o.field !== "string" || !fieldTypes.has(o.field)) return issue(path, `No field "${String(o.field)}".`);
|
|
972
1203
|
reads.push(o.field);
|
|
@@ -979,7 +1210,7 @@ function parseForm(input) {
|
|
|
979
1210
|
}
|
|
980
1211
|
};
|
|
981
1212
|
const checkCondition = (c, path, reads, depth = 0) => {
|
|
982
|
-
if (!
|
|
1213
|
+
if (!isObject2(c)) return issue(path, "A condition must be an object.");
|
|
983
1214
|
if (depth > MAX_CONDITION_DEPTH) return issue(path, "The condition is nested too deeply.");
|
|
984
1215
|
if ("all" in c || "any" in c) {
|
|
985
1216
|
const group = "all" in c ? "all" : "any";
|
|
@@ -988,7 +1219,7 @@ function parseForm(input) {
|
|
|
988
1219
|
return list.forEach((x, i) => checkCondition(x, `${path}.${group}[${i}]`, reads, depth + 1));
|
|
989
1220
|
}
|
|
990
1221
|
if ("not" in c) return checkCondition(c.not, `${path}.not`, reads, depth + 1);
|
|
991
|
-
if (typeof c.op !== "string" || !
|
|
1222
|
+
if (typeof c.op !== "string" || !isObject2(c.left)) {
|
|
992
1223
|
return issue(path, "A comparison needs an `op` and a `left` operand.");
|
|
993
1224
|
}
|
|
994
1225
|
if (!OPERATORS.has(c.op)) issue(`${path}.op`, `Unknown operator "${c.op}".`);
|
|
@@ -1004,7 +1235,7 @@ function parseForm(input) {
|
|
|
1004
1235
|
};
|
|
1005
1236
|
const checkLiterals = (op, left, right, path) => {
|
|
1006
1237
|
if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
|
|
1007
|
-
const literal =
|
|
1238
|
+
const literal = isObject2(right) && "value" in right;
|
|
1008
1239
|
if (right !== void 0 && !literal) return;
|
|
1009
1240
|
let what = "the left side";
|
|
1010
1241
|
let type;
|
|
@@ -1043,7 +1274,7 @@ function parseForm(input) {
|
|
|
1043
1274
|
if (input.logic !== void 0 && !Array.isArray(input.logic)) issue("logic", "Must be an array.");
|
|
1044
1275
|
(Array.isArray(input.logic) ? input.logic : []).forEach((rule, i) => {
|
|
1045
1276
|
const path = `logic[${i}]`;
|
|
1046
|
-
if (!
|
|
1277
|
+
if (!isObject2(rule) || typeof rule.id !== "string" || !isObject2(rule.when)) {
|
|
1047
1278
|
issue(path, "Each rule needs an id and a `when` condition.");
|
|
1048
1279
|
return;
|
|
1049
1280
|
}
|
|
@@ -1062,7 +1293,7 @@ function parseForm(input) {
|
|
|
1062
1293
|
at = rule.from;
|
|
1063
1294
|
inclusive = true;
|
|
1064
1295
|
}
|
|
1065
|
-
if (
|
|
1296
|
+
if (isObject2(rule.to)) {
|
|
1066
1297
|
if (typeof rule.to.ending !== "string" || !endingIds.has(rule.to.ending)) {
|
|
1067
1298
|
issue(`${path}.to`, `No ending "${String(rule.to.ending)}".`);
|
|
1068
1299
|
}
|
|
@@ -1094,7 +1325,7 @@ function parseForm(input) {
|
|
|
1094
1325
|
}
|
|
1095
1326
|
});
|
|
1096
1327
|
input.blocks.forEach((block, i) => {
|
|
1097
|
-
if (!
|
|
1328
|
+
if (!isObject2(block)) return;
|
|
1098
1329
|
const raw = block.kind === "field" ? [block.label, block.description] : block.kind === "content" ? [block.text] : [];
|
|
1099
1330
|
const texts = raw.filter((t) => typeof t === "string");
|
|
1100
1331
|
for (const t of texts) {
|
|
@@ -1114,7 +1345,7 @@ function parseForm(input) {
|
|
|
1114
1345
|
});
|
|
1115
1346
|
const extra = [["description", input.description]];
|
|
1116
1347
|
(Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
|
|
1117
|
-
if (
|
|
1348
|
+
if (isObject2(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
|
|
1118
1349
|
});
|
|
1119
1350
|
for (const [path, t] of extra) {
|
|
1120
1351
|
if (typeof t !== "string") continue;
|