@syncfusion/ej2-react-form-renderer 0.1.2-beta → 34.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +88 -105
  2. package/diConfig.json +85 -0
  3. package/dist/ej2-react-form-renderer.min.js +10 -0
  4. package/dist/ej2-react-form-renderer.umd.min.js +11 -0
  5. package/dist/ej2-react-form-renderer.umd.min.js.map +1 -0
  6. package/dist/es6/ej2-react-form-renderer.es2015.js +37 -0
  7. package/dist/es6/ej2-react-form-renderer.es2015.js.map +1 -0
  8. package/form-renderer.d.ts +4 -0
  9. package/form-renderer.js +4 -0
  10. package/index.d.ts +2 -4
  11. package/index.js +2 -4
  12. package/license +6 -13
  13. package/package.json +19 -38
  14. package/src/form-renderer/formrenderer.component.d.ts +34 -0
  15. package/src/form-renderer/formrenderer.component.js +49 -0
  16. package/src/form-renderer/index.d.ts +1 -0
  17. package/src/form-renderer/index.js +1 -0
  18. package/src/global.js +1 -0
  19. package/src/index.d.ts +2 -6
  20. package/src/index.js +2 -6
  21. package/styles/bds-lite.css +1 -0
  22. package/styles/bds-lite.scss +1 -0
  23. package/styles/bds.css +1 -0
  24. package/styles/bds.scss +1 -0
  25. package/styles/bootstrap-dark-lite.css +1 -0
  26. package/styles/bootstrap-dark-lite.scss +1 -0
  27. package/styles/bootstrap-dark.css +1 -26
  28. package/styles/bootstrap-dark.scss +1 -0
  29. package/styles/bootstrap-lite.css +1 -0
  30. package/styles/bootstrap-lite.scss +1 -0
  31. package/styles/bootstrap.css +1 -26
  32. package/styles/bootstrap.scss +1 -0
  33. package/styles/bootstrap4-lite.css +1 -0
  34. package/styles/bootstrap4-lite.scss +1 -0
  35. package/styles/bootstrap4.css +1 -26
  36. package/styles/bootstrap4.scss +1 -0
  37. package/styles/bootstrap5-dark-lite.css +1 -0
  38. package/styles/bootstrap5-dark-lite.scss +1 -0
  39. package/styles/bootstrap5-dark.css +1 -26
  40. package/styles/bootstrap5-dark.scss +1 -0
  41. package/styles/bootstrap5-lite.css +1 -0
  42. package/styles/bootstrap5-lite.scss +1 -0
  43. package/styles/bootstrap5.3-lite.css +1 -0
  44. package/styles/bootstrap5.3-lite.scss +1 -0
  45. package/styles/bootstrap5.3.css +1 -26
  46. package/styles/bootstrap5.3.scss +1 -0
  47. package/styles/bootstrap5.css +1 -26
  48. package/styles/bootstrap5.scss +1 -0
  49. package/styles/fabric-dark-lite.css +1 -0
  50. package/styles/fabric-dark-lite.scss +1 -0
  51. package/styles/fabric-dark.css +1 -0
  52. package/styles/fabric-dark.scss +1 -0
  53. package/styles/fabric-lite.css +1 -0
  54. package/styles/fabric-lite.scss +1 -0
  55. package/styles/fabric.css +1 -0
  56. package/styles/fabric.scss +1 -0
  57. package/styles/fluent-dark-lite.css +1 -0
  58. package/styles/fluent-dark-lite.scss +1 -0
  59. package/styles/fluent-dark.css +1 -26
  60. package/styles/fluent-dark.scss +1 -0
  61. package/styles/fluent-lite.css +1 -0
  62. package/styles/fluent-lite.scss +1 -0
  63. package/styles/fluent.css +1 -26
  64. package/styles/fluent.scss +1 -0
  65. package/styles/fluent2-lite.css +1 -0
  66. package/styles/fluent2-lite.scss +1 -0
  67. package/styles/fluent2.css +1 -26
  68. package/styles/fluent2.scss +1 -0
  69. package/styles/form-renderer/bds.css +1 -0
  70. package/styles/form-renderer/bds.scss +1 -0
  71. package/styles/form-renderer/bootstrap-dark.css +1 -0
  72. package/styles/form-renderer/bootstrap-dark.scss +1 -0
  73. package/styles/form-renderer/bootstrap.css +1 -0
  74. package/styles/form-renderer/bootstrap.scss +1 -0
  75. package/styles/form-renderer/bootstrap4.css +1 -0
  76. package/styles/form-renderer/bootstrap4.scss +1 -0
  77. package/styles/form-renderer/bootstrap5-dark.css +1 -0
  78. package/styles/form-renderer/bootstrap5-dark.scss +1 -0
  79. package/styles/form-renderer/bootstrap5.3.css +1 -0
  80. package/styles/form-renderer/bootstrap5.3.scss +1 -0
  81. package/styles/form-renderer/bootstrap5.css +1 -0
  82. package/styles/form-renderer/bootstrap5.scss +1 -0
  83. package/styles/form-renderer/fabric-dark.css +1 -0
  84. package/styles/form-renderer/fabric-dark.scss +1 -0
  85. package/styles/form-renderer/fabric.css +1 -0
  86. package/styles/form-renderer/fabric.scss +1 -0
  87. package/styles/form-renderer/fluent-dark.css +1 -0
  88. package/styles/form-renderer/fluent-dark.scss +1 -0
  89. package/styles/form-renderer/fluent.css +1 -0
  90. package/styles/form-renderer/fluent.scss +1 -0
  91. package/styles/form-renderer/fluent2.css +1 -0
  92. package/styles/form-renderer/fluent2.scss +1 -0
  93. package/styles/form-renderer/highcontrast-light.css +1 -0
  94. package/styles/form-renderer/highcontrast-light.scss +1 -0
  95. package/styles/form-renderer/highcontrast.css +1 -0
  96. package/styles/form-renderer/highcontrast.scss +1 -0
  97. package/styles/form-renderer/material-dark.css +1 -0
  98. package/styles/form-renderer/material-dark.scss +1 -0
  99. package/styles/form-renderer/material.css +1 -0
  100. package/styles/form-renderer/material.scss +1 -0
  101. package/styles/form-renderer/material3-dark.css +1 -0
  102. package/styles/form-renderer/material3-dark.scss +1 -0
  103. package/styles/form-renderer/material3.css +1 -0
  104. package/styles/form-renderer/material3.scss +1 -0
  105. package/styles/form-renderer/tailwind-dark.css +1 -0
  106. package/styles/form-renderer/tailwind-dark.scss +1 -0
  107. package/styles/form-renderer/tailwind.css +1 -0
  108. package/styles/form-renderer/tailwind.scss +1 -0
  109. package/styles/form-renderer/tailwind3.css +1 -0
  110. package/styles/form-renderer/tailwind3.scss +1 -0
  111. package/styles/highcontrast-light-lite.css +1 -0
  112. package/styles/highcontrast-light-lite.scss +1 -0
  113. package/styles/highcontrast-light.css +1 -0
  114. package/styles/highcontrast-light.scss +1 -0
  115. package/styles/highcontrast-lite.css +1 -0
  116. package/styles/highcontrast-lite.scss +1 -0
  117. package/styles/highcontrast.css +1 -0
  118. package/styles/highcontrast.scss +1 -0
  119. package/styles/material-dark-lite.css +1 -0
  120. package/styles/material-dark-lite.scss +1 -0
  121. package/styles/material-dark.css +1 -26
  122. package/styles/material-dark.scss +1 -0
  123. package/styles/material-lite.css +1 -0
  124. package/styles/material-lite.scss +1 -0
  125. package/styles/material.css +1 -26
  126. package/styles/material.scss +1 -0
  127. package/styles/material3-dark-lite.css +1 -0
  128. package/styles/material3-dark-lite.scss +1 -0
  129. package/styles/material3-dark.css +1 -26
  130. package/styles/material3-dark.scss +1 -0
  131. package/styles/material3-lite.css +1 -0
  132. package/styles/material3-lite.scss +1 -0
  133. package/styles/material3.css +1 -26
  134. package/styles/material3.scss +1 -0
  135. package/styles/tailwind-dark-lite.css +1 -0
  136. package/styles/tailwind-dark-lite.scss +1 -0
  137. package/styles/tailwind-dark.css +1 -26
  138. package/styles/tailwind-dark.scss +1 -0
  139. package/styles/tailwind-lite.css +1 -0
  140. package/styles/tailwind-lite.scss +1 -0
  141. package/styles/tailwind.css +1 -26
  142. package/styles/tailwind.scss +1 -0
  143. package/styles/tailwind3-lite.css +1 -0
  144. package/styles/tailwind3-lite.scss +1 -0
  145. package/styles/tailwind3.css +1 -26
  146. package/styles/tailwind3.scss +1 -0
  147. package/src/CheckboxGroup.d.ts +0 -12
  148. package/src/CheckboxGroup.js +0 -49
  149. package/src/ConditionalRuleEngine.d.ts +0 -295
  150. package/src/ConditionalRuleEngine.js +0 -566
  151. package/src/CustomTabComponent.d.ts +0 -13
  152. package/src/CustomTabComponent.js +0 -35
  153. package/src/DataGridComponent.d.ts +0 -28
  154. package/src/DataGridComponent.js +0 -147
  155. package/src/FormRenderer.d.ts +0 -84
  156. package/src/FormRenderer.js +0 -707
  157. package/src/FormValidator.d.ts +0 -479
  158. package/src/FormValidator.js +0 -579
  159. package/src/MinMaxRuleEngine.d.ts +0 -95
  160. package/src/MinMaxRuleEngine.js +0 -361
  161. package/src/StaticHtmlRenderer.d.ts +0 -8
  162. package/src/StaticHtmlRenderer.js +0 -21
  163. package/src/expressions/DependencyTracker.d.ts +0 -30
  164. package/src/expressions/DependencyTracker.js +0 -128
  165. package/src/expressions/ExpressionEngine.d.ts +0 -81
  166. package/src/expressions/ExpressionEngine.js +0 -503
  167. package/src/expressions/ExpressionParser.d.ts +0 -39
  168. package/src/expressions/ExpressionParser.js +0 -270
  169. package/src/json-converter.d.ts +0 -171
  170. package/src/json-converter.js +0 -1209
  171. package/src/parser.d.ts +0 -99
  172. package/src/parser.js +0 -753
  173. package/src/renderFormField.d.ts +0 -21
  174. package/src/renderFormField.js +0 -226
  175. package/src/types/FormSchema.d.ts +0 -211
  176. package/src/types/FormSchema.js +0 -1
  177. package/src/utils.js +0 -408
  178. package/styles/bootstrap5.3-dark.css +0 -26
  179. package/styles/fluent2-dark.css +0 -26
  180. package/styles/form-renderer.base.css +0 -499
  181. package/styles/tailwind3-dark.css +0 -10
