@marktiderman/genesis-forms 0.1.0 → 0.2.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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { F as FieldBlock, C as ContentBlock, P as PageBreakBlock, a as FormDefinition, H as HiddenInput, b as FormLayout, L as LogicRule, E as Ending, c as FormSettings, J as Json, d as Condition, B as Block, O as Operator, e as FieldValidator } from './validate-j8Z188mF.cjs';
2
- export { f as FormEvent, g as Operand, R as REQUIRED_MESSAGE, S as StandardSchemaV1, v as validateField } from './validate-j8Z188mF.cjs';
3
- import { F as FormEngine } from './engine-LsPRTzt3.cjs';
4
- export { C as CreateFormOptions, a as FormState, c as createForm, i as isFormEvent, b as isJson, r as replay } from './engine-LsPRTzt3.cjs';
1
+ import { F as FieldBlock, C as ContentBlock, P as PageBreakBlock, a as FormDefinition, H as HiddenInput, b as FormLayout, L as LogicRule, E as Ending, c as FormSettings, J as Json, d as Condition, B as Block, O as Operator, e as FieldValidator } from './validate-BbackAjB.cjs';
2
+ export { f as FormEvent, g as Operand, R as REQUIRED_MESSAGE, S as StandardSchemaV1, v as validateField } from './validate-BbackAjB.cjs';
3
+ import { F as FormEngine } from './engine-Bj8-mMVb.cjs';
4
+ export { C as CreateFormOptions, a as FormState, c as createForm, i as isFormEvent, b as isJson, r as replay } from './engine-Bj8-mMVb.cjs';
5
5
 
6
6
  /** A field block that remembers its key and value type, for answer inference. */
