payload-dynamic-forms 0.0.0-stage → 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +404 -2
  3. package/dist/access.d.ts +7 -0
  4. package/dist/access.js +10 -0
  5. package/dist/access.js.map +1 -0
  6. package/dist/collections/attachments.d.ts +6 -0
  7. package/dist/collections/attachments.js +67 -0
  8. package/dist/collections/attachments.js.map +1 -0
  9. package/dist/collections/blocks.d.ts +27 -0
  10. package/dist/collections/blocks.js +495 -0
  11. package/dist/collections/blocks.js.map +1 -0
  12. package/dist/collections/forms.d.ts +3 -0
  13. package/dist/collections/forms.js +553 -0
  14. package/dist/collections/forms.js.map +1 -0
  15. package/dist/collections/overrides.d.ts +7 -0
  16. package/dist/collections/overrides.js +49 -0
  17. package/dist/collections/overrides.js.map +1 -0
  18. package/dist/collections/ruleFields.d.ts +6 -0
  19. package/dist/collections/ruleFields.js +97 -0
  20. package/dist/collections/ruleFields.js.map +1 -0
  21. package/dist/collections/submissions.d.ts +7 -0
  22. package/dist/collections/submissions.js +225 -0
  23. package/dist/collections/submissions.js.map +1 -0
  24. package/dist/delivery/run.d.ts +36 -0
  25. package/dist/delivery/run.js +187 -0
  26. package/dist/delivery/run.js.map +1 -0
  27. package/dist/delivery/webhook.d.ts +26 -0
  28. package/dist/delivery/webhook.js +71 -0
  29. package/dist/delivery/webhook.js.map +1 -0
  30. package/dist/endpoints/public.d.ts +4 -0
  31. package/dist/endpoints/public.js +27 -0
  32. package/dist/endpoints/public.js.map +1 -0
  33. package/dist/endpoints/resend.d.ts +4 -0
  34. package/dist/endpoints/resend.js +58 -0
  35. package/dist/endpoints/resend.js.map +1 -0
  36. package/dist/endpoints/submit.d.ts +4 -0
  37. package/dist/endpoints/submit.js +320 -0
  38. package/dist/endpoints/submit.js.map +1 -0
  39. package/dist/exports/shared.d.ts +13 -0
  40. package/dist/exports/shared.js +13 -0
  41. package/dist/exports/shared.js.map +1 -0
  42. package/dist/formBlock.d.ts +13 -0
  43. package/dist/formBlock.js +25 -0
  44. package/dist/formBlock.js.map +1 -0
  45. package/dist/hooks/resendTick.d.ts +8 -0
  46. package/dist/hooks/resendTick.js +55 -0
  47. package/dist/hooks/resendTick.js.map +1 -0
  48. package/dist/hooks/validateForm.d.ts +8 -0
  49. package/dist/hooks/validateForm.js +251 -0
  50. package/dist/hooks/validateForm.js.map +1 -0
  51. package/dist/index.d.ts +18 -0
  52. package/dist/index.js +124 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/security/crypto.d.ts +2 -0
  55. package/dist/security/crypto.js +17 -0
  56. package/dist/security/crypto.js.map +1 -0
  57. package/dist/security/rateLimit.d.ts +13 -0
  58. package/dist/security/rateLimit.js +28 -0
  59. package/dist/security/rateLimit.js.map +1 -0
  60. package/dist/security/request.d.ts +3 -0
  61. package/dist/security/request.js +13 -0
  62. package/dist/security/request.js.map +1 -0
  63. package/dist/security/turnstile.d.ts +28 -0
  64. package/dist/security/turnstile.js +56 -0
  65. package/dist/security/turnstile.js.map +1 -0
  66. package/dist/server/forms.d.ts +73 -0
  67. package/dist/server/forms.js +215 -0
  68. package/dist/server/forms.js.map +1 -0
  69. package/dist/server/resend.d.ts +27 -0
  70. package/dist/server/resend.js +84 -0
  71. package/dist/server/resend.js.map +1 -0
  72. package/dist/shared/answers.d.ts +47 -0
  73. package/dist/shared/answers.js +282 -0
  74. package/dist/shared/answers.js.map +1 -0
  75. package/dist/shared/operators.d.ts +41 -0
  76. package/dist/shared/operators.js +54 -0
  77. package/dist/shared/operators.js.map +1 -0
  78. package/dist/shared/rules.d.ts +58 -0
  79. package/dist/shared/rules.js +324 -0
  80. package/dist/shared/rules.js.map +1 -0
  81. package/dist/shared/steps.d.ts +28 -0
  82. package/dist/shared/steps.js +109 -0
  83. package/dist/shared/steps.js.map +1 -0
  84. package/dist/shared/tokens.d.ts +25 -0
  85. package/dist/shared/tokens.js +96 -0
  86. package/dist/shared/tokens.js.map +1 -0
  87. package/dist/shared/types.d.ts +181 -0
  88. package/dist/shared/types.js +5 -0
  89. package/dist/shared/types.js.map +1 -0
  90. package/dist/shared/whatsapp.d.ts +20 -0
  91. package/dist/shared/whatsapp.js +26 -0
  92. package/dist/shared/whatsapp.js.map +1 -0
  93. package/dist/shared/words.d.ts +9 -0
  94. package/dist/shared/words.js +41 -0
  95. package/dist/shared/words.js.map +1 -0
  96. package/dist/types.d.ts +133 -0
  97. package/dist/types.js +3 -0
  98. package/dist/types.js.map +1 -0
  99. package/package.json +113 -4