@@ -1,503 +0,0 @@
1
- import { ExpressionParser } from './ExpressionParser';
2
- import { DependencyTracker } from './DependencyTracker';
3
- import { Internationalization } from '@syncfusion/ej2-base';
4
- /**
5
- * ExpressionEngine - Core evaluation engine
6
- *
7
- * Safely evaluates user-defined expressions against form state
8
- * Supports field references, math operations, functions, and conditionals
9
- *
10
- * @spec Expression_Engine.md
11
- */
12
- export class ExpressionEngine {
13
- parser;
14
- dependencyTracker;
15
- evaluationCache = new Map();
16
- compiledFunctions = new Map();
17
- constructor() {
18
- this.parser = new ExpressionParser();
19
- this.dependencyTracker = new DependencyTracker();
20
- }
21
- /**
22
- * Evaluate expression against form values
23
- */
24
- evaluate(expression, formValues, options = {}) {
25
- try {
26
- if (!expression || expression.trim() === '') {
27
- return null;
28
- }
29
- const { timeout = 5000, maxDepth = 10, locale = 'en-US', throwOnError = false } = options;
30
- // Check cache
31
- const cacheKey = this.getCacheKey(expression, formValues);
32
- if (this.evaluationCache.has(cacheKey)) {
33
- return this.evaluationCache.get(cacheKey);
34
- }
35
- // Get or compile function
36
- let fn = this.compiledFunctions.get(expression);
37
- if (!fn) {
38
- fn = this.parser.compile(expression, {
39
- timeout,
40
- maxDepth,
41
- locale
42
- });
43
- this.compiledFunctions.set(expression, fn);
44
- }
45
- // Execute with context
46
- const context = this.buildContext(formValues);
47
- const startTime = Date.now();
48
- const result = fn(context);
49
- // Validate timeout
50
- if (Date.now() - startTime > timeout) {
51
- const error = new Error(`Expression evaluation exceeded timeout of ${timeout}ms`);
52
- if (throwOnError)
53
- throw error;
54
- console.warn(error);
55
- return null;
56
- }
57
- // Cache result
58
- this.evaluationCache.set(cacheKey, result);
59
- return result;
60
- }
61
- catch (error) {
62
- const message = error instanceof Error ? error.message : String(error);
63
- console.error('❌ Expression evaluation error:', message, { expression, formValues });
64
- if (options.throwOnError) {
65
- throw error;
66
- }
67
- return null;
68
- }
69
- }
70
- /**
71
- * Extract field names referenced in expression
72
- */
73
- extractDependencies(expression) {
74
- if (!expression)
75
- return [];
76
- return this.dependencyTracker.extractFieldReferences(expression);
77
- }
78
- /**
79
- * Build dependency graph for all expressions
80
- */
81
- buildDependencyGraph(components) {
82
- const graph = {};
83
- components.forEach(comp => {
84
- if (comp.expressionValue) {
85
- const deps = this.extractDependencies(comp.expressionValue);
86
- graph[comp.name] = deps;
87
- }
88
- });
89
- return graph;
90
- }
91
- /**
92
- * Detect circular dependencies
93
- */
94
- detectCircularDependencies(graph) {
95
- const visited = new Set();
96
- const recursionStack = new Set();
97
- const cycles = [];
98
- const visit = (node, path) => {
99
- visited.add(node);
100
- recursionStack.add(node);
101
- path.push(node);
102
- const deps = graph[node] || [];
103
- for (const dep of deps) {
104
- if (!visited.has(dep)) {
105
- visit(dep, [...path]);
106
- }
107
- else if (recursionStack.has(dep)) {
108
- const cycleStart = path.indexOf(dep);
109
- cycles.push([...path.slice(cycleStart), dep]);
110
- }
111
- }
112
- recursionStack.delete(node);
113
- };
114
- for (const node of Object.keys(graph)) {
115
- if (!visited.has(node)) {
116
- visit(node, []);
117
- }
118
- }
119
- return cycles;
120
- }
121
- /**
122
- * Get topological order for evaluation
123
- */
124
- getTopologicalOrder(graph) {
125
- const visited = new Set();
126
- const order = [];
127
- const visit = (node) => {
128
- if (visited.has(node))
129
- return;
130
- visited.add(node);
131
- const deps = graph[node] || [];
132
- for (const dep of deps) {
133
- visit(dep);
134
- }
135
- order.push(node);
136
- };
137
- for (const node of Object.keys(graph)) {
138
- visit(node);
139
- }
140
- return order;
141
- }
142
- /**
143
- * Clear internal caches
144
- */
145
- clearCache() {
146
- this.evaluationCache.clear();
147
- this.compiledFunctions.clear();
148
- }
149
- // Private helper methods
150
- getCacheKey(expression, formValues) {
151
- // Simple cache key (in production, use better hashing)
152
- return `${expression}:${JSON.stringify(formValues)}`;
153
- }
154
- buildContext(formValues) {
155
- let intl = new Internationalization();
156
- let dFormatter = intl.getDateFormat({
157
- format: 'MM/dd/yyyy',
158
- type: 'date'
159
- });
160
- return {
161
- // Field values
162
- ...formValues,
163
- // Constants
164
- PI: Math.PI,
165
- TRUE: true,
166
- FALSE: false,
167
- NULL: null,
168
- UNDEFINED: undefined,
169
- Math: Math,
170
- // Aggregate Functions
171
- SUM: (values) => Array.isArray(values) ? values.reduce((a, b) => Number(a) + Number(b), 0) : 0,
172
- AVG: (values) => {
173
- if (!Array.isArray(values) || values.length === 0)
174
- return 0;
175
- const sum = values.reduce((a, b) => Number(a) + Number(b), 0);
176
- return sum / values.length;
177
- },
178
- MIN: (values) => Array.isArray(values) && values.length > 0 ? Math.min(...values.map(Number)) : 0,
179
- MAX: (values) => Array.isArray(values) && values.length > 0 ? Math.max(...values.map(Number)) : 0,
180
- COUNT: (values) => Array.isArray(values) ? values.length : 0,
181
- // Math Functions
182
- ROUND: (value, decimals = 0) => Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals),
183
- FLOOR: Math.floor,
184
- CEIL: Math.ceil,
185
- ABS: Math.abs,
186
- SQRT: Math.sqrt,
187
- // String Functions
188
- UPPER: (text) => String(text).toUpperCase(),
189
- LOWER: (text) => String(text).toLowerCase(),
190
- CONCAT: (...strings) => strings.map(String).join(''),
191
- LENGTH: (text) => String(text).length,
192
- TRIM: (text) => String(text).trim(),
193
- SUBSTRING: (text, start, length) => length !== undefined
194
- ? String(text).substr(start, length)
195
- : String(text).substr(start),
196
- // Date Functions
197
- TODAY: () => {
198
- const value = dFormatter(new Date());
199
- return value;
200
- },
201
- AGE: (birthDate) => {
202
- const birth = typeof birthDate === 'string' ? new Date(birthDate) : birthDate;
203
- const today = new Date();
204
- let age = today.getFullYear() - birth.getFullYear();
205
- const monthDiff = today.getMonth() - birth.getMonth();
206
- if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
207
- age--;
208
- }
209
- return age;
210
- },
211
- DATEFORMAT: (date, format) => {
212
- const d = typeof date === 'string' ? new Date(date) : date;
213
- let formatter = intl.getDateFormat({
214
- format: format,
215
- type: 'date'
216
- });
217
- // Simple date format implementation
218
- return formatter(d);
219
- },
220
- DATEDIFF: (date1, date2) => {
221
- const d1 = typeof date1 === 'string' ? new Date(date1) : date1;
222
- const d2 = typeof date2 === 'string' ? new Date(date2) : date2;
223
- return Math.floor((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
224
- },
225
- // Add days to a date (for date arithmetic like {date} + 5)
226
- ADDDAYS: (date, days) => {
227
- const d = typeof date === 'string' ? new Date(date) : new Date(date);
228
- d.setDate(d.getDate() + Number(days));
229
- return dFormatter(d);
230
- },
231
- // Add days to a date (for date arithmetic like {date} + 5)
232
- SUBDAYS: (date, days) => {
233
- const d = typeof date === 'string' ? new Date(date) : new Date(date);
234
- d.setDate(d.getDate() - Number(days));
235
- return dFormatter(d);
236
- }
237
- };
238
- }
239
- }
240
- /**
241
- * ExpressionValidator - Validates expressions before runtime
242
- *
243
- * Checks syntax, validates field references, and detects circular dependencies
244
- *
245
- * @spec Expression_Engine.md
246
- */
247
- export class ExpressionValidator {
248
- supportedFunctions = [
249
- 'SUM', 'AVG', 'MIN', 'MAX', 'COUNT',
250
- 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'SQRT',
251
- 'UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'TRIM', 'SUBSTRING',
252
- 'TODAY', 'AGE', 'DATEFORMAT', 'DATEDIFF', 'ADDDAYS', 'SUBDAYS'
253
- ];
254
- keywords = ['PI', 'TRUE', 'FALSE', 'NULL', 'UNDEFINED', 'and', 'or', 'in'];
255
- /**
256
- * Validate expression against available fields
257
- */
258
- validate(expression, availableFields) {
259
- const errors = [];
260
- if (!expression || expression.trim() === '') {
261
- return errors; // Empty expression is valid
262
- }
263
- // Check syntax
264
- const syntaxErrors = this.validateSyntax(expression);
265
- errors.push(...syntaxErrors);
266
- // Check field references
267
- const fieldErrors = this.validateFieldReferences(expression, availableFields);
268
- errors.push(...fieldErrors);
269
- // Check function calls
270
- const funcErrors = this.validateFunctionCalls(expression);
271
- errors.push(...funcErrors);
272
- // Check field operations based on types
273
- const opErrors = this.validateFieldOperations(expression, availableFields);
274
- errors.push(...opErrors);
275
- return errors;
276
- }
277
- /**
278
- * Validate field operations - check for invalid operators on string/date types
279
- */
280
- validateFieldOperations(expression, availableFields) {
281
- const errors = [];
282
- // Skip if not field objects with type info
283
- if (!Array.isArray(availableFields) || availableFields.length === 0) {
284
- return errors;
285
- }
286
- const firstField = availableFields[0];
287
- if (typeof firstField === 'string') {
288
- return errors; // No type information available
289
- }
290
- // Build field map from objects
291
- const fieldMap = new Map();
292
- availableFields.forEach(field => {
293
- if (field && field.name) {
294
- fieldMap.set(field.name, field);
295
- }
296
- });
297
- if (fieldMap.size === 0)
298
- return errors;
299
- // Track reported operator positions to avoid duplicates
300
- const reportedOperators = new Set();
301
- // Check each field reference for invalid operations
302
- const fieldRefPattern = /(\{[^}]+\})/g;
303
- let match;
304
- // Invalid operators for string fields (allow only + for concatenation)
305
- const stringInvalidOps = ['-', '*', '/', '%', '^'];
306
- // Operators invalid for date fields (allow + for date arithmetic with numbers)
307
- const dateInvalidOps = ['-', '*', '/', '%', '^', '+'];
308
- while ((match = fieldRefPattern.exec(expression)) !== null) {
309
- const fieldRef = match[0];
310
- const fieldName = fieldRef.slice(1, -1); // Remove { }
311
- const field = fieldMap.get(fieldName);
312
- if (!field)
313
- continue;
314
- const fieldIndex = match.index;
315
- const fieldEndIndex = fieldIndex + fieldRef.length;
316
- // Get context around field reference (including spaces)
317
- const textBefore = expression.substring(Math.max(0, fieldIndex - 10), fieldIndex);
318
- const textAfter = expression.substring(fieldEndIndex, Math.min(expression.length, fieldEndIndex + 10));
319
- // Extract the operator if present (skip spaces)
320
- const operatorBefore = textBefore.trimEnd().slice(-1);
321
- const operatorAfter = textAfter.trimStart().charAt(0);
322
- // Calculate actual position of operators in the original expression
323
- const operatorBeforePos = fieldIndex - (textBefore.length - textBefore.trimEnd().length) - 1;
324
- const operatorAfterPos = fieldEndIndex + (textAfter.length - textAfter.trimStart().length);
325
- // For STRING fields (without format)
326
- if (field.type === 'string' && !field.format) {
327
- if (operatorBefore && stringInvalidOps.includes(operatorBefore)) {
328
- const key = `${operatorBeforePos}:${operatorBefore}`;
329
- if (!reportedOperators.has(key)) {
330
- errors.push({
331
- type: 'InvalidOperation',
332
- message: `Cannot perform '${operatorBefore}' operation on string field '{${fieldName}}'`
333
- });
334
- reportedOperators.add(key);
335
- }
336
- }
337
- if (operatorAfter && stringInvalidOps.includes(operatorAfter)) {
338
- const key = `${operatorAfterPos}:${operatorAfter}`;
339
- if (!reportedOperators.has(key)) {
340
- errors.push({
341
- type: 'InvalidOperation',
342
- message: `Cannot perform '${operatorAfter}' operation on string field '{${fieldName}}'`
343
- });
344
- reportedOperators.add(key);
345
- }
346
- }
347
- }
348
- // For DATE fields - allow + with numbers, reject other operators
349
- if (field.format === 'date' || field.format === 'date-time' || field.format === 'time') {
350
- if (operatorBefore && dateInvalidOps.includes(operatorBefore)) {
351
- const key = `${operatorBeforePos}:${operatorBefore}`;
352
- if (!reportedOperators.has(key)) {
353
- errors.push({
354
- type: 'InvalidOperation',
355
- message: `Cannot perform '${operatorBefore}' operation on date field '{${fieldName}}'`
356
- });
357
- reportedOperators.add(key);
358
- }
359
- }
360
- if (operatorAfter && dateInvalidOps.includes(operatorAfter)) {
361
- const key = `${operatorAfterPos}:${operatorAfter}`;
362
- if (!reportedOperators.has(key)) {
363
- errors.push({
364
- type: 'InvalidOperation',
365
- message: `Cannot perform '${operatorAfter}' operation on date field '{${fieldName}}'`
366
- });
367
- reportedOperators.add(key);
368
- }
369
- }
370
- // Allow + only when combined with numbers or number fields
371
- if (operatorAfter === '+') {
372
- const operandAfter = textAfter.trimStart().substring(1).trim();
373
- const isNumberField = /^\{[^}]+\}/.test(operandAfter) && fieldMap.get(operandAfter.slice(1, -1))?.type === 'number';
374
- const isNumberLiteral = /^\d+/.test(operandAfter);
375
- if (!isNumberField && !isNumberLiteral) {
376
- const key = `${operatorAfterPos}:${operatorAfter}`;
377
- if (!reportedOperators.has(key)) {
378
- errors.push({
379
- type: 'InvalidOperation',
380
- message: `Date field '{${fieldName}}' can only be used with +number or +{numberField}, not '${operatorAfter}${operandAfter.substring(0, 10)}'`
381
- });
382
- reportedOperators.add(key);
383
- }
384
- }
385
- }
386
- if (operatorBefore === '+') {
387
- const operandBefore = textBefore.trimEnd().substring(0, textBefore.trimEnd().length - 1).trim();
388
- const isNumberField = /\{[^}]+\}$/.test(operandBefore) && fieldMap.get(operandBefore.slice(operandBefore.lastIndexOf('{') + 1, -1))?.type === 'number';
389
- const isNumberLiteral = /\d+$/.test(operandBefore);
390
- if (!isNumberField && !isNumberLiteral) {
391
- const key = `${operatorBeforePos}:${operatorBefore}`;
392
- if (!reportedOperators.has(key)) {
393
- errors.push({
394
- type: 'InvalidOperation',
395
- message: `Date field '{${fieldName}}' can only be used with +number or +{numberField}`
396
- });
397
- reportedOperators.add(key);
398
- }
399
- }
400
- }
401
- }
402
- }
403
- return errors;
404
- }
405
- validateSyntax(expression) {
406
- const errors = [];
407
- // Check balanced braces
408
- const openBraces = (expression.match(/\{/g) || []).length;
409
- const closeBraces = (expression.match(/\}/g) || []).length;
410
- if (openBraces !== closeBraces) {
411
- errors.push({
412
- type: 'SyntaxError',
413
- message: 'Unbalanced braces in expression'
414
- });
415
- }
416
- // Check balanced parentheses
417
- const openParens = (expression.match(/\(/g) || []).length;
418
- const closeParens = (expression.match(/\)/g) || []).length;
419
- if (openParens !== closeParens) {
420
- errors.push({
421
- type: 'SyntaxError',
422
- message: 'Unbalanced parentheses in expression'
423
- });
424
- }
425
- // Check balanced $ brackets
426
- const openDollar = (expression.match(/\$\{/g) || []).length;
427
- const closeDollar = (expression.match(/\}/g) || []).length;
428
- if (openDollar > closeDollar) {
429
- errors.push({
430
- type: 'SyntaxError',
431
- message: 'Unbalanced ${ } in expression'
432
- });
433
- }
434
- return errors;
435
- }
436
- validateFieldReferences(expression, availableFields) {
437
- const errors = [];
438
- // Skip field validation if availableFields is not properly populated
439
- // This allows expressions to work at runtime even if we can't validate field names at editor time
440
- if (!availableFields || availableFields.length === 0) {
441
- return errors;
442
- }
443
- const fieldRefPattern = /\{([^{}]+)\}/g;
444
- let match;
445
- // Handle both string[] and AvailableField[] formats
446
- let availableFieldsSet;
447
- if (typeof availableFields[0] === 'string') {
448
- availableFieldsSet = new Set(availableFields);
449
- }
450
- else {
451
- availableFieldsSet = new Set(availableFields.map(f => f.name));
452
- }
453
- while ((match = fieldRefPattern.exec(expression)) !== null) {
454
- const ref = match[1].trim();
455
- // Skip if it's a function call
456
- if (this.isFunctionCall(ref))
457
- continue;
458
- // Skip if it's a math expression (contains operators or numbers)
459
- // This distinguishes between {fieldName} and ${5 + 3}
460
- if (this.isMathExpression(ref))
461
- continue;
462
- // Extract field name from nested paths (e.g., "customer.name" -> "customer")
463
- const fieldName = ref.split(/[.\[\]]/)[0].trim();
464
- // Check if field exists
465
- if (fieldName && !this.keywords.includes(fieldName.toUpperCase())) {
466
- if (!availableFieldsSet.has(fieldName)) {
467
- errors.push({
468
- type: 'UndefinedField',
469
- message: `Field '${fieldName}' is not defined in the form`,
470
- field: fieldName
471
- });
472
- }
473
- }
474
- }
475
- return errors;
476
- }
477
- validateFunctionCalls(expression) {
478
- const errors = [];
479
- const funcPattern = /(\w+)\s*\(/g;
480
- let match;
481
- while ((match = funcPattern.exec(expression)) !== null) {
482
- const funcName = match[1];
483
- // Only validate uppercase functions (user-defined lowercase functions might be allowed)
484
- if (funcName === funcName.toUpperCase() && !this.supportedFunctions.includes(funcName)) {
485
- errors.push({
486
- type: 'InvalidFunction',
487
- message: `Unknown function: '${funcName}'. Supported functions: ${this.supportedFunctions.join(', ')}`
488
- });
489
- }
490
- }
491
- return errors;
492
- }
493
- isFunctionCall(ref) {
494
- return /^\w+\s*\(/.test(ref);
495
- }
496
- isMathExpression(ref) {
497
- // Check if the expression contains math operators, numbers, or other non-field-name patterns
498
- // Math expressions like: "5 + 3", "qty * 2", "age > 18", "sum([1,2,3])"
499
- // should be skipped, while field references like "fieldName" or "customer.name" should be validated
500
- const mathOperatorPattern = /[\+\-\*\/\%\=\<\>\!\&\|\^\~\s\d]/;
501
- return mathOperatorPattern.test(ref);
502
- }
503
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Field type information for validation
3
- */
4
- export interface AvailableField {
5
- name: string;
6
- type: string;
7
- format?: string;
8
- }
9
- /**
10
- * ExpressionParser - Parse and compile expressions
11
- *
12
- * Converts string expressions into safe, compiled functions
13
- * Supports field references, operators, functions, and conditionals
14
- *
15
- * @spec Expression_Engine.md
16
- */
17
- export declare class ExpressionParser {
18
- private supportedFunctions;
19
- private availableFields;
20
- /**
21
- * Initialize parser with available fields for type validation
22
- */
23
- setAvailableFields(fields: AvailableField[]): void;
24
- private validateFieldOperations;
25
- /**
26
- * Validate expression syntax
27
- */
28
- validate(expression: string): {
29
- valid: boolean;
30
- errors: string[];
31
- };
32
- /**
33
- * Compile expression to executable function
34
- */
35
- compile(expression: string, options?: any): Function;
36
- private compileMatrixExpressions;
37
- private compileBareFunctionCalls;
38
- private compileFieldReferences;
39
- }