@marktiderman/genesis-forms 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -30,6 +30,441 @@ function fingerprint(value) {
30
30
  return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
31
31
  }
32
32
 
33
+ // src/formula.ts
34
+ var FORMULA_FUNCTIONS = ["sum", "avg", "min", "max"];
35
+ var MAX_FORMULA_LENGTH = 2e3;
36
+ var MAX_DEPTH = 64;
37
+ var OP_ALIASES = { "\u2212": "-", "\xD7": "*", "\xF7": "/", "\u2260": "!=", "\u2264": "<=", "\u2265": ">=", "=": "==" };
38
+ var TWO_CHAR = /* @__PURE__ */ new Set(["==", "!=", "<=", ">="]);
39
+ var ONE_CHAR = /* @__PURE__ */ new Set(["+", "-", "*", "/", "<", ">", "(", ")", "?", ":", ",", ".", "=", "\u2212", "\xD7", "\xF7", "\u2260", "\u2264", "\u2265"]);
40
+ var COMPARISONS = /* @__PURE__ */ new Set(["==", "!=", "<", "<=", ">", ">="]);
41
+ var FormulaError = class extends Error {
42
+ constructor(message, at) {
43
+ super(message);
44
+ this.at = at;
45
+ }
46
+ at;
47
+ };
48
+ var isIdStart = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
49
+ var isIdPart = (c) => isIdStart(c) || c >= "0" && c <= "9";
50
+ var isDigit = (c) => c >= "0" && c <= "9";
51
+ function tokenize(source) {
52
+ const tokens = [];
53
+ let i = 0;
54
+ while (i < source.length) {
55
+ const c = source[i];
56
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
57
+ i++;
58
+ continue;
59
+ }
60
+ const at = i;
61
+ if (isDigit(c) || c === "." && isDigit(source[i + 1] ?? "")) {
62
+ while (isDigit(source[i] ?? "")) i++;
63
+ if (source[i] === "." && isDigit(source[i + 1] ?? "")) {
64
+ i++;
65
+ while (isDigit(source[i] ?? "")) i++;
66
+ }
67
+ const v = Number(source.slice(at, i));
68
+ if (!Number.isFinite(v)) throw new FormulaError("This number is too large.", at);
69
+ tokens.push({ t: "num", v, at });
70
+ continue;
71
+ }
72
+ if (isIdStart(c)) {
73
+ while (isIdPart(source[i] ?? "")) i++;
74
+ tokens.push({ t: "id", v: source.slice(at, i), at });
75
+ continue;
76
+ }
77
+ if (c === '"' || c === "'" || c === "\u201C") {
78
+ const close = c === "\u201C" ? "\u201D" : c;
79
+ const end = source.indexOf(close, i + 1);
80
+ if (end === -1) throw new FormulaError("This text has no closing quote.", at);
81
+ tokens.push({ t: "str", v: source.slice(i + 1, end), at });
82
+ i = end + 1;
83
+ continue;
84
+ }
85
+ const two = source.slice(i, i + 2);
86
+ if (TWO_CHAR.has(two)) {
87
+ tokens.push({ t: "op", v: two, at });
88
+ i += 2;
89
+ continue;
90
+ }
91
+ if (ONE_CHAR.has(c)) {
92
+ tokens.push({ t: "op", v: OP_ALIASES[c] ?? c, at });
93
+ i++;
94
+ continue;
95
+ }
96
+ throw new FormulaError(`"${c}" can't be used in a formula.`, at);
97
+ }
98
+ tokens.push({ t: "end", at: source.length });
99
+ return tokens;
100
+ }
101
+ function parseFormula(source) {
102
+ if (typeof source !== "string" || source.trim() === "") return { ok: false, message: "Write a formula.", at: 0 };
103
+ if (source.length > MAX_FORMULA_LENGTH) {
104
+ return { ok: false, message: `Keep the formula under ${MAX_FORMULA_LENGTH} characters.`, at: MAX_FORMULA_LENGTH };
105
+ }
106
+ let tokens;
107
+ try {
108
+ tokens = tokenize(source);
109
+ } catch (e) {
110
+ const err = e;
111
+ return { ok: false, message: err.message, at: err.at };
112
+ }
113
+ let pos = 0;
114
+ const refs = [];
115
+ const peek = () => tokens[pos];
116
+ const isOp = (v) => {
117
+ const tok = peek();
118
+ return tok.t === "op" && tok.v === v;
119
+ };
120
+ const expect = (v, message) => {
121
+ if (!isOp(v)) throw new FormulaError(message, peek().at);
122
+ pos++;
123
+ };
124
+ const describe = (tok) => tok.t === "end" ? "the formula ends there" : `found "${String(tok.v)}"`;
125
+ const guard = (depth) => {
126
+ if (depth > MAX_DEPTH) throw new FormulaError("The formula is nested too deeply.", peek().at);
127
+ };
128
+ const ternary = (depth) => {
129
+ guard(depth);
130
+ const test = comparison(depth);
131
+ if (!isOp("?")) return test;
132
+ pos++;
133
+ const then = ternary(depth + 1);
134
+ expect(":", "A `?` needs a `:` and the other value: `test ? a : b`.");
135
+ const otherwise = ternary(depth + 1);
136
+ return { kind: "if", test, then, else: otherwise };
137
+ };
138
+ const comparison = (depth) => {
139
+ const left = additive(depth);
140
+ const tok = peek();
141
+ if (tok.t !== "op" || !COMPARISONS.has(tok.v)) return left;
142
+ pos++;
143
+ const right = additive(depth);
144
+ const next = peek();
145
+ if (next.t === "op" && COMPARISONS.has(next.v)) {
146
+ throw new FormulaError("Compare two values at a time; combine comparisons with `? :`.", next.at);
147
+ }
148
+ return { kind: "binary", op: tok.v, left, right };
149
+ };
150
+ const additive = (depth) => {
151
+ let left = multiplicative(depth);
152
+ for (let tok = peek(); tok.t === "op" && (tok.v === "+" || tok.v === "-"); tok = peek()) {
153
+ pos++;
154
+ left = { kind: "binary", op: tok.v, left, right: multiplicative(depth) };
155
+ }
156
+ return left;
157
+ };
158
+ const multiplicative = (depth) => {
159
+ let left = unary(depth);
160
+ for (let tok = peek(); tok.t === "op" && (tok.v === "*" || tok.v === "/"); tok = peek()) {
161
+ pos++;
162
+ left = { kind: "binary", op: tok.v, left, right: unary(depth) };
163
+ }
164
+ return left;
165
+ };
166
+ const unary = (depth) => {
167
+ guard(depth);
168
+ if (isOp("-")) {
169
+ pos++;
170
+ return { kind: "unary", op: "-", arg: unary(depth + 1) };
171
+ }
172
+ if (isOp("+")) {
173
+ pos++;
174
+ return unary(depth + 1);
175
+ }
176
+ return primary(depth);
177
+ };
178
+ const primary = (depth) => {
179
+ const tok = peek();
180
+ if (tok.t === "num") {
181
+ pos++;
182
+ return { kind: "number", value: tok.v };
183
+ }
184
+ if (tok.t === "str") {
185
+ pos++;
186
+ return { kind: "text", value: tok.v };
187
+ }
188
+ if (tok.t === "op" && tok.v === "(") {
189
+ pos++;
190
+ const inner = ternary(depth + 1);
191
+ expect(")", "A `(` has no matching `)`.");
192
+ return inner;
193
+ }
194
+ if (tok.t === "id") {
195
+ pos++;
196
+ if (isOp("(")) {
197
+ if (!FORMULA_FUNCTIONS.includes(tok.v)) {
198
+ throw new FormulaError(`There is no function "${tok.v}"; use ${FORMULA_FUNCTIONS.join(", ")}.`, tok.at);
199
+ }
200
+ pos++;
201
+ const args = [];
202
+ if (!isOp(")")) {
203
+ args.push(ternary(depth + 1));
204
+ while (isOp(",")) {
205
+ pos++;
206
+ args.push(ternary(depth + 1));
207
+ }
208
+ }
209
+ expect(")", `"${tok.v}(" has no matching ")".`);
210
+ if (!args.length) throw new FormulaError(`${tok.v}() needs something to work on.`, tok.at);
211
+ return { kind: "call", fn: tok.v, args };
212
+ }
213
+ if (isOp(".")) {
214
+ pos++;
215
+ const column = peek();
216
+ if (column.t !== "id") throw new FormulaError(`Name a column after "${tok.v}.".`, column.at);
217
+ pos++;
218
+ refs.push({ key: tok.v, column: column.v });
219
+ return { kind: "ref", key: tok.v, column: column.v, at: tok.at };
220
+ }
221
+ refs.push({ key: tok.v });
222
+ return { kind: "ref", key: tok.v, at: tok.at };
223
+ }
224
+ throw new FormulaError(`Expected a number, an answer or "(", but ${describe(tok)}.`, tok.at);
225
+ };
226
+ try {
227
+ const ast = ternary(0);
228
+ const rest = peek();
229
+ if (rest.t !== "end") throw new FormulaError(`Expected an operator such as + or *, but ${describe(rest)}.`, rest.at);
230
+ return { ok: true, ast, refs };
231
+ } catch (e) {
232
+ if (e instanceof FormulaError) return { ok: false, message: e.message, at: e.at };
233
+ return { ok: false, message: "The formula is nested too deeply.", at: 0 };
234
+ }
235
+ }
236
+ function formulaRefs(source) {
237
+ const parsed = parseFormula(source);
238
+ if (!parsed.ok) return [];
239
+ const seen = /* @__PURE__ */ new Set();
240
+ return parsed.refs.filter((r) => {
241
+ const id = `${r.key}\0${r.column ?? ""}`;
242
+ if (seen.has(id)) return false;
243
+ seen.add(id);
244
+ return true;
245
+ });
246
+ }
247
+ function renameFormulaKey(source, from, to) {
248
+ let tokens;
249
+ try {
250
+ tokens = tokenize(source);
251
+ } catch {
252
+ return source;
253
+ }
254
+ let out = "";
255
+ let last = 0;
256
+ tokens.forEach((tok, i) => {
257
+ if (tok.t !== "id" || tok.v !== from) return;
258
+ const prev = tokens[i - 1];
259
+ const next = tokens[i + 1];
260
+ if (prev?.t === "op" && prev.v === ".") return;
261
+ if (next?.t === "op" && next.v === "(") return;
262
+ out += source.slice(last, tok.at) + to;
263
+ last = tok.at + from.length;
264
+ });
265
+ return out + source.slice(last);
266
+ }
267
+ var NUMERIC = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
268
+ function formulaNumber(v) {
269
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
270
+ if (typeof v === "boolean") return v ? 1 : 0;
271
+ if (typeof v === "string") {
272
+ const t = v.trim().replace(/,/g, "");
273
+ return t.length <= 64 && NUMERIC.test(t) ? finite(Number(t)) : null;
274
+ }
275
+ return null;
276
+ }
277
+ var finite = (n) => Number.isFinite(n) ? n : null;
278
+ var scalar = (v) => Array.isArray(v) ? null : v;
279
+ function truthy(v) {
280
+ if (v === null) return false;
281
+ if (typeof v === "boolean") return v;
282
+ if (typeof v === "number") return v !== 0;
283
+ if (typeof v === "string") return v.trim() !== "";
284
+ return v.length > 0;
285
+ }
286
+ function numbersIn(values, out = []) {
287
+ for (const v of values) {
288
+ if (Array.isArray(v)) numbersIn(v, out);
289
+ else {
290
+ const n = formulaNumber(v);
291
+ if (n !== null) out.push(n);
292
+ }
293
+ }
294
+ return out;
295
+ }
296
+ function compare(op, a, b) {
297
+ const blank2 = (v) => v === null || Array.isArray(v) || typeof v === "string" && v.trim() === "";
298
+ if (blank2(a) || blank2(b)) return op === "!=";
299
+ const x = formulaNumber(a);
300
+ const y = formulaNumber(b);
301
+ if (x !== null && y !== null) {
302
+ if (op === "==") return x === y;
303
+ if (op === "!=") return x !== y;
304
+ if (op === "<") return x < y;
305
+ if (op === "<=") return x <= y;
306
+ if (op === ">") return x > y;
307
+ return x >= y;
308
+ }
309
+ if (op === "==") return String(a) === String(b);
310
+ if (op === "!=") return String(a) !== String(b);
311
+ return false;
312
+ }
313
+ function evaluateFormula(ast, read) {
314
+ const run = (node) => {
315
+ switch (node.kind) {
316
+ case "number":
317
+ case "text":
318
+ return node.value;
319
+ case "ref":
320
+ return read(node.column !== void 0 ? { key: node.key, column: node.column } : { key: node.key });
321
+ case "unary": {
322
+ const n = formulaNumber(scalar(run(node.arg)));
323
+ return n === null ? null : -n;
324
+ }
325
+ case "if":
326
+ return truthy(run(node.test)) ? run(node.then) : run(node.else);
327
+ case "call": {
328
+ const nums = numbersIn(node.args.map(run));
329
+ const total = nums.reduce((a, b) => a + b, 0);
330
+ if (node.fn === "sum") return finite(total);
331
+ if (!nums.length) return null;
332
+ if (node.fn === "avg") return finite(total / nums.length);
333
+ return nums.reduce((a, b) => node.fn === "min" ? Math.min(a, b) : Math.max(a, b));
334
+ }
335
+ case "binary": {
336
+ const a = run(node.left);
337
+ const b = run(node.right);
338
+ if (node.op === "+" || node.op === "-" || node.op === "*" || node.op === "/") {
339
+ const x = formulaNumber(scalar(a));
340
+ const y = formulaNumber(scalar(b));
341
+ if (x === null || y === null) return null;
342
+ if (node.op === "+") return finite(x + y);
343
+ if (node.op === "-") return finite(x - y);
344
+ if (node.op === "*") return finite(x * y);
345
+ return y === 0 ? null : finite(x / y);
346
+ }
347
+ return compare(node.op, a, b);
348
+ }
349
+ }
350
+ };
351
+ try {
352
+ return run(ast);
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
357
+
358
+ // src/calculated.ts
359
+ var CALCULATED_TYPE = "calculated";
360
+ var COMPUTED_FIELD_TYPES = [CALCULATED_TYPE];
361
+ var MAX_CALCULATED_DECIMALS = 10;
362
+ function calculatedProps(field2) {
363
+ const p = field2.props ?? {};
364
+ const out = { formula: typeof p.formula === "string" ? p.formula : "" };
365
+ if (p.format === "number" || p.format === "percent" || p.format === "text") out.format = p.format;
366
+ if (typeof p.decimals === "number" && Number.isInteger(p.decimals) && p.decimals >= 0 && p.decimals <= MAX_CALCULATED_DECIMALS) {
367
+ out.decimals = p.decimals;
368
+ }
369
+ return out;
370
+ }
371
+ var isCalculated = (field2) => field2?.type === CALCULATED_TYPE;
372
+ var round = (n, decimals) => {
373
+ if (decimals === void 0) return n;
374
+ const r = Number(n.toFixed(decimals));
375
+ return Object.is(r, -0) ? 0 : r;
376
+ };
377
+ function calculatedValue(result, field2) {
378
+ const { format, decimals } = calculatedProps(field2);
379
+ if (result === null || Array.isArray(result)) return void 0;
380
+ if (format === "number" || format === "percent") {
381
+ const n = formulaNumber(result);
382
+ return n === null ? void 0 : round(n, decimals);
383
+ }
384
+ if (format === "text") {
385
+ if (typeof result === "number") return decimals === void 0 ? String(result) : result.toFixed(decimals);
386
+ return typeof result === "boolean" ? result ? "Yes" : "No" : result;
387
+ }
388
+ return typeof result === "number" ? round(result, decimals) : result;
389
+ }
390
+ function formatCalculated(value, field2) {
391
+ if (value === void 0 || value === null) return "";
392
+ const { format, decimals } = calculatedProps(field2);
393
+ const fixed = (n) => decimals === void 0 ? String(Number(n.toFixed(6))) : n.toFixed(decimals);
394
+ if (typeof value === "number") return format === "percent" ? `${fixed(value * 100)}%` : fixed(value);
395
+ if (typeof value === "boolean") return value ? "Yes" : "No";
396
+ return typeof value === "string" ? value : "";
397
+ }
398
+ var MAX_FORMULA_ITEMS = 1e4;
399
+ function formulaInput(value, column) {
400
+ if (value === void 0 || value === null) return null;
401
+ if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
402
+ if (column !== void 0) {
403
+ if (!Array.isArray(value)) return null;
404
+ return value.flatMap((row) => {
405
+ if (!row || typeof row !== "object" || Array.isArray(row)) return [];
406
+ const cell = row[column];
407
+ return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
408
+ });
409
+ }
410
+ if (Array.isArray(value)) {
411
+ return value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
412
+ }
413
+ return typeof value === "object" ? null : value;
414
+ }
415
+ var UNREAD = /* @__PURE__ */ Symbol("unread");
416
+ function calculator(definition) {
417
+ const compiled = [];
418
+ const idByKey = /* @__PURE__ */ new Map();
419
+ for (const block of definition.blocks) {
420
+ if (block.kind !== "field") continue;
421
+ idByKey.set(block.key, block.id);
422
+ }
423
+ for (const block of definition.blocks) {
424
+ if (block.kind !== "field" || !isCalculated(block)) continue;
425
+ const parsed = parseFormula(calculatedProps(block).formula);
426
+ const reads = parsed.ok ? parsed.refs.map((r) => idByKey.get(r.key)) : [];
427
+ compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, last: null });
428
+ }
429
+ const withCalculated = (values, path) => {
430
+ if (!compiled.length) return values;
431
+ const visible = new Set(path);
432
+ let next = values;
433
+ const input = (id) => id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id) ? UNREAD : next[id];
434
+ for (const entry of compiled) {
435
+ const { field: field2, ast } = entry;
436
+ const inputs = entry.reads.map(input);
437
+ let value;
438
+ if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
439
+ value = entry.last.value;
440
+ } else {
441
+ const read = (ref) => {
442
+ const raw = input(idByKey.get(ref.key));
443
+ return raw === UNREAD ? null : formulaInput(raw, ref.column);
444
+ };
445
+ value = ast ? calculatedValue(evaluateFormula(ast, read), field2) : void 0;
446
+ entry.last = { inputs, value };
447
+ }
448
+ const current = Object.prototype.hasOwnProperty.call(next, field2.id) ? next[field2.id] : void 0;
449
+ if (current !== value) next = { ...next, [field2.id]: value };
450
+ }
451
+ return next;
452
+ };
453
+ return { active: compiled.length > 0, withCalculated };
454
+ }
455
+ var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
456
+ var calculatedFieldType = {
457
+ type: CALCULATED_TYPE,
458
+ label: "Calculated",
459
+ category: "advanced",
460
+ value: { json: true },
461
+ operators: numberOps,
462
+ a11y: { semantics: "control" },
463
+ // The engine works the value out; any number, text or true / false it made is valid.
464
+ validate: (value) => typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? null : "This value is worked out from other answers.",
465
+ summarize: (value, field2) => formatCalculated(value, field2)
466
+ };
467
+
33
468
  // src/events.ts
