@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.
- package/LowCodeParser.js +1551 -0
- package/astToFormique.js +1128 -0
- package/formique-semantq.js +682 -527
- package/package.json +1 -1
package/astToFormique.js
ADDED
|
@@ -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
|
+
}
|