7
7
  type FieldSpec<K extends string = string, V = unknown> = FieldBlock & {
@@ -11,15 +11,15 @@ type FieldSpec<K extends string = string, V = unknown> = FieldBlock & {
11
11
  };
12
12
  interface FieldOptions {
13
13
  label: string;
14
- description?: string;
15
- placeholder?: string;
16
- required?: boolean;
14
+ description?: string | undefined;
15
+ placeholder?: string | undefined;
16
+ required?: boolean | undefined;
17
17
  /** Personal or secret data (see `isSensitive`). */
18
- sensitive?: boolean;
18
+ sensitive?: boolean | undefined;
19
19
  /** Identifies the respondent (name, email, phone). Not asked when `settings.anonymous` is true. */
20
- identifying?: boolean;
20
+ identifying?: boolean | undefined;
21
21
  /** Defaults to the key. Set it before renaming a key on a live form, so answers stay linked. */
22
- id?: string;
22
+ id?: string | undefined;
23
23
  }
24
24
  interface ChoiceFieldOptions<O extends string> extends FieldOptions {
25
25
  options: readonly {
@@ -28,20 +28,22 @@ interface ChoiceFieldOptions<O extends string> extends FieldOptions {
28
28
  }[];
29
29
  }
30
30
  interface HiddenOptions {
31
- source?: HiddenInput["source"];
32
- sensitive?: boolean;
31
+ source?: HiddenInput["source"] | undefined;
32
+ sensitive?: boolean | undefined;
33
+ /** Identifies the respondent. Not stored when `settings.anonymous` is true. */
34
+ identifying?: boolean | undefined;
33
35
  }
34
36
  declare const f: {
35
37
  shortText: <K extends string>(key: K, o: FieldOptions & {
36
- maxLength?: number;
38
+ maxLength?: number | undefined;
37
39
  }) => FieldSpec<K, string>;
38
40
  longText: <K extends string>(key: K, o: FieldOptions & {
39
- maxLength?: number;
41
+ maxLength?: number | undefined;
40
42
  }) => FieldSpec<K, string>;
41
43
  email: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, string>;
42
44
  phone: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, string>;
43
45
  rating: <K extends string>(key: K, o: FieldOptions & {
44
- max?: number;
46
+ max?: number | undefined;
45
47
  }) => FieldSpec<K, number>;
46
48
  nps: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, number>;
47
49
  singleChoice: <K extends string, O extends string>(key: K, o: ChoiceFieldOptions<O>) => FieldSpec<K, O>;
@@ -56,19 +58,22 @@ declare const f: {
56
58
  pageBreak: (id: string, title?: string) => PageBreakBlock;
57
59
  hidden: {
58
60
  string: (o?: HiddenOptions) => {
59
- sensitive?: true | undefined;
60
61
  type: "string";
61
- source: "url" | "host" | "server";
62
+ source: HiddenInput["source"];
63
+ sensitive?: true;
64
+ identifying?: true;
62
65
  };
63
66
  number: (o?: HiddenOptions) => {
64
- sensitive?: true | undefined;
65
67
  type: "number";
66
- source: "url" | "host" | "server";
68
+ source: HiddenInput["source"];
69
+ sensitive?: true;
70
+ identifying?: true;
67
71
  };
68
72
  boolean: (o?: HiddenOptions) => {
69
- sensitive?: true | undefined;
70
73
  type: "boolean";
71
- source: "url" | "host" | "server";
74
+ source: HiddenInput["source"];
75
+ sensitive?: true;
76
+ identifying?: true;
72
77
  };
73
78
  };
74
79
  };
@@ -111,7 +116,8 @@ declare namespace when {
111
116
  type HiddenSpec = {
112
117
  type: HiddenInput["type"];
113
118
  source: HiddenInput["source"];
114
- sensitive?: boolean;
119
+ sensitive?: boolean | undefined;
120
+ identifying?: boolean | undefined;
115
121
  };
116
122
  type HiddenValue<T extends HiddenSpec["type"]> = T extends "number" ? number : T extends "boolean" ? boolean : string;
117
123
  type AnyBlock = FieldSpec<string, unknown> | ContentBlock | PageBreakBlock;
@@ -187,7 +193,8 @@ interface CoercedHidden {
187
193
  * Convert one raw value to a hidden input's declared type, or `undefined` when
188
194
  * it cannot be. Only string, number and boolean primitives are accepted.
189
195
  * - `string`: a string (at most 1000 characters), or a number or boolean as text.
190
- * - `number`: a finite number, or a non-blank numeric string. `""` is not a number.
196
+ * - `number`: a finite number, or a plain decimal string such as `"12"`, `"-3"`
197
+ * or `"2.5"`. No spaces, hex, exponents or `""`.
191
198
  * - `boolean`: `true`/`false`, or the strings `"true"`/`"false"`.
192
199
  */
193
200
  declare function coerceHiddenValue(value: unknown, type: HiddenInput["type"]): Json | undefined;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { F as FieldBlock, C as ContentBlock, P as PageBreakBlock, a as FormDefinition, H as HiddenInput, b as FormLayout, L as LogicRule, E as Ending, c as FormSettings, J as Json, d as Condition, B as Block, O as Operator, e as FieldValidator } from './validate-j8Z188mF.js';
2
- export { f as FormEvent, g as Operand, R as REQUIRED_MESSAGE, S as StandardSchemaV1, v as validateField } from './validate-j8Z188mF.js';
3
- import { F as FormEngine } from './engine-Cp5t2_Am.js';
4
- export { C as CreateFormOptions, a as FormState, c as createForm, i as isFormEvent, b as isJson, r as replay } from './engine-Cp5t2_Am.js';
1
+ import { F as FieldBlock, C as ContentBlock, P as PageBreakBlock, a as FormDefinition, H as HiddenInput, b as FormLayout, L as LogicRule, E as Ending, c as FormSettings, J as Json, d as Condition, B as Block, O as Operator, e as FieldValidator } from './validate-BbackAjB.js';
2
+ export { f as FormEvent, g as Operand, R as REQUIRED_MESSAGE, S as StandardSchemaV1, v as validateField } from './validate-BbackAjB.js';
3
+ import { F as FormEngine } from './engine-Coc-gQIB.js';
4
+ export { C as CreateFormOptions, a as FormState, c as createForm, i as isFormEvent, b as isJson, r as replay } from './engine-Coc-gQIB.js';
5
5
 
6
6
  /** A field block that remembers its key and value type, for answer inference. */
7
7
  type FieldSpec<K extends string = string, V = unknown> = FieldBlock & {
@@ -11,15 +11,15 @@ type FieldSpec<K extends string = string, V = unknown> = FieldBlock & {
11
11
  };
12
12
  interface FieldOptions {
13
13
  label: string;
14
- description?: string;
15
- placeholder?: string;
16
- required?: boolean;
14
+ description?: string | undefined;
15
+ placeholder?: string | undefined;
16
+ required?: boolean | undefined;
17
17
  /** Personal or secret data (see `isSensitive`). */
18
- sensitive?: boolean;
18
+ sensitive?: boolean | undefined;
19
19
  /** Identifies the respondent (name, email, phone). Not asked when `settings.anonymous` is true. */
20
- identifying?: boolean;
20
+ identifying?: boolean | undefined;
21
21
  /** Defaults to the key. Set it before renaming a key on a live form, so answers stay linked. */
22
- id?: string;
22
+ id?: string | undefined;
23
23
  }
24
24
  interface ChoiceFieldOptions<O extends string> extends FieldOptions {
25
25
  options: readonly {
@@ -28,20 +28,22 @@ interface ChoiceFieldOptions<O extends string> extends FieldOptions {
28
28
  }[];
29
29
  }
30
30
  interface HiddenOptions {
31
- source?: HiddenInput["source"];
32
- sensitive?: boolean;
31
+ source?: HiddenInput["source"] | undefined;
32
+ sensitive?: boolean | undefined;
33
+ /** Identifies the respondent. Not stored when `settings.anonymous` is true. */
34
+ identifying?: boolean | undefined;
33
35
  }
34
36
  declare const f: {
35
37
  shortText: <K extends string>(key: K, o: FieldOptions & {
36
- maxLength?: number;
38
+ maxLength?: number | undefined;
37
39
  }) => FieldSpec<K, string>;
38
40
  longText: <K extends string>(key: K, o: FieldOptions & {
39
- maxLength?: number;
41
+ maxLength?: number | undefined;
40
42
  }) => FieldSpec<K, string>;
41
43
  email: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, string>;
42
44
  phone: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, string>;
43
45
  rating: <K extends string>(key: K, o: FieldOptions & {
44
- max?: number;
46
+ max?: number | undefined;
45
47
  }) => FieldSpec<K, number>;
46
48
  nps: <K extends string>(key: K, o: FieldOptions) => FieldSpec<K, number>;
47
49
  singleChoice: <K extends string, O extends string>(key: K, o: ChoiceFieldOptions<O>) => FieldSpec<K, O>;
@@ -56,19 +58,22 @@ declare const f: {
56
58
  pageBreak: (id: string, title?: string) => PageBreakBlock;
57
59
  hidden: {
58
60
  string: (o?: HiddenOptions) => {
59
- sensitive?: true | undefined;
60
61
  type: "string";
61
- source: "url" | "host" | "server";
62
+ source: HiddenInput["source"];
63
+ sensitive?: true;
64
+ identifying?: true;
62
65
  };
63
66
  number: (o?: HiddenOptions) => {
64
- sensitive?: true | undefined;
65
67
  type: "number";
66
- source: "url" | "host" | "server";
68
+ source: HiddenInput["source"];
69
+ sensitive?: true;
70
+ identifying?: true;
67
71
  };
68
72
  boolean: (o?: HiddenOptions) => {
69
- sensitive?: true | undefined;
70
73
  type: "boolean";
71
- source: "url" | "host" | "server";
74
+ source: HiddenInput["source"];
75
+ sensitive?: true;
76
+ identifying?: true;
72
77
  };
73
78
  };
74
79
  };
@@ -111,7 +116,8 @@ declare namespace when {
111
116
  type HiddenSpec = {
112
117
  type: HiddenInput["type"];
113
118
  source: HiddenInput["source"];
114
- sensitive?: boolean;
119
+ sensitive?: boolean | undefined;
120
+ identifying?: boolean | undefined;
115
121
  };
116
122
  type HiddenValue<T extends HiddenSpec["type"]> = T extends "number" ? number : T extends "boolean" ? boolean : string;
117
123
  type AnyBlock = FieldSpec<string, unknown> | ContentBlock | PageBreakBlock;
@@ -187,7 +193,8 @@ interface CoercedHidden {
187
193
  * Convert one raw value to a hidden input's declared type, or `undefined` when
188
194
  * it cannot be. Only string, number and boolean primitives are accepted.
189
195
  * - `string`: a string (at most 1000 characters), or a number or boolean as text.
190
- * - `number`: a finite number, or a non-blank numeric string. `""` is not a number.
196
+ * - `number`: a finite number, or a plain decimal string such as `"12"`, `"-3"`
197
+ * or `"2.5"`. No spaces, hex, exponents or `""`.
191
198
  * - `boolean`: `true`/`false`, or the strings `"true"`/`"false"`.
192
199
  */
193
200
  declare function coerceHiddenValue(value: unknown, type: HiddenInput["type"]): Json | undefined;
package/dist/index.js CHANGED
@@ -397,7 +397,30 @@ function recallRefs(text) {
397
397
  }
398
398
 
399
399
  // src/parse.ts
400
+ function shown(v) {
401
+ let text;
402
+ try {
403
+ text = JSON.stringify(v);
404
+ } catch {
405
+ text = void 0;
406
+ }
407
+ text ??= typeof v === "bigint" ? `${v}n` : typeof v;
408
+ return text.length > 60 ? `${text.slice(0, 57)}...` : text;
409
+ }
400
410
  var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
411
+ var isInt = (v) => typeof v === "number" && Number.isInteger(v);
412
+ var LITERAL_TYPE = {
413
+ "short-text": "string",
414
+ "long-text": "string",
415
+ email: "string",
416
+ phone: "string",
417
+ rating: "number",
418
+ nps: "number",
419
+ "single-choice": "string",
420
+ "multi-choice": "string",
421
+ "yes-no": "boolean"
422
+ };
423
+ var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
401
424
  var OPERATORS = /* @__PURE__ */ new Set([
402
425
  "eq",
403
426
  "neq",
@@ -470,6 +493,7 @@ function parseForm(input) {
470
493
  const keys = /* @__PURE__ */ new Map();
471
494
  const fieldTypes = /* @__PURE__ */ new Map();
472
495
  const order = /* @__PURE__ */ new Map();
496
+ const choices = /* @__PURE__ */ new Map();
473
497
  input.blocks.forEach((block, i) => {
474
498
  const path = `blocks[${i}]`;
475
499
  if (!isObject(block) || typeof block.id !== "string" || !block.id) {
@@ -503,8 +527,19 @@ function parseForm(input) {
503
527
  if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
504
528
  else optionalText(o.label, `${at}.label`);
505
529
  });
530
+ choices.set(
531
+ block.id,
532
+ new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
533
+ );
506
534
  }
507
535
  }
536
+ const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
537
+ if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
538
+ issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
539
+ }
540
+ if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
541
+ issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
542
+ }
508
543
  }
509
544
  if (validKey) {
510
545
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
@@ -521,6 +556,7 @@ function parseForm(input) {
521
556
  }
522
557
  });
523
558
  const hiddenNames = /* @__PURE__ */ new Set();
559
+ const hiddenTypes = /* @__PURE__ */ new Map();
524
560
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
525
561
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
526
562
  if (!isObject(h) || typeof h.name !== "string" || !h.name) {
@@ -536,7 +572,9 @@ function parseForm(input) {
536
572
  issue(`hidden[${i}].type`, "Must be string, number or boolean.");
537
573
  }
538
574
  optionalBoolean(h.sensitive, `hidden[${i}].sensitive`);
575
+ optionalBoolean(h.identifying, `hidden[${i}].identifying`);
539
576
  hiddenNames.add(h.name);
577
+ if (typeof h.type === "string") hiddenTypes.set(h.name, h.type);
540
578
  });
541
579
  const endingIds = /* @__PURE__ */ new Set();
542
580
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
@@ -585,8 +623,45 @@ function parseForm(input) {
585
623
  if (typeof c.left.field === "string") {
586
624
  const manifest = getFieldType(fieldTypes.get(c.left.field) ?? "");
587
625
  if (manifest && OPERATORS.has(c.op) && !manifest.operators.includes(c.op)) {
588
- issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
626
+ return issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
627
+ }
628
+ }
629
+ checkLiterals(c.op, c.left, c.right, path);
630
+ };
631
+ const checkLiterals = (op, left, right, path) => {
632
+ if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
633
+ const literal = isObject(right) && "value" in right;
634
+ if (right !== void 0 && !literal) return;
635
+ let what = "the left side";
636
+ let type;
637
+ let options;
638
+ if (typeof left.field === "string" && fieldTypes.has(left.field)) {
639
+ const fieldType = fieldTypes.get(left.field);
640
+ what = `"${keyOf(left.field)}"`;
641
+ type = LITERAL_TYPE[fieldType];
642
+ if (fieldType === "single-choice" || fieldType === "multi-choice") options = choices.get(left.field);
643
+ } else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
644
+ what = `hidden input "${left.hidden}"`;
645
+ type = hiddenTypes.get(left.hidden);
646
+ }
647
+ const outcome = op === "neq" ? "always true" : "never true";
648
+ let values;
649
+ if (op === "in") {
650
+ if (!literal || !Array.isArray(right.value)) {
651
+ return void warnings.push({ path: `${path}.right`, message: "`in` needs a list of values; it never matches." });
652
+ }
653
+ values = right.value;
654
+ } else if (!literal) return;
655
+ else values = [right.value];
656
+ if (ORDERING.has(op)) {
657
+ if (type !== void 0 && type !== "number") {
658
+ return void warnings.push({ path: `${path}.op`, message: `"${op}" compares numbers, but ${what} holds a ${type}; it is never true.` });
589
659
  }
660
+ type = "number";
661
+ }
662
+ for (const v of values) {
663
+ const message = type !== void 0 && typeof v !== type ? `${what} holds a ${type}, but ${shown(v)} is not one; this comparison is ${outcome}.` : options && typeof v === "string" && !options.has(v) ? `${shown(v)} is not an option of ${what}; this comparison is ${outcome}.` : null;
664
+ if (message) warnings.push({ path: `${path}.right`, message });
590
665
  }
591
666
  };
592
667
  const keyOf = (id) => [...keys].find(([, v]) => v === id)?.[0] ?? id;
@@ -663,6 +738,18 @@ function parseForm(input) {
663
738
  }
664
739
  }
665
740
  });
741
+ const extra = [["description", input.description]];
742
+ (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
743
+ if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
744
+ });
745
+ for (const [path, t] of extra) {
746
+ if (typeof t !== "string") continue;
747
+ for (const ref of recallRefs(t)) {
748
+ if (ref.kind === "hidden" ? !hiddenNames.has(ref.name) : !keys.has(ref.name)) {
749
+ warnings.push({ path, message: `Recalls unknown ${ref.kind === "hidden" ? "hidden input" : "field"} "${ref.name}"; it shows its fallback text.` });
750
+ }
751
+ }
752
+ }
666
753
  return issues.length ? fail() : { ok: true, form: input, warnings };
667
754
  }
668
755
 
@@ -689,7 +776,8 @@ var choiceProps = (options) => ({
689
776
  var hiddenSpec = (type, o) => ({
690
777
  type,
691
778
  source: o.source ?? "url",
692
- ...o.sensitive ? { sensitive: true } : {}
779
+ ...o.sensitive ? { sensitive: true } : {},
780
+ ...o.identifying ? { identifying: true } : {}
693
781
  });
694
782
  var f = {
695
783
  shortText: (key, o) => field("short-text", key, o, o.maxLength ? { maxLength: o.maxLength } : void 0),
@@ -784,7 +872,8 @@ function defineForm(input) {
784
872
  name,
785
873
  type: spec.type,
786
874
  source: spec.source,
787
- ...spec.sensitive ? { sensitive: true } : {}
875
+ ...spec.sensitive ? { sensitive: true } : {},
876
+ ...spec.identifying ? { identifying: true } : {}
788
877
  }));
789
878
  }
790
879
  if (input.logic?.length) definition.logic = input.logic.map((rule, i) => rule.build(resolve2, `r${i + 1}`));
@@ -809,6 +898,7 @@ ${list}`);
809
898
 
810
899
  // src/hidden.ts
811
900
  var MAX_HIDDEN_STRING_LENGTH = 1e3;
901
+ var DECIMAL = /^-?\d+(\.\d+)?$/;
812
902
  function coerceHiddenValue(value, type) {
813
903
  if (typeof value === "string" && value.length > MAX_HIDDEN_STRING_LENGTH) return void 0;
814
904
  if (type === "string") {
@@ -818,7 +908,7 @@ function coerceHiddenValue(value, type) {
818
908
  }
819
909
  if (type === "number") {
820
910
  if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
821
- if (typeof value !== "string" || value.trim() === "") return void 0;
911
+ if (typeof value !== "string" || !DECIMAL.test(value)) return void 0;
822
912
  const n = Number(value);
823
913
  return Number.isFinite(n) ? n : void 0;
824
914
  }
@@ -999,7 +1089,7 @@ function build(definition, options, replaying = false) {
999
1089
  for (const id of state.path) {
1000
1090
  const field2 = fields.get(id);
1001
1091
  const value = own(state.values, id);
1002
- if (field2 && value !== void 0) out[id] = normalizeValue(value, field2);
1092
+ if (field2 && !isEmptyValue(value, field2)) out[id] = normalizeValue(value, field2);
1003
1093
  }
1004
1094
  return out;
1005
1095
  }