@halleyassist/rule-templater 0.0.17 → 0.0.19

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.
@@ -28,7 +28,7 @@ const AllowedTypeMapping = {
28
28
  'number': ['number_atom', 'math_expr'],
29
29
  'boolean': ['boolean_atom', 'boolean_expr'],
30
30
  'time period': ['time_period_atom'],
31
- 'time period ago': ['time_period_atom'],
31
+ 'time period ago': ['time_period_ago_atom'],
32
32
  'time value': ['time_value_atom', 'tod_atom'],
33
33
  'number time': ['number_atom'],
34
34
  'string array': ['string_array'],
@@ -49,12 +49,71 @@ for(const rule of TemplateGrammar){
49
49
  }
50
50
  }
51
51
 
52
- // Add template_value as an alternative to value_atom so templates can be parsed
53
- const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
54
- if (valueAtomIdx !== -1) {
55
- extendedGrammar[valueAtomIdx] = Object.assign({}, extendedGrammar[valueAtomIdx]);
56
- extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
57
- }
52
+ const cloneRule = (ruleName) => {
53
+ const idx = extendedGrammar.findIndex(rule => rule.name === ruleName);
54
+ if (idx === -1) {
55
+ return null;
56
+ }
57
+
58
+ extendedGrammar[idx] = Object.assign({}, extendedGrammar[idx], {
59
+ bnf: extendedGrammar[idx].bnf.map(alt => Array.isArray(alt) ? alt.slice() : alt)
60
+ });
61
+
62
+ return extendedGrammar[idx];
63
+ };
64
+
65
+ const appendAlternative = (ruleName, alternative) => {
66
+ const rule = cloneRule(ruleName);
67
+ if (!rule) {
68
+ return;
69
+ }
70
+
71
+ const exists = rule.bnf.some(existing => JSON.stringify(existing) === JSON.stringify(alternative));
72
+ if (!exists) {
73
+ rule.bnf.push(alternative);
74
+ }
75
+ };
76
+
77
+ const replaceRule = (ruleName, bnf) => {
78
+ const idx = extendedGrammar.findIndex(rule => rule.name === ruleName);
79
+ if (idx === -1) {
80
+ extendedGrammar.push({name: ruleName, bnf});
81
+ return;
82
+ }
83
+
84
+ extendedGrammar[idx] = Object.assign({}, extendedGrammar[idx], {
85
+ bnf: bnf.map(alt => alt.slice())
86
+ });
87
+ };
88
+
89
+ appendAlternative('number_atom', ['template_value']);
90
+ appendAlternative('number_time_atom', ['template_value', 'WS+', 'unit']);
91
+ appendAlternative('number_time_atom', ['template_value']);
92
+ appendAlternative('tod_atom', ['template_value']);
93
+ appendAlternative('dow_atom', ['template_value']);
94
+ appendAlternative('between_time_only_atom', ['template_value']);
95
+ appendAlternative('between_tod_only_atom', ['template_value']);
96
+ appendAlternative('string_atom', ['template_value']);
97
+ appendAlternative('boolean_atom', ['template_value']);
98
+ appendAlternative('time_value_atom', ['template_value']);
99
+ appendAlternative('time_period_atom', ['template_value']);
100
+ appendAlternative('time_period_ago_atom', ['template_value']);
101
+ appendAlternative('object_atom', ['template_value']);
102
+ appendAlternative('string_array', ['template_value']);
103
+ appendAlternative('number_array', ['template_value']);
104
+ appendAlternative('boolean_array', ['template_value']);
105
+ appendAlternative('object_array', ['template_value']);
106
+
107
+ replaceRule('argument', [
108
+ ['number_time_atom', 'WS*'],
109
+ ['statement', 'WS*']
110
+ ]);
111
+
112
+ replaceRule('simple_result', [
113
+ ['fcall'],
114
+ ['number_time_atom'],
115
+ ['value']
116
+ ]);
58
117
 
59
118
  // Export the parser rules for potential external use
60
119
  const ParserRules = extendedGrammar;
