@halleyassist/rule-templater 0.0.17 → 0.0.18
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 +225 -18
- package/index.d.ts +8 -1
- package/package.json +1 -1
- package/src/GeneralTemplate.js +85 -5
- package/src/RuleTemplate.js +130 -14
- package/src/RuleTemplate.production.js +130 -14
- package/src/TemplateFilters.js +88 -0
- package/src/VariableTemplate.js +74 -6
- package/src/VariableValidate.js +7 -4
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
|
+
- **humanise_list**: Join array values into natural language, optionally with a custom joiner like `humanise_list("or")`
|
|
125
|
+
- **humanise_time**: Convert seconds into a single human-readable unit like `1 hour`; optionally round down to a minimum unit such as `humanise_time("minute")` or `humanise_time("min")`
|
|
124
126
|
- **time_start**: Extract `from` from `time period` / `time period ago` and convert to `time value`
|
|
125
127
|
- **time_end**: Extract `to` from `time period` / `time period ago` and convert to `time value`
|
|
126
128
|
|
|
@@ -140,6 +142,14 @@ const prepared = parsed.prepare({
|
|
|
140
142
|
// Chaining filters
|
|
141
143
|
'${text|trim|upper}' with text=' hello ' → HELLO
|
|
142
144
|
|
|
145
|
+
// Humanise arrays
|
|
146
|
+
'${names|humanise_list}' with names=['a','b','c'] → a, b and c
|
|
147
|
+
'${names|humanise_list("or")}' with names=['a','b','c'] → a, b or c
|
|
148
|
+
|
|
149
|
+
// Humanise time from seconds
|
|
150
|
+
'${duration|humanise_time}' with duration=3600 → 1 hour
|
|
151
|
+
'${duration|humanise_time("minute")}' with duration=71 → 1 minute
|
|
152
|
+
|
|
143
153
|
// Time period conversion
|
|
144
154
|
'${window|time_start}' with window={from:'08:00',to:'12:00'} → 08:00
|
|
145
155
|
```
|
|
@@ -232,7 +242,7 @@ This is useful for comparing against a hub's list of available functions to ensu
|
|
|
232
242
|
|
|
233
243
|
### `ruleTemplate.validate(variables)`
|
|
234
244
|
|
|
235
|
-
Validates that all required variables are provided
|
|
245
|
+
Validates that all required variables are provided, have valid types, and reference only known template filters.
|
|
236
246
|
|
|
237
247
|
**Parameters:**
|
|
238
248
|
- `variables` (object): Object mapping variable names to their values and types
|
|
@@ -243,6 +253,7 @@ Validates that all required variables are provided and have valid types.
|
|
|
243
253
|
**Returns:** Object with:
|
|
244
254
|
- `valid` (boolean): Whether validation passed
|
|
245
255
|
- `errors` (array): Array of error messages (empty if valid)
|
|
256
|
+
- `warnings` (array): Array of non-fatal warnings (empty if none)
|
|
246
257
|
|
|
247
258
|
### `ruleTemplate.prepare(variables)`
|
|
248
259
|
|
|
@@ -272,6 +283,10 @@ Extracts variables from a general string template.
|
|
|
272
283
|
|
|
273
284
|
Prepares a general string template by replacing `${...}` placeholders with values and applying filters.
|
|
274
285
|
|
|
286
|
+
### `generalTemplate.validate()`
|
|
287
|
+
|
|
288
|
+
Validates the template itself and reports any unknown filters used in `${...}` chains.
|
|
289
|
+
|
|
275
290
|
### `RuleTemplate.validateVariableNode(astNode, variableType)` (Static)
|
|
276
291
|
|
|
277
292
|
Helper method to validate that an AST node matches the expected variable type.
|
|
@@ -3888,11 +3888,13 @@ class RuleTemplate {
|
|
|
3888
3888
|
|
|
3889
3889
|
// Extract filters
|
|
3890
3890
|
const filters = [];
|
|
3891
|
+
const filterCalls = [];
|
|
3891
3892
|
for (const child of templateExpr.children || []) {
|
|
3892
3893
|
if (child.type === 'template_filter_call') {
|
|
3893
|
-
const
|
|
3894
|
-
if (
|
|
3895
|
-
filters.push(
|
|
3894
|
+
const filterCall = this._extractFilterCall(child);
|
|
3895
|
+
if (filterCall) {
|
|
3896
|
+
filters.push(filterCall.name);
|
|
3897
|
+
filterCalls.push(filterCall);
|
|
3896
3898
|
}
|
|
3897
3899
|
}
|
|
3898
3900
|
}
|
|
@@ -3901,18 +3903,84 @@ class RuleTemplate {
|
|
|
3901
3903
|
const start = node.start;
|
|
3902
3904
|
const end = node.end;
|
|
3903
3905
|
|
|
3904
|
-
return { name, filters, start, end };
|
|
3906
|
+
return { name, filters, filterCalls, start, end };
|
|
3905
3907
|
}
|
|
3906
3908
|
|
|
3907
3909
|
/**
|
|
3908
|
-
* Extract filter
|
|
3910
|
+
* Extract filter call from template_filter_call node
|
|
3909
3911
|
* @private
|
|
3910
3912
|
*/
|
|
3911
|
-
|
|
3913
|
+
_extractFilterCall(node) {
|
|
3912
3914
|
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
3913
3915
|
if (!filterNameNode || !filterNameNode.text) return null;
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
+
|
|
3917
|
+
const argsNode = node.children?.find(c => c.type === 'template_filter_args');
|
|
3918
|
+
|
|
3919
|
+
return {
|
|
3920
|
+
name: filterNameNode.text.trim(),
|
|
3921
|
+
args: this._extractFilterArgs(argsNode)
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
_extractFilterArgs(node) {
|
|
3926
|
+
if (!node || !Array.isArray(node.children)) {
|
|
3927
|
+
return [];
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
return node.children
|
|
3931
|
+
.filter(child => child.type === 'template_filter_arg')
|
|
3932
|
+
.map(child => this._extractFilterArgValue(child));
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
_extractFilterArgValue(node) {
|
|
3936
|
+
if (!node || !Array.isArray(node.children) || node.children.length === 0) {
|
|
3937
|
+
return this._normalizeFilterArgText(node?.text?.trim() || '');
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
const child = node.children[0];
|
|
3941
|
+
if (!child) {
|
|
3942
|
+
return this._normalizeFilterArgText(node.text?.trim() || '');
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
if (child.type === 'value' && Array.isArray(child.children) && child.children.length > 0) {
|
|
3946
|
+
return this._extractFilterArgValue(child);
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
if (child.type === 'string') {
|
|
3950
|
+
try {
|
|
3951
|
+
return JSON.parse(child.text);
|
|
3952
|
+
} catch (error) {
|
|
3953
|
+
return this._normalizeFilterArgText(child.text);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
if (child.type === 'number') {
|
|
3958
|
+
return Number(child.text);
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
if (child.type === 'true') {
|
|
3962
|
+
return true;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
if (child.type === 'false') {
|
|
3966
|
+
return false;
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
if (child.type === 'null') {
|
|
3970
|
+
return null;
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
return this._normalizeFilterArgText(child.text?.trim() || node.text?.trim() || '');
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
_normalizeFilterArgText(text) {
|
|
3977
|
+
const normalizedText = String(text).trim();
|
|
3978
|
+
|
|
3979
|
+
if ((normalizedText.startsWith('"') && normalizedText.endsWith('"')) || (normalizedText.startsWith("'") && normalizedText.endsWith("'"))) {
|
|
3980
|
+
return normalizedText.slice(1, -1);
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
return normalizedText;
|
|
3916
3984
|
}
|
|
3917
3985
|
|
|
3918
3986
|
/**
|
|
@@ -3932,10 +4000,31 @@ class RuleTemplate {
|
|
|
3932
4000
|
|
|
3933
4001
|
const errors = [];
|
|
3934
4002
|
const warnings = [];
|
|
3935
|
-
const extractedVars = this.
|
|
4003
|
+
const extractedVars = this._extractTemplateVariables();
|
|
4004
|
+
const seenVariables = new Set();
|
|
4005
|
+
const seenFilterErrors = new Set();
|
|
3936
4006
|
|
|
3937
4007
|
for (const varInfo of extractedVars) {
|
|
3938
4008
|
const varName = varInfo.name;
|
|
4009
|
+
|
|
4010
|
+
for (const filter of (varInfo.filterCalls || varInfo.filters || [])) {
|
|
4011
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
4012
|
+
if (filterName && TemplateFilters[filterName]) {
|
|
4013
|
+
continue;
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
const errorMessage = `Unknown filter '${filterName || filter}' for variable '${varName}'`;
|
|
4017
|
+
if (!seenFilterErrors.has(errorMessage)) {
|
|
4018
|
+
errors.push(errorMessage);
|
|
4019
|
+
seenFilterErrors.add(errorMessage);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
if (seenVariables.has(varName)) {
|
|
4024
|
+
continue;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
seenVariables.add(varName);
|
|
3939
4028
|
|
|
3940
4029
|
// Check if variable is provided
|
|
3941
4030
|
if (!variables.hasOwnProperty(varName)) {
|
|
@@ -4008,6 +4097,30 @@ class RuleTemplate {
|
|
|
4008
4097
|
return functionCalls;
|
|
4009
4098
|
}
|
|
4010
4099
|
|
|
4100
|
+
_extractTemplateVariables() {
|
|
4101
|
+
const variables = [];
|
|
4102
|
+
|
|
4103
|
+
const traverse = (node) => {
|
|
4104
|
+
if (!node) return;
|
|
4105
|
+
|
|
4106
|
+
if (node.type === 'template_value') {
|
|
4107
|
+
const variableInfo = this._extractVariableFromNode(node);
|
|
4108
|
+
if (variableInfo) {
|
|
4109
|
+
variables.push(variableInfo);
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
if (node.children) {
|
|
4114
|
+
for (const child of node.children) {
|
|
4115
|
+
traverse(child);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
4119
|
+
|
|
4120
|
+
traverse(this.ast);
|
|
4121
|
+
return variables;
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4011
4124
|
/**
|
|
4012
4125
|
* Prepare the template by replacing variables with their values
|
|
4013
4126
|
* Rebuilds from AST by iterating through children
|
|
@@ -4110,12 +4223,15 @@ class RuleTemplate {
|
|
|
4110
4223
|
|
|
4111
4224
|
// Apply filters if present
|
|
4112
4225
|
if (templateInfo.filters && templateInfo.filters.length > 0) {
|
|
4113
|
-
for (const
|
|
4114
|
-
|
|
4115
|
-
|
|
4226
|
+
for (const filter of (templateInfo.filterCalls || templateInfo.filters)) {
|
|
4227
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
4228
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
4229
|
+
|
|
4230
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
4231
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
4116
4232
|
}
|
|
4117
|
-
|
|
4118
|
-
TemplateFilters[filterName](varData);
|
|
4233
|
+
|
|
4234
|
+
TemplateFilters[filterName](varData, ...filterArgs);
|
|
4119
4235
|
}
|
|
4120
4236
|
}
|
|
4121
4237
|
|
|
@@ -4193,6 +4309,31 @@ module.exports = RuleTemplate;
|
|
|
4193
4309
|
Template filters are functions that transform variable values.
|
|
4194
4310
|
They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
|
|
4195
4311
|
*/
|
|
4312
|
+
const HUMANISE_TIME_UNITS = [
|
|
4313
|
+
{ name: 'year', seconds: 31536000, aliases: ['year', 'years', 'yr', 'yrs', 'y'] },
|
|
4314
|
+
{ name: 'month', seconds: 2592000, aliases: ['month', 'months', 'mo', 'mos'] },
|
|
4315
|
+
{ name: 'week', seconds: 604800, aliases: ['week', 'weeks', 'wk', 'wks', 'w'] },
|
|
4316
|
+
{ name: 'day', seconds: 86400, aliases: ['day', 'days', 'd'] },
|
|
4317
|
+
{ name: 'hour', seconds: 3600, aliases: ['hour', 'hours', 'hr', 'hrs', 'h'] },
|
|
4318
|
+
{ name: 'minute', seconds: 60, aliases: ['minute', 'minutes', 'min', 'mins'] },
|
|
4319
|
+
{ name: 'second', seconds: 1, aliases: ['second', 'seconds', 'sec', 'secs', 's'] }
|
|
4320
|
+
];
|
|
4321
|
+
|
|
4322
|
+
const getHumaniseTimeUnit = minUnit => {
|
|
4323
|
+
if (minUnit === null || minUnit === undefined || minUnit === '') {
|
|
4324
|
+
return null;
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
const normalizedMinUnit = String(minUnit).trim().toLowerCase();
|
|
4328
|
+
const unit = HUMANISE_TIME_UNITS.find(candidate => candidate.aliases.includes(normalizedMinUnit));
|
|
4329
|
+
|
|
4330
|
+
if (!unit) {
|
|
4331
|
+
throw new Error(`Unknown humanise_time min_unit \"${minUnit}\"`);
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
return unit;
|
|
4335
|
+
};
|
|
4336
|
+
|
|
4196
4337
|
const TemplateFilters = {
|
|
4197
4338
|
// Convert value to JSON string representation
|
|
4198
4339
|
string: varData => {
|
|
@@ -4316,6 +4457,69 @@ const TemplateFilters = {
|
|
|
4316
4457
|
|
|
4317
4458
|
},
|
|
4318
4459
|
|
|
4460
|
+
humanise_list: (varData, joiner = 'and') => {
|
|
4461
|
+
if (typeof varData.value === 'string') {
|
|
4462
|
+
varData.type = 'string';
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
if (!Array.isArray(varData.value)) {
|
|
4467
|
+
varData.value = String(varData.value);
|
|
4468
|
+
varData.type = 'string';
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
const items = varData.value.map(item => String(item));
|
|
4473
|
+
|
|
4474
|
+
if (items.length === 0) {
|
|
4475
|
+
varData.value = '';
|
|
4476
|
+
} else if (items.length === 1) {
|
|
4477
|
+
[varData.value] = items;
|
|
4478
|
+
} else if (items.length === 2) {
|
|
4479
|
+
varData.value = `${items[0]} ${joiner} ${items[1]}`;
|
|
4480
|
+
} else {
|
|
4481
|
+
varData.value = `${items.slice(0, -1).join(', ')} ${joiner} ${items[items.length - 1]}`;
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
varData.type = 'string';
|
|
4485
|
+
|
|
4486
|
+
},
|
|
4487
|
+
|
|
4488
|
+
humanise_time: (varData, minUnit = null) => {
|
|
4489
|
+
const rawSeconds = Number(varData.value);
|
|
4490
|
+
|
|
4491
|
+
if (isNaN(rawSeconds)) {
|
|
4492
|
+
throw new Error(`Value "${varData.value}" cannot be converted to seconds`);
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
const isNegative = rawSeconds < 0;
|
|
4496
|
+
const absoluteSeconds = Math.abs(rawSeconds);
|
|
4497
|
+
const minimumUnit = getHumaniseTimeUnit(minUnit);
|
|
4498
|
+
const minimumUnitIndex = minimumUnit
|
|
4499
|
+
? HUMANISE_TIME_UNITS.findIndex(unit => unit.name === minimumUnit.name)
|
|
4500
|
+
: HUMANISE_TIME_UNITS.length - 1;
|
|
4501
|
+
const candidateUnits = HUMANISE_TIME_UNITS.slice(0, minimumUnitIndex + 1);
|
|
4502
|
+
let selectedUnit = candidateUnits.find(unit => absoluteSeconds % unit.seconds === 0);
|
|
4503
|
+
let quantity;
|
|
4504
|
+
|
|
4505
|
+
if (selectedUnit) {
|
|
4506
|
+
quantity = absoluteSeconds / selectedUnit.seconds;
|
|
4507
|
+
} else if (minimumUnit) {
|
|
4508
|
+
selectedUnit = minimumUnit;
|
|
4509
|
+
quantity = Math.floor(absoluteSeconds / selectedUnit.seconds);
|
|
4510
|
+
} else {
|
|
4511
|
+
selectedUnit = HUMANISE_TIME_UNITS[HUMANISE_TIME_UNITS.length - 1];
|
|
4512
|
+
quantity = absoluteSeconds;
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
const signedQuantity = isNegative ? -quantity : quantity;
|
|
4516
|
+
const label = Math.abs(signedQuantity) === 1 ? selectedUnit.name : `${selectedUnit.name}s`;
|
|
4517
|
+
|
|
4518
|
+
varData.value = `${signedQuantity} ${label}`;
|
|
4519
|
+
varData.type = 'string';
|
|
4520
|
+
|
|
4521
|
+
},
|
|
4522
|
+
|
|
4319
4523
|
// Extract start time from time period/time period ago as time value
|
|
4320
4524
|
time_start: varData => {
|
|
4321
4525
|
if (varData.type === 'time period' || varData.type === 'time period ago') {
|
|
@@ -4490,12 +4694,15 @@ class VariableValidate {
|
|
|
4490
4694
|
throw new Error('Variable data filters must be an array');
|
|
4491
4695
|
}
|
|
4492
4696
|
|
|
4493
|
-
for (const
|
|
4494
|
-
|
|
4495
|
-
|
|
4697
|
+
for (const filter of normalizedVarData.filters) {
|
|
4698
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
4699
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
4700
|
+
|
|
4701
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
4702
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
4496
4703
|
}
|
|
4497
4704
|
|
|
4498
|
-
TemplateFilters[filterName](normalizedVarData);
|
|
4705
|
+
TemplateFilters[filterName](normalizedVarData, ...filterArgs);
|
|
4499
4706
|
}
|
|
4500
4707
|
|
|
4501
4708
|
return normalizedVarData;
|
package/index.d.ts
CHANGED
|
@@ -3,9 +3,14 @@ export interface VariablePosition {
|
|
|
3
3
|
end: number;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export interface TemplateFilterCall {
|
|
7
|
+
name: string;
|
|
8
|
+
args: any[];
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export interface VariableInfo {
|
|
7
12
|
name: string;
|
|
8
|
-
filters: string
|
|
13
|
+
filters: Array<string | TemplateFilterCall>;
|
|
9
14
|
positions: VariablePosition[];
|
|
10
15
|
}
|
|
11
16
|
|
|
@@ -134,6 +139,8 @@ export class GeneralTemplate {
|
|
|
134
139
|
|
|
135
140
|
extractVariables(): VariableInfo[];
|
|
136
141
|
|
|
142
|
+
validate(): ValidationResult;
|
|
143
|
+
|
|
137
144
|
prepare(variables: Variables): string;
|
|
138
145
|
}
|
|
139
146
|
|
package/package.json
CHANGED
package/src/GeneralTemplate.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const TemplateFilters = require('./TemplateFilters');
|
|
2
2
|
|
|
3
|
+
const FILTER_PATTERN = /([A-Za-z_][A-Za-z0-9_]*)(?:\((.*)\))?$/;
|
|
4
|
+
|
|
3
5
|
class GeneralTemplate {
|
|
4
6
|
constructor(templateText) {
|
|
5
7
|
this.templateText = templateText;
|
|
@@ -52,6 +54,33 @@ class GeneralTemplate {
|
|
|
52
54
|
return this.getVariables();
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
validate() {
|
|
58
|
+
const errors = [];
|
|
59
|
+
const warnings = [];
|
|
60
|
+
const seenFilterErrors = new Set();
|
|
61
|
+
|
|
62
|
+
for (const variableInfo of this.getVariables()) {
|
|
63
|
+
for (const filter of variableInfo.filters || []) {
|
|
64
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
65
|
+
if (filterName && TemplateFilters[filterName]) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const errorMessage = `Unknown filter '${filterName || filter}' for variable '${variableInfo.name}'`;
|
|
70
|
+
if (!seenFilterErrors.has(errorMessage)) {
|
|
71
|
+
errors.push(errorMessage);
|
|
72
|
+
seenFilterErrors.add(errorMessage);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
valid: errors.length === 0,
|
|
79
|
+
errors,
|
|
80
|
+
warnings
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
prepare(variables) {
|
|
56
85
|
if (!variables || typeof variables !== 'object') {
|
|
57
86
|
throw new Error('Variables must be provided as an object');
|
|
@@ -76,12 +105,15 @@ class GeneralTemplate {
|
|
|
76
105
|
varData = Object.assign({}, varData);
|
|
77
106
|
|
|
78
107
|
if (parsedExpression.filters && parsedExpression.filters.length > 0) {
|
|
79
|
-
for (const
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
for (const filter of parsedExpression.filters) {
|
|
109
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
110
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
111
|
+
|
|
112
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
113
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
82
114
|
}
|
|
83
115
|
|
|
84
|
-
TemplateFilters[filterName](varData);
|
|
116
|
+
TemplateFilters[filterName](varData, ...filterArgs);
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
119
|
|
|
@@ -101,10 +133,58 @@ class GeneralTemplate {
|
|
|
101
133
|
|
|
102
134
|
return {
|
|
103
135
|
name: segments[0],
|
|
104
|
-
filters: segments.slice(1)
|
|
136
|
+
filters: segments.slice(1).map(segment => this._parseFilter(segment))
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_parseFilter(segment) {
|
|
141
|
+
const match = segment.match(FILTER_PATTERN);
|
|
142
|
+
if (!match) {
|
|
143
|
+
return {
|
|
144
|
+
name: segment,
|
|
145
|
+
args: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const [, name, rawArgs] = match;
|
|
150
|
+
return {
|
|
151
|
+
name,
|
|
152
|
+
args: this._parseFilterArgs(rawArgs)
|
|
105
153
|
};
|
|
106
154
|
}
|
|
107
155
|
|
|
156
|
+
_parseFilterArgs(rawArgs) {
|
|
157
|
+
if (!rawArgs || !rawArgs.trim()) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return rawArgs.split(',').map(arg => this._parseFilterArgValue(arg.trim()));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_parseFilterArgValue(rawArg) {
|
|
165
|
+
if ((rawArg.startsWith('"') && rawArg.endsWith('"')) || (rawArg.startsWith("'") && rawArg.endsWith("'"))) {
|
|
166
|
+
return rawArg.slice(1, -1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (rawArg === 'true') {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (rawArg === 'false') {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (rawArg === 'null') {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (rawArg !== '' && !Number.isNaN(Number(rawArg))) {
|
|
182
|
+
return Number(rawArg);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return rawArg;
|
|
186
|
+
}
|
|
187
|
+
|
|
108
188
|
_serializeVariable(varData) {
|
|
109
189
|
if (varData.value === null || varData.value === undefined) {
|
|
110
190
|
return '';
|
package/src/RuleTemplate.js
CHANGED
|
@@ -185,11 +185,13 @@ class RuleTemplate {
|
|
|
185
185
|
|
|
186
186
|
// Extract filters
|
|
187
187
|
const filters = [];
|
|
188
|
+
const filterCalls = [];
|
|
188
189
|
for (const child of templateExpr.children || []) {
|
|
189
190
|
if (child.type === 'template_filter_call') {
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
filters.push(
|
|
191
|
+
const filterCall = this._extractFilterCall(child);
|
|
192
|
+
if (filterCall) {
|
|
193
|
+
filters.push(filterCall.name);
|
|
194
|
+
filterCalls.push(filterCall);
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
}
|
|
@@ -198,18 +200,84 @@ class RuleTemplate {
|
|
|
198
200
|
const start = node.start;
|
|
199
201
|
const end = node.end;
|
|
200
202
|
|
|
201
|
-
return { name, filters, start, end };
|
|
203
|
+
return { name, filters, filterCalls, start, end };
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
/**
|
|
205
|
-
* Extract filter
|
|
207
|
+
* Extract filter call from template_filter_call node
|
|
206
208
|
* @private
|
|
207
209
|
*/
|
|
208
|
-
|
|
210
|
+
_extractFilterCall(node) {
|
|
209
211
|
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
210
212
|
if (!filterNameNode || !filterNameNode.text) return null;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
|
|
214
|
+
const argsNode = node.children?.find(c => c.type === 'template_filter_args');
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
name: filterNameNode.text.trim(),
|
|
218
|
+
args: this._extractFilterArgs(argsNode)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_extractFilterArgs(node) {
|
|
223
|
+
if (!node || !Array.isArray(node.children)) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return node.children
|
|
228
|
+
.filter(child => child.type === 'template_filter_arg')
|
|
229
|
+
.map(child => this._extractFilterArgValue(child));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_extractFilterArgValue(node) {
|
|
233
|
+
if (!node || !Array.isArray(node.children) || node.children.length === 0) {
|
|
234
|
+
return this._normalizeFilterArgText(node?.text?.trim() || '');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const child = node.children[0];
|
|
238
|
+
if (!child) {
|
|
239
|
+
return this._normalizeFilterArgText(node.text?.trim() || '');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (child.type === 'value' && Array.isArray(child.children) && child.children.length > 0) {
|
|
243
|
+
return this._extractFilterArgValue(child);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (child.type === 'string') {
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(child.text);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return this._normalizeFilterArgText(child.text);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (child.type === 'number') {
|
|
255
|
+
return Number(child.text);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (child.type === 'true') {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (child.type === 'false') {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (child.type === 'null') {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this._normalizeFilterArgText(child.text?.trim() || node.text?.trim() || '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_normalizeFilterArgText(text) {
|
|
274
|
+
const normalizedText = String(text).trim();
|
|
275
|
+
|
|
276
|
+
if ((normalizedText.startsWith('"') && normalizedText.endsWith('"')) || (normalizedText.startsWith("'") && normalizedText.endsWith("'"))) {
|
|
277
|
+
return normalizedText.slice(1, -1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return normalizedText;
|
|
213
281
|
}
|
|
214
282
|
|
|
215
283
|
/**
|
|
@@ -229,10 +297,31 @@ class RuleTemplate {
|
|
|
229
297
|
|
|
230
298
|
const errors = [];
|
|
231
299
|
const warnings = [];
|
|
232
|
-
const extractedVars = this.
|
|
300
|
+
const extractedVars = this._extractTemplateVariables();
|
|
301
|
+
const seenVariables = new Set();
|
|
302
|
+
const seenFilterErrors = new Set();
|
|
233
303
|
|
|
234
304
|
for (const varInfo of extractedVars) {
|
|
235
305
|
const varName = varInfo.name;
|
|
306
|
+
|
|
307
|
+
for (const filter of (varInfo.filterCalls || varInfo.filters || [])) {
|
|
308
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
309
|
+
if (filterName && TemplateFilters[filterName]) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const errorMessage = `Unknown filter '${filterName || filter}' for variable '${varName}'`;
|
|
314
|
+
if (!seenFilterErrors.has(errorMessage)) {
|
|
315
|
+
errors.push(errorMessage);
|
|
316
|
+
seenFilterErrors.add(errorMessage);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (seenVariables.has(varName)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
seenVariables.add(varName);
|
|
236
325
|
|
|
237
326
|
// Check if variable is provided
|
|
238
327
|
if (!variables.hasOwnProperty(varName)) {
|
|
@@ -305,6 +394,30 @@ class RuleTemplate {
|
|
|
305
394
|
return functionCalls;
|
|
306
395
|
}
|
|
307
396
|
|
|
397
|
+
_extractTemplateVariables() {
|
|
398
|
+
const variables = [];
|
|
399
|
+
|
|
400
|
+
const traverse = (node) => {
|
|
401
|
+
if (!node) return;
|
|
402
|
+
|
|
403
|
+
if (node.type === 'template_value') {
|
|
404
|
+
const variableInfo = this._extractVariableFromNode(node);
|
|
405
|
+
if (variableInfo) {
|
|
406
|
+
variables.push(variableInfo);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (node.children) {
|
|
411
|
+
for (const child of node.children) {
|
|
412
|
+
traverse(child);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
traverse(this.ast);
|
|
418
|
+
return variables;
|
|
419
|
+
}
|
|
420
|
+
|
|
308
421
|
/**
|
|
309
422
|
* Prepare the template by replacing variables with their values
|
|
310
423
|
* Rebuilds from AST by iterating through children
|
|
@@ -407,12 +520,15 @@ class RuleTemplate {
|
|
|
407
520
|
|
|
408
521
|
// Apply filters if present
|
|
409
522
|
if (templateInfo.filters && templateInfo.filters.length > 0) {
|
|
410
|
-
for (const
|
|
411
|
-
|
|
412
|
-
|
|
523
|
+
for (const filter of (templateInfo.filterCalls || templateInfo.filters)) {
|
|
524
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
525
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
526
|
+
|
|
527
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
528
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
413
529
|
}
|
|
414
|
-
|
|
415
|
-
TemplateFilters[filterName](varData);
|
|
530
|
+
|
|
531
|
+
TemplateFilters[filterName](varData, ...filterArgs);
|
|
416
532
|
}
|
|
417
533
|
}
|
|
418
534
|
|
|
@@ -185,11 +185,13 @@ class RuleTemplate {
|
|
|
185
185
|
|
|
186
186
|
// Extract filters
|
|
187
187
|
const filters = [];
|
|
188
|
+
const filterCalls = [];
|
|
188
189
|
for (const child of templateExpr.children || []) {
|
|
189
190
|
if (child.type === 'template_filter_call') {
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
filters.push(
|
|
191
|
+
const filterCall = this._extractFilterCall(child);
|
|
192
|
+
if (filterCall) {
|
|
193
|
+
filters.push(filterCall.name);
|
|
194
|
+
filterCalls.push(filterCall);
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
}
|
|
@@ -198,18 +200,84 @@ class RuleTemplate {
|
|
|
198
200
|
const start = node.start;
|
|
199
201
|
const end = node.end;
|
|
200
202
|
|
|
201
|
-
return { name, filters, start, end };
|
|
203
|
+
return { name, filters, filterCalls, start, end };
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
/**
|
|
205
|
-
* Extract filter
|
|
207
|
+
* Extract filter call from template_filter_call node
|
|
206
208
|
* @private
|
|
207
209
|
*/
|
|
208
|
-
|
|
210
|
+
_extractFilterCall(node) {
|
|
209
211
|
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
210
212
|
if (!filterNameNode || !filterNameNode.text) return null;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
|
|
214
|
+
const argsNode = node.children?.find(c => c.type === 'template_filter_args');
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
name: filterNameNode.text.trim(),
|
|
218
|
+
args: this._extractFilterArgs(argsNode)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_extractFilterArgs(node) {
|
|
223
|
+
if (!node || !Array.isArray(node.children)) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return node.children
|
|
228
|
+
.filter(child => child.type === 'template_filter_arg')
|
|
229
|
+
.map(child => this._extractFilterArgValue(child));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_extractFilterArgValue(node) {
|
|
233
|
+
if (!node || !Array.isArray(node.children) || node.children.length === 0) {
|
|
234
|
+
return this._normalizeFilterArgText(node?.text?.trim() || '');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const child = node.children[0];
|
|
238
|
+
if (!child) {
|
|
239
|
+
return this._normalizeFilterArgText(node.text?.trim() || '');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (child.type === 'value' && Array.isArray(child.children) && child.children.length > 0) {
|
|
243
|
+
return this._extractFilterArgValue(child);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (child.type === 'string') {
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(child.text);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return this._normalizeFilterArgText(child.text);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (child.type === 'number') {
|
|
255
|
+
return Number(child.text);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (child.type === 'true') {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (child.type === 'false') {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (child.type === 'null') {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this._normalizeFilterArgText(child.text?.trim() || node.text?.trim() || '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_normalizeFilterArgText(text) {
|
|
274
|
+
const normalizedText = String(text).trim();
|
|
275
|
+
|
|
276
|
+
if ((normalizedText.startsWith('"') && normalizedText.endsWith('"')) || (normalizedText.startsWith("'") && normalizedText.endsWith("'"))) {
|
|
277
|
+
return normalizedText.slice(1, -1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return normalizedText;
|
|
213
281
|
}
|
|
214
282
|
|
|
215
283
|
/**
|
|
@@ -229,10 +297,31 @@ class RuleTemplate {
|
|
|
229
297
|
|
|
230
298
|
const errors = [];
|
|
231
299
|
const warnings = [];
|
|
232
|
-
const extractedVars = this.
|
|
300
|
+
const extractedVars = this._extractTemplateVariables();
|
|
301
|
+
const seenVariables = new Set();
|
|
302
|
+
const seenFilterErrors = new Set();
|
|
233
303
|
|
|
234
304
|
for (const varInfo of extractedVars) {
|
|
235
305
|
const varName = varInfo.name;
|
|
306
|
+
|
|
307
|
+
for (const filter of (varInfo.filterCalls || varInfo.filters || [])) {
|
|
308
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
309
|
+
if (filterName && TemplateFilters[filterName]) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const errorMessage = `Unknown filter '${filterName || filter}' for variable '${varName}'`;
|
|
314
|
+
if (!seenFilterErrors.has(errorMessage)) {
|
|
315
|
+
errors.push(errorMessage);
|
|
316
|
+
seenFilterErrors.add(errorMessage);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (seenVariables.has(varName)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
seenVariables.add(varName);
|
|
236
325
|
|
|
237
326
|
// Check if variable is provided
|
|
238
327
|
if (!variables.hasOwnProperty(varName)) {
|
|
@@ -305,6 +394,30 @@ class RuleTemplate {
|
|
|
305
394
|
return functionCalls;
|
|
306
395
|
}
|
|
307
396
|
|
|
397
|
+
_extractTemplateVariables() {
|
|
398
|
+
const variables = [];
|
|
399
|
+
|
|
400
|
+
const traverse = (node) => {
|
|
401
|
+
if (!node) return;
|
|
402
|
+
|
|
403
|
+
if (node.type === 'template_value') {
|
|
404
|
+
const variableInfo = this._extractVariableFromNode(node);
|
|
405
|
+
if (variableInfo) {
|
|
406
|
+
variables.push(variableInfo);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (node.children) {
|
|
411
|
+
for (const child of node.children) {
|
|
412
|
+
traverse(child);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
traverse(this.ast);
|
|
418
|
+
return variables;
|
|
419
|
+
}
|
|
420
|
+
|
|
308
421
|
/**
|
|
309
422
|
* Prepare the template by replacing variables with their values
|
|
310
423
|
* Rebuilds from AST by iterating through children
|
|
@@ -407,12 +520,15 @@ class RuleTemplate {
|
|
|
407
520
|
|
|
408
521
|
// Apply filters if present
|
|
409
522
|
if (templateInfo.filters && templateInfo.filters.length > 0) {
|
|
410
|
-
for (const
|
|
411
|
-
|
|
412
|
-
|
|
523
|
+
for (const filter of (templateInfo.filterCalls || templateInfo.filters)) {
|
|
524
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
525
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
526
|
+
|
|
527
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
528
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
413
529
|
}
|
|
414
|
-
|
|
415
|
-
TemplateFilters[filterName](varData);
|
|
530
|
+
|
|
531
|
+
TemplateFilters[filterName](varData, ...filterArgs);
|
|
416
532
|
}
|
|
417
533
|
}
|
|
418
534
|
|
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;
|