@halleyassist/rule-templater 0.0.16 → 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 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 and have valid types.
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 filterName = this._extractFilterName(child);
3894
- if (filterName) {
3895
- filters.push(filterName);
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,38 +3903,128 @@ 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 name from template_filter_call node
3910
+ * Extract filter call from template_filter_call node
3909
3911
  * @private
3910
3912
  */
3911
- _extractFilterName(node) {
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
- return filterNameNode.text.trim();
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
  /**
3919
3987
  * Validate variable types against the AST
3920
3988
  * @param {Object} variables - Object mapping variable names to {type} objects
3921
- * @returns {Object} Object with validation results: {valid: boolean, errors: []}
3989
+ * @param {Object} [functionBlob] - Optional HalleyFunctionBlob used for non-fatal function warnings
3990
+ * @returns {Object} Object with validation results: {valid: boolean, errors: [], warnings: []}
3922
3991
  */
3923
- validate(variables) {
3992
+ validate(variables, functionBlob) {
3924
3993
  if (!variables || typeof variables !== 'object') {
3925
3994
  return {
3926
3995
  valid: false,
3927
- errors: ['Variables must be provided as an object']
3996
+ errors: ['Variables must be provided as an object'],
3997
+ warnings: []
3928
3998
  };
3929
3999
  }
3930
4000
 
3931
4001
  const errors = [];
3932
- const extractedVars = this.extractVariables();
4002
+ const warnings = [];
4003
+ const extractedVars = this._extractTemplateVariables();
4004
+ const seenVariables = new Set();
4005
+ const seenFilterErrors = new Set();
3933
4006
 
3934
4007
  for (const varInfo of extractedVars) {
3935
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);
3936
4028
 
3937
4029
  // Check if variable is provided
3938
4030
  if (!variables.hasOwnProperty(varName)) {
@@ -3961,13 +4053,74 @@ class RuleTemplate {
3961
4053
  }
3962
4054
  }
3963
4055
  }
4056
+
4057
+ if (functionBlob && typeof functionBlob.validate === 'function') {
4058
+ for (const functionCall of this._extractFunctionCalls()) {
4059
+ warnings.push(...functionBlob.validate(functionCall.name, functionCall.arguments));
4060
+ }
4061
+ }
3964
4062
 
3965
4063
  return {
3966
4064
  valid: errors.length === 0,
3967
- errors
4065
+ errors,
4066
+ warnings
3968
4067
  };
3969
4068
  }
3970
4069
 
4070
+ _extractFunctionCalls() {
4071
+ const functionCalls = [];
4072
+
4073
+ const traverse = (node) => {
4074
+ if (!node) return;
4075
+
4076
+ if (node.type === 'fcall') {
4077
+ const functionName = node.children?.find(c => c.type === 'fname')?.text?.trim();
4078
+ const argumentsNode = node.children?.find(c => c.type === 'arguments');
4079
+ if (functionName) {
4080
+ functionCalls.push({
4081
+ name: functionName,
4082
+ arguments: argumentsNode?.children
4083
+ ?.filter(c => c.type === 'argument')
4084
+ .map(c => c.text) || []
4085
+ });
4086
+ }
4087
+ }
4088
+
4089
+ if (node.children) {
4090
+ for (const child of node.children) {
4091
+ traverse(child);
4092
+ }
4093
+ }
4094
+ };
4095
+
4096
+ traverse(this.ast);
4097
+ return functionCalls;
4098
+ }
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
+
3971
4124
  /**
3972
4125
  * Prepare the template by replacing variables with their values
3973
4126
  * Rebuilds from AST by iterating through children
@@ -4070,12 +4223,15 @@ class RuleTemplate {
4070
4223
 
4071
4224
  // Apply filters if present
4072
4225
  if (templateInfo.filters && templateInfo.filters.length > 0) {
4073
- for (const filterName of templateInfo.filters) {
4074
- if (!TemplateFilters[filterName]) {
4075
- throw new Error(`Unknown filter '${filterName}'`);
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}'`);
4076
4232
  }
4077
-
4078
- TemplateFilters[filterName](varData);
4233
+
4234
+ TemplateFilters[filterName](varData, ...filterArgs);
4079
4235
  }
4080
4236
  }
4081
4237
 
@@ -4153,6 +4309,31 @@ module.exports = RuleTemplate;
4153
4309
  Template filters are functions that transform variable values.
4154
4310
  They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
4155
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
+
4156
4337
  const TemplateFilters = {
4157
4338
  // Convert value to JSON string representation
4158
4339
  string: varData => {
@@ -4276,6 +4457,69 @@ const TemplateFilters = {
4276
4457
 
4277
4458
  },
4278
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
+
4279
4523
  // Extract start time from time period/time period ago as time value
4280
4524
  time_start: varData => {
4281
4525
  if (varData.type === 'time period' || varData.type === 'time period ago') {
@@ -4450,12 +4694,15 @@ class VariableValidate {
4450
4694
  throw new Error('Variable data filters must be an array');
4451
4695
  }
4452
4696
 
4453
- for (const filterName of normalizedVarData.filters) {
4454
- if (!TemplateFilters[filterName]) {
4455
- throw new Error(`Unknown filter '${filterName}'`);
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}'`);
4456
4703
  }
4457
4704
 
4458
- TemplateFilters[filterName](normalizedVarData);
4705
+ TemplateFilters[filterName](normalizedVarData, ...filterArgs);
4459
4706
  }
4460
4707
 
4461
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
 
@@ -26,6 +31,7 @@ export interface Variables {
26
31
  export interface ValidationResult {
27
32
  valid: boolean;
28
33
  errors: string[];
34
+ warnings: string[];
29
35
  }
30
36
 
31
37
  export interface VariableValidationResult {
@@ -60,6 +66,17 @@ export interface TemplateFiltersType {
60
66
  [key: string]: FilterFunction;
61
67
  }
62
68
 
69
+ export interface HalleyFunctionDefinition {
70
+ name: string;
71
+ arguments: string[];
72
+ }
73
+
74
+ export interface HalleyFunctionBlobData {
75
+ _schema?: number;
76
+ version?: string;
77
+ functions?: HalleyFunctionDefinition[];
78
+ }
79
+
63
80
  export class RuleTemplate {
64
81
  ruleTemplateText: string;
65
82
  ast: ASTNode;
@@ -90,7 +107,7 @@ export class RuleTemplate {
90
107
  * @param variables Object mapping variable names to {value, type} objects
91
108
  * @returns Object with validation results: {valid, errors}
92
109
  */
93
- validate(variables: Variables): ValidationResult;
110
+ validate(variables: Variables, functionBlob?: HalleyFunctionBlob): ValidationResult;
94
111
 
95
112
  /**
96
113
  * Prepare the template by replacing variables with their values
@@ -122,9 +139,23 @@ export class GeneralTemplate {
122
139
 
123
140
  extractVariables(): VariableInfo[];
124
141
 
142
+ validate(): ValidationResult;
143
+
125
144
  prepare(variables: Variables): string;
126
145
  }
127
146
 
147
+ export class HalleyFunctionBlob {
148
+ _schema?: number;
149
+ version?: string;
150
+ functions: HalleyFunctionDefinition[];
151
+
152
+ constructor(jsonData: HalleyFunctionBlobData);
153
+
154
+ static fromURL(url: string): Promise<HalleyFunctionBlob>;
155
+
156
+ validate(functionName: string, variables?: any[]): string[];
157
+ }
158
+
128
159
  export class VariableTemplate {
129
160
  templateText: string;
130
161
  ast: ASTNode;
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const RuleTemplate = require('./src/RuleTemplate');
2
2
  const GeneralTemplate = require('./src/GeneralTemplate');
3
+ const HalleyFunctionBlob = require('./src/HalleyFunctionBlob');
3
4
  const VariableTemplate = require('./src/VariableTemplate');
4
5
  const VariableValidate = require('./src/VariableValidate');
5
6
 
@@ -9,4 +10,5 @@ module.exports.VariableTypes = RuleTemplate.VariableTypes;
9
10
  module.exports.TemplateFilters = RuleTemplate.TemplateFilters;
10
11
  module.exports.VariableValidate = VariableValidate;
11
12
  module.exports.GeneralTemplate = GeneralTemplate;
13
+ module.exports.HalleyFunctionBlob = HalleyFunctionBlob;
12
14
  module.exports.VariableTemplate = VariableTemplate;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-templater",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "The grammar for HalleyAssist rules",
5
5
  "main": "src/RuleTemplate.production.js",
6
6
  "browser": "./dist/rule-templater.browser.js",
@@ -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 filterName of parsedExpression.filters) {
80
- if (!TemplateFilters[filterName]) {
81
- throw new Error(`Unknown filter '${filterName}'`);
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 '';