34
469
  function isPlainObject(value) {
35
470
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
@@ -324,7 +759,7 @@ function intRange(field2, min, maxDefault) {
324
759
  const max = typeof field2.props?.max === "number" ? field2.props.max : maxDefault;
325
760
  return { min, max };
326
761
  }
327
- var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
762
+ var numberOps2 = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
328
763
  var textOps = ["eq", "neq", "contains", "answered", "empty"];
329
764
  function maxLength(field2, fallback) {
330
765
  return typeof field2.props?.maxLength === "number" ? field2.props.maxLength : fallback;
@@ -402,23 +837,30 @@ var builtins = [
402
837
  label: "Rating",
403
838
  category: "scale",
404
839
  value: { json: true },
405
- operators: numberOps,
840
+ operators: numberOps2,
406
841
  a11y: { semantics: "group" },
407
842
  validate: (value, field2) => {
408
843
  const { min, max } = intRange(field2, 1, 5);
409
844
  return Number.isInteger(value) && value >= min && value <= max ? null : `Choose a rating from ${min} to ${max}.`;
410
845
  },
411
- summarize: (value, field2) => `${value}/${intRange(field2, 1, 5).max}`
846
+ summarize: (value, field2) => {
847
+ const { max } = intRange(field2, 1, 5);
848
+ const end = scaleEndLabel(field2, value, 1, max);
849
+ return end ? `${value}/${max} (${end})` : `${value}/${max}`;
850
+ }
412
851
  },
413
852
  {
414
853
  type: "nps",
415
854
  label: "Net Promoter Score",
416
855
  category: "scale",
417
856
  value: { json: true },
418
- operators: numberOps,
857
+ operators: numberOps2,
419
858
  a11y: { semantics: "group" },
420
859
  validate: (value) => Number.isInteger(value) && value >= 0 && value <= 10 ? null : "Choose a score from 0 to 10.",
421
- summarize: (value) => `${value}/10`
860
+ summarize: (value, field2) => {
861
+ const end = scaleEndLabel(field2, value, 0, 10);
862
+ return end ? `${value}/10 (${end})` : `${value}/10`;
863
+ }
422
864
  },
423
865
  {
424
866
  type: "single-choice",
@@ -452,9 +894,125 @@ var builtins = [
452
894
  a11y: { semantics: "group" },
453
895
  validate: (value) => typeof value === "boolean" ? null : "Choose yes or no.",
454
896
  summarize: (value) => value ? "Yes" : "No"
897
+ },
898
+ {
899
+ type: "dropdown",
900
+ label: "Dropdown",
901
+ category: "choice",
902
+ value: { json: true },
903
+ operators: ["eq", "neq", "in", "answered", "empty"],
904
+ a11y: { semantics: "control" },
905
+ validate: (value, field2) => typeof value === "string" && choiceOptions(field2).some((o) => o.value === value) ? null : "Choose one of the options.",
906
+ summarize: (value, field2) => optionLabel(field2, String(value))
907
+ },
908
+ {
909
+ type: "number",
910
+ label: "Number",
911
+ category: "text",
912
+ value: { json: true },
913
+ operators: numberOps2,
914
+ a11y: { semantics: "control", inputMode: "decimal" },
915
+ validate: (value, field2) => {
916
+ if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
917
+ const { min, max, step } = numberLimits(field2);
918
+ if (min !== void 0 && max !== void 0 && (value < min || value > max)) {
919
+ return `Enter a number from ${min} to ${max}.`;
920
+ }
921
+ if (min !== void 0 && value < min) return `Enter ${min} or more.`;
922
+ if (max !== void 0 && value > max) return `Enter ${max} or less.`;
923
+ if (step !== void 0 && !onStep(value, min ?? 0, step)) {
924
+ return step === 1 && Number.isInteger(min ?? 0) ? "Enter a whole number." : `Enter a number in steps of ${step}.`;
925
+ }
926
+ return null;
927
+ },
928
+ summarize: (value) => String(value),
929
+ samples: (field2) => {
930
+ const { min, max } = numberLimits(field2);
931
+ return [min, max, 0, 1].filter((v) => v !== void 0);
932
+ }
933
+ },
934
+ {
935
+ type: "date",
936
+ label: "Date",
937
+ category: "date",
938
+ value: { json: true },
939
+ operators: ["eq", "neq", "answered", "empty"],
940
+ a11y: { semantics: "control" },
941
+ validate: (value, field2) => {
942
+ if (typeof value !== "string" || !isIsoDate(value.trim())) return "Enter a date like 2026-10-22.";
943
+ const date = value.trim();
944
+ const { min, max } = dateLimits(field2);
945
+ if (min && max && (date < min || date > max)) return `Choose a date from ${min} to ${max}.`;
946
+ if (min && date < min) return `Choose ${min} or later.`;
947
+ if (max && date > max) return `Choose ${max} or earlier.`;
948
+ return null;
949
+ },
950
+ normalize: (value) => typeof value === "string" ? value.trim() : value,
951
+ samples: (field2) => {
952
+ const { min, max } = dateLimits(field2);
953
+ return [min ?? max ?? "2000-01-01"];
954
+ }
955
+ },
956
+ {
957
+ type: "consent",
958
+ label: "Consent checkbox",
959
+ category: "choice",
960
+ value: { json: true },
961
+ operators: ["answered", "empty"],
962
+ a11y: { semantics: "group" },
963
+ // Unchecked is unanswered, so a required consent must be checked.
964
+ isEmpty: (value) => value === void 0 || value === null || value === false,
965
+ validate: (value) => value === true ? null : "Check the box to agree.",
966
+ summarize: () => "Agreed",
967
+ samples: () => [true]
455
968
  }
456
969
  ];
457
- for (const manifest of [...builtins, ...structuredFieldTypes]) registerFieldType(manifest);
970
+ function scaleLabels(field2) {
971
+ const out = {};
972
+ const low = field2.props?.lowLabel;
973
+ const high = field2.props?.highLabel;
974
+ if (typeof low === "string" && low.trim()) out.low = low.trim();
975
+ if (typeof high === "string" && high.trim()) out.high = high.trim();
976
+ return out;
977
+ }
978
+ function ratingDisplay(field2) {
979
+ return field2.props?.display === "stars" ? "stars" : "numbers";
980
+ }
981
+ function scaleEndLabel(field2, value, min, max) {
982
+ const { low, high } = scaleLabels(field2);
983
+ return value === min ? low : value === max ? high : void 0;
984
+ }
985
+ var finite2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
986
+ function numberLimits(field2) {
987
+ const out = {};
988
+ const min = finite2(field2.props?.min);
989
+ const max = finite2(field2.props?.max);
990
+ const step = finite2(field2.props?.step);
991
+ if (min !== void 0) out.min = min;
992
+ if (max !== void 0) out.max = max;
993
+ if (step !== void 0 && step > 0) out.step = step;
994
+ return out;
995
+ }
996
+ function onStep(value, base, step) {
997
+ const n = (value - base) / step;
998
+ return Math.abs(n - Math.round(n)) < 1e-9 * Math.max(1, Math.abs(n));
999
+ }
1000
+ function isIsoDate(value) {
1001
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
1002
+ const [y, m, d] = value.split("-").map(Number);
1003
+ if (y < 1 || m < 1 || m > 12 || d < 1) return false;
1004
+ const days = [31, y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1005
+ return d <= days[m - 1];
1006
+ }
1007
+ function dateLimits(field2) {
1008
+ const out = {};
1009
+ const min = field2.props?.min;
1010
+ const max = field2.props?.max;
1011
+ if (typeof min === "string" && isIsoDate(min)) out.min = min;
1012
+ if (typeof max === "string" && isIsoDate(max)) out.max = max;
1013
+ return out;
1014
+ }
1015
+ for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
458
1016
 
459
1017
  // src/logic.ts
460
1018
  function own(record, key) {
@@ -626,8 +1184,15 @@ var LITERAL_TYPE = {
626
1184
  "single-choice": "string",
627
1185
  "multi-choice": "string",
628
1186
  "yes-no": "boolean",
629
- list: "string"
1187
+ list: "string",
1188
+ dropdown: "string",
1189
+ number: "number",
1190
+ date: "string",
1191
+ consent: "boolean",
1192
+ // A calculated field holds a number unless its format says text; see `literalType`.
1193
+ calculated: "number"
630
1194
  };
1195
+ var CHOICE_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
631
1196
  var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
632
1197
  var OPERATORS = /* @__PURE__ */ new Set([
633
1198
  "eq",
@@ -707,7 +1272,44 @@ function parseForm(input) {
707
1272
  } else if (type === "text-highlights") {
708
1273
  optionalText(props2.text, `${path}.text`);
709
1274
  optionalInt(props2.maxHighlights, `${path}.maxHighlights`, 1);
1275
+ } else if (type === CALCULATED_TYPE) {
1276
+ if (props2.format !== void 0 && props2.format !== "number" && props2.format !== "percent" && props2.format !== "text") {
1277
+ issue(`${path}.format`, "Must be number, percent or text.");
1278
+ }
1279
+ if (props2.decimals !== void 0 && !(isInt(props2.decimals) && props2.decimals >= 0 && props2.decimals <= MAX_CALCULATED_DECIMALS)) {
1280
+ issue(`${path}.decimals`, `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
1281
+ }
1282
+ }
1283
+ };
1284
+ const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
1285
+ const fieldProps = (type, props2, path) => {
1286
+ if (type === "rating" || type === "nps") {
1287
+ optionalText(props2.lowLabel, `${path}.lowLabel`);
1288
+ optionalText(props2.highLabel, `${path}.highLabel`);
1289
+ }
1290
+ if (type === "rating" && props2.display !== void 0 && props2.display !== "numbers" && props2.display !== "stars") {
1291
+ issue(`${path}.display`, "Must be numbers or stars.");
1292
+ }
1293
+ if (type === "number") {
1294
+ for (const k of ["min", "max", "step"]) {
1295
+ if (props2[k] !== void 0 && !isFiniteNumber(props2[k])) issue(`${path}.${k}`, "Must be a number.");
1296
+ }
1297
+ if (isFiniteNumber(props2.step) && props2.step <= 0) issue(`${path}.step`, "Must be above 0.");
1298
+ if (isFiniteNumber(props2.min) && isFiniteNumber(props2.max) && props2.min > props2.max) {
1299
+ issue(`${path}.min`, "Can't be more than the largest number.");
1300
+ }
1301
+ }
1302
+ if (type === "date") {
1303
+ for (const k of ["min", "max"]) {
1304
+ if (props2[k] !== void 0 && !(typeof props2[k] === "string" && isIsoDate(props2[k]))) {
1305
+ issue(`${path}.${k}`, "Must be a date like 2026-10-22.");
1306
+ }
1307
+ }
1308
+ if (typeof props2.min === "string" && typeof props2.max === "string" && isIsoDate(props2.min) && isIsoDate(props2.max) && props2.min > props2.max) {
1309
+ issue(`${path}.min`, "Can't be after the latest date.");
1310
+ }
710
1311
  }
1312
+ if (type === "consent") optionalText(props2.text, `${path}.text`);
711
1313
  };
712
1314
  if (!isObject2(input)) {
713
1315
  issue("", "A form definition must be an object.");
@@ -742,7 +1344,9 @@ function parseForm(input) {
742
1344
  const keys = /* @__PURE__ */ new Map();
743
1345
  const fieldTypes = /* @__PURE__ */ new Map();
744
1346
  const order = /* @__PURE__ */ new Map();
1347
+ const tableColumnKeys = /* @__PURE__ */ new Map();
745
1348
  const choices = /* @__PURE__ */ new Map();
1349
+ const calculatedLiteral = /* @__PURE__ */ new Map();
746
1350
  input.blocks.forEach((block, i) => {
747
1351
  const path = `blocks[${i}]`;
748
1352
  if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
@@ -789,13 +1393,27 @@ function parseForm(input) {
789
1393
  if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
790
1394
  issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
791
1395
  }
792
- if (isObject2(block.props) && typeof block.type === "string") structuredProps(block.type, block.props, `${path}.props`);
1396
+ }
1397
+ if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
1398
+ structuredProps(block.type, block.props ?? {}, `${path}.props`);
1399
+ fieldProps(block.type, block.props ?? {}, `${path}.props`);
793
1400
  }
794
1401
  if (validKey) {
795
1402
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
796
1403
  keys.set(block.key, block.id);
797
1404
  }
798
1405
  fieldTypes.set(block.id, typeof block.type === "string" ? block.type : "");
1406
+ if (block.type === CALCULATED_TYPE) {
1407
+ const format = isObject2(block.props) ? block.props.format : void 0;
1408
+ if (format === "text") calculatedLiteral.set(block.id, "string");
1409
+ else if (format !== "number" && format !== "percent") calculatedLiteral.set(block.id, void 0);
1410
+ }
1411
+ if (block.type === "table" && isObject2(block.props) && Array.isArray(block.props.columns)) {
1412
+ tableColumnKeys.set(
1413
+ block.id,
1414
+ new Set(block.props.columns.flatMap((c) => isObject2(c) && typeof c.key === "string" ? [c.key] : []))
1415
+ );
1416
+ }
799
1417
  } else if (block.kind === "content") {
800
1418
  if (block.type !== "heading" && block.type !== "text") issue(`${path}.type`, "Must be heading or text.");
801
1419
  text(block.text, `${path}.text`);
@@ -805,6 +1423,38 @@ function parseForm(input) {
805
1423
  issue(`${path}.kind`, "Unknown block kind.");
806
1424
  }
807
1425
  });
1426
+ input.blocks.forEach((block, i) => {
1427
+ if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
1428
+ const path = `blocks[${i}].props.formula`;
1429
+ if (block.required === true) {
1430
+ warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
1431
+ }
1432
+ const formula = isObject2(block.props) ? block.props.formula : void 0;
1433
+ if (typeof formula !== "string" || !formula.trim()) return issue(path, "A calculated field needs a formula.");
1434
+ const parsed = parseFormula(formula);
1435
+ if (!parsed.ok) return issue(path, `${parsed.message} (at character ${parsed.at + 1})`);
1436
+ const reported = /* @__PURE__ */ new Set();
1437
+ for (const ref of parsed.refs) {
1438
+ const name2 = ref.column === void 0 ? ref.key : `${ref.key}.${ref.column}`;
1439
+ if (reported.has(name2)) continue;
1440
+ reported.add(name2);
1441
+ const target = keys.get(ref.key);
1442
+ const targetType = target === void 0 ? void 0 : fieldTypes.get(target);
1443
+ const at = target === void 0 ? void 0 : order.get(target);
1444
+ if (target === void 0 || targetType === void 0) issue(path, `The formula uses "${ref.key}", which is not the answer name of a question in this form.`);
1445
+ else if (target === block.id) issue(path, "The formula uses this field's own value.");
1446
+ else if (targetType === CALCULATED_TYPE && (at ?? 0) > i) {
1447
+ issue(path, `The formula uses "${ref.key}", a calculated field that comes after this one; move it earlier.`);
1448
+ } else if (targetType === "table") {
1449
+ const columns = tableColumnKeys.get(target);
1450
+ if (ref.column === void 0) issue(path, `"${ref.key}" is a table: name one of its columns, like ${ref.key}.${[...columns ?? []][0] ?? "column"}.`);
1451
+ else if (columns && !columns.has(ref.column)) issue(path, `The table "${ref.key}" has no column "${ref.column}".`);
1452
+ } else if (ref.column !== void 0) issue(path, `"${ref.key}" is not a table, so it has no column "${ref.column}".`);
1453
+ else if (targetType === "media-notes" || targetType === "text-highlights") {
1454
+ warnings.push({ path, message: `"${ref.key}" holds notes, not numbers; the formula reads it as blank.` });
1455
+ }
1456
+ }
1457
+ });
808
1458
  const hiddenNames = /* @__PURE__ */ new Set();
809
1459
  const hiddenTypes = /* @__PURE__ */ new Map();
810
1460
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
@@ -888,8 +1538,8 @@ function parseForm(input) {
888
1538
  if (typeof left.field === "string" && fieldTypes.has(left.field)) {
889
1539
  const fieldType = fieldTypes.get(left.field);
890
1540
  what = `"${keyOf(left.field)}"`;
891
- type = LITERAL_TYPE[fieldType];
892
- if (fieldType === "single-choice" || fieldType === "multi-choice") options = choices.get(left.field);
1541
+ type = calculatedLiteral.has(left.field) ? calculatedLiteral.get(left.field) : LITERAL_TYPE[fieldType];
1542
+ if (CHOICE_TYPES.has(fieldType)) options = choices.get(left.field);
893
1543
  } else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
894
1544
  what = `hidden input "${left.hidden}"`;
895
1545
  type = hiddenTypes.get(left.hidden);
@@ -1035,11 +1685,33 @@ var f = {
1035
1685
  longText: (key, o) => field("long-text", key, o, o.maxLength ? { maxLength: o.maxLength } : void 0),
1036
1686
  email: (key, o) => field("email", key, o),
1037
1687
  phone: (key, o) => field("phone", key, o),
1038
- rating: (key, o) => field("rating", key, o, { max: o.max ?? 5 }),
1039
- nps: (key, o) => field("nps", key, o),
1688
+ /**
1689
+ * A 1-to-`max` scale. `lowLabel` and `highLabel` are optional words under
1690
+ * its ends ("Poor", "Great"); `display: "stars"` draws stars instead of numbers.
1691
+ */
1692
+ rating: (key, o) => field(
1693
+ "rating",
1694
+ key,
1695
+ o,
1696
+ props({ max: o.max ?? 5, lowLabel: o.lowLabel || void 0, highLabel: o.highLabel || void 0, display: o.display })
1697
+ ),
1698
+ /** A 0-to-10 Net Promoter Score. The end labels default to "Not likely" and "Very likely" in the renderer. */
1699
+ nps: (key, o) => field("nps", key, o, props({ lowLabel: o.lowLabel || void 0, highLabel: o.highLabel || void 0 })),
1040
1700
  singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
1041
1701
  multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
1042
1702
  yesNo: (key, o) => field("yes-no", key, o),
1703
+ /** One option from a compact menu: for a long list (a country, a class), where single choice would be a wall of buttons. */
1704
+ dropdown: (key, o) => field("dropdown", key, o, choiceProps(o.options)),
1705
+ /** A number, optionally between `min` and `max` and on a `step` (from `min`, else 0): `step: 1` for whole numbers. */
1706
+ number: (key, o) => field("number", key, o, props({ min: o.min, max: o.max, step: o.step })),
1707
+ /** A calendar day, stored as `YYYY-MM-DD`, optionally between `min` and `max` (also `YYYY-MM-DD`). */
1708
+ date: (key, o) => field("date", key, o, props({ min: o.min, max: o.max })),
1709
+ /**
1710
+ * A single checkbox to agree to something. `text` is the checkbox's own
1711
+ * words ("I agree to the terms"); the renderer defaults to "I agree".
1712
+ * Checked stores `true`; unchecked is unanswered, so `required` means it must be checked.
1713
+ */
1714
+ consent: (key, o) => field("consent", key, o, props({ text: o.text || void 0 })),
1043
1715
  /** Several short answers, one per line: goals, action items, three things that went well. */
1044
1716
  list: (key, o) => field(
1045
1717
  "list",
@@ -1047,6 +1719,12 @@ var f = {
1047
1719
  o,
1048
1720
  props({ min: o.min, max: o.max, itemMaxLength: o.itemMaxLength, placeholder: o.itemPlaceholder })
1049
1721
  ),
1722
+ /**
1723
+ * A value worked out from other answers and shown read-only, such as a
1724
+ * total or a score: `formula` names answers by key (`hours * rate`,
1725
+ * `sum(plan.cost)`). Stored with the answers; never required.
1726
+ */
1727
+ calculated: (key, o) => field("calculated", key, o, props({ formula: o.formula, format: o.format, decimals: o.decimals })),
1050
1728
  /**
1051
1729
  * A small table each answer fills row by row, one answer per column: an
1052
1730
  * action plan (What / Who / By when), pros and cons.
@@ -1106,9 +1784,9 @@ function effects(fromKey, condition) {
1106
1784
  })
1107
1785
  };
1108
1786
  }
1109
- var compare = (key, op, value) => (r) => value === void 0 ? { op, left: { field: r(key) } } : { op, left: { field: r(key) }, right: { value } };
1787
+ var compare2 = (key, op, value) => (r) => value === void 0 ? { op, left: { field: r(key) } } : { op, left: { field: r(key) }, right: { value } };
1110
1788
  function when(key) {
1111
- const on = (op, value) => effects(key, compare(key, op, value));
1789
+ const on = (op, value) => effects(key, compare2(key, op, value));
1112
1790
  return {
1113
1791
  is: (value) => on("eq", value),
1114
1792
  isNot: (value) => on("neq", value),
@@ -1236,7 +1914,7 @@ function runCustom(validator, value) {
1236
1914
  }
1237
1915
  function validateField(field2, value, options = {}) {
1238
1916
  if (isEmptyValue(value, field2)) {
1239
- return field2.required && options.requireAnswers !== false ? REQUIRED_MESSAGE : null;
1917
+ return field2.required && field2.type !== CALCULATED_TYPE && options.requireAnswers !== false ? REQUIRED_MESSAGE : null;
1240
1918
  }
1241
1919
  const manifest = getFieldType(field2.type);
1242
1920
  if (!manifest) return `Unknown field type "${field2.type}".`;
@@ -1267,9 +1945,21 @@ function build(definition, options, replaying = false) {
1267
1945
  if (!declared.has(name)) initialHidden[name] = value;
1268
1946
  }
1269
1947
  Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
1270
- const first = computePath(definition, scopeOf({}, initialHidden));
1948
+ const calc = calculator(definition);
1949
+ const settle = (values, hidden) => {
1950
+ let { path } = computePath(definition, scopeOf(values, hidden));
1951
+ if (!calc.active) return { values, path };
1952
+ for (let round2 = 0; round2 < 4; round2++) {
1953
+ const next = calc.withCalculated(values, path);
1954
+ if (next === values) break;
1955
+ values = next;
1956
+ path = computePath(definition, scopeOf(values, hidden)).path;
1957
+ }
1958
+ return { values, path };
1959
+ };
1960
+ const first = settle({}, initialHidden);
1271
1961
  let state = {
1272
- values: {},
1962
+ values: first.values,
1273
1963
  hidden: initialHidden,
1274
1964
  path: first.path,
1275
1965
  current: first.path[0] ?? null,
@@ -1304,12 +1994,25 @@ function build(definition, options, replaying = false) {
1304
1994
  }
1305
1995
  return errors;
1306
1996
  };
1997
+ let unsettled = false;
1998
+ const settleNow = () => {
1999
+ if (!unsettled) return;
2000
+ unsettled = false;
2001
+ const { values, path } = settle(state.values, state.hidden);
2002
+ state = { ...state, values, path };
2003
+ };
1307
2004
  const answer = (id, value, event) => {
1308
- if (state.status === "completed" || own(state.values, id) === value) return;
1309
- const values = { ...state.values, [id]: value };
1310
- const { path } = computePath(definition, scopeOf(values, state.hidden));
2005
+ if (state.status === "completed" || own(state.values, id) === value || isCalculated(fields.get(id))) return;
1311
2006
  const errors = { ...state.errors };
1312
2007
  delete errors[id];
2008
+ if (batch) {
2009
+ state.values = { ...state.values, [id]: value };
2010
+ state.errors = errors;
2011
+ log(event);
2012
+ unsettled = true;
2013
+ return;
2014
+ }
2015
+ const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
1313
2016
  emit({ ...state, values, path, errors, events: log(event) });
1314
2017
  };
1315
2018
  const engine = {
@@ -1381,10 +2084,14 @@ function build(definition, options, replaying = false) {
1381
2084
  }
1382
2085
  };
1383
2086
  if (options.initialValues) {
2087
+ const wasBatch = batch;
2088
+ batch = true;
1384
2089
  for (const [id, value] of Object.entries(options.initialValues)) {
1385
2090
  if (!fields.has(id) || value === void 0) continue;
1386
2091
  answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
1387
2092
  }
2093
+ settleNow();
2094
+ batch = wasBatch;
1388
2095
  }
1389
2096
  const apply = (event) => {
1390
2097
  if (event.type === "answer") {
@@ -1392,11 +2099,19 @@ function build(definition, options, replaying = false) {
1392
2099
  const { at: _at, ...logged } = event;
1393
2100
  answer(event.field, event.value, logged);
1394
2101
  }
1395
- } else if (event.type === "next") engine.next();
1396
- else if (event.type === "back") engine.back();
1397
- else if (event.type === "submit") engine.submit();
2102
+ } else if (event.type === "next") {
2103
+ settleNow();
2104
+ engine.next();
2105
+ } else if (event.type === "back") {
2106
+ settleNow();
2107
+ engine.back();
2108
+ } else if (event.type === "submit") {
2109
+ settleNow();
2110
+ engine.submit();
2111
+ }
1398
2112
  };
1399
2113
  const finish = () => {
2114
+ settleNow();
1400
2115
  batch = false;
1401
2116
  state = { ...state, events: [...state.events] };
1402
2117
  };
@@ -1545,7 +2260,13 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
1545
2260
  return `${left} ${op} ${right}`;
1546
2261
  }
1547
2262
 
2263
+ exports.CALCULATED_TYPE = CALCULATED_TYPE;
2264
+ exports.COMPUTED_FIELD_TYPES = COMPUTED_FIELD_TYPES;
2265
+ exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
1548
2266
  exports.HIGHLIGHT_NOTE_MESSAGE = HIGHLIGHT_NOTE_MESSAGE;
2267
+ exports.MAX_CALCULATED_DECIMALS = MAX_CALCULATED_DECIMALS;
2268
+ exports.MAX_FORMULA_ITEMS = MAX_FORMULA_ITEMS;
2269
+ exports.MAX_FORMULA_LENGTH = MAX_FORMULA_LENGTH;
1549
2270
  exports.MAX_HIDDEN_STRING_LENGTH = MAX_HIDDEN_STRING_LENGTH;
1550
2271
  exports.MAX_HIGHLIGHT_NOTE_LENGTH = MAX_HIGHLIGHT_NOTE_LENGTH;
1551
2272
  exports.MAX_HIGHLIGHT_QUOTE_LENGTH = MAX_HIGHLIGHT_QUOTE_LENGTH;
@@ -1554,33 +2275,47 @@ exports.MAX_TABLE_COLUMNS = MAX_TABLE_COLUMNS;
1554
2275
  exports.MAX_TIMESTAMP_SECONDS = MAX_TIMESTAMP_SECONDS;
1555
2276
  exports.REQUIRED_MESSAGE = REQUIRED_MESSAGE;
1556
2277
  exports.STRUCTURED_FIELD_TYPES = STRUCTURED_FIELD_TYPES;
2278
+ exports.calculatedProps = calculatedProps;
2279
+ exports.calculatedValue = calculatedValue;
1557
2280
  exports.canonicalJson = canonicalJson;
1558
2281
  exports.choiceOptions = choiceOptions;
1559
2282
  exports.coerceHidden = coerceHidden;
1560
2283
  exports.coerceHiddenValue = coerceHiddenValue;
1561
2284
  exports.computePath = computePath;
1562
2285
  exports.createForm = createForm;
2286
+ exports.dateLimits = dateLimits;
1563
2287
  exports.defineForm = defineForm;
1564
2288
  exports.describeCondition = describeCondition;
1565
2289
  exports.evaluate = evaluate;
2290
+ exports.evaluateFormula = evaluateFormula;
1566
2291
  exports.explain = explain;
1567
2292
  exports.f = f;
1568
2293
  exports.fingerprint = fingerprint;
2294
+ exports.formatCalculated = formatCalculated;
1569
2295
  exports.formatTimestamp = formatTimestamp;
2296
+ exports.formulaInput = formulaInput;
2297
+ exports.formulaNumber = formulaNumber;
2298
+ exports.formulaRefs = formulaRefs;
1570
2299
  exports.getFieldType = getFieldType;
1571
2300
  exports.isEmptyValue = isEmptyValue;
1572
2301
  exports.isFormEvent = isFormEvent;
2302
+ exports.isIsoDate = isIsoDate;
1573
2303
  exports.isJson = isJson;
1574
2304
  exports.isSensitive = isSensitive;
1575
2305
  exports.listFieldTypes = listFieldTypes;
1576
2306
  exports.listLimits = listLimits;
1577
2307
  exports.normalizeValue = normalizeValue;
2308
+ exports.numberLimits = numberLimits;
1578
2309
  exports.parseForm = parseForm;
2310
+ exports.parseFormula = parseFormula;
1579
2311
  exports.parseTimestamp = parseTimestamp;
2312
+ exports.ratingDisplay = ratingDisplay;
1580
2313
  exports.recall = recall;
1581
2314
  exports.registerFieldType = registerFieldType;
2315
+ exports.renameFormulaKey = renameFormulaKey;
1582
2316
  exports.replay = replay;
1583
2317
  exports.ruleVisible = ruleVisible;
2318
+ exports.scaleLabels = scaleLabels;
1584
2319
  exports.steps = steps;
1585
2320
  exports.summarizeValue = summarizeValue;
1586
2321
  exports.tableColumns = tableColumns;