@@ -185,11 +244,13 @@ class RuleTemplate {
185
244
 
186
245
  // Extract filters
187
246
  const filters = [];
247
+ const filterCalls = [];
188
248
  for (const child of templateExpr.children || []) {
189
249
  if (child.type === 'template_filter_call') {
190
- const filterName = this._extractFilterName(child);
191
- if (filterName) {
192
- filters.push(filterName);
250
+ const filterCall = this._extractFilterCall(child);
251
+ if (filterCall) {
252
+ filters.push(filterCall.name);
253
+ filterCalls.push(filterCall);
193
254
  }
194
255
  }
195
256
  }
@@ -198,18 +259,84 @@ class RuleTemplate {
198
259
  const start = node.start;
199
260
  const end = node.end;
200
261
 
201
- return { name, filters, start, end };
262
+ return { name, filters, filterCalls, start, end };
202
263
  }
203
264
 
204
265
  /**
205
- * Extract filter name from template_filter_call node
266
+ * Extract filter call from template_filter_call node
206
267
  * @private
207
268
  */
208
- _extractFilterName(node) {
269
+ _extractFilterCall(node) {
209
270
  const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
210
271
  if (!filterNameNode || !filterNameNode.text) return null;
211
-
212
- return filterNameNode.text.trim();
272
+
273
+ const argsNode = node.children?.find(c => c.type === 'template_filter_args');
274
+
275
+ return {
276
+ name: filterNameNode.text.trim(),
277
+ args: this._extractFilterArgs(argsNode)
278
+ };
279
+ }
280
+
281
+ _extractFilterArgs(node) {
282
+ if (!node || !Array.isArray(node.children)) {
283
+ return [];
284
+ }
285
+
286
+ return node.children
287
+ .filter(child => child.type === 'template_filter_arg')
288
+ .map(child => this._extractFilterArgValue(child));
289
+ }
290
+
291
+ _extractFilterArgValue(node) {
292
+ if (!node || !Array.isArray(node.children) || node.children.length === 0) {
293
+ return this._normalizeFilterArgText(node?.text?.trim() || '');
294
+ }
295
+
296
+ const child = node.children[0];
297
+ if (!child) {
298
+ return this._normalizeFilterArgText(node.text?.trim() || '');
299
+ }
300
+
301
+ if (child.type === 'value' && Array.isArray(child.children) && child.children.length > 0) {
302
+ return this._extractFilterArgValue(child);
303
+ }
304
+
305
+ if (child.type === 'string') {
306
+ try {
307
+ return JSON.parse(child.text);
308
+ } catch (error) {
309
+ return this._normalizeFilterArgText(child.text);
310
+ }
311
+ }
312
+
313
+ if (child.type === 'number') {
314
+ return Number(child.text);
315
+ }
316
+
317
+ if (child.type === 'true') {
318
+ return true;
319
+ }
320
+
321
+ if (child.type === 'false') {
322
+ return false;
323
+ }
324
+
325
+ if (child.type === 'null') {
326
+ return null;
327
+ }
328
+
329
+ return this._normalizeFilterArgText(child.text?.trim() || node.text?.trim() || '');
330
+ }
331
+
332
+ _normalizeFilterArgText(text) {
333
+ const normalizedText = String(text).trim();
334
+
335
+ if ((normalizedText.startsWith('"') && normalizedText.endsWith('"')) || (normalizedText.startsWith("'") && normalizedText.endsWith("'"))) {
336
+ return normalizedText.slice(1, -1);
337
+ }
338
+
339
+ return normalizedText;
213
340
  }
214
341
 
215
342
  /**
@@ -229,10 +356,31 @@ class RuleTemplate {
229
356
 
230
357
  const errors = [];
231
358
  const warnings = [];
232
- const extractedVars = this.extractVariables();
359
+ const extractedVars = this._extractTemplateVariables();
360
+ const seenVariables = new Set();
361
+ const seenFilterErrors = new Set();
233
362
 
234
363
  for (const varInfo of extractedVars) {
235
364
  const varName = varInfo.name;
365
+
366
+ for (const filter of (varInfo.filterCalls || varInfo.filters || [])) {
367
+ const filterName = typeof filter === 'string' ? filter : filter?.name;
368
+ if (filterName && TemplateFilters[filterName]) {
369
+ continue;
370
+ }
371
+
372
+ const errorMessage = `Unknown filter '${filterName || filter}' for variable '${varName}'`;
373
+ if (!seenFilterErrors.has(errorMessage)) {
374
+ errors.push(errorMessage);
375
+ seenFilterErrors.add(errorMessage);
376
+ }
377
+ }
378
+
379
+ if (seenVariables.has(varName)) {
380
+ continue;
381
+ }
382
+
383
+ seenVariables.add(varName);
236
384
 
237
385
  // Check if variable is provided
238
386
  if (!variables.hasOwnProperty(varName)) {
@@ -262,6 +410,23 @@ class RuleTemplate {
262
410
  }
263
411
  }
264
412
 
413
+ const canValidatePreparedRule = errors.length === 0
414
+ && Array.from(seenVariables).every(varName => {
415
+ const varData = variables[varName];
416
+ return varData
417
+ && typeof varData === 'object'
418
+ && Object.prototype.hasOwnProperty.call(varData, 'type')
419
+ && Object.prototype.hasOwnProperty.call(varData, 'value');
420
+ });
421
+
422
+ if (canValidatePreparedRule) {
423
+ try {
424
+ RuleParser.toAst(this.prepare(variables));
425
+ } catch (error) {
426
+ errors.push(`Prepared rule is invalid: ${error.message}`);
427
+ }
428
+ }
429
+
265
430
  if (functionBlob && typeof functionBlob.validate === 'function') {
266
431
  for (const functionCall of this._extractFunctionCalls()) {
267
432
  warnings.push(...functionBlob.validate(functionCall.name, functionCall.arguments));
@@ -305,6 +470,30 @@ class RuleTemplate {
305
470
  return functionCalls;
306
471
  }
307
472
 
473
+ _extractTemplateVariables() {
474
+ const variables = [];
475
+
476
+ const traverse = (node) => {
477
+ if (!node) return;
478
+
479
+ if (node.type === 'template_value') {
480
+ const variableInfo = this._extractVariableFromNode(node);
481
+ if (variableInfo) {
482
+ variables.push(variableInfo);
483
+ }
484
+ }
485
+
486
+ if (node.children) {
487
+ for (const child of node.children) {
488
+ traverse(child);
489
+ }
490
+ }
491
+ };
492
+
493
+ traverse(this.ast);
494
+ return variables;
495
+ }
496
+
308
497
  /**
309
498
  * Prepare the template by replacing variables with their values
310
499
  * Rebuilds from AST by iterating through children
@@ -407,12 +596,15 @@ class RuleTemplate {
407
596
 
408
597
  // Apply filters if present
409
598
  if (templateInfo.filters && templateInfo.filters.length > 0) {
410
- for (const filterName of templateInfo.filters) {
411
- if (!TemplateFilters[filterName]) {
412
- throw new Error(`Unknown filter '${filterName}'`);
599
+ for (const filter of (templateInfo.filterCalls || templateInfo.filters)) {
600
+ const filterName = typeof filter === 'string' ? filter : filter?.name;
601
+ const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
602
+
603
+ if (!filterName || !TemplateFilters[filterName]) {
604
+ throw new Error(`Unknown filter '${filterName || filter}'`);
413
605
  }
414
-
415
- TemplateFilters[filterName](varData);
606
+
607
+ TemplateFilters[filterName](varData, ...filterArgs);
416
608
  }
417
609
  }
418
610
 
@@ -443,12 +635,12 @@ class RuleTemplate {
443
635
  return value ? 'true' : 'false';
444
636
  }
445
637
 
446
- if (type === 'time period' || type === 'time period ago') {
447
- let ret = `${value.from} TO ${value.to}`;
448
- if(value.ago) {
449
- ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
450
- }
451
- return ret;
638
+ if (type === 'time period') {
639
+ return `BETWEEN ${value.from} AND ${value.to}`;
640
+ }
641
+
642
+ if (type === 'time period ago') {
643
+ return `${value.ago[0]} ${value.ago[1]} AGO BETWEEN ${value.from} AND ${value.to}`;
452
644
  }
453
645
 
454
646
  if (type === 'object' || type === 'string array' || type === 'number array' || type === 'boolean array' || type === 'object array') {
@@ -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_ago_atom'],
28
30
  'time value': ['time_value_atom', 'tod_atom'],
29
31
  'string array': ['string_array'],
30
32
  'number array': ['number_array'],
@@ -44,11 +46,71 @@ for(const rule of TemplateGrammar){
44
46
  }
45
47
  }
46
48
 
47
- // Add template_value as an alternative to value_atom so templates can be parsed
48
- const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
49
- if (valueAtomIdx !== -1) {
50
- extendedGrammar[valueAtomIdx].bnf.push(['template_value']);
51
- }
49
+ const cloneRule = (ruleName) => {
50
+ const idx = extendedGrammar.findIndex(rule => rule.name === ruleName);
51
+ if (idx === -1) {
52
+ return null;
53
+ }
54
+
55
+ extendedGrammar[idx] = Object.assign({}, extendedGrammar[idx], {
56
+ bnf: extendedGrammar[idx].bnf.map(alt => Array.isArray(alt) ? alt.slice() : alt)
57
+ });
58
+
59
+ return extendedGrammar[idx];
60
+ };
61
+
62
+ const appendAlternative = (ruleName, alternative) => {
63
+ const rule = cloneRule(ruleName);
64
+ if (!rule) {
65
+ return;
66
+ }
67
+
68
+ const exists = rule.bnf.some(existing => JSON.stringify(existing) === JSON.stringify(alternative));
69
+ if (!exists) {
70
+ rule.bnf.push(alternative);
71
+ }
72
+ };
73
+
74
+ const replaceRule = (ruleName, bnf) => {
75
+ const idx = extendedGrammar.findIndex(rule => rule.name === ruleName);
76
+ if (idx === -1) {
77
+ extendedGrammar.push({name: ruleName, bnf});
78
+ return;
79
+ }
80
+
81
+ extendedGrammar[idx] = Object.assign({}, extendedGrammar[idx], {
82
+ bnf: bnf.map(alt => alt.slice())
83
+ });
84
+ };
85
+
86
+ appendAlternative('number_atom', ['template_value']);
87
+ appendAlternative('number_time_atom', ['template_value', 'WS+', 'unit']);
88
+ appendAlternative('number_time_atom', ['template_value']);
89
+ appendAlternative('tod_atom', ['template_value']);
90
+ appendAlternative('dow_atom', ['template_value']);
91
+ appendAlternative('between_time_only_atom', ['template_value']);
92
+ appendAlternative('between_tod_only_atom', ['template_value']);
93
+ appendAlternative('string_atom', ['template_value']);
94
+ appendAlternative('boolean_atom', ['template_value']);
95
+ appendAlternative('time_value_atom', ['template_value']);
96
+ appendAlternative('time_period_atom', ['template_value']);
97
+ appendAlternative('time_period_ago_atom', ['template_value']);
98
+ appendAlternative('object_atom', ['template_value']);
99
+ appendAlternative('string_array', ['template_value']);
100
+ appendAlternative('number_array', ['template_value']);
101
+ appendAlternative('boolean_array', ['template_value']);
102
+ appendAlternative('object_array', ['template_value']);
103
+
104
+ replaceRule('argument', [
105
+ ['number_time_atom', 'WS*'],
106
+ ['statement', 'WS*']
107
+ ]);
108
+
109
+ replaceRule('simple_result', [
110
+ ['fcall'],
111
+ ['number_time_atom'],
112
+ ['value']
113
+ ]);
52
114
 
53
115
  // Export the parser rules for potential external use
54
116
  const ParserRules = extendedGrammar;
@@ -374,6 +436,10 @@ class RuleTemplate {
374
436
  return String(value);
375
437
  } else if (type === 'boolean') {
376
438
  return value ? 'true' : 'false';
439
+ } else if (type === 'time period') {
440
+ return `BETWEEN ${value.from} AND ${value.to}`;
441
+ } else if (type === 'time period ago') {
442
+ return `${value.ago[0]} ${value.ago[1]} AGO BETWEEN ${value.from} AND ${value.to}`;
377
443
  } else {
378
444
  // Default behavior - just insert the value as-is
379
445
  return String(value);
@@ -2,6 +2,31 @@
2
2
  Template filters are functions that transform variable values.
3
3
  They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
4
4
  */
5
+ const HUMANISE_TIME_UNITS = [
6
+ { name: 'year', seconds: 31536000, aliases: ['year', 'years', 'yr', 'yrs', 'y'] },
7
+ { name: 'month', seconds: 2592000, aliases: ['month', 'months', 'mo', 'mos'] },
8
+ { name: 'week', seconds: 604800, aliases: ['week', 'weeks', 'wk', 'wks', 'w'] },
9
+ { name: 'day', seconds: 86400, aliases: ['day', 'days', 'd'] },
10
+ { name: 'hour', seconds: 3600, aliases: ['hour', 'hours', 'hr', 'hrs', 'h'] },
11
+ { name: 'minute', seconds: 60, aliases: ['minute', 'minutes', 'min', 'mins'] },
12
+ { name: 'second', seconds: 1, aliases: ['second', 'seconds', 'sec', 'secs', 's'] }
13
+ ];
14
+
15
+ const getHumaniseTimeUnit = minUnit => {
16
+ if (minUnit === null || minUnit === undefined || minUnit === '') {
17
+ return null;
18
+ }
19
+
20
+ const normalizedMinUnit = String(minUnit).trim().toLowerCase();
21
+ const unit = HUMANISE_TIME_UNITS.find(candidate => candidate.aliases.includes(normalizedMinUnit));
22
+
23
+ if (!unit) {
24
+ throw new Error(`Unknown humanise_time min_unit \"${minUnit}\"`);
25
+ }
26
+
27
+ return unit;
28
+ };
29
+
5
30
  const TemplateFilters = {
6
31
  // Convert value to JSON string representation
7
32
  string: varData => {
@@ -125,6 +150,69 @@ const TemplateFilters = {
125
150
 
126
151
  },
127
152
 
153
+ humanise_list: (varData, joiner = 'and') => {
154
+ if (typeof varData.value === 'string') {
155
+ varData.type = 'string';
156
+ return;
157
+ }
158
+
159
+ if (!Array.isArray(varData.value)) {
160
+ varData.value = String(varData.value);
161
+ varData.type = 'string';
162
+ return;
163
+ }
164
+
165
+ const items = varData.value.map(item => String(item));
166
+
167
+ if (items.length === 0) {
168
+ varData.value = '';
169
+ } else if (items.length === 1) {
170
+ [varData.value] = items;
171
+ } else if (items.length === 2) {
172
+ varData.value = `${items[0]} ${joiner} ${items[1]}`;
173
+ } else {
174
+ varData.value = `${items.slice(0, -1).join(', ')} ${joiner} ${items[items.length - 1]}`;
175
+ }
176
+
177
+ varData.type = 'string';
178
+
179
+ },
180
+
181
+ humanise_time: (varData, minUnit = null) => {
182
+ const rawSeconds = Number(varData.value);
183
+
184
+ if (isNaN(rawSeconds)) {
185
+ throw new Error(`Value "${varData.value}" cannot be converted to seconds`);
186
+ }
187
+
188
+ const isNegative = rawSeconds < 0;
189
+ const absoluteSeconds = Math.abs(rawSeconds);
190
+ const minimumUnit = getHumaniseTimeUnit(minUnit);
191
+ const minimumUnitIndex = minimumUnit
192
+ ? HUMANISE_TIME_UNITS.findIndex(unit => unit.name === minimumUnit.name)
193
+ : HUMANISE_TIME_UNITS.length - 1;
194
+ const candidateUnits = HUMANISE_TIME_UNITS.slice(0, minimumUnitIndex + 1);
195
+ let selectedUnit = candidateUnits.find(unit => absoluteSeconds % unit.seconds === 0);
196
+ let quantity;
197
+
198
+ if (selectedUnit) {
199
+ quantity = absoluteSeconds / selectedUnit.seconds;
200
+ } else if (minimumUnit) {
201
+ selectedUnit = minimumUnit;
202
+ quantity = Math.floor(absoluteSeconds / selectedUnit.seconds);
203
+ } else {
204
+ selectedUnit = HUMANISE_TIME_UNITS[HUMANISE_TIME_UNITS.length - 1];
205
+ quantity = absoluteSeconds;
206
+ }
207
+
208
+ const signedQuantity = isNegative ? -quantity : quantity;
209
+ const label = Math.abs(signedQuantity) === 1 ? selectedUnit.name : `${selectedUnit.name}s`;
210
+
211
+ varData.value = `${signedQuantity} ${label}`;
212
+ varData.type = 'string';
213
+
214
+ },
215
+
128
216
  // Extract start time from time period/time period ago as time value
129
217
  time_start: varData => {
130
218
  if (varData.type === 'time period' || varData.type === 'time period ago') {
@@ -37,7 +37,7 @@ class VariableTemplate {
37
37
  extractVariable() {
38
38
  return {
39
39
  name: this.variable.name,
40
- filters: this.variable.filters.slice()
40
+ filters: this.variable.filters.map(filter => filter.name)
41
41
  };
42
42
  }
43
43
 
@@ -62,12 +62,15 @@ class VariableTemplate {
62
62
 
63
63
  varData = VariableTemplate._cloneVarData(varData);
64
64
 
65
- for (const filterName of this.variable.filters) {
66
- if (!TemplateFilters[filterName]) {
67
- throw new Error(`Unknown filter '${filterName}'`);
65
+ for (const filter of this.variable.filters) {
66
+ const filterName = typeof filter === 'string' ? filter : filter?.name;
67
+ const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
68
+
69
+ if (!filterName || !TemplateFilters[filterName]) {
70
+ throw new Error(`Unknown filter '${filterName || filter}'`);
68
71
  }
69
72
 
70
- TemplateFilters[filterName](varData);
73
+ TemplateFilters[filterName](varData, ...filterArgs);
71
74
  }
72
75
 
73
76
  return varData;
@@ -98,7 +101,11 @@ class VariableTemplate {
98
101
  const filterNameNode = child.children?.find(c => c.type === 'template_filter_name');
99
102
  const filterName = filterNameNode?.text?.trim();
100
103
  if (filterName) {
101
- filters.push(filterName);
104
+ const argsNode = child.children?.find(c => c.type === 'template_filter_args');
105
+ filters.push({
106
+ name: filterName,
107
+ args: VariableTemplate._extractFilterArgs(argsNode)
108
+ });
102
109
  }
103
110
  }
104
111
  }
@@ -121,6 +128,67 @@ class VariableTemplate {
121
128
 
122
129
  return cloned;
123
130
  }
131
+
132
+ static _extractFilterArgs(node) {
133
+ if (!node || !Array.isArray(node.children)) {
134
+ return [];
135
+ }
136
+
137
+ return node.children
138
+ .filter(child => child.type === 'template_filter_arg')
139
+ .map(child => VariableTemplate._extractFilterArgValue(child));
140
+ }
141
+
142
+ static _extractFilterArgValue(node) {
143
+ if (!node || !Array.isArray(node.children) || node.children.length === 0) {
144
+ return VariableTemplate._normalizeFilterArgText(node?.text?.trim() || '');
145
+ }
146
+
147
+ const child = node.children[0];
148
+ if (!child) {
149
+ return VariableTemplate._normalizeFilterArgText(node.text?.trim() || '');
150
+ }
151
+
152
+ if (child.type === 'value' && Array.isArray(child.children) && child.children.length > 0) {
153
+ return VariableTemplate._extractFilterArgValue(child);
154
+ }
155
+
156
+ if (child.type === 'string') {
157
+ try {
158
+ return JSON.parse(child.text);
159
+ } catch (error) {
160
+ return VariableTemplate._normalizeFilterArgText(child.text);
161
+ }
162
+ }
163
+
164
+ if (child.type === 'number') {
165
+ return Number(child.text);
166
+ }
167
+
168
+ if (child.type === 'true') {
169
+ return true;
170
+ }
171
+
172
+ if (child.type === 'false') {
173
+ return false;
174
+ }
175
+
176
+ if (child.type === 'null') {
177
+ return null;
178
+ }
179
+
180
+ return VariableTemplate._normalizeFilterArgText(child.text?.trim() || node.text?.trim() || '');
181
+ }
182
+
183
+ static _normalizeFilterArgText(text) {
184
+ const normalizedText = String(text).trim();
185
+
186
+ if ((normalizedText.startsWith('"') && normalizedText.endsWith('"')) || (normalizedText.startsWith("'") && normalizedText.endsWith("'"))) {
187
+ return normalizedText.slice(1, -1);
188
+ }
189
+
190
+ return normalizedText;
191
+ }
124
192
  }
125
193
 
126
194
  module.exports = VariableTemplate;
@@ -139,12 +139,15 @@ class VariableValidate {
139
139
  throw new Error('Variable data filters must be an array');
140
140
  }
141
141
 
142
- for (const filterName of normalizedVarData.filters) {
143
- if (!TemplateFilters[filterName]) {
144
- throw new Error(`Unknown filter '${filterName}'`);
142
+ for (const filter of normalizedVarData.filters) {
143
+ const filterName = typeof filter === 'string' ? filter : filter?.name;
144
+ const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
145
+
146
+ if (!filterName || !TemplateFilters[filterName]) {
147
+ throw new Error(`Unknown filter '${filterName || filter}'`);
145
148
  }
146
149
 
147
- TemplateFilters[filterName](normalizedVarData);
150
+ TemplateFilters[filterName](normalizedVarData, ...filterArgs);
148
151
  }
149
152
 
150
153
  return normalizedVarData;
@@ -204,7 +207,7 @@ class VariableValidate {
204
207
  return null;
205
208
  }
206
209
 
207
- return `${value.from.trim()} TO ${value.to.trim()}`;
210
+ return `BETWEEN ${value.from.trim()} AND ${value.to.trim()}`;
208
211
  }
209
212
 
210
213
  static _serializeTimePeriodAgo(value) {
@@ -221,7 +224,7 @@ class VariableValidate {
221
224
  return null;
222
225
  }
223
226
 
224
- return `${value.from.trim()} TO ${value.to.trim()} AGO ${amount} ${unit.trim()}`;
227
+ return `${amount} ${unit.trim()} AGO BETWEEN ${value.from.trim()} AND ${value.to.trim()}`;
225
228
  }
226
229
 
227
230
  static _serializeNumberTime(value) {
@@ -338,7 +341,7 @@ VariableValidate.validators = Object.freeze({
338
341
  'time period': (value) => VariableValidate._validateWithRule(value, 'time_period_atom', {
339
342
  normalize: VariableValidate._serializeTimePeriod,
340
343
  emptyMessage: 'Time period variables must be objects with string from/to properties',
341
- parseMessage: 'Time period variables must serialize to FROM TO TO syntax',
344
+ parseMessage: 'Time period variables must serialize to BETWEEN FROM AND TO syntax',
342
345
  semanticCheck: (rawValue) => {
343
346
  const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
344
347
  if (fromError) return `Invalid time period from value: ${fromError}`;
@@ -352,7 +355,7 @@ VariableValidate.validators = Object.freeze({
352
355
  'time period ago': (value) => VariableValidate._validateWithRule(value, 'time_period_ago_atom', {
353
356
  normalize: VariableValidate._serializeTimePeriodAgo,
354
357
  emptyMessage: 'Time period ago variables must be objects with from, to, and ago properties',
355
- parseMessage: 'Time period ago variables must serialize to FROM TO TO AGO AMOUNT UNIT syntax',
358
+ parseMessage: 'Time period ago variables must serialize to AMOUNT UNIT AGO BETWEEN FROM AND TO syntax',
356
359
  semanticCheck: (rawValue) => {
357
360
  const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
358
361
  if (fromError) return `Invalid time period ago from value: ${fromError}`;