@@ -0,0 +1,324 @@
1
+ /** Lower-case kebab-case: ` Project Type ` → `project-type`. */ export function normalizeKey(value) {
2
+ if (typeof value !== 'string') {
3
+ return '';
4
+ }
5
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
6
+ }
7
+ /**
8
+ * Splits a rule key into the field key and an optional option attribute:
9
+ * `Need.fieldSet` → `{ key: 'need', attribute: 'fieldSet' }`. The attribute keeps its case.
10
+ */ export function splitRuleKey(value) {
11
+ if (typeof value !== 'string') {
12
+ return {
13
+ key: ''
14
+ };
15
+ }
16
+ const trimmed = value.trim();
17
+ const dot = trimmed.indexOf('.');
18
+ if (dot === -1) {
19
+ return {
20
+ key: normalizeKey(trimmed)
21
+ };
22
+ }
23
+ const attribute = trimmed.slice(dot + 1).trim();
24
+ return {
25
+ attribute: attribute || undefined,
26
+ key: normalizeKey(trimmed.slice(0, dot))
27
+ };
28
+ }
29
+ export const DISPLAY_BLOCK_TYPES = [
30
+ 'heading',
31
+ 'helpText'
32
+ ];
33
+ export const CHOICE_BLOCK_TYPES = [
34
+ 'select',
35
+ 'radio',
36
+ 'checkboxGroup'
37
+ ];
38
+ export function isInputBlock(block) {
39
+ return Boolean(block?.blockType) && !DISPLAY_BLOCK_TYPES.includes(block.blockType);
40
+ }
41
+ export function isChoiceBlock(block) {
42
+ return Boolean(block?.blockType) && CHOICE_BLOCK_TYPES.includes(block.blockType);
43
+ }
44
+ export function getFieldKey(block) {
45
+ return isInputBlock(block) ? normalizeKey(block.fieldKey) : '';
46
+ }
47
+ export function getSectionKey(section) {
48
+ return normalizeKey(section?.key);
49
+ }
50
+ export function getSections(form) {
51
+ return (form.sections || []).filter(Boolean);
52
+ }
53
+ /** Every input of the form in order, with its normalised key. */ export function getInputFields(form) {
54
+ return getSections(form).flatMap((section)=>(section.fields || []).filter(isInputBlock).map((field)=>({
55
+ field,
56
+ key: getFieldKey(field),
57
+ section
58
+ })).filter((entry)=>entry.key));
59
+ }
60
+ export function getInputFieldMap(form) {
61
+ const map = new Map();
62
+ for (const entry of getInputFields(form)){
63
+ if (!map.has(entry.key)) {
64
+ map.set(entry.key, entry);
65
+ }
66
+ }
67
+ return map;
68
+ }
69
+ export function getFieldLabel(field, key = getFieldKey(field)) {
70
+ return field.words?.label?.trim() || key;
71
+ }
72
+ export function getRowLabel(field, key = getFieldKey(field)) {
73
+ return field.words?.rowLabel?.trim() || getFieldLabel(field, key);
74
+ }
75
+ function isBlank(value) {
76
+ if (value === null || value === undefined) {
77
+ return true;
78
+ }
79
+ if (typeof value === 'string') {
80
+ return value.trim().length === 0;
81
+ }
82
+ if (Array.isArray(value)) {
83
+ return value.length === 0;
84
+ }
85
+ if (typeof value === 'object') {
86
+ const v = value;
87
+ if (Array.isArray(v.filenames)) {
88
+ return v.filenames.length === 0;
89
+ }
90
+ return Object.keys(v).length === 0;
91
+ }
92
+ return false;
93
+ }
94
+ /** True when an answer counts as given: `false` and empty strings, lists and objects don't. */ export function hasAnswer(value) {
95
+ if (typeof value === 'boolean') {
96
+ return value;
97
+ }
98
+ return !isBlank(value);
99
+ }
100
+ export function toStringList(value) {
101
+ if (Array.isArray(value)) {
102
+ return value.map((entry)=>String(entry ?? '').trim()).filter(Boolean);
103
+ }
104
+ if (typeof value === 'string') {
105
+ return value.split(',').map((entry)=>entry.trim()).filter(Boolean);
106
+ }
107
+ if (value === null || value === undefined || value === false) {
108
+ return [];
109
+ }
110
+ return [
111
+ String(value).trim()
112
+ ].filter(Boolean);
113
+ }
114
+ function asText(value) {
115
+ if (Array.isArray(value)) {
116
+ return value.map((v)=>String(v ?? '')).join(', ');
117
+ }
118
+ if (value === null || value === undefined) {
119
+ return '';
120
+ }
121
+ return String(value);
122
+ }
123
+ function compareNumberOrDate(left, right) {
124
+ const l = typeof left === 'number' ? left : Number(String(left ?? '').trim());
125
+ const r = typeof right === 'number' ? right : Number(String(right ?? '').trim());
126
+ const leftText = String(left ?? '').trim();
127
+ const rightText = String(right ?? '').trim();
128
+ if (leftText && rightText && Number.isFinite(l) && Number.isFinite(r)) {
129
+ return l - r;
130
+ }
131
+ const ld = Date.parse(leftText);
132
+ const rd = Date.parse(rightText);
133
+ if (!Number.isNaN(ld) && !Number.isNaN(rd)) {
134
+ return ld - rd;
135
+ }
136
+ return null;
137
+ }
138
+ /** The options a choice field offers (typed, or resolved from a source by the server). */ export function getOptions(field) {
139
+ return (field?.options || []).filter((o)=>Boolean(o && o.value != null));
140
+ }
141
+ /**
142
+ * The value a rule row reads: the answer, or for `<key>.<attribute>` the attribute of the
143
+ * chosen option(s) of that field.
144
+ */ export function readRuleValue(ruleKey, answers, form) {
145
+ const { attribute, key } = splitRuleKey(ruleKey);
146
+ if (!key) {
147
+ return undefined;
148
+ }
149
+ const answer = answers[key];
150
+ if (!attribute) {
151
+ return answer;
152
+ }
153
+ if (!form) {
154
+ return undefined;
155
+ }
156
+ const entry = getInputFieldMap(form).get(key);
157
+ const options = getOptions(entry?.field);
158
+ const chosen = toStringList(answer);
159
+ const values = chosen.map((value)=>options.find((o)=>String(o.value) === value)?.attributes?.[attribute]).filter((v)=>v !== undefined && v !== null);
160
+ if (Array.isArray(answer)) {
161
+ return values.map((v)=>String(v));
162
+ }
163
+ return values[0];
164
+ }
165
+ export function evaluateRow(row, answers, form) {
166
+ const operator = row?.operator;
167
+ if (!row || !operator || !splitRuleKey(row.fieldKey).key) {
168
+ return true;
169
+ }
170
+ const answer = readRuleValue(row.fieldKey, answers, form);
171
+ const expected = String(row.value ?? '').trim();
172
+ switch(operator){
173
+ case 'contains':
174
+ return asText(answer).toLowerCase().includes(expected.toLowerCase());
175
+ case 'equals':
176
+ return asText(answer).trim() === expected;
177
+ case 'greaterThan':
178
+ {
179
+ const c = compareNumberOrDate(answer, expected);
180
+ return c !== null && c > 0;
181
+ }
182
+ case 'hasValue':
183
+ return hasAnswer(answer);
184
+ case 'includesOption':
185
+ {
186
+ const haystack = toStringList(answer);
187
+ return toStringList(expected).some((needle)=>haystack.includes(needle));
188
+ }
189
+ case 'lessThan':
190
+ {
191
+ const c = compareNumberOrDate(answer, expected);
192
+ return c !== null && c < 0;
193
+ }
194
+ case 'notContains':
195
+ return !asText(answer).toLowerCase().includes(expected.toLowerCase());
196
+ case 'notEquals':
197
+ return asText(answer).trim() !== expected;
198
+ case 'notHasValue':
199
+ return !hasAnswer(answer);
200
+ default:
201
+ return true;
202
+ }
203
+ }
204
+ /** A group matches when all rows match (`all`, default) or any row does (`any`); empty matches. */ export function evaluateRules(group, answers, form) {
205
+ const rows = (group?.rows || []).filter(Boolean);
206
+ if (!rows.length) {
207
+ return true;
208
+ }
209
+ if (group?.match === 'any') {
210
+ return rows.some((row)=>evaluateRow(row, answers, form));
211
+ }
212
+ return rows.every((row)=>evaluateRow(row, answers, form));
213
+ }
214
+ export function isSectionVisible(section, answers, form) {
215
+ return evaluateRules(section.visibilityRules, answers, form);
216
+ }
217
+ /** A field is shown when its section is shown and its own rules match. */ export function isFieldVisible(section, field, answers, form) {
218
+ if (!isSectionVisible(section, answers, form)) {
219
+ return false;
220
+ }
221
+ if (!isInputBlock(field)) {
222
+ return true;
223
+ }
224
+ return evaluateRules(field.visibilityRules, answers, form);
225
+ }
226
+ export function getVisibleSections(form, answers) {
227
+ return getSections(form).filter((section)=>isSectionVisible(section, answers, form));
228
+ }
229
+ function readParam(params, name) {
230
+ if (!params || !name) {
231
+ return [];
232
+ }
233
+ if (typeof params === 'string') {
234
+ return new URLSearchParams(params).getAll(name);
235
+ }
236
+ if (typeof params.get === 'function') {
237
+ const p = params;
238
+ if (typeof p.getAll === 'function') {
239
+ return p.getAll(name);
240
+ }
241
+ const v = p.get(name);
242
+ return v === null ? [] : [
243
+ v
244
+ ];
245
+ }
246
+ const v = params[name];
247
+ if (v === undefined) {
248
+ return [];
249
+ }
250
+ return Array.isArray(v) ? v : [
251
+ v
252
+ ];
253
+ }
254
+ /** The starting answer of a field from its default value. */ export function defaultAnswer(field) {
255
+ const d = field.defaultValue;
256
+ if (field.blockType === 'checkbox') {
257
+ return d === true || [
258
+ '1',
259
+ 'on',
260
+ 'true',
261
+ 'yes'
262
+ ].includes(String(d ?? '').toLowerCase());
263
+ }
264
+ if (field.blockType === 'checkboxGroup') {
265
+ return toStringList(d);
266
+ }
267
+ if (field.blockType === 'fileUpload') {
268
+ return '';
269
+ }
270
+ if (d === null || d === undefined) {
271
+ return '';
272
+ }
273
+ if (typeof d === 'number' || typeof d === 'boolean') {
274
+ return d;
275
+ }
276
+ return String(d);
277
+ }
278
+ /**
279
+ * Starting answers for a renderer: each input's default, or the value of its link parameter
280
+ * when the app passes the page's search parameters. Choice fields take the parameter only when
281
+ * it is one of their options.
282
+ */ export function initialAnswers(form, searchParams) {
283
+ const answers = {};
284
+ for (const { field, key } of getInputFields(form)){
285
+ answers[key] = defaultAnswer(field);
286
+ const param = field.prefillParam?.trim();
287
+ if (!param || field.blockType === 'fileUpload') {
288
+ continue;
289
+ }
290
+ const raw = readParam(searchParams, param).flatMap((v)=>field.blockType === 'checkboxGroup' ? toStringList(v) : [
291
+ v.trim()
292
+ ]).filter(Boolean);
293
+ if (!raw.length) {
294
+ continue;
295
+ }
296
+ if (isChoiceBlock(field)) {
297
+ const listed = new Set(getOptions(field).map((o)=>String(o.value)));
298
+ const ok = raw.filter((v)=>listed.has(v));
299
+ if (!ok.length) {
300
+ continue;
301
+ }
302
+ answers[key] = field.blockType === 'checkboxGroup' ? ok : ok[0];
303
+ continue;
304
+ }
305
+ if (field.blockType === 'checkbox') {
306
+ answers[key] = [
307
+ '1',
308
+ 'on',
309
+ 'true',
310
+ 'yes'
311
+ ].includes(raw[0].toLowerCase());
312
+ continue;
313
+ }
314
+ if (field.blockType === 'number') {
315
+ const n = Number(raw[0]);
316
+ answers[key] = Number.isFinite(n) ? n : raw[0];
317
+ continue;
318
+ }
319
+ answers[key] = raw[0];
320
+ }
321
+ return answers;
322
+ }
323
+
324
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shared/rules.ts"],"sourcesContent":["import type {\n Answers,\n AnswerValue,\n ChoiceOption,\n FieldBlock,\n FormLike,\n FormSection,\n InputBlock,\n RuleGroup,\n RuleRow,\n} from './types.js'\n\n/** Lower-case kebab-case: ` Project Type ` → `project-type`. */\nexport function normalizeKey(value: unknown): string {\n if (typeof value !== 'string') {\n return ''\n }\n return value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n}\n\n/**\n * Splits a rule key into the field key and an optional option attribute:\n * `Need.fieldSet` → `{ key: 'need', attribute: 'fieldSet' }`. The attribute keeps its case.\n */\nexport function splitRuleKey(value: unknown): { attribute?: string; key: string } {\n if (typeof value !== 'string') {\n return { key: '' }\n }\n const trimmed = value.trim()\n const dot = trimmed.indexOf('.')\n if (dot === -1) {\n return { key: normalizeKey(trimmed) }\n }\n const attribute = trimmed.slice(dot + 1).trim()\n return { attribute: attribute || undefined, key: normalizeKey(trimmed.slice(0, dot)) }\n}\n\nexport const DISPLAY_BLOCK_TYPES: readonly string[] = ['heading', 'helpText']\nexport const CHOICE_BLOCK_TYPES: readonly string[] = ['select', 'radio', 'checkboxGroup']\n\nexport function isInputBlock(block: FieldBlock | null | undefined): block is InputBlock {\n return Boolean(block?.blockType) && !DISPLAY_BLOCK_TYPES.includes(block!.blockType)\n}\n\nexport function isChoiceBlock(block: FieldBlock | null | undefined): boolean {\n return Boolean(block?.blockType) && CHOICE_BLOCK_TYPES.includes(block!.blockType)\n}\n\nexport function getFieldKey(block: FieldBlock | null | undefined): string {\n return isInputBlock(block) ? normalizeKey(block.fieldKey) : ''\n}\n\nexport function getSectionKey(section: FormSection | null | undefined): string {\n return normalizeKey(section?.key)\n}\n\nexport function getSections(form: FormLike): FormSection[] {\n return (form.sections || []).filter(Boolean)\n}\n\nexport type InputEntry = { field: InputBlock; key: string; section: FormSection }\n\n/** Every input of the form in order, with its normalised key. */\nexport function getInputFields(form: FormLike): InputEntry[] {\n return getSections(form).flatMap((section) =>\n (section.fields || [])\n .filter(isInputBlock)\n .map((field) => ({ field, key: getFieldKey(field), section }))\n .filter((entry) => entry.key),\n )\n}\n\nexport function getInputFieldMap(form: FormLike): Map<string, InputEntry> {\n const map = new Map<string, InputEntry>()\n for (const entry of getInputFields(form)) {\n if (!map.has(entry.key)) {\n map.set(entry.key, entry)\n }\n }\n return map\n}\n\nexport function getFieldLabel(field: InputBlock, key = getFieldKey(field)): string {\n return field.words?.label?.trim() || key\n}\n\nexport function getRowLabel(field: InputBlock, key = getFieldKey(field)): string {\n return field.words?.rowLabel?.trim() || getFieldLabel(field, key)\n}\n\nfunction isBlank(value: unknown): boolean {\n if (value === null || value === undefined) {\n return true\n }\n if (typeof value === 'string') {\n return value.trim().length === 0\n }\n if (Array.isArray(value)) {\n return value.length === 0\n }\n if (typeof value === 'object') {\n const v = value as Record<string, unknown>\n if (Array.isArray(v.filenames)) {\n return v.filenames.length === 0\n }\n return Object.keys(v).length === 0\n }\n return false\n}\n\n/** True when an answer counts as given: `false` and empty strings, lists and objects don't. */\nexport function hasAnswer(value: unknown): boolean {\n if (typeof value === 'boolean') {\n return value\n }\n return !isBlank(value)\n}\n\nexport function toStringList(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry ?? '').trim()).filter(Boolean)\n }\n if (typeof value === 'string') {\n return value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n }\n if (value === null || value === undefined || value === false) {\n return []\n }\n return [String(value).trim()].filter(Boolean)\n}\n\nfunction asText(value: unknown): string {\n if (Array.isArray(value)) {\n return value.map((v) => String(v ?? '')).join(', ')\n }\n if (value === null || value === undefined) {\n return ''\n }\n return String(value)\n}\n\nfunction compareNumberOrDate(left: unknown, right: unknown): null | number {\n const l = typeof left === 'number' ? left : Number(String(left ?? '').trim())\n const r = typeof right === 'number' ? right : Number(String(right ?? '').trim())\n const leftText = String(left ?? '').trim()\n const rightText = String(right ?? '').trim()\n if (leftText && rightText && Number.isFinite(l) && Number.isFinite(r)) {\n return l - r\n }\n const ld = Date.parse(leftText)\n const rd = Date.parse(rightText)\n if (!Number.isNaN(ld) && !Number.isNaN(rd)) {\n return ld - rd\n }\n return null\n}\n\n/** The options a choice field offers (typed, or resolved from a source by the server). */\nexport function getOptions(field: InputBlock | undefined): ChoiceOption[] {\n return (field?.options || []).filter((o): o is ChoiceOption => Boolean(o && o.value != null))\n}\n\n/**\n * The value a rule row reads: the answer, or for `<key>.<attribute>` the attribute of the\n * chosen option(s) of that field.\n */\nexport function readRuleValue(ruleKey: unknown, answers: Answers, form?: FormLike): unknown {\n const { attribute, key } = splitRuleKey(ruleKey)\n if (!key) {\n return undefined\n }\n const answer = answers[key]\n if (!attribute) {\n return answer\n }\n if (!form) {\n return undefined\n }\n const entry = getInputFieldMap(form).get(key)\n const options = getOptions(entry?.field)\n const chosen = toStringList(answer)\n const values = chosen\n .map((value) => options.find((o) => String(o.value) === value)?.attributes?.[attribute])\n .filter((v) => v !== undefined && v !== null)\n if (Array.isArray(answer)) {\n return values.map((v) => String(v))\n }\n return values[0]\n}\n\nexport function evaluateRow(row: null | RuleRow | undefined, answers: Answers, form?: FormLike): boolean {\n const operator = row?.operator\n if (!row || !operator || !splitRuleKey(row.fieldKey).key) {\n return true\n }\n const answer = readRuleValue(row.fieldKey, answers, form)\n const expected = String(row.value ?? '').trim()\n\n switch (operator) {\n case 'contains':\n return asText(answer).toLowerCase().includes(expected.toLowerCase())\n case 'equals':\n return asText(answer).trim() === expected\n case 'greaterThan': {\n const c = compareNumberOrDate(answer, expected)\n return c !== null && c > 0\n }\n case 'hasValue':\n return hasAnswer(answer)\n case 'includesOption': {\n const haystack = toStringList(answer)\n return toStringList(expected).some((needle) => haystack.includes(needle))\n }\n case 'lessThan': {\n const c = compareNumberOrDate(answer, expected)\n return c !== null && c < 0\n }\n case 'notContains':\n return !asText(answer).toLowerCase().includes(expected.toLowerCase())\n case 'notEquals':\n return asText(answer).trim() !== expected\n case 'notHasValue':\n return !hasAnswer(answer)\n default:\n return true\n }\n}\n\n/** A group matches when all rows match (`all`, default) or any row does (`any`); empty matches. */\nexport function evaluateRules(\n group: null | RuleGroup | undefined,\n answers: Answers,\n form?: FormLike,\n): boolean {\n const rows = (group?.rows || []).filter(Boolean)\n if (!rows.length) {\n return true\n }\n if (group?.match === 'any') {\n return rows.some((row) => evaluateRow(row, answers, form))\n }\n return rows.every((row) => evaluateRow(row, answers, form))\n}\n\nexport function isSectionVisible(section: FormSection, answers: Answers, form?: FormLike): boolean {\n return evaluateRules(section.visibilityRules, answers, form)\n}\n\n/** A field is shown when its section is shown and its own rules match. */\nexport function isFieldVisible(\n section: FormSection,\n field: FieldBlock,\n answers: Answers,\n form?: FormLike,\n): boolean {\n if (!isSectionVisible(section, answers, form)) {\n return false\n }\n if (!isInputBlock(field)) {\n return true\n }\n return evaluateRules(field.visibilityRules, answers, form)\n}\n\nexport function getVisibleSections(form: FormLike, answers: Answers): FormSection[] {\n return getSections(form).filter((section) => isSectionVisible(section, answers, form))\n}\n\ntype SearchParamsLike =\n | { get(name: string): null | string; getAll?(name: string): string[] }\n | Record<string, string | string[] | undefined>\n | string\n | URLSearchParams\n\nfunction readParam(params: SearchParamsLike | undefined, name: string): string[] {\n if (!params || !name) {\n return []\n }\n if (typeof params === 'string') {\n return new URLSearchParams(params).getAll(name)\n }\n if (typeof (params as URLSearchParams).get === 'function') {\n const p = params as URLSearchParams\n if (typeof p.getAll === 'function') {\n return p.getAll(name)\n }\n const v = p.get(name)\n return v === null ? [] : [v]\n }\n const v = (params as Record<string, string | string[] | undefined>)[name]\n if (v === undefined) {\n return []\n }\n return Array.isArray(v) ? v : [v]\n}\n\n/** The starting answer of a field from its default value. */\nexport function defaultAnswer(field: InputBlock): AnswerValue {\n const d = field.defaultValue\n if (field.blockType === 'checkbox') {\n return d === true || ['1', 'on', 'true', 'yes'].includes(String(d ?? '').toLowerCase())\n }\n if (field.blockType === 'checkboxGroup') {\n return toStringList(d)\n }\n if (field.blockType === 'fileUpload') {\n return ''\n }\n if (d === null || d === undefined) {\n return ''\n }\n if (typeof d === 'number' || typeof d === 'boolean') {\n return d\n }\n return String(d)\n}\n\n/**\n * Starting answers for a renderer: each input's default, or the value of its link parameter\n * when the app passes the page's search parameters. Choice fields take the parameter only when\n * it is one of their options.\n */\nexport function initialAnswers(form: FormLike, searchParams?: SearchParamsLike): Answers {\n const answers: Answers = {}\n for (const { field, key } of getInputFields(form)) {\n answers[key] = defaultAnswer(field)\n const param = field.prefillParam?.trim()\n if (!param || field.blockType === 'fileUpload') {\n continue\n }\n const raw = readParam(searchParams, param)\n .flatMap((v) => (field.blockType === 'checkboxGroup' ? toStringList(v) : [v.trim()]))\n .filter(Boolean)\n if (!raw.length) {\n continue\n }\n if (isChoiceBlock(field)) {\n const listed = new Set(getOptions(field).map((o) => String(o.value)))\n const ok = raw.filter((v) => listed.has(v))\n if (!ok.length) {\n continue\n }\n answers[key] = field.blockType === 'checkboxGroup' ? ok : ok[0]\n continue\n }\n if (field.blockType === 'checkbox') {\n answers[key] = ['1', 'on', 'true', 'yes'].includes(raw[0].toLowerCase())\n continue\n }\n if (field.blockType === 'number') {\n const n = Number(raw[0])\n answers[key] = Number.isFinite(n) ? n : raw[0]\n continue\n }\n answers[key] = raw[0]!\n }\n return answers\n}\n"],"names":["normalizeKey","value","trim","toLowerCase","replace","splitRuleKey","key","trimmed","dot","indexOf","attribute","slice","undefined","DISPLAY_BLOCK_TYPES","CHOICE_BLOCK_TYPES","isInputBlock","block","Boolean","blockType","includes","isChoiceBlock","getFieldKey","fieldKey","getSectionKey","section","getSections","form","sections","filter","getInputFields","flatMap","fields","map","field","entry","getInputFieldMap","Map","has","set","getFieldLabel","words","label","getRowLabel","rowLabel","isBlank","length","Array","isArray","v","filenames","Object","keys","hasAnswer","toStringList","String","split","asText","join","compareNumberOrDate","left","right","l","Number","r","leftText","rightText","isFinite","ld","Date","parse","rd","isNaN","getOptions","options","o","readRuleValue","ruleKey","answers","answer","get","chosen","values","find","attributes","evaluateRow","row","operator","expected","c","haystack","some","needle","evaluateRules","group","rows","match","every","isSectionVisible","visibilityRules","isFieldVisible","getVisibleSections","readParam","params","name","URLSearchParams","getAll","p","defaultAnswer","d","defaultValue","initialAnswers","searchParams","param","prefillParam","raw","listed","Set","ok","n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { Answers, FormLike, FormWords } from './types.js';
2
+ /**
3
+ * The sections a visitor goes through. Step-by-step forms start at the first visible section;
4
+ * from each section the first matching next-step rule that targets a visible section wins, else
5
+ * the next visible section. Single-page forms: every visible section.
6
+ */
7
+ export declare function getStepPath(form: FormLike, answers: Answers): string[];
8
+ /** The section after `currentKey` on the path, or null at the end. */
9
+ export declare function getNextSectionKey(form: FormLike, currentKey: string, answers: Answers): null | string;
10
+ /** Where a visitor is on the path: `current` of `total`, and the form's step progress words. */
11
+ export declare function getStepProgress(form: FormLike, answers: Answers, currentKey: string, words?: Partial<FormWords>): {
12
+ current: number;
13
+ text: string;
14
+ total: number;
15
+ };
16
+ export type Visibility = {
17
+ /** Answers of the shown fields only. */
18
+ answers: Answers;
19
+ /** Keys of the inputs that are shown (and, in steps, on the path). */
20
+ fieldKeys: Set<string>;
21
+ /** Keys of the sections that count: visible ones, or in steps the ones on the path. */
22
+ sectionKeys: string[];
23
+ };
24
+ /**
25
+ * What is shown for these answers. Hidden answers are dropped and the rules are read again until
26
+ * nothing changes, so an answer left in a hidden field never decides what else is shown.
27
+ */
28
+ export declare function getVisibility(form: FormLike, answers: Answers): Visibility;
@@ -0,0 +1,109 @@
1
+ import { evaluateRules, getInputFields, getSectionKey, getVisibleSections, isSectionVisible, normalizeKey } from './rules.js';
2
+ import { interpolate } from './tokens.js';
3
+ import { DEFAULT_WORDS } from './words.js';
4
+ function nextFrom(form, visible, index, answers) {
5
+ const section = visible[index];
6
+ for (const rule of (section.nextStepRules || []).filter(Boolean)){
7
+ if (!evaluateRules(rule, answers, form)) {
8
+ continue;
9
+ }
10
+ const target = normalizeKey(rule.targetSectionKey);
11
+ const targetIndex = visible.findIndex((s)=>getSectionKey(s) === target);
12
+ if (target && targetIndex !== -1) {
13
+ return targetIndex;
14
+ }
15
+ }
16
+ return index + 1;
17
+ }
18
+ /**
19
+ * The sections a visitor goes through. Step-by-step forms start at the first visible section;
20
+ * from each section the first matching next-step rule that targets a visible section wins, else
21
+ * the next visible section. Single-page forms: every visible section.
22
+ */ export function getStepPath(form, answers) {
23
+ const visible = getVisibleSections(form, answers);
24
+ if (form.presentationMode !== 'steps') {
25
+ return visible.map(getSectionKey);
26
+ }
27
+ const path = [];
28
+ const seen = new Set();
29
+ let index = 0;
30
+ while(index < visible.length && !seen.has(index)){
31
+ seen.add(index);
32
+ path.push(getSectionKey(visible[index]));
33
+ index = nextFrom(form, visible, index, answers);
34
+ }
35
+ return path;
36
+ }
37
+ /** The section after `currentKey` on the path, or null at the end. */ export function getNextSectionKey(form, currentKey, answers) {
38
+ const visible = getVisibleSections(form, answers);
39
+ const index = visible.findIndex((s)=>getSectionKey(s) === normalizeKey(currentKey));
40
+ if (index === -1) {
41
+ return visible[0] ? getSectionKey(visible[0]) : null;
42
+ }
43
+ const next = nextFrom(form, visible, index, answers);
44
+ return visible[next] ? getSectionKey(visible[next]) : null;
45
+ }
46
+ /** Where a visitor is on the path: `current` of `total`, and the form's step progress words. */ export function getStepProgress(form, answers, currentKey, words) {
47
+ const path = getStepPath(form, answers);
48
+ const current = path.indexOf(normalizeKey(currentKey)) + 1;
49
+ const total = path.length;
50
+ const template = words?.stepProgress || form.wording?.stepProgress || DEFAULT_WORDS.stepProgress;
51
+ return {
52
+ current,
53
+ text: interpolate(template, {
54
+ current,
55
+ total
56
+ }),
57
+ total
58
+ };
59
+ }
60
+ /**
61
+ * What is shown for these answers. Hidden answers are dropped and the rules are read again until
62
+ * nothing changes, so an answer left in a hidden field never decides what else is shown.
63
+ */ export function getVisibility(form, answers) {
64
+ const inputs = getInputFields(form);
65
+ let current = answers;
66
+ let result = {
67
+ answers: {},
68
+ fieldKeys: new Set(),
69
+ sectionKeys: []
70
+ };
71
+ for(let round = 0; round < 10; round += 1){
72
+ const sectionKeys = getStepPath(form, current);
73
+ const active = new Set(sectionKeys);
74
+ const fieldKeys = new Set();
75
+ for (const { field, key, section } of inputs){
76
+ if (!active.has(getSectionKey(section))) {
77
+ continue;
78
+ }
79
+ if (!isSectionVisible(section, current, form)) {
80
+ continue;
81
+ }
82
+ if (!evaluateRules(field.visibilityRules, current, form)) {
83
+ continue;
84
+ }
85
+ fieldKeys.add(key);
86
+ }
87
+ const next = {};
88
+ for (const key of fieldKeys){
89
+ if (key in answers) {
90
+ next[key] = answers[key];
91
+ }
92
+ }
93
+ const same = result.fieldKeys.size === fieldKeys.size && [
94
+ ...fieldKeys
95
+ ].every((k)=>result.fieldKeys.has(k));
96
+ result = {
97
+ answers: next,
98
+ fieldKeys,
99
+ sectionKeys
100
+ };
101
+ if (same && round > 0) {
102
+ break;
103
+ }
104
+ current = next;
105
+ }
106
+ return result;
107
+ }
108
+
109
+ //# sourceMappingURL=steps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shared/steps.ts"],"sourcesContent":["import type { Answers, FormLike, FormSection, FormWords } from './types.js'\n\nimport {\n evaluateRules,\n getInputFields,\n getSectionKey,\n getVisibleSections,\n isSectionVisible,\n normalizeKey,\n} from './rules.js'\nimport { interpolate } from './tokens.js'\nimport { DEFAULT_WORDS } from './words.js'\n\nfunction nextFrom(\n form: FormLike,\n visible: FormSection[],\n index: number,\n answers: Answers,\n): number {\n const section = visible[index]\n for (const rule of (section.nextStepRules || []).filter(Boolean)) {\n if (!evaluateRules(rule, answers, form)) {\n continue\n }\n const target = normalizeKey(rule.targetSectionKey)\n const targetIndex = visible.findIndex((s) => getSectionKey(s) === target)\n if (target && targetIndex !== -1) {\n return targetIndex\n }\n }\n return index + 1\n}\n\n/**\n * The sections a visitor goes through. Step-by-step forms start at the first visible section;\n * from each section the first matching next-step rule that targets a visible section wins, else\n * the next visible section. Single-page forms: every visible section.\n */\nexport function getStepPath(form: FormLike, answers: Answers): string[] {\n const visible = getVisibleSections(form, answers)\n if (form.presentationMode !== 'steps') {\n return visible.map(getSectionKey)\n }\n const path: string[] = []\n const seen = new Set<number>()\n let index = 0\n while (index < visible.length && !seen.has(index)) {\n seen.add(index)\n path.push(getSectionKey(visible[index]))\n index = nextFrom(form, visible, index, answers)\n }\n return path\n}\n\n/** The section after `currentKey` on the path, or null at the end. */\nexport function getNextSectionKey(form: FormLike, currentKey: string, answers: Answers): null | string {\n const visible = getVisibleSections(form, answers)\n const index = visible.findIndex((s) => getSectionKey(s) === normalizeKey(currentKey))\n if (index === -1) {\n return visible[0] ? getSectionKey(visible[0]) : null\n }\n const next = nextFrom(form, visible, index, answers)\n return visible[next] ? getSectionKey(visible[next]) : null\n}\n\n/** Where a visitor is on the path: `current` of `total`, and the form's step progress words. */\nexport function getStepProgress(\n form: FormLike,\n answers: Answers,\n currentKey: string,\n words?: Partial<FormWords>,\n): { current: number; text: string; total: number } {\n const path = getStepPath(form, answers)\n const current = path.indexOf(normalizeKey(currentKey)) + 1\n const total = path.length\n const template = words?.stepProgress || form.wording?.stepProgress || DEFAULT_WORDS.stepProgress\n return { current, text: interpolate(template, { current, total }), total }\n}\n\nexport type Visibility = {\n /** Answers of the shown fields only. */\n answers: Answers\n /** Keys of the inputs that are shown (and, in steps, on the path). */\n fieldKeys: Set<string>\n /** Keys of the sections that count: visible ones, or in steps the ones on the path. */\n sectionKeys: string[]\n}\n\n/**\n * What is shown for these answers. Hidden answers are dropped and the rules are read again until\n * nothing changes, so an answer left in a hidden field never decides what else is shown.\n */\nexport function getVisibility(form: FormLike, answers: Answers): Visibility {\n const inputs = getInputFields(form)\n let current: Answers = answers\n let result: Visibility = { answers: {}, fieldKeys: new Set(), sectionKeys: [] }\n for (let round = 0; round < 10; round += 1) {\n const sectionKeys = getStepPath(form, current)\n const active = new Set(sectionKeys)\n const fieldKeys = new Set<string>()\n for (const { field, key, section } of inputs) {\n if (!active.has(getSectionKey(section))) {\n continue\n }\n if (!isSectionVisible(section, current, form)) {\n continue\n }\n if (!evaluateRules(field.visibilityRules, current, form)) {\n continue\n }\n fieldKeys.add(key)\n }\n const next: Answers = {}\n for (const key of fieldKeys) {\n if (key in answers) {\n next[key] = answers[key]!\n }\n }\n const same =\n result.fieldKeys.size === fieldKeys.size && [...fieldKeys].every((k) => result.fieldKeys.has(k))\n result = { answers: next, fieldKeys, sectionKeys }\n if (same && round > 0) {\n break\n }\n current = next\n }\n return result\n}\n"],"names":["evaluateRules","getInputFields","getSectionKey","getVisibleSections","isSectionVisible","normalizeKey","interpolate","DEFAULT_WORDS","nextFrom","form","visible","index","answers","section","rule","nextStepRules","filter","Boolean","target","targetSectionKey","targetIndex","findIndex","s","getStepPath","presentationMode","map","path","seen","Set","length","has","add","push","getNextSectionKey","currentKey","next","getStepProgress","words","current","indexOf","total","template","stepProgress","wording","text","getVisibility","inputs","result","fieldKeys","sectionKeys","round","active","field","key","visibilityRules","same","size","every","k"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { Answers, FieldTypeChecks, FormLike } from './types.js';
2
+ export type TokenValue = boolean | null | number | string | undefined;
3
+ export type MessageRow = {
4
+ key: string;
5
+ label: string;
6
+ value: string;
7
+ };
8
+ export type InterpolateOptions = {
9
+ /** Escape values for HTML and turn `{{*}}` lines into `<br>`. */
10
+ html?: boolean;
11
+ /** Rows for `{{*}}` and `{{*:table}}`. */
12
+ rows?: MessageRow[];
13
+ /** Remove line breaks (for subjects and headers). */
14
+ singleLine?: boolean;
15
+ };
16
+ export declare function escapeHtml(value: string): string;
17
+ /**
18
+ * Fills `{{key}}` (dashes and underscores allowed), `{{key.attribute}}`, `{{*}}` and
19
+ * `{{*:table}}`. Unknown tokens become empty.
20
+ */
21
+ export declare function interpolate(template: null | string | undefined, values: Record<string, TokenValue>, options?: InterpolateOptions): string;
22
+ /** One row per answered, shown field, in form order, using each field's row label. */
23
+ export declare function buildMessageRows(form: FormLike, answers: Answers, fieldTypes?: Record<string, FieldTypeChecks>): MessageRow[];
24
+ /** Token values for a form's shown answers: `{{key}}`, `{{key.attribute}}` plus `extra`. */
25
+ export declare function buildTemplateValues(form: FormLike, answers: Answers, extra?: Record<string, TokenValue>, fieldTypes?: Record<string, FieldTypeChecks>): Record<string, TokenValue>;
@@ -0,0 +1,96 @@
1
+ import { answerToText } from './answers.js';
2
+ import { getInputFields, getOptions, getRowLabel, hasAnswer, isChoiceBlock, toStringList } from './rules.js';
3
+ import { getVisibility } from './steps.js';
4
+ import { fillWords } from './words.js';
5
+ const TOKEN = /\{\{\s*(\*(?::table)?|\w[\w-]*(?:\.[\w-]+)?)\s*\}\}/g;
6
+ export function escapeHtml(value) {
7
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
8
+ }
9
+ function rowsAsLines(rows, html) {
10
+ const lines = rows.map((row)=>`${row.label}: ${row.value}`);
11
+ return html ? lines.map(escapeHtml).join('<br>\n') : lines.join('\n');
12
+ }
13
+ function rowsAsTable(rows, html) {
14
+ if (!html) {
15
+ return rowsAsLines(rows, false);
16
+ }
17
+ const body = rows.map((row)=>`<tr><th align="left">${escapeHtml(row.label)}</th><td>${escapeHtml(row.value)}</td></tr>`).join('');
18
+ return `<table><tbody>${body}</tbody></table>`;
19
+ }
20
+ /**
21
+ * Fills `{{key}}` (dashes and underscores allowed), `{{key.attribute}}`, `{{*}}` and
22
+ * `{{*:table}}`. Unknown tokens become empty.
23
+ */ export function interpolate(template, values, options = {}) {
24
+ if (!template) {
25
+ return '';
26
+ }
27
+ const html = Boolean(options.html);
28
+ const out = template.replace(TOKEN, (_match, name)=>{
29
+ if (name === '*') {
30
+ return rowsAsLines(options.rows || [], html);
31
+ }
32
+ if (name === '*:table') {
33
+ return rowsAsTable(options.rows || [], html);
34
+ }
35
+ const value = values[name];
36
+ const text = value === undefined || value === null ? '' : String(value);
37
+ return html ? escapeHtml(text) : text;
38
+ });
39
+ return options.singleLine ? out.replace(/[\r\n]+/g, ' ').trim() : out;
40
+ }
41
+ /** One row per answered, shown field, in form order, using each field's row label. */ export function buildMessageRows(form, answers, fieldTypes) {
42
+ const { answers: shown } = getVisibility(form, answers);
43
+ const words = fillWords(form.wording);
44
+ const rows = [];
45
+ for (const { field, key } of getInputFields(form)){
46
+ if (!(key in shown) || !hasAnswer(shown[key])) {
47
+ continue;
48
+ }
49
+ const value = answerToText(field, shown[key], words, fieldTypes);
50
+ if (!value.trim()) {
51
+ continue;
52
+ }
53
+ rows.push({
54
+ key,
55
+ label: getRowLabel(field, key),
56
+ value
57
+ });
58
+ }
59
+ return rows;
60
+ }
61
+ function chosenAttributes(field, value) {
62
+ const options = getOptions(field);
63
+ const chosen = toStringList(value).map((v)=>options.find((o)=>String(o.value) === v)).filter(Boolean);
64
+ const out = {};
65
+ for (const option of chosen){
66
+ for (const [name, attr] of Object.entries(option.attributes || {})){
67
+ if (attr === null || attr === undefined) {
68
+ continue;
69
+ }
70
+ out[name] = out[name] ? `${out[name]}, ${String(attr)}` : String(attr);
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+ /** Token values for a form's shown answers: `{{key}}`, `{{key.attribute}}` plus `extra`. */ export function buildTemplateValues(form, answers, extra = {}, fieldTypes) {
76
+ const { answers: shown } = getVisibility(form, answers);
77
+ const words = fillWords(form.wording);
78
+ const values = {
79
+ formTitle: form.title || '',
80
+ ...extra
81
+ };
82
+ for (const { field, key } of getInputFields(form)){
83
+ if (!(key in shown)) {
84
+ continue;
85
+ }
86
+ values[key] = answerToText(field, shown[key], words, fieldTypes);
87
+ if (isChoiceBlock(field)) {
88
+ for (const [name, attr] of Object.entries(chosenAttributes(field, shown[key]))){
89
+ values[`${key}.${name}`] = attr;
90
+ }
91
+ }
92
+ }
93
+ return values;
94
+ }
95
+
96
+ //# sourceMappingURL=tokens.js.map