@halleyassist/rule-templater 0.0.9 → 0.0.11
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/README.md +10 -2
- package/dist/rule-templater.browser.js +165 -53
- package/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/RuleTemplater.js +38 -20
- package/src/RuleTemplater.production.js +38 -20
- package/src/TemplateFilters.js +127 -33
package/README.md
CHANGED
|
@@ -121,6 +121,8 @@ const prepared = parsed.prepare({
|
|
|
121
121
|
- **round**: Round number to nearest integer
|
|
122
122
|
- **floor**: Round number down
|
|
123
123
|
- **ceil**: Round number up
|
|
124
|
+
- **time_start**: Extract `from` from `time period` / `time period ago` and convert to `time value`
|
|
125
|
+
- **time_end**: Extract `to` from `time period` / `time period ago` and convert to `time value`
|
|
124
126
|
|
|
125
127
|
#### Filter Examples
|
|
126
128
|
|
|
@@ -137,6 +139,9 @@ const prepared = parsed.prepare({
|
|
|
137
139
|
|
|
138
140
|
// Chaining filters
|
|
139
141
|
'${text|trim|upper}' with text=' hello ' → HELLO
|
|
142
|
+
|
|
143
|
+
// Time period conversion
|
|
144
|
+
'${window|time_start}' with window={from:'08:00',to:'12:00'} → 08:00
|
|
140
145
|
```
|
|
141
146
|
|
|
142
147
|
## API
|
|
@@ -222,13 +227,16 @@ Helper method to validate that an AST node matches the expected variable type.
|
|
|
222
227
|
|
|
223
228
|
Access to the filter functions used by the template engine. Can be extended with custom filters.
|
|
224
229
|
|
|
230
|
+
Custom filters receive a cloned `varData` object (`{ value, type }`) used for template rendering. They can mutate both fields without mutating the original input variables.
|
|
231
|
+
|
|
225
232
|
**Example:**
|
|
226
233
|
```javascript
|
|
227
234
|
const RuleTemplate = require('@halleyassist/rule-templater');
|
|
228
235
|
|
|
229
236
|
// Add a custom filter
|
|
230
|
-
RuleTemplate.TemplateFilters.reverse = (
|
|
231
|
-
|
|
237
|
+
RuleTemplate.TemplateFilters.reverse = (varData) => {
|
|
238
|
+
varData.value = String(varData.value).split('').reverse().join('');
|
|
239
|
+
return varData;
|
|
232
240
|
};
|
|
233
241
|
|
|
234
242
|
// Use the custom filter
|
|
@@ -3663,6 +3663,7 @@ const VariableTypes = [
|
|
|
3663
3663
|
'boolean',
|
|
3664
3664
|
'object',
|
|
3665
3665
|
'time period',
|
|
3666
|
+
'time period ago',
|
|
3666
3667
|
'time value',
|
|
3667
3668
|
'string array',
|
|
3668
3669
|
'number array',
|
|
@@ -3675,6 +3676,7 @@ const AllowedTypeMapping = {
|
|
|
3675
3676
|
'number': ['number_atom', 'math_expr'],
|
|
3676
3677
|
'boolean': ['boolean_atom', 'boolean_expr'],
|
|
3677
3678
|
'time period': ['time_period_atom'],
|
|
3679
|
+
'time period ago': ['time_period_atom'],
|
|
3678
3680
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
3679
3681
|
'string array': ['string_array'],
|
|
3680
3682
|
'number array': ['number_array'],
|
|
@@ -3697,7 +3699,8 @@ for(const rule of TemplateGrammar){
|
|
|
3697
3699
|
// Add template_value as an alternative to value_atom so templates can be parsed
|
|
3698
3700
|
const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
|
|
3699
3701
|
if (valueAtomIdx !== -1) {
|
|
3700
|
-
extendedGrammar[valueAtomIdx].
|
|
3702
|
+
extendedGrammar[valueAtomIdx] = Object.assign({}, extendedGrammar[valueAtomIdx]);
|
|
3703
|
+
extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
|
|
3701
3704
|
}
|
|
3702
3705
|
|
|
3703
3706
|
// Export the parser rules for potential external use
|
|
@@ -3984,12 +3987,12 @@ class RuleTemplate {
|
|
|
3984
3987
|
throw new Error(`Variable '${varName}' not provided in variables object`);
|
|
3985
3988
|
}
|
|
3986
3989
|
|
|
3987
|
-
|
|
3990
|
+
let varData = variables[varName];
|
|
3988
3991
|
if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
|
|
3989
3992
|
throw new Error(`Variable '${varName}' must be an object with 'value' property`);
|
|
3990
3993
|
}
|
|
3991
|
-
|
|
3992
|
-
|
|
3994
|
+
|
|
3995
|
+
varData = Object.assign({}, varData);
|
|
3993
3996
|
|
|
3994
3997
|
// Require type property for all variables
|
|
3995
3998
|
if (!varData.hasOwnProperty('type')) {
|
|
@@ -3997,8 +4000,8 @@ class RuleTemplate {
|
|
|
3997
4000
|
}
|
|
3998
4001
|
|
|
3999
4002
|
// Validate type
|
|
4000
|
-
if (!VariableTypes.includes(type)) {
|
|
4001
|
-
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
4003
|
+
if (!VariableTypes.includes(varData.type)) {
|
|
4004
|
+
throw new Error(`Invalid variable type '${varData.type}' for variable '${varName}'`);
|
|
4002
4005
|
}
|
|
4003
4006
|
|
|
4004
4007
|
// Apply filters if present
|
|
@@ -4007,27 +4010,42 @@ class RuleTemplate {
|
|
|
4007
4010
|
if (!TemplateFilters[filterName]) {
|
|
4008
4011
|
throw new Error(`Unknown filter '${filterName}'`);
|
|
4009
4012
|
}
|
|
4010
|
-
|
|
4013
|
+
|
|
4014
|
+
TemplateFilters[filterName](varData);
|
|
4011
4015
|
}
|
|
4012
|
-
// After applying filters, the result is already a string representation
|
|
4013
|
-
return String(value);
|
|
4014
4016
|
}
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
+
|
|
4018
|
+
return this._serializeVarData(varData, varName);
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
_serializeVarData(varData, varName) {
|
|
4022
|
+
const { value, type } = varData;
|
|
4023
|
+
|
|
4024
|
+
if (!VariableTypes.includes(type)) {
|
|
4025
|
+
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4017
4028
|
if (type === 'string') {
|
|
4018
|
-
// Escape backslashes first, then quotes in string values.
|
|
4019
|
-
// Order is critical: escaping backslashes first prevents double-escaping.
|
|
4020
|
-
// E.g., "test\" becomes "test\\" then "test\\\"" (correct)
|
|
4021
|
-
// If reversed, "test\" would become "test\\"" then "test\\\\"" (incorrect)
|
|
4022
4029
|
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
4023
|
-
}
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
if (type === 'number') {
|
|
4024
4033
|
return String(value);
|
|
4025
|
-
}
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
if (type === 'boolean') {
|
|
4026
4037
|
return value ? 'true' : 'false';
|
|
4027
|
-
} else {
|
|
4028
|
-
// Default behavior - just insert the value as-is
|
|
4029
|
-
return String(value);
|
|
4030
4038
|
}
|
|
4039
|
+
|
|
4040
|
+
if (type === 'time period' || type === 'time period ago') {
|
|
4041
|
+
let ret = `${value.from} TO ${value.to}`;
|
|
4042
|
+
if(value.ago) {
|
|
4043
|
+
ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
|
|
4044
|
+
}
|
|
4045
|
+
return ret;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
return String(value);
|
|
4031
4049
|
}
|
|
4032
4050
|
|
|
4033
4051
|
/**
|
|
@@ -4062,59 +4080,153 @@ They are applied in the template syntax as ${variable|filter} or ${variable|filt
|
|
|
4062
4080
|
*/
|
|
4063
4081
|
const TemplateFilters = {
|
|
4064
4082
|
// Convert value to JSON string representation
|
|
4065
|
-
string:
|
|
4066
|
-
|
|
4083
|
+
string: varData => {
|
|
4084
|
+
varData.value = String(varData.value);
|
|
4085
|
+
varData.type = 'string';
|
|
4086
|
+
|
|
4087
|
+
},
|
|
4088
|
+
|
|
4067
4089
|
// Convert to uppercase
|
|
4068
|
-
upper:
|
|
4069
|
-
|
|
4090
|
+
upper: varData => {
|
|
4091
|
+
varData.value = String(varData.value).toUpperCase();
|
|
4092
|
+
varData.type = 'string';
|
|
4093
|
+
|
|
4094
|
+
},
|
|
4095
|
+
|
|
4070
4096
|
// Convert to lowercase
|
|
4071
|
-
lower:
|
|
4072
|
-
|
|
4097
|
+
lower: varData => {
|
|
4098
|
+
varData.value = String(varData.value).toLowerCase();
|
|
4099
|
+
varData.type = 'string';
|
|
4100
|
+
|
|
4101
|
+
},
|
|
4102
|
+
|
|
4073
4103
|
// Capitalize first letter
|
|
4074
|
-
capitalize:
|
|
4075
|
-
const str = String(value);
|
|
4076
|
-
|
|
4104
|
+
capitalize: varData => {
|
|
4105
|
+
const str = String(varData.value);
|
|
4106
|
+
varData.value = str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
4107
|
+
varData.type = 'string';
|
|
4108
|
+
|
|
4077
4109
|
},
|
|
4078
|
-
|
|
4110
|
+
|
|
4079
4111
|
// Convert to title case
|
|
4080
|
-
title:
|
|
4081
|
-
|
|
4112
|
+
title: varData => {
|
|
4113
|
+
varData.value = String(varData.value).split(' ')
|
|
4082
4114
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
4083
4115
|
.join(' ');
|
|
4116
|
+
varData.type = 'string';
|
|
4117
|
+
|
|
4084
4118
|
},
|
|
4085
|
-
|
|
4119
|
+
|
|
4086
4120
|
// Trim whitespace
|
|
4087
|
-
trim:
|
|
4088
|
-
|
|
4121
|
+
trim: varData => {
|
|
4122
|
+
varData.value = String(varData.value).trim();
|
|
4123
|
+
varData.type = 'string';
|
|
4124
|
+
|
|
4125
|
+
},
|
|
4126
|
+
|
|
4089
4127
|
// Convert to number
|
|
4090
|
-
number:
|
|
4091
|
-
|
|
4128
|
+
number: varData => {
|
|
4129
|
+
varData.value = Number(varData.value);
|
|
4130
|
+
varData.type = 'number';
|
|
4131
|
+
|
|
4132
|
+
},
|
|
4133
|
+
|
|
4092
4134
|
// Convert to boolean
|
|
4093
|
-
boolean:
|
|
4094
|
-
|
|
4135
|
+
boolean: varData => {
|
|
4136
|
+
const value = varData.value;
|
|
4137
|
+
if (typeof value === 'boolean') {
|
|
4138
|
+
varData.type = 'boolean';
|
|
4139
|
+
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4095
4142
|
if (typeof value === 'string') {
|
|
4096
4143
|
const lower = value.toLowerCase();
|
|
4097
|
-
if (lower === 'true' || lower === '1' || lower === 'yes')
|
|
4098
|
-
|
|
4144
|
+
if (lower === 'true' || lower === '1' || lower === 'yes') {
|
|
4145
|
+
varData.value = true;
|
|
4146
|
+
varData.type = 'boolean';
|
|
4147
|
+
|
|
4148
|
+
}
|
|
4149
|
+
if (lower === 'false' || lower === '0' || lower === 'no') {
|
|
4150
|
+
varData.value = false;
|
|
4151
|
+
varData.type = 'boolean';
|
|
4152
|
+
|
|
4153
|
+
}
|
|
4099
4154
|
}
|
|
4100
|
-
|
|
4155
|
+
|
|
4156
|
+
varData.value = Boolean(value);
|
|
4157
|
+
varData.type = 'boolean';
|
|
4158
|
+
|
|
4101
4159
|
},
|
|
4102
|
-
|
|
4160
|
+
|
|
4103
4161
|
// Convert to absolute value (for numbers)
|
|
4104
|
-
abs:
|
|
4105
|
-
|
|
4162
|
+
abs: varData => {
|
|
4163
|
+
varData.value = Math.abs(Number(varData.value));
|
|
4164
|
+
varData.type = 'number';
|
|
4165
|
+
|
|
4166
|
+
},
|
|
4167
|
+
|
|
4106
4168
|
// Round number
|
|
4107
|
-
round:
|
|
4108
|
-
|
|
4169
|
+
round: varData => {
|
|
4170
|
+
varData.value = Math.round(Number(varData.value));
|
|
4171
|
+
varData.type = 'number';
|
|
4172
|
+
|
|
4173
|
+
},
|
|
4174
|
+
|
|
4109
4175
|
// Floor number
|
|
4110
|
-
floor:
|
|
4111
|
-
|
|
4176
|
+
floor: varData => {
|
|
4177
|
+
varData.value = Math.floor(Number(varData.value));
|
|
4178
|
+
varData.type = 'number';
|
|
4179
|
+
|
|
4180
|
+
},
|
|
4181
|
+
|
|
4112
4182
|
// Ceil number
|
|
4113
|
-
ceil:
|
|
4114
|
-
|
|
4183
|
+
ceil: varData => {
|
|
4184
|
+
varData.value = Math.ceil(Number(varData.value));
|
|
4185
|
+
varData.type = 'number';
|
|
4186
|
+
|
|
4187
|
+
},
|
|
4188
|
+
|
|
4115
4189
|
// Default value if empty/null/undefined
|
|
4116
|
-
default: (
|
|
4117
|
-
|
|
4190
|
+
default: (varData, defaultValue = '') => {
|
|
4191
|
+
varData.value = (varData.value === null || varData.value === undefined || varData.value === '') ? defaultValue : varData.value;
|
|
4192
|
+
if (typeof varData.value === 'string') {
|
|
4193
|
+
varData.type = 'string';
|
|
4194
|
+
} else if (typeof varData.value === 'number') {
|
|
4195
|
+
varData.type = 'number';
|
|
4196
|
+
} else if (typeof varData.value === 'boolean') {
|
|
4197
|
+
varData.type = 'boolean';
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
},
|
|
4201
|
+
|
|
4202
|
+
// Extract start time from time period/time period ago as time value
|
|
4203
|
+
time_start: varData => {
|
|
4204
|
+
if (varData.type === 'time period' || varData.type === 'time period ago') {
|
|
4205
|
+
if (!varData.value || typeof varData.value !== 'object' || !Object.prototype.hasOwnProperty.call(varData.value, 'from')) {
|
|
4206
|
+
throw new Error('time_start filter requires value.from for time period types');
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
varData.value = varData.value.from;
|
|
4210
|
+
varData.type = 'time value';
|
|
4211
|
+
return;
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
throw new Error('time_start filter requires variable type to be \"time period\" or \"time period ago\"');
|
|
4215
|
+
},
|
|
4216
|
+
|
|
4217
|
+
// Extract end time from time period/time period ago as time value
|
|
4218
|
+
time_end: varData => {
|
|
4219
|
+
if (varData.type === 'time period' || varData.type === 'time period ago') {
|
|
4220
|
+
if (!varData.value || typeof varData.value !== 'object' || !Object.prototype.hasOwnProperty.call(varData.value, 'to')) {
|
|
4221
|
+
throw new Error('time_end filter requires value.from for time period types');
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
varData.value = varData.value.to;
|
|
4225
|
+
varData.type = 'time value';
|
|
4226
|
+
return;
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
throw new Error('time_end filter requires variable type to be \"time period\" or \"time period ago\"');
|
|
4118
4230
|
}
|
|
4119
4231
|
}
|
|
4120
4232
|
|
package/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface ASTNode {
|
|
|
30
30
|
[key: string]: any;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export type FilterFunction = (
|
|
33
|
+
export type FilterFunction = (varData: VariableValue, ...args: any[]) => VariableValue | any;
|
|
34
34
|
|
|
35
35
|
export interface TemplateFiltersType {
|
|
36
36
|
string: FilterFunction;
|
|
@@ -46,6 +46,7 @@ export interface TemplateFiltersType {
|
|
|
46
46
|
floor: FilterFunction;
|
|
47
47
|
ceil: FilterFunction;
|
|
48
48
|
default: FilterFunction;
|
|
49
|
+
time_start: FilterFunction;
|
|
49
50
|
[key: string]: FilterFunction;
|
|
50
51
|
}
|
|
51
52
|
|
package/package.json
CHANGED
package/src/RuleTemplater.js
CHANGED
|
@@ -13,6 +13,7 @@ const VariableTypes = [
|
|
|
13
13
|
'boolean',
|
|
14
14
|
'object',
|
|
15
15
|
'time period',
|
|
16
|
+
'time period ago',
|
|
16
17
|
'time value',
|
|
17
18
|
'string array',
|
|
18
19
|
'number array',
|
|
@@ -25,6 +26,7 @@ const AllowedTypeMapping = {
|
|
|
25
26
|
'number': ['number_atom', 'math_expr'],
|
|
26
27
|
'boolean': ['boolean_atom', 'boolean_expr'],
|
|
27
28
|
'time period': ['time_period_atom'],
|
|
29
|
+
'time period ago': ['time_period_atom'],
|
|
28
30
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
29
31
|
'string array': ['string_array'],
|
|
30
32
|
'number array': ['number_array'],
|
|
@@ -47,7 +49,8 @@ for(const rule of TemplateGrammar){
|
|
|
47
49
|
// Add template_value as an alternative to value_atom so templates can be parsed
|
|
48
50
|
const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
|
|
49
51
|
if (valueAtomIdx !== -1) {
|
|
50
|
-
extendedGrammar[valueAtomIdx].
|
|
52
|
+
extendedGrammar[valueAtomIdx] = Object.assign({}, extendedGrammar[valueAtomIdx]);
|
|
53
|
+
extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
// Export the parser rules for potential external use
|
|
@@ -334,12 +337,12 @@ class RuleTemplate {
|
|
|
334
337
|
throw new Error(`Variable '${varName}' not provided in variables object`);
|
|
335
338
|
}
|
|
336
339
|
|
|
337
|
-
|
|
340
|
+
let varData = variables[varName];
|
|
338
341
|
if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
|
|
339
342
|
throw new Error(`Variable '${varName}' must be an object with 'value' property`);
|
|
340
343
|
}
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
|
|
345
|
+
varData = Object.assign({}, varData);
|
|
343
346
|
|
|
344
347
|
// Require type property for all variables
|
|
345
348
|
if (!varData.hasOwnProperty('type')) {
|
|
@@ -347,8 +350,8 @@ class RuleTemplate {
|
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
// Validate type
|
|
350
|
-
if (!VariableTypes.includes(type)) {
|
|
351
|
-
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
353
|
+
if (!VariableTypes.includes(varData.type)) {
|
|
354
|
+
throw new Error(`Invalid variable type '${varData.type}' for variable '${varName}'`);
|
|
352
355
|
}
|
|
353
356
|
|
|
354
357
|
// Apply filters if present
|
|
@@ -357,27 +360,42 @@ class RuleTemplate {
|
|
|
357
360
|
if (!TemplateFilters[filterName]) {
|
|
358
361
|
throw new Error(`Unknown filter '${filterName}'`);
|
|
359
362
|
}
|
|
360
|
-
|
|
363
|
+
|
|
364
|
+
TemplateFilters[filterName](varData);
|
|
361
365
|
}
|
|
362
|
-
// After applying filters, the result is already a string representation
|
|
363
|
-
return String(value);
|
|
364
366
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
+
|
|
368
|
+
return this._serializeVarData(varData, varName);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_serializeVarData(varData, varName) {
|
|
372
|
+
const { value, type } = varData;
|
|
373
|
+
|
|
374
|
+
if (!VariableTypes.includes(type)) {
|
|
375
|
+
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
376
|
+
}
|
|
377
|
+
|
|
367
378
|
if (type === 'string') {
|
|
368
|
-
// Escape backslashes first, then quotes in string values.
|
|
369
|
-
// Order is critical: escaping backslashes first prevents double-escaping.
|
|
370
|
-
// E.g., "test\" becomes "test\\" then "test\\\"" (correct)
|
|
371
|
-
// If reversed, "test\" would become "test\\"" then "test\\\\"" (incorrect)
|
|
372
379
|
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
373
|
-
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (type === 'number') {
|
|
374
383
|
return String(value);
|
|
375
|
-
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (type === 'boolean') {
|
|
376
387
|
return value ? 'true' : 'false';
|
|
377
|
-
} else {
|
|
378
|
-
// Default behavior - just insert the value as-is
|
|
379
|
-
return String(value);
|
|
380
388
|
}
|
|
389
|
+
|
|
390
|
+
if (type === 'time period' || type === 'time period ago') {
|
|
391
|
+
let ret = `${value.from} TO ${value.to}`;
|
|
392
|
+
if(value.ago) {
|
|
393
|
+
ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
|
|
394
|
+
}
|
|
395
|
+
return ret;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return String(value);
|
|
381
399
|
}
|
|
382
400
|
|
|
383
401
|
/**
|
|
@@ -13,6 +13,7 @@ const VariableTypes = [
|
|
|
13
13
|
'boolean',
|
|
14
14
|
'object',
|
|
15
15
|
'time period',
|
|
16
|
+
'time period ago',
|
|
16
17
|
'time value',
|
|
17
18
|
'string array',
|
|
18
19
|
'number array',
|
|
@@ -25,6 +26,7 @@ const AllowedTypeMapping = {
|
|
|
25
26
|
'number': ['number_atom', 'math_expr'],
|
|
26
27
|
'boolean': ['boolean_atom', 'boolean_expr'],
|
|
27
28
|
'time period': ['time_period_atom'],
|
|
29
|
+
'time period ago': ['time_period_atom'],
|
|
28
30
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
29
31
|
'string array': ['string_array'],
|
|
30
32
|
'number array': ['number_array'],
|
|
@@ -47,7 +49,8 @@ for(const rule of TemplateGrammar){
|
|
|
47
49
|
// Add template_value as an alternative to value_atom so templates can be parsed
|
|
48
50
|
const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
|
|
49
51
|
if (valueAtomIdx !== -1) {
|
|
50
|
-
extendedGrammar[valueAtomIdx].
|
|
52
|
+
extendedGrammar[valueAtomIdx] = Object.assign({}, extendedGrammar[valueAtomIdx]);
|
|
53
|
+
extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
// Export the parser rules for potential external use
|
|
@@ -334,12 +337,12 @@ class RuleTemplate {
|
|
|
334
337
|
throw new Error(`Variable '${varName}' not provided in variables object`);
|
|
335
338
|
}
|
|
336
339
|
|
|
337
|
-
|
|
340
|
+
let varData = variables[varName];
|
|
338
341
|
if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
|
|
339
342
|
throw new Error(`Variable '${varName}' must be an object with 'value' property`);
|
|
340
343
|
}
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
|
|
345
|
+
varData = Object.assign({}, varData);
|
|
343
346
|
|
|
344
347
|
// Require type property for all variables
|
|
345
348
|
if (!varData.hasOwnProperty('type')) {
|
|
@@ -347,8 +350,8 @@ class RuleTemplate {
|
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
// Validate type
|
|
350
|
-
if (!VariableTypes.includes(type)) {
|
|
351
|
-
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
353
|
+
if (!VariableTypes.includes(varData.type)) {
|
|
354
|
+
throw new Error(`Invalid variable type '${varData.type}' for variable '${varName}'`);
|
|
352
355
|
}
|
|
353
356
|
|
|
354
357
|
// Apply filters if present
|
|
@@ -357,27 +360,42 @@ class RuleTemplate {
|
|
|
357
360
|
if (!TemplateFilters[filterName]) {
|
|
358
361
|
throw new Error(`Unknown filter '${filterName}'`);
|
|
359
362
|
}
|
|
360
|
-
|
|
363
|
+
|
|
364
|
+
TemplateFilters[filterName](varData);
|
|
361
365
|
}
|
|
362
|
-
// After applying filters, the result is already a string representation
|
|
363
|
-
return String(value);
|
|
364
366
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
+
|
|
368
|
+
return this._serializeVarData(varData, varName);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_serializeVarData(varData, varName) {
|
|
372
|
+
const { value, type } = varData;
|
|
373
|
+
|
|
374
|
+
if (!VariableTypes.includes(type)) {
|
|
375
|
+
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
376
|
+
}
|
|
377
|
+
|
|
367
378
|
if (type === 'string') {
|
|
368
|
-
// Escape backslashes first, then quotes in string values.
|
|
369
|
-
// Order is critical: escaping backslashes first prevents double-escaping.
|
|
370
|
-
// E.g., "test\" becomes "test\\" then "test\\\"" (correct)
|
|
371
|
-
// If reversed, "test\" would become "test\\"" then "test\\\\"" (incorrect)
|
|
372
379
|
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
373
|
-
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (type === 'number') {
|
|
374
383
|
return String(value);
|
|
375
|
-
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (type === 'boolean') {
|
|
376
387
|
return value ? 'true' : 'false';
|
|
377
|
-
} else {
|
|
378
|
-
// Default behavior - just insert the value as-is
|
|
379
|
-
return String(value);
|
|
380
388
|
}
|
|
389
|
+
|
|
390
|
+
if (type === 'time period' || type === 'time period ago') {
|
|
391
|
+
let ret = `${value.from} TO ${value.to}`;
|
|
392
|
+
if(value.ago) {
|
|
393
|
+
ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
|
|
394
|
+
}
|
|
395
|
+
return ret;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return String(value);
|
|
381
399
|
}
|
|
382
400
|
|
|
383
401
|
/**
|
package/src/TemplateFilters.js
CHANGED
|
@@ -4,59 +4,153 @@ They are applied in the template syntax as ${variable|filter} or ${variable|filt
|
|
|
4
4
|
*/
|
|
5
5
|
const TemplateFilters = {
|
|
6
6
|
// Convert value to JSON string representation
|
|
7
|
-
string:
|
|
8
|
-
|
|
7
|
+
string: varData => {
|
|
8
|
+
varData.value = String(varData.value);
|
|
9
|
+
varData.type = 'string';
|
|
10
|
+
|
|
11
|
+
},
|
|
12
|
+
|
|
9
13
|
// Convert to uppercase
|
|
10
|
-
upper:
|
|
11
|
-
|
|
14
|
+
upper: varData => {
|
|
15
|
+
varData.value = String(varData.value).toUpperCase();
|
|
16
|
+
varData.type = 'string';
|
|
17
|
+
|
|
18
|
+
},
|
|
19
|
+
|
|
12
20
|
// Convert to lowercase
|
|
13
|
-
lower:
|
|
14
|
-
|
|
21
|
+
lower: varData => {
|
|
22
|
+
varData.value = String(varData.value).toLowerCase();
|
|
23
|
+
varData.type = 'string';
|
|
24
|
+
|
|
25
|
+
},
|
|
26
|
+
|
|
15
27
|
// Capitalize first letter
|
|
16
|
-
capitalize:
|
|
17
|
-
const str = String(value);
|
|
18
|
-
|
|
28
|
+
capitalize: varData => {
|
|
29
|
+
const str = String(varData.value);
|
|
30
|
+
varData.value = str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
31
|
+
varData.type = 'string';
|
|
32
|
+
|
|
19
33
|
},
|
|
20
|
-
|
|
34
|
+
|
|
21
35
|
// Convert to title case
|
|
22
|
-
title:
|
|
23
|
-
|
|
36
|
+
title: varData => {
|
|
37
|
+
varData.value = String(varData.value).split(' ')
|
|
24
38
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
25
39
|
.join(' ');
|
|
40
|
+
varData.type = 'string';
|
|
41
|
+
|
|
26
42
|
},
|
|
27
|
-
|
|
43
|
+
|
|
28
44
|
// Trim whitespace
|
|
29
|
-
trim:
|
|
30
|
-
|
|
45
|
+
trim: varData => {
|
|
46
|
+
varData.value = String(varData.value).trim();
|
|
47
|
+
varData.type = 'string';
|
|
48
|
+
|
|
49
|
+
},
|
|
50
|
+
|
|
31
51
|
// Convert to number
|
|
32
|
-
number:
|
|
33
|
-
|
|
52
|
+
number: varData => {
|
|
53
|
+
varData.value = Number(varData.value);
|
|
54
|
+
varData.type = 'number';
|
|
55
|
+
|
|
56
|
+
},
|
|
57
|
+
|
|
34
58
|
// Convert to boolean
|
|
35
|
-
boolean:
|
|
36
|
-
|
|
59
|
+
boolean: varData => {
|
|
60
|
+
const value = varData.value;
|
|
61
|
+
if (typeof value === 'boolean') {
|
|
62
|
+
varData.type = 'boolean';
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
if (typeof value === 'string') {
|
|
38
67
|
const lower = value.toLowerCase();
|
|
39
|
-
if (lower === 'true' || lower === '1' || lower === 'yes')
|
|
40
|
-
|
|
68
|
+
if (lower === 'true' || lower === '1' || lower === 'yes') {
|
|
69
|
+
varData.value = true;
|
|
70
|
+
varData.type = 'boolean';
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
if (lower === 'false' || lower === '0' || lower === 'no') {
|
|
74
|
+
varData.value = false;
|
|
75
|
+
varData.type = 'boolean';
|
|
76
|
+
|
|
77
|
+
}
|
|
41
78
|
}
|
|
42
|
-
|
|
79
|
+
|
|
80
|
+
varData.value = Boolean(value);
|
|
81
|
+
varData.type = 'boolean';
|
|
82
|
+
|
|
43
83
|
},
|
|
44
|
-
|
|
84
|
+
|
|
45
85
|
// Convert to absolute value (for numbers)
|
|
46
|
-
abs:
|
|
47
|
-
|
|
86
|
+
abs: varData => {
|
|
87
|
+
varData.value = Math.abs(Number(varData.value));
|
|
88
|
+
varData.type = 'number';
|
|
89
|
+
|
|
90
|
+
},
|
|
91
|
+
|
|
48
92
|
// Round number
|
|
49
|
-
round:
|
|
50
|
-
|
|
93
|
+
round: varData => {
|
|
94
|
+
varData.value = Math.round(Number(varData.value));
|
|
95
|
+
varData.type = 'number';
|
|
96
|
+
|
|
97
|
+
},
|
|
98
|
+
|
|
51
99
|
// Floor number
|
|
52
|
-
floor:
|
|
53
|
-
|
|
100
|
+
floor: varData => {
|
|
101
|
+
varData.value = Math.floor(Number(varData.value));
|
|
102
|
+
varData.type = 'number';
|
|
103
|
+
|
|
104
|
+
},
|
|
105
|
+
|
|
54
106
|
// Ceil number
|
|
55
|
-
ceil:
|
|
56
|
-
|
|
107
|
+
ceil: varData => {
|
|
108
|
+
varData.value = Math.ceil(Number(varData.value));
|
|
109
|
+
varData.type = 'number';
|
|
110
|
+
|
|
111
|
+
},
|
|
112
|
+
|
|
57
113
|
// Default value if empty/null/undefined
|
|
58
|
-
default: (
|
|
59
|
-
|
|
114
|
+
default: (varData, defaultValue = '') => {
|
|
115
|
+
varData.value = (varData.value === null || varData.value === undefined || varData.value === '') ? defaultValue : varData.value;
|
|
116
|
+
if (typeof varData.value === 'string') {
|
|
117
|
+
varData.type = 'string';
|
|
118
|
+
} else if (typeof varData.value === 'number') {
|
|
119
|
+
varData.type = 'number';
|
|
120
|
+
} else if (typeof varData.value === 'boolean') {
|
|
121
|
+
varData.type = 'boolean';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Extract start time from time period/time period ago as time value
|
|
127
|
+
time_start: varData => {
|
|
128
|
+
if (varData.type === 'time period' || varData.type === 'time period ago') {
|
|
129
|
+
if (!varData.value || typeof varData.value !== 'object' || !Object.prototype.hasOwnProperty.call(varData.value, 'from')) {
|
|
130
|
+
throw new Error('time_start filter requires value.from for time period types');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
varData.value = varData.value.from;
|
|
134
|
+
varData.type = 'time value';
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw new Error('time_start filter requires variable type to be \"time period\" or \"time period ago\"');
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Extract end time from time period/time period ago as time value
|
|
142
|
+
time_end: varData => {
|
|
143
|
+
if (varData.type === 'time period' || varData.type === 'time period ago') {
|
|
144
|
+
if (!varData.value || typeof varData.value !== 'object' || !Object.prototype.hasOwnProperty.call(varData.value, 'to')) {
|
|
145
|
+
throw new Error('time_end filter requires value.from for time period types');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
varData.value = varData.value.to;
|
|
149
|
+
varData.type = 'time value';
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error('time_end filter requires variable type to be \"time period\" or \"time period ago\"');
|
|
60
154
|
}
|
|
61
155
|
}
|
|
62
156
|
|