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