@marktiderman/genesis-forms 0.1.1 → 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.
@@ -1,6 +1,6 @@
1
- import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-LsPRTzt3.cjs';
2
- export { d as compactEvents } from './engine-LsPRTzt3.cjs';
3
- import { J as Json, a as FormDefinition, f as FormEvent } from './validate-j8Z188mF.cjs';
1
+ import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-Bj8-mMVb.cjs';
2
+ export { d as compactEvents } from './engine-Bj8-mMVb.cjs';
3
+ import { J as Json, a as FormDefinition, f as FormEvent } from './validate-BbackAjB.cjs';
4
4
 
5
5
  interface CoverageOptions {
6
6
  /** Stop after this many explored states and report `truncated`. Default 5000. */
package/dist/testing.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-Cp5t2_Am.js';
2
- export { d as compactEvents } from './engine-Cp5t2_Am.js';
3
- import { J as Json, a as FormDefinition, f as FormEvent } from './validate-j8Z188mF.js';
1
+ import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-Coc-gQIB.js';
2
+ export { d as compactEvents } from './engine-Coc-gQIB.js';
3
+ import { J as Json, a as FormDefinition, f as FormEvent } from './validate-BbackAjB.js';
4
4
 
5
5
  interface CoverageOptions {
6
6
  /** Stop after this many explored states and report `truncated`. Default 5000. */
package/dist/testing.js CHANGED
@@ -228,6 +228,7 @@ function compactEvents(events) {
228
228
 
229
229
  // src/hidden.ts
230
230
  var MAX_HIDDEN_STRING_LENGTH = 1e3;
231
+ var DECIMAL = /^-?\d+(\.\d+)?$/;
231
232
  function coerceHiddenValue(value, type) {
232
233
  if (typeof value === "string" && value.length > MAX_HIDDEN_STRING_LENGTH) return void 0;
233
234
  if (type === "string") {
@@ -237,7 +238,7 @@ function coerceHiddenValue(value, type) {
237
238
  }
238
239
  if (type === "number") {
239
240
  if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
240
- if (typeof value !== "string" || value.trim() === "") return void 0;
241
+ if (typeof value !== "string" || !DECIMAL.test(value)) return void 0;
241
242
  const n = Number(value);
242
243
  return Number.isFinite(n) ? n : void 0;
243
244
  }
@@ -562,7 +563,7 @@ function build(definition, options, replaying = false) {
562
563
  for (const id of state.path) {
563
564
  const field = fields.get(id);
564
565
  const value = own(state.values, id);
565
- if (field && value !== void 0) out[id] = normalizeValue(value, field);
566
+ if (field && !isEmptyValue(value, field)) out[id] = normalizeValue(value, field);
566
567
  }
567
568
  return out;
568
569
  }
@@ -770,7 +771,30 @@ function coverage(form, opts = {}) {
770
771
  }
771
772
 
772
773
  // src/parse.ts
774
+ function shown(v) {
775
+ let text;
776
+ try {
777
+ text = JSON.stringify(v);
778
+ } catch {
779
+ text = void 0;
780
+ }
781
+ text ??= typeof v === "bigint" ? `${v}n` : typeof v;
782
+ return text.length > 60 ? `${text.slice(0, 57)}...` : text;
783
+ }
773
784
  var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
785
+ var isInt = (v) => typeof v === "number" && Number.isInteger(v);
786
+ var LITERAL_TYPE = {
787
+ "short-text": "string",
788
+ "long-text": "string",
789
+ email: "string",
790
+ phone: "string",
791
+ rating: "number",
792
+ nps: "number",
793
+ "single-choice": "string",
794
+ "multi-choice": "string",
795
+ "yes-no": "boolean"
796
+ };
797
+ var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
774
798
  var OPERATORS = /* @__PURE__ */ new Set([
775
799
  "eq",
776
800
  "neq",
@@ -843,6 +867,7 @@ function parseForm(input) {
843
867
  const keys = /* @__PURE__ */ new Map();
844
868
  const fieldTypes = /* @__PURE__ */ new Map();
845
869
  const order = /* @__PURE__ */ new Map();
870
+ const choices = /* @__PURE__ */ new Map();
846
871
  input.blocks.forEach((block, i) => {
847
872
  const path = `blocks[${i}]`;
848
873
  if (!isObject(block) || typeof block.id !== "string" || !block.id) {
@@ -876,8 +901,19 @@ function parseForm(input) {
876
901
  if (!isObject(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
877
902
  else optionalText(o.label, `${at}.label`);
878
903
  });
904
+ choices.set(
905
+ block.id,
906
+ new Set(options.flatMap((o) => isObject(o) && typeof o.value === "string" ? [o.value] : []))
907
+ );
879
908
  }
880
909
  }
910
+ const { max, maxLength: maxLength2 } = isObject(block.props) ? block.props : {};
911
+ if (block.type === "rating" && max !== void 0 && !(isInt(max) && max >= 2 && max <= 10)) {
912
+ issue(`${path}.props.max`, "Must be a whole number from 2 to 10.");
913
+ }
914
+ if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
915
+ issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
916
+ }
881
917
  }
882
918
  if (validKey) {
883
919
  if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
@@ -894,6 +930,7 @@ function parseForm(input) {
894
930
  }
895
931
  });
896
932
  const hiddenNames = /* @__PURE__ */ new Set();
933
+ const hiddenTypes = /* @__PURE__ */ new Map();
897
934
  if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
898
935
  (Array.isArray(input.hidden) ? input.hidden : []).forEach((h, i) => {
899
936
  if (!isObject(h) || typeof h.name !== "string" || !h.name) {
@@ -909,7 +946,9 @@ function parseForm(input) {
909
946
  issue(`hidden[${i}].type`, "Must be string, number or boolean.");
910
947
  }
911
948
  optionalBoolean(h.sensitive, `hidden[${i}].sensitive`);
949
+ optionalBoolean(h.identifying, `hidden[${i}].identifying`);
912
950
  hiddenNames.add(h.name);
951
+ if (typeof h.type === "string") hiddenTypes.set(h.name, h.type);
913
952
  });
914
953
  const endingIds = /* @__PURE__ */ new Set();
915
954
  if (input.endings !== void 0 && !Array.isArray(input.endings)) issue("endings", "Must be an array.");
@@ -958,8 +997,45 @@ function parseForm(input) {
958
997
  if (typeof c.left.field === "string") {
959
998
  const manifest = getFieldType(fieldTypes.get(c.left.field) ?? "");
960
999
  if (manifest && OPERATORS.has(c.op) && !manifest.operators.includes(c.op)) {
961
- issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
1000
+ return issue(`${path}.op`, `"${c.op}" does not apply to a ${manifest.type} field.`);
1001
+ }
1002
+ }
1003
+ checkLiterals(c.op, c.left, c.right, path);
1004
+ };
1005
+ const checkLiterals = (op, left, right, path) => {
1006
+ if (op === "answered" || op === "empty" || !OPERATORS.has(op)) return;
1007
+ const literal = isObject(right) && "value" in right;
1008
+ if (right !== void 0 && !literal) return;
1009
+ let what = "the left side";
1010
+ let type;
1011
+ let options;
1012
+ if (typeof left.field === "string" && fieldTypes.has(left.field)) {
1013
+ const fieldType = fieldTypes.get(left.field);
1014
+ what = `"${keyOf(left.field)}"`;
1015
+ type = LITERAL_TYPE[fieldType];
1016
+ if (fieldType === "single-choice" || fieldType === "multi-choice") options = choices.get(left.field);
1017
+ } else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
1018
+ what = `hidden input "${left.hidden}"`;
1019
+ type = hiddenTypes.get(left.hidden);
1020
+ }
1021
+ const outcome = op === "neq" ? "always true" : "never true";
1022
+ let values;
1023
+ if (op === "in") {
1024
+ if (!literal || !Array.isArray(right.value)) {
1025
+ return void warnings.push({ path: `${path}.right`, message: "`in` needs a list of values; it never matches." });
1026
+ }
1027
+ values = right.value;
1028
+ } else if (!literal) return;
1029
+ else values = [right.value];
1030
+ if (ORDERING.has(op)) {
1031
+ if (type !== void 0 && type !== "number") {
1032
+ return void warnings.push({ path: `${path}.op`, message: `"${op}" compares numbers, but ${what} holds a ${type}; it is never true.` });
962
1033
  }
1034
+ type = "number";
1035
+ }
1036
+ for (const v of values) {
1037
+ 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;
1038
+ if (message) warnings.push({ path: `${path}.right`, message });
963
1039
  }
964
1040
  };
965
1041
  const keyOf = (id) => [...keys].find(([, v]) => v === id)?.[0] ?? id;
@@ -1036,6 +1112,18 @@ function parseForm(input) {
1036
1112
  }
1037
1113
  }
1038
1114
  });
1115
+ const extra = [["description", input.description]];
1116
+ (Array.isArray(input.endings) ? input.endings : []).forEach((e, i) => {
1117
+ if (isObject(e)) extra.push([`endings[${i}]`, e.title], [`endings[${i}]`, e.description]);
1118
+ });
1119
+ for (const [path, t] of extra) {
1120
+ if (typeof t !== "string") continue;
1121
+ for (const ref of recallRefs(t)) {
1122
+ if (ref.kind === "hidden" ? !hiddenNames.has(ref.name) : !keys.has(ref.name)) {
1123
+ warnings.push({ path, message: `Recalls unknown ${ref.kind === "hidden" ? "hidden input" : "field"} "${ref.name}"; it shows its fallback text.` });
1124
+ }
1125
+ }
1126
+ }
1039
1127
  return issues.length ? fail() : { ok: true, form: input, warnings };
1040
1128
  }
1041
1129
 
@@ -1072,18 +1160,16 @@ function validateSubmission(definition, submission, options = {}) {
1072
1160
  if (invalid) return { ok: false, errors: { "": invalid } };
1073
1161
  const input = wellFormed(submission);
1074
1162
  if (!input) return MALFORMED;
1075
- const ignored = [];
1163
+ const ignored = /* @__PURE__ */ new Set();
1076
1164
  const client = coerceHidden(definition, input.hidden, { sources: ["url", "host"] });
1077
- for (const name of client.ignored) ignored.push(`hidden.${name}`);
1165
+ for (const name of client.ignored) ignored.add(`hidden.${name}`);
1078
1166
  const hidden = client.values;
1079
1167
  const context = coerceHidden(definition, options.context, { sources: ["server"] }).values;
1080
1168
  const fields = fieldMap(definition);
1081
- const dropped = /* @__PURE__ */ new Set();
1082
1169
  const accept = (id) => {
1083
- if (!fields.has(id)) return void ignored.push(id);
1170
+ if (!fields.has(id)) return void ignored.add(id);
1084
1171
  if (!withheld(definition, fields.get(id))) return true;
1085
- if (!dropped.has(id)) ignored.push(id);
1086
- dropped.add(id);
1172
+ ignored.add(id);
1087
1173
  return false;
1088
1174
  };
1089
1175
  const engineHidden = { ...hidden, ...context };
@@ -1136,15 +1222,25 @@ function validateSubmission(definition, submission, options = {}) {
1136
1222
  if (message === null) answers[id] = normalizeValue(value, field);
1137
1223
  }
1138
1224
  }
1225
+ const store = (values, report) => {
1226
+ if (definition.settings?.anonymous !== true) return values;
1227
+ const identifying = new Set((definition.hidden ?? []).filter((h) => h.identifying === true).map((h) => h.name));
1228
+ const kept = {};
1229
+ for (const [name, value] of Object.entries(values)) {
1230
+ if (!identifying.has(name)) kept[name] = value;
1231
+ else if (report) ignored.add(`hidden.${name}`);
1232
+ }
1233
+ return kept;
1234
+ };
1139
1235
  return {
1140
1236
  ok: true,
1141
1237
  submission: {
1142
1238
  status: complete ? "completed" : "draft",
1143
1239
  answers,
1144
- hidden,
1145
- context,
1240
+ hidden: store(hidden, true),
1241
+ context: store(context, false),
1146
1242
  ending: complete ? state.ending : null,
1147
- ignored
1243
+ ignored: [...ignored]
1148
1244
  }
1149
1245
  };
1150
1246
  }