@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.
- package/README.md +16 -1
- package/dist/rule-templater.browser.js +489 -57
- package/index.d.ts +8 -1
- package/package.json +1 -1
- package/src/GeneralTemplate.js +91 -11
- package/src/RuleTemplate.ebnf.js +0 -9
- package/src/RuleTemplate.js +219 -27
- package/src/RuleTemplate.production.ebnf.js +1 -1
- package/src/RuleTemplate.production.js +219 -27
- package/src/RuleTemplater.production.js +71 -5
- package/src/TemplateFilters.js +88 -0
- package/src/VariableTemplate.js +74 -6
- package/src/VariableValidate.js +11 -8
|
@@ -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': ['
|
|
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
|
-
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
191
|
-
if (
|
|
192
|
-
filters.push(
|
|
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
|
|
266
|
+
* Extract filter call from template_filter_call node
|
|
206
267
|
* @private
|
|
207
268
|
*/
|
|
208
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
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'
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return
|
|
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
|
-
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
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);
|
package/src/TemplateFilters.js
CHANGED
|
@@ -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') {
|
package/src/VariableTemplate.js
CHANGED
|
@@ -37,7 +37,7 @@ class VariableTemplate {
|
|
|
37
37
|
extractVariable() {
|
|
38
38
|
return {
|
|
39
39
|
name: this.variable.name,
|
|
40
|
-
filters: this.variable.filters.
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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;
|
package/src/VariableValidate.js
CHANGED
|
@@ -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
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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 `${
|
|
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
|
|
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
|
|
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}`;
|