@formique/semantq 1.0.8 → 1.0.10

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.
@@ -0,0 +1,1128 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * FormiqueParser - A comprehensive form definition parser that converts an Abstract Syntax Tree (AST)
5
+ * representation into a structured form schema with validation rules, input types, and conditional logic.
6
+ *
7
+ * Handles complex form structures including:
8
+ * - Dynamic single-select fields with scenario-based options
9
+ * - Input type inference from field names and attributes
10
+ * - Validation rules extraction and categorization
11
+ * - Conditional field dependencies (dependsOn/dependents)
12
+ * - Multiple input types (text, email, select, radio, checkbox, file, etc.)
13
+ * - Form-level settings and parameters
14
+ * - Option lists for selectable inputs
15
+ * - Required field detection via asterisk notation
16
+ *
17
+ * The parser processes AST nodes to build a complete form configuration including:
18
+ * - Form schema with field definitions, validations, and attributes
19
+ * - Form settings for behavioral configuration
20
+ * - Form parameters for HTML form attributes
21
+ * - Support for complex field relationships and dynamic content
22
+ *
23
+ * @class
24
+ * @param {Object} ast - The Abstract Syntax Tree representing the form structure
25
+ * @returns {Object} Configuration object containing formSchema, formSettings, and formParams
26
+ */
27
+
28
+ export default class astToFormique{
29
+ constructor (ast) {
30
+ this.ast = ast;
31
+ this.formSchema=[];
32
+ this.formSettings={};
33
+ this.formParams={};
34
+
35
+ this.formAttributes = [
36
+ "action",
37
+ "method",
38
+ "enctype",
39
+ "name",
40
+ "target",
41
+ "autocomplete",
42
+ "novalidate",
43
+ "rel",
44
+ "accept-charset",
45
+ "id",
46
+ "class",
47
+ "style",
48
+ "title",
49
+ "lang",
50
+ "dir",
51
+ "hidden",
52
+ "tabindex",
53
+ "accesskey",
54
+ "draggable",
55
+ "contenteditable",
56
+ "spellcheck",
57
+ "onsubmit",
58
+ "onreset",
59
+ "onchange",
60
+ "oninput",
61
+ "onfocus",
62
+ "onblur",
63
+ "onkeydown",
64
+ "onkeyup",
65
+ "onclick",
66
+ "ondblclick",
67
+ "onmouseover",
68
+ "onmouseout",
69
+ "aria-label",
70
+ "aria-labelledby",
71
+ "aria-describedby",
72
+ "role"
73
+ ];
74
+
75
+ this.inputAttributes = [
76
+ "id",
77
+ "class",
78
+ "type",
79
+ "value",
80
+ "name",
81
+ "placeholder",
82
+ "autofocus",
83
+ "size",
84
+ "accept",
85
+ "form",
86
+ "list"
87
+ ];
88
+
89
+
90
+ this.validationAttributes = [
91
+ // Basic validations
92
+ 'required',
93
+ 'disabled',
94
+ 'readonly',
95
+
96
+ // Text/pattern validations
97
+ 'minlength',
98
+ 'maxlength',
99
+ 'pattern',
100
+
101
+ // Numeric validations
102
+ 'min',
103
+ 'max',
104
+ 'step',
105
+
106
+ // Date/time validations
107
+ 'min',
108
+ 'max',
109
+
110
+ // File validations
111
+ 'accept',
112
+ 'multiple',
113
+ 'filesize', // Note: Typically implemented via JavaScript
114
+
115
+ // Special input types
116
+ 'checked', // For checkboxes/radios
117
+ 'selected', // For options
118
+ 'placeholder', // Not validation but affects input
119
+
120
+ // Custom data attributes (commonly used for validation)
121
+ 'data-validate',
122
+ 'data-required',
123
+ 'data-min',
124
+ 'data-max',
125
+ 'data-minlength',
126
+ 'data-maxlength',
127
+ 'data-pattern',
128
+ 'data-error',
129
+ 'data-validate-on',
130
+ 'data-equal-to',
131
+
132
+ // ARIA validation attributes
133
+ 'aria-required',
134
+ 'aria-invalid',
135
+
136
+ // Form-level validation
137
+ 'novalidate',
138
+ 'formnovalidate'
139
+ ];
140
+
141
+
142
+ this.ignoreAttributes =[
143
+
144
+ 'oneof',
145
+ 'oneOf',
146
+ 'one',
147
+ 'manyof',
148
+ 'manyOf',
149
+ 'many',
150
+ 'radio',
151
+ 'select',
152
+ 'mutli-select',
153
+ 'multiselect',
154
+ 'multipleselect',
155
+ 'multipleSelect',
156
+ 'multiple-select',
157
+ 'multiple',
158
+ 'checkbox',
159
+ 'selected',
160
+ 'default'
161
+
162
+ ];
163
+
164
+
165
+ this.inputTypeMaps = {
166
+ oneof: 'radio',
167
+ one: 'radio',
168
+ radio: 'radio',
169
+ select: 'singleSelect',
170
+ singleSelect: 'singleSelect', // optional addition
171
+ manyof: 'checkbox',
172
+ many: 'checkbox',
173
+ checkbox: 'checkbox',
174
+ 'multi-select': 'multipleSelect',
175
+ multiselect: 'multipleSelect',
176
+ multipleselect: 'multipleSelect',
177
+ 'multiple-select': 'multipleSelect',
178
+ multiple: 'multipleSelect',
179
+ selectMany: 'multipleSelect',
180
+ selectOne:'singleSelect',
181
+ manyselect: 'multipleSelect',
182
+ oneselect:'singleSelect',
183
+ selectmany: 'multipleSelect',
184
+ selectone:'singleSelect',
185
+
186
+ };
187
+
188
+ this.regularInputTypes = [
189
+ 'text',
190
+ 'password',
191
+ 'email',
192
+ 'number',
193
+ 'range',
194
+ 'date',
195
+ 'datetime-local',
196
+ 'time',
197
+ 'month',
198
+ 'week',
199
+ 'search',
200
+ 'tel',
201
+ 'url',
202
+ 'color',
203
+ 'select',
204
+ 'checkbox',
205
+ 'radio',
206
+ 'file',
207
+ 'hidden',
208
+ 'submit',
209
+ 'reset',
210
+ 'button',
211
+ 'image'
212
+ ];
213
+
214
+ this.specialInputTypes = [
215
+ 'singleSelect',
216
+ 'dynamicSingleSelect',
217
+ 'multipleSelect'
218
+ ];
219
+
220
+
221
+ // Exhaustive type inference map
222
+ this.typeInferenceRules = {
223
+ // Email fields
224
+ email: { type: 'email', priority: 10 },
225
+ 'e-mail': { type: 'email', priority: 9 },
226
+ mail: { type: 'email', priority: 8 },
227
+
228
+
229
+ // Name fields (all text type)
230
+ name: { type: 'text', subtype: 'name', priority: 10 },
231
+ 'first-name': { type: 'text', subtype: 'given-name', priority: 10 },
232
+ 'firstname': { type: 'text', subtype: 'given-name', priority: 9 },
233
+ 'given-name': { type: 'text', subtype: 'given-name', priority: 10 },
234
+ 'last-name': { type: 'text', subtype: 'family-name', priority: 10 },
235
+ 'lastname': { type: 'text', subtype: 'family-name', priority: 9 },
236
+ surname: { type: 'text', subtype: 'family-name', priority: 10 },
237
+ 'family-name': { type: 'text', subtype: 'family-name', priority: 10 },
238
+ 'full-name': { type: 'text', subtype: 'full-name', priority: 10 },
239
+ 'middle-name': { type: 'text', subtype: 'additional-name', priority: 9 },
240
+ 'middle-initial': { type: 'text', subtype: 'additional-name', priority: 8 },
241
+ nickname: { type: 'text', subtype: 'nickname', priority: 9 },
242
+ username: { type: 'text', subtype: 'username', priority: 9 },
243
+ displayname: { type: 'text', subtype: 'display-name', priority: 8 },
244
+
245
+
246
+ // General text fields
247
+ title: { type: 'text', subtype: 'title', priority: 9 },
248
+ subject: { type: 'text', subtype: 'subject', priority: 8 },
249
+ description: { type: 'text', subtype: 'description', priority: 9 },
250
+ note: { type: 'text', subtype: 'note', priority: 8 },
251
+ bio: { type: 'text', subtype: 'bio', priority: 8 },
252
+ address: { type: 'text', subtype: 'address', priority: 9 },
253
+ city: { type: 'text', subtype: 'city', priority: 9 },
254
+ state: { type: 'text', subtype: 'state', priority: 9 },
255
+ province: { type: 'text', subtype: 'province', priority: 9 },
256
+ country: { type: 'text', subtype: 'country', priority: 9 },
257
+ zipcode: { type: 'text', subtype: 'postal-code', priority: 9 },
258
+ 'postal-code': { type: 'text', subtype: 'postal-code', priority: 9 },
259
+ company: { type: 'text', subtype: 'organization', priority: 9 },
260
+ organization: { type: 'text', subtype: 'organization', priority: 9 },
261
+ job: { type: 'text', subtype: 'job-title', priority: 8 },
262
+ 'job-title': { type: 'text', subtype: 'job-title', priority: 9 },
263
+ occupation: { type: 'text', subtype: 'job-title', priority: 8 },
264
+ message: { type: 'textarea', subtype: 'message', priority: 9 },
265
+ comment: { type: 'textarea', subtype: 'comment', priority: 8 },
266
+
267
+
268
+
269
+ // Telephone fields
270
+ tel: { type: 'tel', priority: 10 },
271
+ phone: { type: 'tel', priority: 10 },
272
+ mobile: { type: 'tel', priority: 10 },
273
+ telephone: { type: 'tel', priority: 10 },
274
+ cell: { type: 'tel', priority: 9 },
275
+ 'cell-phone': { type: 'tel', priority: 9 },
276
+
277
+ // Numeric fields
278
+ // Numeric fields - Add these to your existing object
279
+ count: { type: 'number', priority: 9 },
280
+ price: { type: 'number', priority: 9 },
281
+ total: { type: 'number', priority: 8 },
282
+ amount: { type: 'number', priority: 9 },
283
+ quantity: { type: 'number', priority: 9 },
284
+ qty: { type: 'number', priority: 8 },
285
+ age: { type: 'number', priority: 8 },
286
+ sum: { type: 'number', priority: 7 },
287
+ value: { type: 'number', priority: 7 },
288
+ percent: { type: 'number', priority: 8 },
289
+ percentage: { type: 'number', priority: 8 },
290
+ discount: { type: 'number', priority: 7 },
291
+
292
+ // Currency fields (also numeric but might need special handling)
293
+ cost: { type: 'number', priority: 9 },
294
+ payment: { type: 'number', priority: 8 },
295
+ salary: { type: 'number', priority: 8 },
296
+ fee: { type: 'number', priority: 8 },
297
+
298
+ // Date fields
299
+ date: { type: 'date', priority: 10 },
300
+ dob: { type: 'date', priority: 9 },
301
+ 'birth-date': { type: 'date', priority: 8 },
302
+ 'start-date': { type: 'date', priority: 7 },
303
+ 'end-date': { type: 'date', priority: 7 },
304
+
305
+ // Password fields
306
+ password: { type: 'password', priority: 10 },
307
+ pwd: { type: 'password', priority: 8 },
308
+ secret: { type: 'password', priority: 6 },
309
+
310
+ // URL fields
311
+ url: { type: 'url', priority: 10 },
312
+ website: { type: 'url', priority: 8 },
313
+ link: { type: 'url', priority: 7 },
314
+
315
+ // Special cases
316
+ color: { type: 'color', priority: 10 },
317
+ search: { type: 'search', priority: 10 },
318
+ range: { type: 'range', priority: 10 },
319
+
320
+ // File uploads
321
+ file: { type: 'file', priority: 10 },
322
+ upload: { type: 'file', priority: 9 },
323
+ document: { type: 'file', priority: 8 },
324
+ attachment: { type: 'file', priority: 8 },
325
+ resume: { type: 'file', priority: 7 },
326
+ cv: { type: 'file', priority: 7 },
327
+ portfolio: { type: 'file', priority: 7 },
328
+
329
+ // Image-specific
330
+ image: { type: 'file', accept: 'image/*', priority: 10 },
331
+ photo: { type: 'file', accept: 'image/*', priority: 9 },
332
+ avatar: { type: 'file', accept: 'image/*', priority: 9 },
333
+ picture: { type: 'file', accept: 'image/*', priority: 8 },
334
+ logo: { type: 'file', accept: 'image/*', priority: 8 },
335
+ banner: { type: 'file', accept: 'image/*', priority: 7 },
336
+ thumbnail: { type: 'file', accept: 'image/*', priority: 7 },
337
+
338
+ // Media files
339
+ video: { type: 'file', accept: 'video/*', priority: 9 },
340
+ audio: { type: 'file', accept: 'audio/*', priority: 9 },
341
+ recording: { type: 'file', accept: 'audio/*', priority: 7 },
342
+
343
+ // Specific file types
344
+ pdf: { type: 'file', accept: '.pdf', priority: 8 },
345
+ spreadsheet: { type: 'file', accept: '.csv,.xls,.xlsx', priority: 7 },
346
+ excel: { type: 'file', accept: '.xls,.xlsx', priority: 8 },
347
+ word: { type: 'file', accept: '.doc,.docx', priority: 8 },
348
+ presentation: { type: 'file', accept: '.ppt,.pptx', priority: 7 },
349
+
350
+ // Multiple files
351
+ files: { type: 'file', multiple: true, priority: 8 },
352
+ images: { type: 'file', accept: 'image/*', multiple: true, priority: 8 },
353
+ gallery: { type: 'file', accept: 'image/*', multiple: true, priority: 7 },
354
+
355
+
356
+ gender: { type: 'radio', priority: 10 },
357
+ sex: { type: 'radio', priority: 9 },
358
+ title: { type: 'select', priority: 8 },
359
+ pronoun: { type: 'select', priority: 8 },
360
+ salutation: { type: 'select', priority: 7 },
361
+
362
+ // Boolean (Yes/No)
363
+ newsletter: { type: 'radio', priority: 7 },
364
+ agree: { type: 'radio', priority: 6 },
365
+ smoker: { type: 'radio', priority: 5 },
366
+ terms: { type: 'radio', priority: 6 },
367
+
368
+ // Categories
369
+ maritalstatus: { type: 'select', priority: 7 },
370
+ employment: { type: 'select', priority: 7 },
371
+ education: { type: 'select', priority: 6 },
372
+ status: { type: 'select', priority: 5 },
373
+
374
+ // Location
375
+ country: { type: 'select', priority: 9 },
376
+ language: { type: 'select', priority: 6 },
377
+ region: { type: 'select', priority: 5 },
378
+ state: { type: 'select', priority: 5 },
379
+
380
+ // Surveys/Ratings
381
+ rating: { type: 'radio', priority: 5 },
382
+ satisfaction: { type: 'radio', priority: 5 },
383
+ feedback: { type: 'radio', priority: 4 },
384
+
385
+
386
+ reset: { type: 'reset', priority: 10 },
387
+
388
+
389
+ };
390
+
391
+ // Default fallback
392
+ this.defaultType = 'text';
393
+
394
+ this.traverse();
395
+ this.addSubmit();
396
+
397
+
398
+
399
+ return {
400
+ formSchema: this.formSchema,
401
+ formSettings: this.formSettings,
402
+ formParams: this.formParams
403
+ };
404
+
405
+ }
406
+
407
+
408
+ // formDirective , formProperties, formProperty, formFields,
409
+ // optionsAttribute, fieldsAttribute
410
+
411
+
412
+ inferInputType(fieldName) {
413
+ if (!fieldName) return this.defaultType;
414
+
415
+ const lowerName = fieldName.toLowerCase().trim();
416
+ const matches = [];
417
+
418
+ // Check for exact matches first
419
+ if (this.typeInferenceRules[lowerName]) {
420
+ return this.typeInferenceRules[lowerName].type;
421
+ }
422
+
423
+ // Check for partial matches (e.g., "userEmail" contains "email")
424
+ for (const [key, rule] of Object.entries(this.typeInferenceRules)) {
425
+ if (lowerName.includes(key)) {
426
+ matches.push({ ...rule, key });
427
+ }
428
+ }
429
+
430
+ // If multiple matches, use the one with highest priority
431
+ if (matches.length > 0) {
432
+ matches.sort((a, b) => b.priority - a.priority);
433
+ return matches[0].type;
434
+ }
435
+
436
+ // Check for common patterns
437
+ if (/.*password.*/i.test(fieldName)) return 'password';
438
+ if (/.*(mail|email).*/i.test(fieldName)) return 'email';
439
+ if (/.*(tel|phone|mobile).*/i.test(fieldName)) return 'tel';
440
+ if (/.*(date|dob|birth).*/i.test(fieldName)) return 'date';
441
+
442
+ return this.defaultType;
443
+ }
444
+
445
+
446
+
447
+ cleanFieldName(str) {
448
+ const [rawName = '', rawType = ''] = str.split(':');
449
+
450
+ const input_name = rawName
451
+ .trim()
452
+ .replace(/[^\w-]/g, ''); // keeps letters, digits, underscores, hyphens
453
+
454
+ const input_type = rawType.trim(); // untouched for now
455
+
456
+ return { input_name, input_type };
457
+ }
458
+
459
+
460
+ cleanToInputType(str) {
461
+ //console.log("STR", str);
462
+
463
+ if (this.specialInputTypes.includes(str)) {
464
+ //console.log("STR", str);
465
+ return str;
466
+ }
467
+
468
+ const cleaned = str.trim().toLowerCase();
469
+
470
+
471
+ if (cleaned.includes('datetime-local')) {
472
+ // Keep only letters and hyphen
473
+ return cleaned.replace(/[^a-z-]/g, '');
474
+ }
475
+
476
+ // Otherwise, keep only letters
477
+ return cleaned.replace(/[^a-z]/g, '');
478
+ }
479
+
480
+
481
+
482
+ toTitleCase(str) {
483
+ return str
484
+ .toLowerCase()
485
+ .split(/[\s_-]+/) // split by space, dash, or underscore
486
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
487
+ .join(' ');
488
+ }
489
+
490
+ isRequired(str) {
491
+ return str.replace(/\s+/g, '').includes('*');
492
+ }
493
+
494
+
495
+ traverse() {
496
+ const nodeHandlers = {
497
+ FormDirective: this.buildDirective.bind(this),
498
+ FormProperties: this.buildProperties.bind(this),
499
+ // FIX: Add the handler for individual form properties
500
+ FormProperty: this.buildProperty.bind(this),
501
+ FormFields: this.buildFields.bind(this),
502
+ FormField: this.buildField.bind(this),
503
+ OptionsAttribute: this.buildOptionsAttribute.bind(this)
504
+ };
505
+
506
+ const traverseNode = (node) => {
507
+ if (!node || typeof node !== 'object') return;
508
+
509
+ const handler = nodeHandlers[node.type];
510
+ if (handler) {
511
+ handler(node);
512
+ }
513
+
514
+ // Handle nested structures specific to this AST format
515
+ if (node.properties && node.properties.properties && Array.isArray(node.properties.properties)) {
516
+ // This ensures individual FormProperty nodes (like theme: dark) are visited.
517
+ node.properties.properties.forEach(traverseNode);
518
+ }
519
+ if (node.fields && Array.isArray(node.fields)) {
520
+ node.fields.forEach(traverseNode);
521
+ }
522
+ if (node.attributes && Array.isArray(node.attributes)) {
523
+ node.attributes.forEach(traverseNode);
524
+ }
525
+ if (node.values && Array.isArray(node.values)) {
526
+ node.values.forEach(traverseNode);
527
+ }
528
+ };
529
+
530
+ // Handle the array-based AST structure
531
+ if (Array.isArray(this.ast)) {
532
+ this.ast.forEach(traverseNode);
533
+ }
534
+ }
535
+
536
+
537
+ extractAttributeKeys(nodes) {
538
+
539
+ if (!Array.isArray(nodes)) {
540
+ throw new Error('Input must be an array of nodes');
541
+ }
542
+
543
+ return nodes
544
+ // Step 1: Filter only FieldAttribute nodes
545
+ .filter(node => node?.type === 'FieldAttribute')
546
+
547
+ // Step 2: Extract keys safely
548
+ .map(node => node?.key)
549
+
550
+ // Step 3: Remove any undefined/null keys
551
+ .filter(Boolean)
552
+
553
+ // Step 4: Remove potential duplicates (optional)
554
+ .filter((key, index, self) => self.indexOf(key) === index);
555
+ }
556
+
557
+
558
+ extractOptionValues(attributes) {
559
+ if (!Array.isArray(attributes)) {
560
+ throw new Error('Input must be an array of attributes');
561
+ }
562
+
563
+ // Get all option values from only the 'options' OptionsAttribute and remove duplicates
564
+ const allOptions = attributes
565
+ // 💡 FIX: Filter strictly for nodes of type 'OptionsAttribute' AND key 'options'
566
+ .filter(attr => attr?.type === 'OptionsAttribute' && attr.key === 'options')
567
+ .flatMap(attr => attr.values || [])
568
+ .map(option => option?.value)
569
+ .filter(Boolean);
570
+
571
+ // Remove duplicates by using a Set
572
+ return [...new Set(allOptions)];
573
+ }
574
+
575
+
576
+ extractDependentValues(attributes) {
577
+ if (!Array.isArray(attributes)) {
578
+ throw new Error('Input must be an array of attributes');
579
+ }
580
+
581
+ return attributes
582
+ // Find the OptionsAttribute node
583
+ .filter(attr => attr?.type === 'OptionsAttribute' && attr?.key === 'dependents')
584
+ // Get the values array
585
+ .flatMap(attr => attr.values || [])
586
+ // Extract each option's value
587
+ .map(option => option?.value)
588
+ // Remove any undefined/null values
589
+ .filter(Boolean);
590
+ }
591
+
592
+
593
+ inputTypeResolver(fieldName, attributeKeys) {
594
+
595
+ // first option - handle dynamicSingleSelect
596
+ //console.log("CHK",fieldName);
597
+ if (fieldName.includes('-')) {
598
+ return "dynamicSingleSelect"; //
599
+ }
600
+
601
+
602
+
603
+ // second option - explicit field name directive
604
+ if (fieldName.includes(':')) {
605
+ const chunks = fieldName.split(':');
606
+ //console.log("LAPHA",chunks[1]);
607
+ return chunks[1]; // e.g., 'date' from 'dob:date'
608
+ }
609
+
610
+ // Third option - low code type definition
611
+ const matchedKey = attributeKeys.find(key => key in this.inputTypeMaps);
612
+ if (matchedKey) {
613
+ //console.log("HERE",matchedKey);
614
+ // console.log("HERE 2",this.inputTypeMaps[matchedKey]);
615
+
616
+ return this.inputTypeMaps[matchedKey];
617
+ }
618
+
619
+ // Fourth 3: inference with text fall back
620
+ return this.inferInputType(fieldName);
621
+
622
+
623
+ }
624
+
625
+
626
+ getOptionValuesByKey(attributes, targetKey) {
627
+ if (!Array.isArray(attributes)) throw new Error('Input must be an array of attributes');
628
+ if (!targetKey) throw new Error('Target key is required');
629
+
630
+ const optionsAttribute = attributes.find(
631
+ attr => attr?.type === 'OptionsAttribute' && attr?.key === targetKey
632
+ );
633
+
634
+ return optionsAttribute?.values
635
+ ? optionsAttribute.values
636
+ .map(option => option?.value)
637
+ .filter(Boolean)
638
+ .map(value => value.toLowerCase()) // Normalize to lowercase
639
+ : [];
640
+ }
641
+
642
+
643
+
644
+ buildDynamicSingleSelect(node, rawFieldName) {
645
+
646
+ const cleanString = this.cleanFieldName(rawFieldName);
647
+ const fieldName = cleanString.input_name;
648
+
649
+ let fieldSchema = [];
650
+ fieldSchema.push('dynamicSingleSelect', fieldName, this.toTitleCase(fieldName));
651
+
652
+ let validations;
653
+ let attributes;
654
+ let mainSelectOptions = []; // Index 5
655
+
656
+ let inputParams;
657
+
658
+ if (node.attributes.length > 0) {
659
+ inputParams = this.handleAttributes(node.attributes);
660
+ } else {
661
+ inputParams = { validations: {}, attributes: {} }
662
+ }
663
+
664
+
665
+ validations = inputParams.validations;
666
+ attributes = inputParams.attributes;
667
+
668
+ if (this.isRequired(rawFieldName)) {
669
+ validations['required'] = true;
670
+ }
671
+
672
+ // 1. CRITICAL FIX: Extract 'options' (main dropdown values) from attributes and store separately (Index 5).
673
+ if (attributes.options) {
674
+ mainSelectOptions = attributes.options;
675
+ delete attributes.options;
676
+ }
677
+
678
+ // Since the AST is now clean, we assume the only remaining attributes are standard HTML attributes (or empty {}).
679
+ // The previous workaround for fragmented 'South' and 'Africa' keys is now removed.
680
+
681
+ // Push Schema Elements (Index 3 and 4)
682
+ fieldSchema.push(validations); // Index 3
683
+ fieldSchema.push(attributes); // Index 4 (Should be {} if no HTML attributes were defined)
684
+
685
+
686
+ /// NOW BUILD SCENARIO (SUB-OPTIONS) BLOCKS (Index 6)
687
+
688
+ // optionValues retrieves all unique keys that are NOT 'options'
689
+ const optionValues = this.extractOptionValues(node.attributes);
690
+ let scenarioBlocks = [];
691
+
692
+
693
+ if (optionValues.length > 0) {
694
+ optionValues.forEach(option => {
695
+
696
+ let schema = {};
697
+ const lowerCaseOption = option.toLowerCase();
698
+
699
+ // Set the ID/label for the scenario block
700
+ schema['id'] = option; //lowerCaseOption;
701
+ schema['label'] = option;
702
+
703
+ // Use the option string directly as the attribute key for lookup (e.g., 'South Africa' -> 'South Africa')
704
+ const attributeKey = option;
705
+
706
+ /// Add sub-options now
707
+ const keyOptions = this.getOptionValuesByKey(node.attributes, attributeKey);
708
+ let options = [];
709
+
710
+ if (keyOptions.length > 0) {
711
+ keyOptions.forEach(subOption => {
712
+ options.push({ value: subOption.toLowerCase(), label: this.toTitleCase(subOption) })
713
+ });
714
+ schema['options'] = options;
715
+ scenarioBlocks.push(schema);
716
+ }
717
+ });
718
+ }
719
+
720
+
721
+ // 3. PUSH MAIN OPTIONS: Add the main select options list (Index 5)
722
+ fieldSchema.push(mainSelectOptions);
723
+
724
+ // 4. PUSH SCENARIO BLOCKS: Add the list of scenario blocks (Index 6)
725
+ fieldSchema.push(scenarioBlocks);
726
+
727
+
728
+ // 5. Final push to the form schema
729
+ this.formSchema.push(fieldSchema);
730
+ }
731
+
732
+
733
+
734
+
735
+
736
+ // Builder methods - implement these according to your needs
737
+ buildDirective(node) {
738
+ //console.log(`Processing FormDirective: ${node.name.value}`);
739
+
740
+ this.formParams['id'] = node.name.value;
741
+ // Handle directive specific logic
742
+ }
743
+
744
+ buildProperties(node) {
745
+ // console.log(`Processing FormProperties with ${node.properties.length} properties`);
746
+ }
747
+
748
+ buildProperty(node) {
749
+ //console.log(`Processing FormProperty: ${node.key.value}`);
750
+ const key = node.key.value;
751
+ let val;
752
+
753
+ // 💡 FIX: Correctly extract the value based on the AST structure.
754
+ // Simple values (StringLiteral, BooleanLiteral, NumberLiteral, Identifier)
755
+ if (node.value && node.value.type) {
756
+ val = node.value.value;
757
+ } else if (node.value !== undefined) {
758
+ // Fallback for raw, unparsed values or flags
759
+ val = node.value;
760
+ } else {
761
+ // Treat properties with no explicit value as 'true' (if supported by grammar)
762
+ val = true;
763
+ }
764
+
765
+ //console.log(node);
766
+
767
+ // if this is regular form attribute then it goes to formParams
768
+ if (this.formAttributes.includes(key)) {
769
+ this.formParams[key] = val
770
+ } else {
771
+
772
+ // Handle special case for 'sendTo' which might use a structure like OptionList
773
+ if (key === 'sendTo') {
774
+ // Assuming 'sendTo' value is parsed as an OptionList (Array of Options)
775
+ if (node.value && Array.isArray(node.value.values)) {
776
+ const sendToEmails = node.value.values.map(option => option.value);
777
+ this.formSettings[key] = sendToEmails;
778
+ } else {
779
+ // Fallback for single email string
780
+ this.formSettings[key] = val;
781
+ }
782
+ } else
783
+ {
784
+ // All other custom form settings
785
+ this.formSettings[key] = val
786
+ }
787
+
788
+ }
789
+
790
+ //console.log(this.formSettings);
791
+ //console.log(this.formParams);
792
+ }
793
+
794
+
795
+
796
+ buildFields(node) {
797
+ //console.log(`Processing FormFields with ${node.fields} fields`);
798
+
799
+
800
+
801
+
802
+ }
803
+
804
+
805
+
806
+ /* eslint-disable no-unused-vars */
807
+ /* eslint-disable no-useless-escape */
808
+ // The rest of the class definition from Part 1 goes here...
809
+
810
+
811
+ buildField(node) {
812
+ const rawFieldName = node.name;
813
+ const cleanString = this.cleanFieldName(rawFieldName);
814
+ const cleanFieldName = cleanString.input_name;
815
+
816
+ let fieldType;
817
+
818
+ // 1. Determine the Field Type
819
+ if (cleanString.input_type) {
820
+ fieldType = this.cleanToInputType(cleanString.input_type);
821
+ } else {
822
+ let attributeKeys;
823
+ if (node.attributes.length > 0) {
824
+ attributeKeys = this.extractAttributeKeys(node.attributes);
825
+
826
+ // Check for 'manyof' attribute to force 'checkbox' type
827
+ if (attributeKeys.includes('manyof')) {
828
+ fieldType = 'checkbox';
829
+ } else {
830
+ fieldType = this.inputTypeResolver(cleanFieldName, attributeKeys);
831
+ }
832
+ } else {
833
+ fieldType = this.inferInputType(cleanFieldName);
834
+ }
835
+ }
836
+
837
+ // Handle Dynamic Selects (assuming external function)
838
+ if (fieldType === 'dynamicSingleSelect') {
839
+ this.buildDynamicSingleSelect(node, rawFieldName);
840
+ return;
841
+ }
842
+
843
+ // Initialize Schema Structure
844
+ const fieldSchema = [];
845
+ const fieldLabel = this.toTitleCase(cleanFieldName);
846
+
847
+ // Push Base Definition: [type, name, label]
848
+ fieldSchema.push(fieldType, cleanFieldName, fieldLabel);
849
+
850
+ let validations = {};
851
+ let attributes = {};
852
+
853
+ // 2. Process Attributes and Validations
854
+ let inputParams;
855
+ if (node.attributes.length > 0) {
856
+ // Pass rawFieldName to handleAttributes for specific logic (like 'accept')
857
+ inputParams = this.handleAttributes(node.attributes, rawFieldName);
858
+ } else {
859
+ inputParams = { validations: {}, attributes: {} };
860
+ }
861
+
862
+ validations = inputParams.validations;
863
+ attributes = inputParams.attributes;
864
+
865
+ // 3. Clean up Attributes and Add Required Validation
866
+ // CRITICAL: We only delete the attributes that are used to build the final list
867
+ // but should NOT appear in the attributes object. Dependents/dependsOn SHOULD remain.
868
+ delete attributes.options;
869
+ delete attributes.selected;
870
+ delete attributes.default;
871
+ // delete attributes.dependents; // REMOVED: This attribute needs to be in the final object
872
+ // delete attributes.dependsOn; // REMOVED: This attribute needs to be in the final object
873
+ // delete attributes.condition; // REMOVED: This attribute needs to be in the final object
874
+ delete attributes.manyof; // If 'manyof' is not needed in the final attributes, delete it.
875
+
876
+ if (this.isRequired(rawFieldName)) {
877
+ validations['required'] = true;
878
+ }
879
+
880
+ // Push Validation and Attributes: [..., validations, attributes]
881
+ fieldSchema.push(validations);
882
+ fieldSchema.push(attributes);
883
+
884
+ // 4. Handle Option-Based Fields (The list of choices)
885
+ if (node.attributes.length > 0 && (fieldType === 'checkbox' || fieldType === 'radio' || fieldType === 'select' || fieldType === 'multipleSelect' || fieldType === 'singleSelect')) {
886
+
887
+ // Helper function to correctly retrieve single string OR array of selected values.
888
+ const getSelectedValues = (attributes) => {
889
+ const isMultiSelect = (fieldType === 'checkbox' || fieldType === 'multipleSelect');
890
+
891
+ // 1. Look for OptionsAttributes (list: selected: a, b)
892
+ const selectedOptionsAttr = node.attributes.find(attr =>
893
+ attr?.type === "OptionsAttribute" && (attr?.key === "selected" || attr?.key === "default")
894
+ );
895
+
896
+ if (selectedOptionsAttr && selectedOptionsAttr.values && selectedOptionsAttr.values.length > 0) {
897
+ const values = selectedOptionsAttr.values.map(v => v.value.toLowerCase());
898
+ return isMultiSelect ? values : values[0]; // array or first item
899
+ }
900
+
901
+ // 2. Look for FieldAttributes (single: selected: a)
902
+ const selectedAttr = node.attributes.find(attr =>
903
+ attr?.type === "FieldAttribute" && (attr?.key === "selected" || attr?.key === "default")
904
+ );
905
+
906
+ if (selectedAttr) {
907
+ // Use a safe value retrieval/lower-casing
908
+ const rawValue = selectedAttr.value?.value || selectedAttr.value;
909
+ const value = (typeof rawValue === 'string' ? rawValue.toLowerCase() : rawValue);
910
+
911
+ return isMultiSelect ? [value].filter(Boolean) : value; // array or string
912
+ }
913
+
914
+ return isMultiSelect ? [] : null;
915
+ };
916
+
917
+ const isMultiSelection = (fieldType === 'checkbox' || fieldType === 'multipleSelect');
918
+ const selectedValues = getSelectedValues(node.attributes);
919
+ const optionValues = this.extractOptionValues(node.attributes);
920
+ let options = [];
921
+
922
+ if (optionValues.length > 0) {
923
+ optionValues.forEach(option => {
924
+ //const optValue = option.toLowerCase();
925
+ const optValue = option;
926
+ let isSelected = false;
927
+
928
+ if (isMultiSelection) {
929
+ // Check if value is IN the array of selected values
930
+ isSelected = Array.isArray(selectedValues) && selectedValues.includes(optValue);
931
+ } else {
932
+ // Check if value EQUALS the single selected string
933
+ isSelected = (optValue === selectedValues);
934
+ }
935
+
936
+ if (isSelected) {
937
+ options.push({value: optValue, label: this.toTitleCase(option), selected: true});
938
+ } else {
939
+ options.push({value: optValue, label: this.toTitleCase(option)});
940
+ }
941
+ });
942
+
943
+ // Push Options Array: [..., validations, attributes, options_array]
944
+ fieldSchema.push(options);
945
+ }
946
+ }
947
+
948
+ // 5. Finalize Schema
949
+ this.formSchema.push(fieldSchema);
950
+ }
951
+
952
+
953
+
954
+ handleAttributes(attributesAST, fieldName) {
955
+ let validations = {};
956
+ let attributes = {};
957
+
958
+ // Helper to extract the final value from a nested AST node (unchanged)
959
+ const extractValue = (attrValue) => {
960
+ let value;
961
+ if (attrValue && typeof attrValue === 'object') {
962
+ if (attrValue.value !== undefined) {
963
+ value = attrValue.value;
964
+ } else {
965
+ value = attrValue;
966
+ }
967
+ } else {
968
+ value = attrValue;
969
+ }
970
+ if (typeof value === 'string') {
971
+ value = value.trim();
972
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
973
+ value = value.slice(1, -1);
974
+ }
975
+ }
976
+ return value;
977
+ };
978
+
979
+ // ----------------------------------------------------------------------
980
+ // 1. Initial Pass: Process all attributes (including single-value dependents/dependsOn)
981
+ // ----------------------------------------------------------------------
982
+ attributesAST.forEach(attr => {
983
+ const key = attr.key;
984
+
985
+ if (attr.type === 'FieldAttribute') {
986
+ let value = extractValue(attr.value);
987
+
988
+ // Only skip list-building keys ('selected', 'default', 'options').
989
+ if (this.ignoreAttributes && this.ignoreAttributes.includes(key)) return;
990
+ if (['selected', 'default', 'options'].includes(key)) return;
991
+
992
+ // Categorize key
993
+ if (this.inputAttributes && this.inputAttributes.includes(key)) {
994
+ attributes[key] = value;
995
+ } else if (this.validationAttributes && this.validationAttributes.includes(key)) {
996
+ validations[key] = value;
997
+ } else {
998
+ // For 'dependents', 'dependsOn', 'manyof', and any unrecognized attributes
999
+
1000
+ // 💡 CRITICAL FIX: Ensure 'dependents' value is always an array
1001
+ if (key === 'dependents') {
1002
+ // This handles AST parsing a single value as a FieldAttribute.
1003
+ attributes[key] = Array.isArray(value) ? value : [value];
1004
+ } else {
1005
+ attributes[key] = value;
1006
+ }
1007
+ }
1008
+ }
1009
+ // ----------------------------------------------------------------------
1010
+ // 2. Process OptionsAttribute (List) Nodes for 'dependents', 'accept', and 'dependsOn'
1011
+ // ----------------------------------------------------------------------
1012
+ else if (attr.type === 'OptionsAttribute') {
1013
+
1014
+ // ⭐ NEW/FIXED HANDLING: Process 'dependents' (List of field names)
1015
+ if (key === 'dependents') {
1016
+ const dependentFields = attr.values
1017
+ .map(option => extractValue(option))
1018
+ .filter(Boolean);
1019
+
1020
+ if (dependentFields.length > 0) {
1021
+ attributes[key] = dependentFields;
1022
+ }
1023
+ }
1024
+
1025
+ // SPECIAL HANDLING: Process 'accept' for file inputs
1026
+ if (key === 'accept' && fieldName.includes(':file')) {
1027
+ const acceptValues = attr.values
1028
+ .map(option => extractValue(option))
1029
+ .filter(Boolean);
1030
+
1031
+ if (acceptValues.length > 0) {
1032
+ attributes[key] = acceptValues.join(',');
1033
+ }
1034
+ }
1035
+
1036
+ // SPECIAL HANDLING: Process 'dependsOn' (Conditional Logic) if it's a list
1037
+ if (key === 'dependsOn' && attr.values && attr.values.length >= 2) {
1038
+ const dependsOnValue = extractValue(attr.values[0]);
1039
+ const dependsOnCondition = extractValue(attr.values[1]);
1040
+
1041
+ if (dependsOnValue && dependsOnCondition) {
1042
+ attributes['dependsOn'] = dependsOnValue;
1043
+ attributes['condition'] = dependsOnCondition.toLowerCase();
1044
+ }
1045
+ }
1046
+ }
1047
+ });
1048
+
1049
+
1050
+ // ----------------------------------------------------------------------
1051
+ // 3. Handle 'options' Extraction (List of choices) - UNCHANGED
1052
+ // ----------------------------------------------------------------------
1053
+ const optionsAttr = attributesAST.find(attr => attr.type === "OptionsAttribute" && attr.key === "options");
1054
+ if (optionsAttr?.values) {
1055
+ const options = optionsAttr.values.map(option => ({
1056
+ value: extractValue(option),
1057
+ label: extractValue(option) // Simple case: value is also the label
1058
+ }));
1059
+ if (options.length > 0) {
1060
+ attributes['options'] = options;
1061
+ }
1062
+ }
1063
+
1064
+ // ----------------------------------------------------------------------
1065
+ // 4. Handle 'selected' (Single or Multi-Select) - UNCHANGED
1066
+ // ----------------------------------------------------------------------
1067
+ const selectedListAttr = attributesAST.find(attr => attr.type === "OptionsAttribute" && attr.key === "selected");
1068
+ if (selectedListAttr && selectedListAttr.values) {
1069
+ const selectedValues = selectedListAttr.values
1070
+ .map(option => extractValue(option))
1071
+ .filter(Boolean);
1072
+ if (selectedValues.length > 0) {
1073
+ attributes['selected'] = selectedValues;
1074
+ }
1075
+ } else {
1076
+ const selectedSingleAttr = attributesAST.find(attr => attr.type === "FieldAttribute" && attr.key === "selected");
1077
+ if (selectedSingleAttr) {
1078
+ attributes['selected'] = extractValue(selectedSingleAttr.value);
1079
+ }
1080
+ }
1081
+
1082
+ // ----------------------------------------------------------------------
1083
+ // 5. Handle 'default' (Single Value) - UNCHANGED
1084
+ // ----------------------------------------------------------------------
1085
+ const defaultAttr = attributesAST.find(attr => attr.type === "FieldAttribute" && attr.key === "default");
1086
+ if (defaultAttr) {
1087
+ attributes['default'] = extractValue(defaultAttr.value);
1088
+ }
1089
+
1090
+ return {
1091
+ validations,
1092
+ attributes
1093
+ };
1094
+ }
1095
+
1096
+
1097
+ buildOptionsAttribute(node) {
1098
+ //console.log(`Processing OptionsAttribute with ${node.values.length} values`);
1099
+ }
1100
+
1101
+ buildFieldAttribute(node) {
1102
+ //console.log(`Processing FieldAttribute: ${node.key} = ${node.value}`);
1103
+ }
1104
+
1105
+ buildOption(node) {
1106
+ //console.log(`Processing Option: ${node.value} (quoted: ${node.quoted})`);
1107
+ }
1108
+
1109
+ buildIdentifier(node) {
1110
+ // console.log(`Processing Identifier: ${node.value}`);
1111
+ }
1112
+
1113
+ buildStringLiteral(node) {
1114
+ // console.log(`Processing StringLiteral: "${node.value}"`);
1115
+ }
1116
+
1117
+
1118
+
1119
+ addSubmit () {
1120
+
1121
+ this.formSchema.push(['submit','submit','Submit']);
1122
+
1123
+
1124
+ }
1125
+
1126
+ // class wrapper - nothing below this point
1127
+
1128
+ }