@formique/semantq 1.0.7 → 1.0.9

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,1054 @@
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
+ 'one',
146
+ 'manyof',
147
+ 'many',
148
+ 'radio',
149
+ 'select',
150
+ 'mutli-select',
151
+ 'multiselect',
152
+ 'multipleselect',
153
+ 'multiple-select',
154
+ 'multiple',
155
+ 'checkbox',
156
+ 'selected',
157
+ 'default'
158
+
159
+ ];
160
+
161
+
162
+ this.inputTypeMaps = {
163
+ oneof: 'radio',
164
+ one: 'radio',
165
+ radio: 'radio',
166
+ select: 'singleSelect',
167
+ singleSelect: 'singleSelect', // optional addition
168
+ manyof: 'checkbox',
169
+ many: 'checkbox',
170
+ checkbox: 'checkbox',
171
+ 'multi-select': 'multipleSelect',
172
+ multiselect: 'multipleSelect',
173
+ multipleselect: 'multipleSelect',
174
+ 'multiple-select': 'multipleSelect',
175
+ multiple: 'multipleSelect',
176
+ selectMany: 'multipleSelect',
177
+ selectOne:'singleSelect',
178
+ manyselect: 'multipleSelect',
179
+ oneselect:'singleSelect',
180
+ selectmany: 'multipleSelect',
181
+ selectone:'singleSelect',
182
+
183
+ };
184
+
185
+ this.regularInputTypes = [
186
+ 'text',
187
+ 'password',
188
+ 'email',
189
+ 'number',
190
+ 'range',
191
+ 'date',
192
+ 'datetime-local',
193
+ 'time',
194
+ 'month',
195
+ 'week',
196
+ 'search',
197
+ 'tel',
198
+ 'url',
199
+ 'color',
200
+ 'checkbox',
201
+ 'radio',
202
+ 'file',
203
+ 'hidden',
204
+ 'submit',
205
+ 'reset',
206
+ 'button',
207
+ 'image'
208
+ ];
209
+
210
+ // Exhaustive type inference map
211
+ this.typeInferenceRules = {
212
+ // Email fields
213
+ email: { type: 'email', priority: 10 },
214
+ 'e-mail': { type: 'email', priority: 9 },
215
+ mail: { type: 'email', priority: 8 },
216
+
217
+
218
+ // Name fields (all text type)
219
+ name: { type: 'text', subtype: 'name', priority: 10 },
220
+ 'first-name': { type: 'text', subtype: 'given-name', priority: 10 },
221
+ 'firstname': { type: 'text', subtype: 'given-name', priority: 9 },
222
+ 'given-name': { type: 'text', subtype: 'given-name', priority: 10 },
223
+ 'last-name': { type: 'text', subtype: 'family-name', priority: 10 },
224
+ 'lastname': { type: 'text', subtype: 'family-name', priority: 9 },
225
+ surname: { type: 'text', subtype: 'family-name', priority: 10 },
226
+ 'family-name': { type: 'text', subtype: 'family-name', priority: 10 },
227
+ 'full-name': { type: 'text', subtype: 'full-name', priority: 10 },
228
+ 'middle-name': { type: 'text', subtype: 'additional-name', priority: 9 },
229
+ 'middle-initial': { type: 'text', subtype: 'additional-name', priority: 8 },
230
+ nickname: { type: 'text', subtype: 'nickname', priority: 9 },
231
+ username: { type: 'text', subtype: 'username', priority: 9 },
232
+ displayname: { type: 'text', subtype: 'display-name', priority: 8 },
233
+
234
+
235
+ // General text fields
236
+ title: { type: 'text', subtype: 'title', priority: 9 },
237
+ subject: { type: 'text', subtype: 'subject', priority: 8 },
238
+ description: { type: 'text', subtype: 'description', priority: 9 },
239
+ note: { type: 'text', subtype: 'note', priority: 8 },
240
+ bio: { type: 'text', subtype: 'bio', priority: 8 },
241
+ address: { type: 'text', subtype: 'address', priority: 9 },
242
+ city: { type: 'text', subtype: 'city', priority: 9 },
243
+ state: { type: 'text', subtype: 'state', priority: 9 },
244
+ province: { type: 'text', subtype: 'province', priority: 9 },
245
+ country: { type: 'text', subtype: 'country', priority: 9 },
246
+ zipcode: { type: 'text', subtype: 'postal-code', priority: 9 },
247
+ 'postal-code': { type: 'text', subtype: 'postal-code', priority: 9 },
248
+ company: { type: 'text', subtype: 'organization', priority: 9 },
249
+ organization: { type: 'text', subtype: 'organization', priority: 9 },
250
+ job: { type: 'text', subtype: 'job-title', priority: 8 },
251
+ 'job-title': { type: 'text', subtype: 'job-title', priority: 9 },
252
+ occupation: { type: 'text', subtype: 'job-title', priority: 8 },
253
+ message: { type: 'textarea', subtype: 'message', priority: 9 },
254
+ comment: { type: 'textarea', subtype: 'comment', priority: 8 },
255
+
256
+
257
+
258
+ // Telephone fields
259
+ tel: { type: 'tel', priority: 10 },
260
+ phone: { type: 'tel', priority: 10 },
261
+ mobile: { type: 'tel', priority: 10 },
262
+ telephone: { type: 'tel', priority: 10 },
263
+ cell: { type: 'tel', priority: 9 },
264
+ 'cell-phone': { type: 'tel', priority: 9 },
265
+
266
+ // Numeric fields
267
+ // Numeric fields - Add these to your existing object
268
+ count: { type: 'number', priority: 9 },
269
+ price: { type: 'number', priority: 9 },
270
+ total: { type: 'number', priority: 8 },
271
+ amount: { type: 'number', priority: 9 },
272
+ quantity: { type: 'number', priority: 9 },
273
+ qty: { type: 'number', priority: 8 },
274
+ age: { type: 'number', priority: 8 },
275
+ sum: { type: 'number', priority: 7 },
276
+ value: { type: 'number', priority: 7 },
277
+ percent: { type: 'number', priority: 8 },
278
+ percentage: { type: 'number', priority: 8 },
279
+ discount: { type: 'number', priority: 7 },
280
+
281
+ // Currency fields (also numeric but might need special handling)
282
+ cost: { type: 'number', priority: 9 },
283
+ payment: { type: 'number', priority: 8 },
284
+ salary: { type: 'number', priority: 8 },
285
+ fee: { type: 'number', priority: 8 },
286
+
287
+ // Date fields
288
+ date: { type: 'date', priority: 10 },
289
+ dob: { type: 'date', priority: 9 },
290
+ 'birth-date': { type: 'date', priority: 8 },
291
+ 'start-date': { type: 'date', priority: 7 },
292
+ 'end-date': { type: 'date', priority: 7 },
293
+
294
+ // Password fields
295
+ password: { type: 'password', priority: 10 },
296
+ pwd: { type: 'password', priority: 8 },
297
+ secret: { type: 'password', priority: 6 },
298
+
299
+ // URL fields
300
+ url: { type: 'url', priority: 10 },
301
+ website: { type: 'url', priority: 8 },
302
+ link: { type: 'url', priority: 7 },
303
+
304
+ // Special cases
305
+ color: { type: 'color', priority: 10 },
306
+ search: { type: 'search', priority: 10 },
307
+ range: { type: 'range', priority: 10 },
308
+
309
+ // File uploads
310
+ file: { type: 'file', priority: 10 },
311
+ upload: { type: 'file', priority: 9 },
312
+ document: { type: 'file', priority: 8 },
313
+ attachment: { type: 'file', priority: 8 },
314
+ resume: { type: 'file', priority: 7 },
315
+ cv: { type: 'file', priority: 7 },
316
+ portfolio: { type: 'file', priority: 7 },
317
+
318
+ // Image-specific
319
+ image: { type: 'file', accept: 'image/*', priority: 10 },
320
+ photo: { type: 'file', accept: 'image/*', priority: 9 },
321
+ avatar: { type: 'file', accept: 'image/*', priority: 9 },
322
+ picture: { type: 'file', accept: 'image/*', priority: 8 },
323
+ logo: { type: 'file', accept: 'image/*', priority: 8 },
324
+ banner: { type: 'file', accept: 'image/*', priority: 7 },
325
+ thumbnail: { type: 'file', accept: 'image/*', priority: 7 },
326
+
327
+ // Media files
328
+ video: { type: 'file', accept: 'video/*', priority: 9 },
329
+ audio: { type: 'file', accept: 'audio/*', priority: 9 },
330
+ recording: { type: 'file', accept: 'audio/*', priority: 7 },
331
+
332
+ // Specific file types
333
+ pdf: { type: 'file', accept: '.pdf', priority: 8 },
334
+ spreadsheet: { type: 'file', accept: '.csv,.xls,.xlsx', priority: 7 },
335
+ excel: { type: 'file', accept: '.xls,.xlsx', priority: 8 },
336
+ word: { type: 'file', accept: '.doc,.docx', priority: 8 },
337
+ presentation: { type: 'file', accept: '.ppt,.pptx', priority: 7 },
338
+
339
+ // Multiple files
340
+ files: { type: 'file', multiple: true, priority: 8 },
341
+ images: { type: 'file', accept: 'image/*', multiple: true, priority: 8 },
342
+ gallery: { type: 'file', accept: 'image/*', multiple: true, priority: 7 },
343
+
344
+
345
+ gender: { type: 'radio', priority: 10 },
346
+ sex: { type: 'radio', priority: 9 },
347
+ title: { type: 'select', priority: 8 },
348
+ pronoun: { type: 'select', priority: 8 },
349
+ salutation: { type: 'select', priority: 7 },
350
+
351
+ // Boolean (Yes/No)
352
+ newsletter: { type: 'radio', priority: 7 },
353
+ agree: { type: 'radio', priority: 6 },
354
+ smoker: { type: 'radio', priority: 5 },
355
+ terms: { type: 'radio', priority: 6 },
356
+
357
+ // Categories
358
+ maritalstatus: { type: 'select', priority: 7 },
359
+ employment: { type: 'select', priority: 7 },
360
+ education: { type: 'select', priority: 6 },
361
+ status: { type: 'select', priority: 5 },
362
+
363
+ // Location
364
+ country: { type: 'select', priority: 9 },
365
+ language: { type: 'select', priority: 6 },
366
+ region: { type: 'select', priority: 5 },
367
+ state: { type: 'select', priority: 5 },
368
+
369
+ // Surveys/Ratings
370
+ rating: { type: 'radio', priority: 5 },
371
+ satisfaction: { type: 'radio', priority: 5 },
372
+ feedback: { type: 'radio', priority: 4 },
373
+
374
+
375
+ reset: { type: 'reset', priority: 10 },
376
+
377
+
378
+ };
379
+
380
+ // Default fallback
381
+ this.defaultType = 'text';
382
+
383
+ this.traverse();
384
+ this.addSubmit();
385
+
386
+
387
+
388
+ return {
389
+ formSchema: this.formSchema,
390
+ formSettings: this.formSettings,
391
+ formParams: this.formParams
392
+ };
393
+
394
+ }
395
+
396
+
397
+ // formDirective , formProperties, formProperty, formFields,
398
+ // optionsAttribute, fieldsAttribute
399
+
400
+
401
+ inferInputType(fieldName) {
402
+ if (!fieldName) return this.defaultType;
403
+
404
+ const lowerName = fieldName.toLowerCase().trim();
405
+ const matches = [];
406
+
407
+ // Check for exact matches first
408
+ if (this.typeInferenceRules[lowerName]) {
409
+ return this.typeInferenceRules[lowerName].type;
410
+ }
411
+
412
+ // Check for partial matches (e.g., "userEmail" contains "email")
413
+ for (const [key, rule] of Object.entries(this.typeInferenceRules)) {
414
+ if (lowerName.includes(key)) {
415
+ matches.push({ ...rule, key });
416
+ }
417
+ }
418
+
419
+ // If multiple matches, use the one with highest priority
420
+ if (matches.length > 0) {
421
+ matches.sort((a, b) => b.priority - a.priority);
422
+ return matches[0].type;
423
+ }
424
+
425
+ // Check for common patterns
426
+ if (/.*password.*/i.test(fieldName)) return 'password';
427
+ if (/.*(mail|email).*/i.test(fieldName)) return 'email';
428
+ if (/.*(tel|phone|mobile).*/i.test(fieldName)) return 'tel';
429
+ if (/.*(date|dob|birth).*/i.test(fieldName)) return 'date';
430
+
431
+ return this.defaultType;
432
+ }
433
+
434
+
435
+
436
+ cleanFieldName(str) {
437
+ const [rawName = '', rawType = ''] = str.split(':');
438
+
439
+ const input_name = rawName
440
+ .trim()
441
+ .replace(/[^\w-]/g, ''); // keeps letters, digits, underscores, hyphens
442
+
443
+ const input_type = rawType.trim(); // untouched for now
444
+
445
+ return { input_name, input_type };
446
+ }
447
+
448
+
449
+ cleanToInputType(str) {
450
+ const cleaned = str.trim().toLowerCase();
451
+
452
+ if (cleaned.includes('datetime-local')) {
453
+ // Keep only letters and hyphen
454
+ return cleaned.replace(/[^a-z-]/g, '');
455
+ }
456
+
457
+ // Otherwise, keep only letters
458
+ return cleaned.replace(/[^a-z]/g, '');
459
+ }
460
+
461
+
462
+
463
+ toTitleCase(str) {
464
+ return str
465
+ .toLowerCase()
466
+ .split(/[\s_-]+/) // split by space, dash, or underscore
467
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
468
+ .join(' ');
469
+ }
470
+
471
+ isRequired(str) {
472
+ return str.replace(/\s+/g, '').includes('*');
473
+ }
474
+
475
+
476
+ traverse() {
477
+ const nodeHandlers = {
478
+ FormDirective: this.buildDirective.bind(this),
479
+ FormProperties: this.buildProperties.bind(this),
480
+ FormFields: this.buildFields.bind(this),
481
+ FormField: this.buildField.bind(this),
482
+ OptionsAttribute: this.buildOptionsAttribute.bind(this)
483
+ };
484
+
485
+ const traverseNode = (node) => {
486
+ if (!node || typeof node !== 'object') return;
487
+
488
+ const handler = nodeHandlers[node.type];
489
+ if (handler) {
490
+ handler(node);
491
+ }
492
+
493
+ // Handle nested structures specific to this AST format
494
+ if (node.properties && node.properties.properties && Array.isArray(node.properties.properties)) {
495
+ node.properties.properties.forEach(traverseNode);
496
+ }
497
+ if (node.fields && Array.isArray(node.fields)) {
498
+ node.fields.forEach(traverseNode);
499
+ }
500
+ if (node.attributes && Array.isArray(node.attributes)) {
501
+ node.attributes.forEach(traverseNode);
502
+ }
503
+ if (node.values && Array.isArray(node.values)) {
504
+ node.values.forEach(traverseNode);
505
+ }
506
+ };
507
+
508
+ // Handle the array-based AST structure
509
+ if (Array.isArray(this.ast)) {
510
+ this.ast.forEach(traverseNode);
511
+ }
512
+ }
513
+
514
+
515
+ extractAttributeKeys(nodes) {
516
+
517
+ if (!Array.isArray(nodes)) {
518
+ throw new Error('Input must be an array of nodes');
519
+ }
520
+
521
+ return nodes
522
+ // Step 1: Filter only FieldAttribute nodes
523
+ .filter(node => node?.type === 'FieldAttribute')
524
+
525
+ // Step 2: Extract keys safely
526
+ .map(node => node?.key)
527
+
528
+ // Step 3: Remove any undefined/null keys
529
+ .filter(Boolean)
530
+
531
+ // Step 4: Remove potential duplicates (optional)
532
+ .filter((key, index, self) => self.indexOf(key) === index);
533
+ }
534
+
535
+
536
+ extractOptionValues(attributes) {
537
+ if (!Array.isArray(attributes)) {
538
+ throw new Error('Input must be an array of attributes');
539
+ }
540
+
541
+ // Get all option values from all OptionsAttributes and remove duplicates
542
+ const allOptions = attributes
543
+ .filter(attr => attr?.type === 'OptionsAttribute')
544
+ .flatMap(attr => attr.values || [])
545
+ .map(option => option?.value)
546
+ .filter(Boolean);
547
+
548
+ // Remove duplicates by using a Set
549
+ return [...new Set(allOptions)];
550
+ }
551
+
552
+
553
+
554
+ extractDependentValues(attributes) {
555
+ if (!Array.isArray(attributes)) {
556
+ throw new Error('Input must be an array of attributes');
557
+ }
558
+
559
+ return attributes
560
+ // Find the OptionsAttribute node
561
+ .filter(attr => attr?.type === 'OptionsAttribute' && attr?.key === 'dependents')
562
+ // Get the values array
563
+ .flatMap(attr => attr.values || [])
564
+ // Extract each option's value
565
+ .map(option => option?.value)
566
+ // Remove any undefined/null values
567
+ .filter(Boolean);
568
+ }
569
+
570
+
571
+ inputTypeResolver(fieldName, attributeKeys) {
572
+
573
+ // first option - handle dynamicSingleSelect
574
+ //console.log("CHK",fieldName);
575
+ if (fieldName.includes('-')) {
576
+ return "dynamicSingleSelect"; //
577
+ }
578
+
579
+
580
+
581
+ // second option - explicit field name directive
582
+ if (fieldName.includes(':')) {
583
+ const chunks = fieldName.split(':');
584
+ //console.log("LAPHA",chunks[1]);
585
+ return chunks[1]; // e.g., 'date' from 'dob:date'
586
+ }
587
+
588
+ // Third option - low code type definition
589
+ const matchedKey = attributeKeys.find(key => key in this.inputTypeMaps);
590
+ if (matchedKey) {
591
+ //console.log("HERE",matchedKey);
592
+ // console.log("HERE 2",this.inputTypeMaps[matchedKey]);
593
+
594
+ return this.inputTypeMaps[matchedKey];
595
+ }
596
+
597
+ // Fourth 3: inference with text fall back
598
+ return this.inferInputType(fieldName);
599
+
600
+
601
+ }
602
+
603
+
604
+ getOptionValuesByKey(attributes, targetKey) {
605
+ if (!Array.isArray(attributes)) throw new Error('Input must be an array of attributes');
606
+ if (!targetKey) throw new Error('Target key is required');
607
+
608
+ const optionsAttribute = attributes.find(
609
+ attr => attr?.type === 'OptionsAttribute' && attr?.key === targetKey
610
+ );
611
+
612
+ return optionsAttribute?.values
613
+ ? optionsAttribute.values
614
+ .map(option => option?.value)
615
+ .filter(Boolean)
616
+ .map(value => value.toLowerCase()) // Normalize to lowercase
617
+ : [];
618
+ }
619
+
620
+
621
+
622
+ buildDynamicSingleSelect(node, rawFieldName) {
623
+
624
+ const cleanString = this.cleanFieldName(rawFieldName);
625
+ const fieldName = cleanString.input_name;
626
+
627
+ let fieldSchema = [];
628
+ fieldSchema.push('dynamicSingleSelect',fieldName, this.toTitleCase(fieldName));
629
+
630
+ let validations;
631
+ let attributes;
632
+
633
+
634
+ //console.log("HERE",validations);
635
+ let inputParams;
636
+
637
+ if (node.attributes.length > 0 ) {
638
+ inputParams = this.handleAttributes(node.attributes);
639
+ //console.log("InputParams",inputParams);
640
+ } else {
641
+ inputParams = { validations: {}, attributes: {} }
642
+ }
643
+
644
+
645
+ //console.log("InputParams",inputParams);
646
+
647
+ validations = inputParams.validations;
648
+ attributes = inputParams.attributes;
649
+
650
+ if (this.isRequired(rawFieldName)) {
651
+ validations['required'] = true;
652
+
653
+ }
654
+
655
+
656
+ //console.log("VALS",validations);
657
+ //console.log("ATTRs",attributes);
658
+
659
+ fieldSchema.push(validations)
660
+ fieldSchema.push(attributes)
661
+
662
+ /// NOW BUILD SCENARIO (SELECT STATE) BLOCKS
663
+
664
+
665
+ /// E.G. Countries for which we want display states reactively
666
+ const optionValues = this.extractOptionValues(node.attributes);
667
+ //console.log("optionValues",optionValues);
668
+
669
+ let scenarioBlocks =[];
670
+
671
+
672
+ if (optionValues.length > 0 ) {
673
+
674
+ optionValues.forEach(option => {
675
+
676
+ let schema = {};
677
+
678
+ schema['id']= option.toLowerCase();
679
+ schema['label']= option;
680
+
681
+ /// add options now
682
+ const keyOptions = this.getOptionValuesByKey(node.attributes, option);
683
+ //console.log(keyOptions);
684
+ let options = [];
685
+
686
+ if (keyOptions.length > 0) {
687
+ keyOptions.forEach(subOption => {
688
+ options.push({value: subOption.toLowerCase(), label: this.toTitleCase(subOption)})
689
+ })
690
+ schema['options']= options;
691
+ scenarioBlocks.push(schema);
692
+
693
+ }
694
+
695
+
696
+
697
+ // now get option options
698
+ //scenarioBlock.push(schema);
699
+ fieldSchema.push(scenarioBlocks);
700
+
701
+
702
+ // options.push({value: option, label: this.toTitleCase(option)})
703
+ });
704
+
705
+ this.formSchema.push(fieldSchema);
706
+
707
+ //console.log("THERE",JSON.stringify(fieldSchema,null,2));
708
+
709
+ }
710
+
711
+
712
+
713
+ }
714
+
715
+
716
+
717
+
718
+
719
+ // Builder methods - implement these according to your needs
720
+ buildDirective(node) {
721
+ //console.log(`Processing FormDirective: ${node.name.value}`);
722
+
723
+ this.formParams['id'] = node.name.value;
724
+ // Handle directive specific logic
725
+ }
726
+
727
+ buildProperties(node) {
728
+ // console.log(`Processing FormProperties with ${node.properties.length} properties`);
729
+ }
730
+
731
+ buildProperty(node) {
732
+ //console.log(`Processing FormProperty: ${node.key.value} = ${node.value.value}`);
733
+ const key = node.key.value;
734
+ const val = node.value.values[0].value;
735
+
736
+ //console.log(node);
737
+
738
+ // if this is regular form attribute then it goes to formParams
739
+ if (this.formAttributes.includes(key)) {
740
+ this.formParams[key] = val
741
+ } else {
742
+
743
+ if (key === 'sendTo') {
744
+ const sendToEmails = node.value.values.map(option => option.value);
745
+ this.formSettings[key] = sendToEmails
746
+ } else
747
+ {
748
+ this.formSettings[key] = val
749
+ }
750
+
751
+ }
752
+
753
+
754
+ //console.log(this.formSettings);
755
+ //console.log(this.formParams);
756
+
757
+
758
+ }
759
+
760
+ buildFields(node) {
761
+ //console.log(`Processing FormFields with ${node.fields} fields`);
762
+
763
+
764
+
765
+
766
+ }
767
+
768
+
769
+
770
+ /* eslint-disable no-unused-vars */
771
+ /* eslint-disable no-useless-escape */
772
+ // The rest of the class definition from Part 1 goes here...
773
+ // ...
774
+ // ...
775
+
776
+ buildField(node) {
777
+ const rawFieldName = node.name;
778
+ const cleanString = this.cleanFieldName(rawFieldName);
779
+ const cleanFieldName = cleanString.input_name;
780
+
781
+ let fieldType;
782
+
783
+ if (cleanString.input_type) {
784
+ fieldType = this.cleanToInputType(cleanString.input_type);
785
+ } else {
786
+ let attributeKeys;
787
+ if (node.attributes.length > 0) {
788
+ attributeKeys = this.extractAttributeKeys(node.attributes);
789
+ fieldType = this.inputTypeResolver(cleanFieldName, attributeKeys);
790
+ } else {
791
+ fieldType = this.inferInputType(cleanFieldName);
792
+ }
793
+ }
794
+
795
+ if (fieldType === 'dynamicSingleSelect') {
796
+ this.buildDynamicSingleSelect(node, rawFieldName);
797
+ return;
798
+ }
799
+
800
+ const fieldSchema = [];
801
+ const fieldLabel = this.toTitleCase(cleanFieldName);
802
+
803
+ fieldSchema.push(fieldType, cleanFieldName, fieldLabel);
804
+
805
+ let validations = {};
806
+ let attributes = {};
807
+
808
+ let inputParams;
809
+ if (node.attributes.length > 0) {
810
+ inputParams = this.handleAttributes(node.attributes);
811
+ } else {
812
+ inputParams = { validations: {}, attributes: {} };
813
+ }
814
+
815
+ validations = inputParams.validations;
816
+ attributes = inputParams.attributes;
817
+
818
+ if (this.isRequired(rawFieldName)) {
819
+ validations['required'] = true;
820
+ }
821
+
822
+ fieldSchema.push(validations);
823
+ fieldSchema.push(attributes);
824
+
825
+ // Handle options for select, radio, checkbox fields
826
+ if (node.attributes.length > 0 && (fieldType === 'checkbox' || fieldType === 'radio' || fieldType === 'select' || fieldType === 'multipleSelect')) {
827
+ const getDefaultValue = (attributes) => {
828
+ console.log("AST", JSON.stringify(this.ast,null,2))
829
+
830
+
831
+ if (!Array.isArray(attributes)) {
832
+ console.debug("getDefaultValue: Input is not an array, returning null.");
833
+ return null;
834
+ }
835
+
836
+ // Look for FieldAttributes with 'selected' or 'default'
837
+ const selectedAttr = attributes.find(attr =>
838
+ attr?.type === "FieldAttribute" &&
839
+ (attr?.key === "selected" || attr?.key === "default")
840
+ );
841
+
842
+ // Look for OptionsAttributes with 'selected' or 'default'
843
+ const selectedOptionsAttr = attributes.find(attr =>
844
+ attr?.type === "OptionsAttribute" &&
845
+ (attr?.key === "selected" || attr?.key === "default")
846
+ );
847
+
848
+ console.debug("getDefaultValue: Found FieldAttribute:", selectedAttr);
849
+ console.debug("getDefaultValue: Found OptionsAttribute:", selectedOptionsAttr);
850
+
851
+ if (selectedAttr) {
852
+ // ... (existing logic)
853
+ // The previous logic was simplified, here is a more explicit check
854
+ const value = selectedAttr.value?.value?.toLowerCase() || selectedAttr.value?.toLowerCase();
855
+ console.debug(`getDefaultValue: Returning value from FieldAttribute: ${value}`);
856
+ return value;
857
+ }
858
+
859
+ if (selectedOptionsAttr && selectedOptionsAttr.values && selectedOptionsAttr.values.length > 0) {
860
+ const value = selectedOptionsAttr.values[0].value.toLowerCase();
861
+ console.debug(`getDefaultValue: Returning value from OptionsAttribute: ${value}`);
862
+ return value;
863
+ }
864
+
865
+ console.debug("getDefaultValue: No matching attribute found, returning null.");
866
+ return null;
867
+ };
868
+ const defaultValue = getDefaultValue(node.attributes);
869
+ console.log("defaultValue",defaultValue);
870
+ const optionValues = this.extractOptionValues(node.attributes);
871
+ let options = [];
872
+
873
+ if (optionValues.length > 0) {
874
+ optionValues.forEach(option => {
875
+ if (option.toLowerCase() === defaultValue) {
876
+ options.push({value: option.toLowerCase(), label: this.toTitleCase(option), selected: true});
877
+ } else {
878
+ options.push({value: option.toLowerCase(), label: this.toTitleCase(option)});
879
+ }
880
+ });
881
+ fieldSchema.push(options);
882
+ }
883
+ }
884
+
885
+ this.formSchema.push(fieldSchema);
886
+ }
887
+
888
+
889
+ /*
890
+ buildCheckboxField(node, rawFieldName) {
891
+ const cleanString = this.cleanFieldName(rawFieldName);
892
+ const fieldName = cleanString.input_name;
893
+ const fieldSchema = ['checkbox', fieldName, this.toTitleCase(fieldName)];
894
+
895
+ let validations = {};
896
+ let attributes = {};
897
+
898
+ // Extract options from all OptionsAttributes
899
+ const allOptions = [];
900
+
901
+ node.attributes.forEach(attr => {
902
+ if (attr.type === 'OptionsAttribute') {
903
+ const optionValues = attr.values.map(option => option.value);
904
+ allOptions.push(...optionValues);
905
+ }
906
+ });
907
+
908
+ // Build options array
909
+ const options = allOptions.map(option => ({
910
+ value: option.toLowerCase(),
911
+ label: this.toTitleCase(option)
912
+ }));
913
+
914
+ // Handle validations and attributes
915
+ if (node.attributes.length > 0) {
916
+ const inputParams = this.handleAttributes(node.attributes);
917
+ validations = inputParams.validations;
918
+ attributes = inputParams.attributes;
919
+ }
920
+
921
+ if (this.isRequired(rawFieldName)) {
922
+ validations['required'] = true;
923
+ }
924
+
925
+ fieldSchema.push(validations);
926
+ fieldSchema.push(attributes);
927
+ fieldSchema.push(options);
928
+
929
+ this.formSchema.push(fieldSchema);
930
+ }
931
+ */
932
+
933
+
934
+
935
+
936
+ handleAttributes(attributesAST) {
937
+ let validations = {};
938
+ let attributes = {};
939
+
940
+ attributesAST.forEach(attr => {
941
+ if (attr.type === 'FieldAttribute') {
942
+ const key = attr.key;
943
+ let value;
944
+
945
+ if (typeof attr.value === 'object') {
946
+ value = attr.value.value;
947
+ } else {
948
+ value = attr.value;
949
+ }
950
+
951
+ // Skip ignored keys (including selected/default which are handled separately)
952
+ if (this.ignoreAttributes.includes(key) || key === 'selected' || key === 'default') return;
953
+
954
+ // Categorize key as input attribute or validation
955
+ if (this.inputAttributes.includes(key)) {
956
+ attributes[key] = value;
957
+ } else if (this.validationAttributes.includes(key)) {
958
+ validations[key] = value;
959
+ }
960
+ }
961
+ });
962
+
963
+ // Handle dependents extraction
964
+ const getDependents = (attributesAST = []) => {
965
+ const dependentsAttr = attributesAST.find(attr => attr.key === "dependents");
966
+ if (!dependentsAttr) return [];
967
+
968
+ if (dependentsAttr.type === "OptionsAttribute" && dependentsAttr.values) {
969
+ return dependentsAttr.values
970
+ .map(option => option.value)
971
+ .filter(Boolean);
972
+ }
973
+
974
+ if (dependentsAttr.value !== undefined) {
975
+ if (Array.isArray(dependentsAttr.value)) {
976
+ return dependentsAttr.value.filter(Boolean);
977
+ }
978
+ const values = typeof dependentsAttr.value === 'string'
979
+ ? dependentsAttr.value.split(',').map(v => v.trim())
980
+ : [dependentsAttr.value];
981
+ return values.filter(Boolean);
982
+ }
983
+
984
+ return [];
985
+ };
986
+
987
+ let dependents = getDependents(attributesAST);
988
+ if (dependents.length > 0) {
989
+ attributes['dependents'] = dependents;
990
+ }
991
+
992
+ const getDependsOn = (attributesAST) => {
993
+ if (!Array.isArray(attributesAST)) return null;
994
+
995
+ const dependsOnAttr = attributesAST.find(
996
+ attr => attr?.type === "OptionsAttribute" && attr?.key === "dependsOn"
997
+ );
998
+
999
+ if (!dependsOnAttr?.values || dependsOnAttr.values.length < 2) {
1000
+ return null;
1001
+ }
1002
+
1003
+ return {
1004
+ dependsOnValue: dependsOnAttr.values[0]?.value || '',
1005
+ dependsOnCondition: dependsOnAttr.values[1]?.value || ''
1006
+ };
1007
+ };
1008
+
1009
+ const dependency = getDependsOn(attributesAST);
1010
+ if (dependency) {
1011
+ const dependsOnCondition = dependency.dependsOnCondition.toLowerCase();
1012
+ attributes['dependsOn'] = dependency.dependsOnValue;
1013
+ attributes['condition'] = `${dependsOnCondition}`;
1014
+ }
1015
+
1016
+ return {
1017
+ validations,
1018
+ attributes
1019
+ };
1020
+ }
1021
+
1022
+
1023
+
1024
+ buildOptionsAttribute(node) {
1025
+ //console.log(`Processing OptionsAttribute with ${node.values.length} values`);
1026
+ }
1027
+
1028
+ buildFieldAttribute(node) {
1029
+ //console.log(`Processing FieldAttribute: ${node.key} = ${node.value}`);
1030
+ }
1031
+
1032
+ buildOption(node) {
1033
+ //console.log(`Processing Option: ${node.value} (quoted: ${node.quoted})`);
1034
+ }
1035
+
1036
+ buildIdentifier(node) {
1037
+ // console.log(`Processing Identifier: ${node.value}`);
1038
+ }
1039
+
1040
+ buildStringLiteral(node) {
1041
+ // console.log(`Processing StringLiteral: "${node.value}"`);
1042
+ }
1043
+
1044
+
1045
+ addSubmit () {
1046
+
1047
+ this.formSchema.push(['submit','submit','Submit']);
1048
+
1049
+
1050
+ }
1051
+
1052
+ // class wrapper - nothing below this point
1053
+
1054
+ }