@halleyassist/rule-templater 0.0.11 → 0.0.13

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
@@ -144,6 +144,49 @@ const prepared = parsed.prepare({
144
144
  '${window|time_start}' with window={from:'08:00',to:'12:00'} → 08:00
145
145
  ```
146
146
 
147
+ ### General String Templating
148
+
149
+ For non-rule text templates, use `GeneralTemplate`:
150
+
151
+ ```javascript
152
+ const RuleTemplate = require('@halleyassist/rule-templater');
153
+ const { GeneralTemplate } = RuleTemplate;
154
+
155
+ const template = 'If a door is opened between ${ALERT_PERIOD | time_start} AND ${ALERT_PERIOD | time_end}';
156
+
157
+ const variables = GeneralTemplate.getVariables(template);
158
+ // [{ name: 'ALERT_PERIOD', filters: ['time_start', 'time_end'], positions: [...] }]
159
+
160
+ const parsed = GeneralTemplate.parse(template);
161
+ const prepared = parsed.prepare({
162
+ ALERT_PERIOD: {
163
+ value: { from: '08:00', to: '12:00' },
164
+ type: 'time period'
165
+ }
166
+ });
167
+ // If a door is opened between 08:00 AND 12:00
168
+ ```
169
+
170
+ ### Variable Template
171
+
172
+ For a single variable expression, use `VariableTemplate`:
173
+
174
+ ```javascript
175
+ const { VariableTemplate } = require('@halleyassist/rule-templater');
176
+
177
+ const parsed = VariableTemplate.parse('ALERT_PERIOD|time_start|upper');
178
+ const variable = parsed.extractVariable();
179
+ // { name: 'ALERT_PERIOD', filters: ['time_start', 'upper'] }
180
+
181
+ const formatted = parsed.format({
182
+ ALERT_PERIOD: {
183
+ value: { from: '08:00', to: '12:00' },
184
+ type: 'time period'
185
+ }
186
+ });
187
+ // { value: '08:00', type: 'string' }
188
+ ```
189
+
147
190
  ## API
148
191
 
149
192
  ### `RuleTemplate.parse(ruleTemplate)`
@@ -213,6 +256,22 @@ Prepares the template by replacing variables with their values and applying any
213
256
 
214
257
  **Returns:** The prepared rule string with variables replaced and filters applied
215
258
 
259
+ ### `GeneralTemplate.parse(templateText)`
260
+
261
+ Parses a general string template and returns a `GeneralTemplate` instance.
262
+
263
+ ### `GeneralTemplate.getVariables(templateText)` (Static)
264
+
265
+ Extracts variables from a general string template without creating an instance manually.
266
+
267
+ ### `generalTemplate.getVariables()`
268
+
269
+ Extracts variables from a general string template.
270
+
271
+ ### `generalTemplate.prepare(variables)`
272
+
273
+ Prepares a general string template by replacing `${...}` placeholders with values and applying filters.
274
+
216
275
  ### `RuleTemplate.validateVariableNode(astNode, variableType)` (Static)
217
276
 
218
277
  Helper method to validate that an AST node matches the expected variable type.
@@ -255,6 +314,7 @@ The following variable types are supported:
255
314
  - `boolean`
256
315
  - `object`
257
316
  - `time period`
317
+ - `time period ago`
258
318
  - `time value`
259
319
  - `string array`
260
320
  - `number array`
@@ -1,4 +1,4 @@
1
- (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.RuleTemplater = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
1
+ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.RuleTemplate = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
2
2
  module.exports=[{"name":"statement_main","bnf":[["statement","EOF"]]},{"name":"logical_operator","bnf":[["AND"],["OR"]]},{"name":"%statement[2]","bnf":[["logical_operator","expression"]],"fragment":true},{"name":"statement","bnf":[["expression","%statement[2]*"]]},{"name":"expression","bnf":[["not_expression"],["standard_expression"],["parenthesis_expression"]]},{"name":"parenthesis_expression","bnf":[["BEGIN_PARENTHESIS","WS*","statement","WS*","END_PARENTHESIS"]]},{"name":"%not_expression[2]","bnf":[["result"],["parenthesis_expression"]],"fragment":true},{"name":"not_expression","bnf":[["NOT","%not_expression[2]"]]},{"name":"%standard_expression[2][1]","bnf":[["WS*","eq_approx"]],"fragment":true},{"name":"%standard_expression[2][2]","bnf":[["WS*","basic_rhs"]],"fragment":true},{"name":"%standard_expression[2][3][1]","bnf":[["WS+","IS"]],"fragment":true},{"name":"%standard_expression[2][3]","bnf":[["%standard_expression[2][3][1]?","WS+","between"]],"fragment":true},{"name":"%standard_expression[2][4]","bnf":[["WS+","in_expr"]],"fragment":true},{"name":"%standard_expression[2]","bnf":[["%standard_expression[2][1]"],["%standard_expression[2][2]"],["%standard_expression[2][3]"],["%standard_expression[2][4]"]],"fragment":true},{"name":"standard_expression","bnf":[["result","%standard_expression[2]?"]]},{"name":"basic_rhs","bnf":[["operator","WS*","result"]]},{"name":"eq_approx","bnf":[["eq_operator","WS*","\"~\"","WS*","result"]]},{"name":"PLUS","bnf":[["\"+\""]]},{"name":"MINUS","bnf":[["\"-\""]]},{"name":"MULTIPLY","bnf":[["\"*\""]]},{"name":"DIVIDE","bnf":[["\"/\""]]},{"name":"MODULUS","bnf":[["\"%\""]]},{"name":"DEFAULT_VAL","bnf":[["\"??\""]]},{"name":"arithmetic_operator","bnf":[["PLUS"],["MINUS"],["MULTIPLY"],["DIVIDE"],["MODULUS"],["DEFAULT_VAL"]]},{"name":"number_atom","bnf":[["number"]]},{"name":"number_time_atom","bnf":[["number_time"]]},{"name":"tod_atom","bnf":[["number_tod"]]},{"name":"dow_atom","bnf":[["dow"]]},{"name":"arithmetic_operand","bnf":[["fcall"],["number_time_atom"],["number_atom"]]},{"name":"%arithmetic_result[5]","bnf":[["arithmetic_result"],["arithmetic_operand"]],"fragment":true},{"name":"arithmetic_result","bnf":[["arithmetic_operand","WS*","arithmetic_operator","WS*","%arithmetic_result[5]"]]},{"name":"simple_result","bnf":[["fcall"],["value"]]},{"name":"result","bnf":[["arithmetic_result"],["simple_result"]]},{"name":"value_atom","bnf":[["false"],["true"],["array"],["time_period"],["number_time_atom"],["number_atom"],["tod_atom"],["string"]]},{"name":"value","bnf":[["value_atom"]]},{"name":"BEGIN_ARRAY","bnf":[["WS*",/\x5B/,"WS*"]]},{"name":"BEGIN_OBJECT","bnf":[["WS*",/\x7B/,"WS*"]]},{"name":"END_ARRAY","bnf":[["WS*",/\x5D/,"WS*"]]},{"name":"END_OBJECT","bnf":[["WS*",/\x7D/,"WS*"]]},{"name":"NAME_SEPARATOR","bnf":[["WS*",/\x3A/,"WS*"]]},{"name":"VALUE_SEPARATOR","bnf":[["WS*",/\x2C/,"WS*"]]},{"name":"WS","bnf":[[/[\x20\x09\x0A\x0D]/]]},{"name":"operator","bnf":[["GTE"],["LTE"],["GT"],["LT"],["EQ"],["NEQ"]]},{"name":"eq_operator","bnf":[["EQ"],["NEQ"]]},{"name":"BEGIN_ARGUMENT","bnf":[["\"(\""]]},{"name":"END_ARGUMENT","bnf":[["\")\""]]},{"name":"BEGIN_PARENTHESIS","bnf":[["\"(\""]]},{"name":"END_PARENTHESIS","bnf":[["\")\""]]},{"name":"BEGIN_IN","bnf":[[/[Ii]/,/[Nn]/]]},{"name":"in_expr","bnf":[["BEGIN_IN","WS*","BEGIN_PARENTHESIS","WS*","arguments","END_PARENTHESIS"]]},{"name":"argument","bnf":[["statement","WS*"]]},{"name":"%arguments[2]","bnf":[["WS*","\",\"","WS*","argument"]],"fragment":true},{"name":"arguments","bnf":[["argument","%arguments[2]*"]]},{"name":"%fname[1]","bnf":[[/[A-Za-z0-9]/]]},{"name":"fname","bnf":[["%fname[1]+"]]},{"name":"fcall","bnf":[["fname","WS*","BEGIN_ARGUMENT","WS*","arguments?","END_ARGUMENT"]]},{"name":"%between_dash_or_and[1]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_dash_or_and[2]","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"between_dash_or_and","bnf":[["%between_dash_or_and[1]"],["%between_dash_or_and[2]"]]},{"name":"between_number_inner","bnf":[["number_atom"],["number_time_atom"]]},{"name":"between_number","bnf":[["between_number_inner","between_dash_or_and","between_number_inner"]]},{"name":"between_number_time_inner","bnf":[["number_time_atom"]]},{"name":"%between_number_time[4]","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_number_time","bnf":[["between_number_time_inner","between_dash_or_and","between_number_time_inner","%between_number_time[4]?"]]},{"name":"between_tod_inner","bnf":[["tod_atom"]]},{"name":"%between_tod[2]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_tod[4]","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_tod","bnf":[["between_tod_inner","%between_tod[2]","between_tod_inner","%between_tod[4]?"]]},{"name":"%between[3]","bnf":[["between_number"],["between_tod"]],"fragment":true},{"name":"between","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","%between[3]"]]},{"name":"dow","bnf":[[/[Mm]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Oo]/,/[Nn]/],[/[Tt]/,/[Uu]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Uu]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Dd]/,/[Nn]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ww]/,/[Ee]/,/[Dd]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Hh]/,/[Uu]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/],[/[Ff]/,/[Rr]/,/[Ii]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ff]/,/[Rr]/,/[Ii]/],[/[Ss]/,/[Aa]/,/[Tt]/,/[Uu]/,/[Rr]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Aa]/,/[Tt]/],[/[Ss]/,/[Uu]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Uu]/,/[Nn]/]]},{"name":"dow_range_inner","bnf":[["dow_atom"]]},{"name":"%dow_range[4]","bnf":[["WS+",/[Tt]/,/[Oo]/,"WS+","dow_range_inner"]],"fragment":true},{"name":"dow_range","bnf":[[/[Oo]/,/[Nn]/,"WS+","dow_range_inner","%dow_range[4]?"]]},{"name":"between_time_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_number_time"]]},{"name":"between_tod_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_tod"]]},{"name":"between_time_only_atom","bnf":[["between_time_only"]]},{"name":"between_tod_only_atom","bnf":[["between_tod_only"]]},{"name":"%AND[1]","bnf":[["WS*",/&/,/&/,"WS*"]],"fragment":true},{"name":"%AND[2]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"AND","bnf":[["%AND[1]"],["%AND[2]"]]},{"name":"%OR[1]","bnf":[["WS*",/\|/,/\|/,"WS*"]],"fragment":true},{"name":"%OR[2]","bnf":[["WS+",/[Oo]/,/[Rr]/,"WS+"]],"fragment":true},{"name":"OR","bnf":[["%OR[1]"],["%OR[2]"]]},{"name":"AGO","bnf":[[/[Aa]/,/[Gg]/,/[Oo]/]]},{"name":"GT","bnf":[["\">\""]]},{"name":"LT","bnf":[["\"<\""]]},{"name":"GTE","bnf":[["\">=\""]]},{"name":"LTE","bnf":[["\"<=\""]]},{"name":"IS","bnf":[[/[Ii]/,/[Ss]/]]},{"name":"EQ","bnf":[["\"==\""],["\"=\""]]},{"name":"NEQ","bnf":[["\"!=\""]]},{"name":"%NOT[1]","bnf":[[/!/,"WS*"]],"fragment":true},{"name":"%NOT[2]","bnf":[[/[Nn]/,/[Oo]/,/[Tt]/,"WS+"]],"fragment":true},{"name":"NOT","bnf":[["%NOT[1]"],["%NOT[2]"]]},{"name":"false","bnf":[[/[Ff]/,/[Aa]/,/[Ll]/,/[Ss]/,/[Ee]/]]},{"name":"null","bnf":[[/[Nn]/,/[Uu]/,/[Ll]/,/[Ll]/]]},{"name":"true","bnf":[[/[Tt]/,/[Rr]/,/[Uu]/,/[Ee]/]]},{"name":"%array[2][2]","bnf":[["VALUE_SEPARATOR","value"]],"fragment":true},{"name":"%array[2]","bnf":[["value","%array[2][2]*"]],"fragment":true},{"name":"array","bnf":[["BEGIN_ARRAY","%array[2]?","END_ARRAY"]]},{"name":"unit","bnf":[[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/,/[Ss]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/,/[Ss]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/,/[Ss]/],[/[Dd]/,/[Aa]/,/[Yy]/,/[Ss]/],[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/],[/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/]]},{"name":"%number[2][1]","bnf":[[/[0-9]/]]},{"name":"%number[2]","bnf":[["%number[2][1]+"]],"fragment":true},{"name":"%number[3][2]","bnf":[[/[0-9]/]]},{"name":"%number[3]","bnf":[["\".\"","%number[3][2]+"]],"fragment":true},{"name":"%number[4][2]","bnf":[["\"-\""],["\"+\""]],"fragment":true},{"name":"%number[4][3][2]","bnf":[[/[0-9]/]]},{"name":"%number[4][3]","bnf":[["\"0\""],[/[1-9]/,"%number[4][3][2]*"]],"fragment":true},{"name":"%number[4]","bnf":[["\"e\"","%number[4][2]?","%number[4][3]"]],"fragment":true},{"name":"number","bnf":[["\"-\"?","%number[2]","%number[3]?","%number[4]?"]]},{"name":"number_time","bnf":[["number","WS+","unit"]]},{"name":"%number_tod[1][1]","bnf":[[/[0-9]/]]},{"name":"%number_tod[1]","bnf":[["%number_tod[1][1]+"]],"fragment":true},{"name":"%number_tod[3][1]","bnf":[[/[0-9]/]]},{"name":"%number_tod[3]","bnf":[["%number_tod[3][1]+"]],"fragment":true},{"name":"number_tod","bnf":[["%number_tod[1]","\":\"","%number_tod[3]"]]},{"name":"%time_period_ago[2]","bnf":[["WS+","number_time_atom"]],"fragment":true},{"name":"time_period_ago","bnf":[["number_time_atom","%time_period_ago[2]*","WS+","AGO"]]},{"name":"%time_period_ago_between[2]","bnf":[["WS+","number_time_atom"]],"fragment":true},{"name":"%time_period_ago_between[6]","bnf":[["between_time_only_atom"],["between_tod_only_atom"]],"fragment":true},{"name":"time_period_ago_between","bnf":[["number_time_atom","%time_period_ago_between[2]*","WS+","AGO","WS+","%time_period_ago_between[6]"]]},{"name":"time_period_const","bnf":[[/[Tt]/,/[Oo]/,/[Dd]/,/[Aa]/,/[Yy]/],["time_period_ago"]]},{"name":"time_period","bnf":[["time_period_ago_between"],["time_period_const"],["between_tod_only"],["between_time_only"]]},{"name":"%string[2][1]","bnf":[[/[\x20-\x21]/],[/[\x23-\x5B]/],[/[\x5D-\uFFFF]/]],"fragment":true},{"name":"%string[2][2]","bnf":[[/\x22/],[/\x5C/],[/\x2F/],[/\x62/],[/\x66/],[/\x6E/],[/\x72/],[/\x74/],[/\x75/,"HEXDIG","HEXDIG","HEXDIG","HEXDIG"]],"fragment":true},{"name":"%string[2]","bnf":[["%string[2][1]"],[/\x5C/,"%string[2][2]"]],"fragment":true},{"name":"string","bnf":[["'\"'","%string[2]*","'\"'"]]},{"name":"HEXDIG","bnf":[[/[a-fA-F0-9]/]]}]
3
3
  },{}],2:[function(require,module,exports){
4
4
  const {Parser} = require('ebnf/dist/Parser.js'), {ParsingError} = require('ebnf'), RuleParseError = require('./errors/RuleParseError');
@@ -3644,10 +3644,10 @@ Object.defineProperty(exports, "ParsingError", { enumerable: true, get: function
3644
3644
  exports.Grammars = require("./Grammars");
3645
3645
 
3646
3646
  },{"./Grammars":8,"./Parser":9,"./ParsingError":10,"./TokenError":12}],14:[function(require,module,exports){
3647
+ module.exports = require('./RuleTemplate.production.js');
3648
+ },{"./RuleTemplate.production.js":16}],15:[function(require,module,exports){
3647
3649
  module.exports=[{"name":"TEMPLATE_BEGIN","bnf":[["\"${\""]]},{"name":"TEMPLATE_END","bnf":[["\"}\""]]},{"name":"PIPE","bnf":[["\"|\""]]},{"name":"%IDENT[2]","bnf":[[/[A-Za-z0-9_]/]]},{"name":"IDENT","bnf":[[/[A-Za-z_]/,"%IDENT[2]*"]]},{"name":"DOT","bnf":[["\".\""]]},{"name":"template_value","bnf":[["TEMPLATE_BEGIN","WS*","template_expr","WS*","TEMPLATE_END"]]},{"name":"%template_expr[2]","bnf":[["WS*","template_pipe","WS*","template_filter_call"]],"fragment":true},{"name":"template_expr","bnf":[["template_path","%template_expr[2]*"]]},{"name":"template_pipe","bnf":[["PIPE"]]},{"name":"%template_path[2]","bnf":[["WS*","DOT","WS*","IDENT"]],"fragment":true},{"name":"template_path","bnf":[["IDENT","%template_path[2]*"]]},{"name":"%template_filter_call[2]","bnf":[["WS*","BEGIN_ARGUMENT","WS*","template_filter_args?","WS*","END_ARGUMENT"]],"fragment":true},{"name":"template_filter_call","bnf":[["template_filter_name","%template_filter_call[2]?"]]},{"name":"template_filter_name","bnf":[["IDENT"]]},{"name":"%template_filter_args[2]","bnf":[["WS*","\",\"","WS*","template_filter_arg"]],"fragment":true},{"name":"template_filter_args","bnf":[["template_filter_arg","%template_filter_args[2]*"]]},{"name":"template_filter_arg","bnf":[["value"],["template_value"]]},{"name":"number_atom","bnf":[["number"],["template_value"]]},{"name":"number_time_atom","bnf":[["number_time"],["template_value","WS+","unit"],["template_value"]]},{"name":"tod_atom","bnf":[["number_tod"],["template_value"]]},{"name":"dow_atom","bnf":[["dow"],["template_value"]]},{"name":"between_time_only_atom","bnf":[["between_time_only"],["template_value"]]},{"name":"between_tod_only_atom","bnf":[["between_tod_only"],["template_value"]]}]
3648
- },{}],15:[function(require,module,exports){
3649
- module.exports = require('./RuleTemplater.production.js');
3650
- },{"./RuleTemplater.production.js":16}],16:[function(require,module,exports){
3650
+ },{}],16:[function(require,module,exports){
3651
3651
  // Note: We are coupled closely with the ebnf grammar structure of rule-parser
3652
3652
  const TemplateGrammar = require('./RuleTemplate.production.ebnf.js'),
3653
3653
  TemplateFilters = require('./TemplateFilters'),
@@ -4068,12 +4068,13 @@ class RuleTemplate {
4068
4068
  }
4069
4069
  }
4070
4070
 
4071
- // Export the class and parser rules
4071
+ RuleTemplate.ParserRules = ParserRules;
4072
+ RuleTemplate.VariableTypes = VariableTypes;
4073
+ RuleTemplate.TemplateFilters = TemplateFilters;
4074
+
4072
4075
  module.exports = RuleTemplate;
4073
- module.exports.ParserRules = ParserRules;
4074
- module.exports.VariableTypes = VariableTypes;
4075
- module.exports.TemplateFilters = TemplateFilters;
4076
- },{"./RuleTemplate.production.ebnf.js":14,"./TemplateFilters":17,"@halleyassist/rule-parser":2,"ebnf":13}],17:[function(require,module,exports){
4076
+
4077
+ },{"./RuleTemplate.production.ebnf.js":15,"./TemplateFilters":17,"@halleyassist/rule-parser":2,"ebnf":13}],17:[function(require,module,exports){
4077
4078
  /*
4078
4079
  Template filters are functions that transform variable values.
4079
4080
  They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
@@ -4231,5 +4232,5 @@ const TemplateFilters = {
4231
4232
  }
4232
4233
 
4233
4234
  module.exports = TemplateFilters;
4234
- },{}]},{},[15])(15)
4235
+ },{}]},{},[14])(14)
4235
4236
  });
package/index.d.ts CHANGED
@@ -10,8 +10,12 @@ export interface VariableInfo {
10
10
  }
11
11
 
12
12
  export interface VariableValue {
13
- value: string | number | boolean;
14
- type?: 'string' | 'number' | 'boolean' | 'object' | 'time period' | 'time value' | 'string array' | 'number array' | 'boolean array' | 'object array';
13
+ value: string | number | boolean | {
14
+ from: string;
15
+ to: string;
16
+ ago?: [number, string];
17
+ };
18
+ type?: 'string' | 'number' | 'boolean' | 'object' | 'time period' | 'time period ago' | 'time value' | 'string array' | 'number array' | 'boolean array' | 'object array';
15
19
  }
16
20
 
17
21
  export interface Variables {
@@ -50,7 +54,7 @@ export interface TemplateFiltersType {
50
54
  [key: string]: FilterFunction;
51
55
  }
52
56
 
53
- export default class RuleTemplate {
57
+ export class RuleTemplate {
54
58
  ruleTemplateText: string;
55
59
  ast: ASTNode;
56
60
 
@@ -99,6 +103,42 @@ export default class RuleTemplate {
99
103
  static validateVariableNode(astNode: ASTNode | null | undefined, variableType: string): boolean;
100
104
  }
101
105
 
106
+ export class GeneralTemplate {
107
+ templateText: string;
108
+
109
+ constructor(templateText: string);
110
+
111
+ static parse(templateText: string): GeneralTemplate;
112
+
113
+ static getVariables(templateText: string): VariableInfo[];
114
+
115
+ getVariables(): VariableInfo[];
116
+
117
+ extractVariables(): VariableInfo[];
118
+
119
+ prepare(variables: Variables): string;
120
+ }
121
+
122
+ export class VariableTemplate {
123
+ templateText: string;
124
+ ast: ASTNode;
125
+ variable: {
126
+ name: string;
127
+ filters: string[];
128
+ };
129
+
130
+ constructor(templateText: string, ast: ASTNode, variableInfo: { name: string; filters: string[] });
131
+
132
+ static parse(templateText: string): VariableTemplate;
133
+
134
+ extractVariable(): {
135
+ name: string;
136
+ filters: string[];
137
+ };
138
+
139
+ format(variableData: VariableValue | Variables): VariableValue;
140
+ }
141
+
102
142
  export const ParserRules: any[];
103
143
  export const VariableTypes: string[];
104
144
  export const TemplateFilters: TemplateFiltersType;
package/index.js CHANGED
@@ -1 +1,10 @@
1
- module.exports = require('./src/RuleTemplater')
1
+ const RuleTemplate = require('./src/RuleTemplate');
2
+ const GeneralTemplate = require('./src/GeneralTemplate');
3
+ const VariableTemplate = require('./src/VariableTemplate');
4
+
5
+ module.exports.RuleTemplate = RuleTemplate;
6
+ module.exports.ParserRules = RuleTemplate.ParserRules;
7
+ module.exports.VariableTypes = RuleTemplate.VariableTypes;
8
+ module.exports.TemplateFilters = RuleTemplate.TemplateFilters;
9
+ module.exports.GeneralTemplate = GeneralTemplate;
10
+ module.exports.VariableTemplate = VariableTemplate;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-templater",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "The grammar for HalleyAssist rules",
5
- "main": "src/RuleTemplater.production.js",
5
+ "main": "src/RuleTemplate.production.js",
6
6
  "browser": "./dist/rule-templater.browser.js",
7
7
  "types": "index.d.ts",
8
8
  "scripts": {
@@ -0,0 +1,125 @@
1
+ const TemplateFilters = require('./TemplateFilters');
2
+
3
+ class GeneralTemplate {
4
+ constructor(templateText) {
5
+ this.templateText = templateText;
6
+ }
7
+
8
+ static parse(templateText) {
9
+ return new GeneralTemplate(templateText);
10
+ }
11
+
12
+ static getVariables(templateText) {
13
+ return GeneralTemplate.parse(templateText).getVariables();
14
+ }
15
+
16
+ getVariables() {
17
+ const variables = [];
18
+ const variableMap = new Map();
19
+ const pattern = /\$\{([^}]*)\}/g;
20
+
21
+ for (const match of this.templateText.matchAll(pattern)) {
22
+ const parsedExpression = this._parseTemplateExpression(match[1]);
23
+ if (parsedExpression) {
24
+ if (variableMap.has(parsedExpression.name)) {
25
+ const existing = variableMap.get(parsedExpression.name);
26
+ existing.positions.push({
27
+ start: match.index,
28
+ end: match.index + match[0].length
29
+ });
30
+ existing.filters = Array.from(new Set(existing.filters.concat(parsedExpression.filters)));
31
+ } else {
32
+ variableMap.set(parsedExpression.name, {
33
+ name: parsedExpression.name,
34
+ filters: parsedExpression.filters,
35
+ positions: [{
36
+ start: match.index,
37
+ end: match.index + match[0].length
38
+ }]
39
+ });
40
+ }
41
+ }
42
+ }
43
+
44
+ for (const variable of variableMap.values()) {
45
+ variables.push(variable);
46
+ }
47
+
48
+ return variables;
49
+ }
50
+
51
+ extractVariables() {
52
+ return this.getVariables();
53
+ }
54
+
55
+ prepare(variables) {
56
+ if (!variables || typeof variables !== 'object') {
57
+ throw new Error('Variables must be provided as an object');
58
+ }
59
+
60
+ return this.templateText.replace(/\$\{([^}]*)\}/g, (matchText, expression) => {
61
+ const parsedExpression = this._parseTemplateExpression(expression);
62
+ if (!parsedExpression) {
63
+ return matchText;
64
+ }
65
+
66
+ const varName = parsedExpression.name;
67
+ if (!Object.prototype.hasOwnProperty.call(variables, varName)) {
68
+ throw new Error(`Variable '${varName}' not provided in variables object`);
69
+ }
70
+
71
+ let varData = variables[varName];
72
+ if (typeof varData !== 'object' || !Object.prototype.hasOwnProperty.call(varData, 'value')) {
73
+ throw new Error(`Variable '${varName}' must be an object with 'value' property`);
74
+ }
75
+
76
+ varData = Object.assign({}, varData);
77
+
78
+ 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}'`);
82
+ }
83
+
84
+ TemplateFilters[filterName](varData);
85
+ }
86
+ }
87
+
88
+ return this._serializeVariable(varData);
89
+ });
90
+ }
91
+
92
+ _parseTemplateExpression(expression) {
93
+ if (!expression) {
94
+ return null;
95
+ }
96
+
97
+ const segments = expression.split('|').map(s => s.trim()).filter(Boolean);
98
+ if (segments.length === 0) {
99
+ return null;
100
+ }
101
+
102
+ return {
103
+ name: segments[0],
104
+ filters: segments.slice(1)
105
+ };
106
+ }
107
+
108
+ _serializeVariable(varData) {
109
+ if (varData.value === null || varData.value === undefined) {
110
+ return '';
111
+ }
112
+
113
+ if (varData.type === 'time period' || varData.type === 'time period ago') {
114
+ let ret = `${varData.value.from} TO ${varData.value.to}`;
115
+ if (varData.value.ago) {
116
+ ret += ` AGO ${varData.value.ago[0]} ${varData.value.ago[1]}`;
117
+ }
118
+ return ret;
119
+ }
120
+
121
+ return String(varData.value);
122
+ }
123
+ }
124
+
125
+ module.exports = GeneralTemplate;
@@ -0,0 +1 @@
1
+ module.exports = require('./RuleTemplate.production.js');
@@ -418,8 +418,8 @@ class RuleTemplate {
418
418
  }
419
419
  }
420
420
 
421
- // Export the class and parser rules
421
+ RuleTemplate.ParserRules = ParserRules;
422
+ RuleTemplate.VariableTypes = VariableTypes;
423
+ RuleTemplate.TemplateFilters = TemplateFilters;
424
+
422
425
  module.exports = RuleTemplate;
423
- module.exports.ParserRules = ParserRules;
424
- module.exports.VariableTypes = VariableTypes;
425
- module.exports.TemplateFilters = TemplateFilters;
@@ -0,0 +1,425 @@
1
+ // Note: We are coupled closely with the ebnf grammar structure of rule-parser
2
+ const TemplateGrammar = require('./RuleTemplate.production.ebnf.js'),
3
+ TemplateFilters = require('./TemplateFilters'),
4
+ RuleParser = require('@halleyassist/rule-parser'),
5
+ RuleParserRules = RuleParser.ParserRules,
6
+ {Parser} = require('ebnf');
7
+
8
+ let ParserCache = null;
9
+
10
+ const VariableTypes = [
11
+ 'string',
12
+ 'number',
13
+ 'boolean',
14
+ 'object',
15
+ 'time period',
16
+ 'time period ago',
17
+ 'time value',
18
+ 'string array',
19
+ 'number array',
20
+ 'boolean array',
21
+ 'object array'
22
+ ]
23
+
24
+ const AllowedTypeMapping = {
25
+ 'string': ['string_atom', 'string_concat'],
26
+ 'number': ['number_atom', 'math_expr'],
27
+ 'boolean': ['boolean_atom', 'boolean_expr'],
28
+ 'time period': ['time_period_atom'],
29
+ 'time period ago': ['time_period_atom'],
30
+ 'time value': ['time_value_atom', 'tod_atom'],
31
+ 'string array': ['string_array'],
32
+ 'number array': ['number_array'],
33
+ 'boolean array': ['boolean_array'],
34
+ 'object': ['object_atom'],
35
+ 'object array': ['object_array']
36
+ };
37
+
38
+ // Merge the base grammar with template-specific grammar rules
39
+ const extendedGrammar = [...RuleParserRules]
40
+ for(const rule of TemplateGrammar){
41
+ const idx = extendedGrammar.findIndex(r => r.name === rule.name);
42
+ if(idx !== -1){
43
+ extendedGrammar[idx] = rule;
44
+ } else {
45
+ extendedGrammar.push(rule);
46
+ }
47
+ }
48
+
49
+ // Add template_value as an alternative to value_atom so templates can be parsed
50
+ const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
51
+ if (valueAtomIdx !== -1) {
52
+ extendedGrammar[valueAtomIdx] = Object.assign({}, extendedGrammar[valueAtomIdx]);
53
+ extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
54
+ }
55
+
56
+ // Export the parser rules for potential external use
57
+ const ParserRules = extendedGrammar;
58
+
59
+ class RuleTemplate {
60
+ constructor(ruleTemplateText, ast) {
61
+ this.ruleTemplateText = ruleTemplateText;
62
+ this.ast = ast;
63
+ }
64
+
65
+ /**
66
+ * Parse a rule template string and return a RuleTemplate instance
67
+ * @param {string} ruleTemplate - The template string to parse
68
+ * @returns {RuleTemplate} Instance with AST and template text
69
+ */
70
+ static parse(ruleTemplate){
71
+ if(!ParserCache){
72
+ ParserCache = new Parser(ParserRules, {debug: false})
73
+ }
74
+
75
+ const ast = RuleParser.toAst(ruleTemplate.trim(), ParserCache);
76
+ return new RuleTemplate(ruleTemplate, ast);
77
+ }
78
+
79
+ /**
80
+ * Extract variables from the template using the AST
81
+ * @returns {Array} Array of {name, filters: [], positions: [{start, end}]} objects
82
+ */
83
+ extractVariables(){
84
+ const variables = [];
85
+ const variableMap = new Map();
86
+
87
+ const traverse = (node) => {
88
+ if (!node) return;
89
+
90
+ // Check if this is a template_value node
91
+ if (node.type === 'template_value') {
92
+ // Extract the variable information
93
+ const varInfo = this._extractVariableFromNode(node);
94
+ if (varInfo) {
95
+ // Add position to existing variable or create new entry
96
+ if (variableMap.has(varInfo.name)) {
97
+ const existing = variableMap.get(varInfo.name);
98
+ existing.positions.push({
99
+ start: varInfo.start,
100
+ end: varInfo.end
101
+ });
102
+ } else {
103
+ variableMap.set(varInfo.name, {
104
+ name: varInfo.name,
105
+ filters: varInfo.filters,
106
+ positions: [{
107
+ start: varInfo.start,
108
+ end: varInfo.end
109
+ }]
110
+ });
111
+ }
112
+ }
113
+ }
114
+
115
+ // Traverse children
116
+ if (node.children) {
117
+ for (const child of node.children) {
118
+ traverse(child);
119
+ }
120
+ }
121
+ };
122
+
123
+ traverse(this.ast);
124
+
125
+ // Convert map to array
126
+ for (const variable of variableMap.values()) {
127
+ variables.push(variable);
128
+ }
129
+
130
+ return variables;
131
+ }
132
+
133
+ /**
134
+ * Extract function calls from the template using the AST
135
+ * @returns {Array} Array of unique function names used in the template
136
+ */
137
+ extractFunctions(){
138
+ const functions = new Set();
139
+
140
+ const traverse = (node) => {
141
+ if (!node) return;
142
+
143
+ // Check if this is a function call node
144
+ if (node.type === 'fcall') {
145
+ // Find the function name in children
146
+ const fnameNode = node.children?.find(c => c.type === 'fname');
147
+ if (fnameNode && fnameNode.text) {
148
+ functions.add(fnameNode.text.trim());
149
+ }
150
+ }
151
+
152
+ // Traverse children
153
+ if (node.children) {
154
+ for (const child of node.children) {
155
+ traverse(child);
156
+ }
157
+ }
158
+ };
159
+
160
+ traverse(this.ast);
161
+
162
+ // Convert set to sorted array for consistent output
163
+ return Array.from(functions).sort();
164
+ }
165
+
166
+ /**
167
+ * Extract variable name and filters from a template_value AST node
168
+ * @private
169
+ */
170
+ _extractVariableFromNode(node) {
171
+ if (node.type !== 'template_value') return null;
172
+
173
+ // Find the template_expr child
174
+ const templateExpr = node.children?.find(c => c.type === 'template_expr');
175
+ if (!templateExpr) return null;
176
+
177
+ // Extract the path (variable name)
178
+ const templatePath = templateExpr.children?.find(c => c.type === 'template_path');
179
+ if (!templatePath || !templatePath.text) return null;
180
+
181
+ const name = templatePath.text.trim();
182
+
183
+ // Extract filters
184
+ const filters = [];
185
+ for (const child of templateExpr.children || []) {
186
+ if (child.type === 'template_filter_call') {
187
+ const filterName = this._extractFilterName(child);
188
+ if (filterName) {
189
+ filters.push(filterName);
190
+ }
191
+ }
192
+ }
193
+
194
+ // Extract position information from the node
195
+ const start = node.start;
196
+ const end = node.end;
197
+
198
+ return { name, filters, start, end };
199
+ }
200
+
201
+ /**
202
+ * Extract filter name from template_filter_call node
203
+ * @private
204
+ */
205
+ _extractFilterName(node) {
206
+ const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
207
+ if (!filterNameNode || !filterNameNode.text) return null;
208
+
209
+ return filterNameNode.text.trim();
210
+ }
211
+
212
+ /**
213
+ * Validate variable types against the AST
214
+ * @param {Object} variables - Object mapping variable names to {type} objects
215
+ * @returns {Object} Object with validation results: {valid: boolean, errors: []}
216
+ */
217
+ validate(variables) {
218
+ if (!variables || typeof variables !== 'object') {
219
+ return {
220
+ valid: false,
221
+ errors: ['Variables must be provided as an object']
222
+ };
223
+ }
224
+
225
+ const errors = [];
226
+ const extractedVars = this.extractVariables();
227
+
228
+ for (const varInfo of extractedVars) {
229
+ const varName = varInfo.name;
230
+
231
+ // Check if variable is provided
232
+ if (!variables.hasOwnProperty(varName)) {
233
+ errors.push(`Variable '${varName}' not provided in variables object`);
234
+ continue;
235
+ }
236
+
237
+ const varData = variables[varName];
238
+ if (typeof varData !== 'object') {
239
+ errors.push(`Variable '${varName}' must be an object`);
240
+ continue;
241
+ }
242
+
243
+ const { type } = varData;
244
+
245
+ // Validate type if provided
246
+ if (type && !VariableTypes.includes(type)) {
247
+ errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
248
+ }
249
+ }
250
+
251
+ return {
252
+ valid: errors.length === 0,
253
+ errors
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Prepare the template by replacing variables with their values
259
+ * Rebuilds from AST by iterating through children
260
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
261
+ * @returns {string} The prepared rule string
262
+ */
263
+ prepare(variables){
264
+ if (!variables || typeof variables !== 'object') {
265
+ throw new Error('Variables must be provided as an object');
266
+ }
267
+
268
+ // Rebuild the rule string from AST
269
+ return this._rebuildFromAST(this.ast, variables);
270
+ }
271
+
272
+ /**
273
+ * Rebuild rule string from AST node, replacing template_value nodes with variable values
274
+ * @private
275
+ * @param {Object} node - AST node
276
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
277
+ * @returns {string} Rebuilt string
278
+ */
279
+ _rebuildFromAST(node, variables) {
280
+ if (!node) return '';
281
+
282
+ // If this is a template_value node, replace it with the computed value
283
+ if (node.type === 'template_value') {
284
+ return this._computeTemplateReplacement(node, variables);
285
+ }
286
+
287
+ // If node has no children, it's a leaf - return its text
288
+ if (!node.children || node.children.length === 0) {
289
+ return node.text || '';
290
+ }
291
+
292
+ // Node has children - rebuild by iterating through children and preserving gaps
293
+ let result = '';
294
+ const originalText = node.text || '';
295
+ let lastEnd = node.start || 0;
296
+
297
+ for (const child of node.children) {
298
+ // Add any text between the last child's end and this child's start (gaps/syntax)
299
+ if (child.start !== undefined && child.start > lastEnd) {
300
+ result += originalText.substring(lastEnd - (node.start || 0), child.start - (node.start || 0));
301
+ }
302
+
303
+ // Add the child's rebuilt text
304
+ result += this._rebuildFromAST(child, variables);
305
+
306
+ // Update lastEnd to this child's end position
307
+ if (child.end !== undefined) {
308
+ lastEnd = child.end;
309
+ }
310
+ }
311
+
312
+ // Add any remaining text after the last child
313
+ if (node.end !== undefined && lastEnd < node.end) {
314
+ result += originalText.substring(lastEnd - (node.start || 0), node.end - (node.start || 0));
315
+ }
316
+
317
+ return result;
318
+ }
319
+
320
+ /**
321
+ * Compute the replacement value for a template_value node
322
+ * @private
323
+ * @param {Object} node - template_value AST node
324
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
325
+ * @returns {string} Replacement string
326
+ */
327
+ _computeTemplateReplacement(node, variables) {
328
+ const templateInfo = this._extractVariableFromNode(node);
329
+ if (!templateInfo) {
330
+ throw new Error(`Failed to extract variable information from template node`);
331
+ }
332
+
333
+ const varName = templateInfo.name;
334
+
335
+ // Validate variable is provided
336
+ if (!variables.hasOwnProperty(varName)) {
337
+ throw new Error(`Variable '${varName}' not provided in variables object`);
338
+ }
339
+
340
+ let varData = variables[varName];
341
+ if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
342
+ throw new Error(`Variable '${varName}' must be an object with 'value' property`);
343
+ }
344
+
345
+ varData = Object.assign({}, varData);
346
+
347
+ // Require type property for all variables
348
+ if (!varData.hasOwnProperty('type')) {
349
+ throw new Error(`Variable '${varName}' must have a 'type' property`);
350
+ }
351
+
352
+ // Validate type
353
+ if (!VariableTypes.includes(varData.type)) {
354
+ throw new Error(`Invalid variable type '${varData.type}' for variable '${varName}'`);
355
+ }
356
+
357
+ // Apply filters if present
358
+ if (templateInfo.filters && templateInfo.filters.length > 0) {
359
+ for (const filterName of templateInfo.filters) {
360
+ if (!TemplateFilters[filterName]) {
361
+ throw new Error(`Unknown filter '${filterName}'`);
362
+ }
363
+
364
+ TemplateFilters[filterName](varData);
365
+ }
366
+ }
367
+
368
+ return this._serializeVarData(varData, varName);
369
+ }
370
+
371
+ _serializeVarData(varData, varName) {
372
+ const { value, type } = varData;
373
+
374
+ if (!VariableTypes.includes(type)) {
375
+ throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
376
+ }
377
+
378
+ if (type === 'string') {
379
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
380
+ }
381
+
382
+ if (type === 'number') {
383
+ return String(value);
384
+ }
385
+
386
+ if (type === 'boolean') {
387
+ return value ? 'true' : 'false';
388
+ }
389
+
390
+ if (type === 'time period' || type === 'time period ago') {
391
+ let ret = `${value.from} TO ${value.to}`;
392
+ if(value.ago) {
393
+ ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
394
+ }
395
+ return ret;
396
+ }
397
+
398
+ return String(value);
399
+ }
400
+
401
+ /**
402
+ * Helper method to validate if an AST node matches a variable type
403
+ * @param {Object} astNode - The AST node to validate
404
+ * @param {string} variableType - The expected variable type
405
+ * @returns {boolean} True if valid, false otherwise
406
+ */
407
+ static validateVariableNode(astNode, variableType) {
408
+ if (!astNode || !astNode.type) {
409
+ return false;
410
+ }
411
+
412
+ const allowedTypes = AllowedTypeMapping[variableType];
413
+ if (!allowedTypes) {
414
+ return false;
415
+ }
416
+
417
+ return allowedTypes.includes(astNode.type);
418
+ }
419
+ }
420
+
421
+ RuleTemplate.ParserRules = ParserRules;
422
+ RuleTemplate.VariableTypes = VariableTypes;
423
+ RuleTemplate.TemplateFilters = TemplateFilters;
424
+
425
+ module.exports = RuleTemplate;
@@ -1 +1 @@
1
- module.exports = require('./RuleTemplater.production.js');
1
+ module.exports = require('./RuleTemplate.production.js');
@@ -13,7 +13,6 @@ const VariableTypes = [
13
13
  'boolean',
14
14
  'object',
15
15
  'time period',
16
- 'time period ago',
17
16
  'time value',
18
17
  'string array',
19
18
  'number array',
@@ -26,7 +25,6 @@ const AllowedTypeMapping = {
26
25
  'number': ['number_atom', 'math_expr'],
27
26
  'boolean': ['boolean_atom', 'boolean_expr'],
28
27
  'time period': ['time_period_atom'],
29
- 'time period ago': ['time_period_atom'],
30
28
  'time value': ['time_value_atom', 'tod_atom'],
31
29
  'string array': ['string_array'],
32
30
  'number array': ['number_array'],
@@ -49,8 +47,7 @@ for(const rule of TemplateGrammar){
49
47
  // Add template_value as an alternative to value_atom so templates can be parsed
50
48
  const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
51
49
  if (valueAtomIdx !== -1) {
52
- extendedGrammar[valueAtomIdx] = Object.assign({}, extendedGrammar[valueAtomIdx]);
53
- extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
50
+ extendedGrammar[valueAtomIdx].bnf.push(['template_value']);
54
51
  }
55
52
 
56
53
  // Export the parser rules for potential external use
@@ -337,12 +334,12 @@ class RuleTemplate {
337
334
  throw new Error(`Variable '${varName}' not provided in variables object`);
338
335
  }
339
336
 
340
- let varData = variables[varName];
337
+ const varData = variables[varName];
341
338
  if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
342
339
  throw new Error(`Variable '${varName}' must be an object with 'value' property`);
343
340
  }
344
-
345
- varData = Object.assign({}, varData);
341
+
342
+ let { value, type } = varData;
346
343
 
347
344
  // Require type property for all variables
348
345
  if (!varData.hasOwnProperty('type')) {
@@ -350,8 +347,8 @@ class RuleTemplate {
350
347
  }
351
348
 
352
349
  // Validate type
353
- if (!VariableTypes.includes(varData.type)) {
354
- throw new Error(`Invalid variable type '${varData.type}' for variable '${varName}'`);
350
+ if (!VariableTypes.includes(type)) {
351
+ throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
355
352
  }
356
353
 
357
354
  // Apply filters if present
@@ -360,42 +357,27 @@ class RuleTemplate {
360
357
  if (!TemplateFilters[filterName]) {
361
358
  throw new Error(`Unknown filter '${filterName}'`);
362
359
  }
363
-
364
- TemplateFilters[filterName](varData);
360
+ value = TemplateFilters[filterName](value);
365
361
  }
362
+ // After applying filters, the result is already a string representation
363
+ return String(value);
366
364
  }
367
-
368
- return this._serializeVarData(varData, varName);
369
- }
370
-
371
- _serializeVarData(varData, varName) {
372
- const { value, type } = varData;
373
-
374
- if (!VariableTypes.includes(type)) {
375
- throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
376
- }
377
-
365
+
366
+ // Convert value to string representation based on type
378
367
  if (type === 'string') {
368
+ // Escape backslashes first, then quotes in string values.
369
+ // Order is critical: escaping backslashes first prevents double-escaping.
370
+ // E.g., "test\" becomes "test\\" then "test\\\"" (correct)
371
+ // If reversed, "test\" would become "test\\"" then "test\\\\"" (incorrect)
379
372
  return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
380
- }
381
-
382
- if (type === 'number') {
373
+ } else if (type === 'number') {
383
374
  return String(value);
384
- }
385
-
386
- if (type === 'boolean') {
375
+ } else if (type === 'boolean') {
387
376
  return value ? 'true' : 'false';
377
+ } else {
378
+ // Default behavior - just insert the value as-is
379
+ return String(value);
388
380
  }
389
-
390
- if (type === 'time period' || type === 'time period ago') {
391
- let ret = `${value.from} TO ${value.to}`;
392
- if(value.ago) {
393
- ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
394
- }
395
- return ret;
396
- }
397
-
398
- return String(value);
399
381
  }
400
382
 
401
383
  /**
@@ -0,0 +1,126 @@
1
+ const { Parser } = require('ebnf');
2
+ const RuleTemplate = require('./RuleTemplate');
3
+ const TemplateFilters = require('./TemplateFilters');
4
+
5
+ let ParserCache = null;
6
+
7
+ class VariableTemplate {
8
+ constructor(templateText, ast, variableInfo) {
9
+ this.templateText = templateText;
10
+ this.ast = ast;
11
+ this.variable = variableInfo;
12
+ }
13
+
14
+ static parse(templateText) {
15
+ if (typeof templateText !== 'string') {
16
+ throw new Error('Variable template must be a string');
17
+ }
18
+
19
+ const expressionText = VariableTemplate._normalizeExpression(templateText);
20
+ if (!expressionText) {
21
+ throw new Error('Variable template cannot be empty');
22
+ }
23
+
24
+ if (!ParserCache) {
25
+ ParserCache = new Parser(RuleTemplate.ParserRules, { debug: false });
26
+ }
27
+
28
+ const ast = ParserCache.getAST(expressionText, 'template_expr');
29
+ const variableInfo = VariableTemplate._extractVariableFromAst(ast);
30
+ if (!variableInfo) {
31
+ throw new Error('Invalid variable template expression');
32
+ }
33
+
34
+ return new VariableTemplate(templateText, ast, variableInfo);
35
+ }
36
+
37
+ extractVariable() {
38
+ return {
39
+ name: this.variable.name,
40
+ filters: this.variable.filters.slice()
41
+ };
42
+ }
43
+
44
+ format(variableData) {
45
+ let varData = variableData;
46
+
47
+ if (!varData || typeof varData !== 'object') {
48
+ throw new Error('Variable data must be provided as an object');
49
+ }
50
+
51
+ if (!Object.prototype.hasOwnProperty.call(varData, 'value')) {
52
+ if (!Object.prototype.hasOwnProperty.call(varData, this.variable.name)) {
53
+ throw new Error(`Variable '${this.variable.name}' not provided`);
54
+ }
55
+
56
+ varData = varData[this.variable.name];
57
+ }
58
+
59
+ if (!varData || typeof varData !== 'object' || !Object.prototype.hasOwnProperty.call(varData, 'value')) {
60
+ throw new Error(`Variable '${this.variable.name}' must be an object with 'value' property`);
61
+ }
62
+
63
+ varData = VariableTemplate._cloneVarData(varData);
64
+
65
+ for (const filterName of this.variable.filters) {
66
+ if (!TemplateFilters[filterName]) {
67
+ throw new Error(`Unknown filter '${filterName}'`);
68
+ }
69
+
70
+ TemplateFilters[filterName](varData);
71
+ }
72
+
73
+ return varData;
74
+ }
75
+
76
+ static _normalizeExpression(templateText) {
77
+ const trimmed = templateText.trim();
78
+ if (trimmed.startsWith('${') && trimmed.endsWith('}')) {
79
+ return trimmed.slice(2, -1).trim();
80
+ }
81
+
82
+ return trimmed;
83
+ }
84
+
85
+ static _extractVariableFromAst(ast) {
86
+ if (!ast || ast.type !== 'template_expr') {
87
+ return null;
88
+ }
89
+
90
+ const templatePath = ast.children?.find(c => c.type === 'template_path');
91
+ if (!templatePath || !templatePath.text) {
92
+ return null;
93
+ }
94
+
95
+ const filters = [];
96
+ for (const child of ast.children || []) {
97
+ if (child.type === 'template_filter_call') {
98
+ const filterNameNode = child.children?.find(c => c.type === 'template_filter_name');
99
+ const filterName = filterNameNode?.text?.trim();
100
+ if (filterName) {
101
+ filters.push(filterName);
102
+ }
103
+ }
104
+ }
105
+
106
+ return {
107
+ name: templatePath.text.trim(),
108
+ filters
109
+ };
110
+ }
111
+
112
+ static _cloneVarData(varData) {
113
+ const cloned = Object.assign({}, varData);
114
+ if (cloned.value && typeof cloned.value === 'object') {
115
+ if (Array.isArray(cloned.value)) {
116
+ cloned.value = cloned.value.slice();
117
+ } else {
118
+ cloned.value = Object.assign({}, cloned.value);
119
+ }
120
+ }
121
+
122
+ return cloned;
123
+ }
124
+ }
125
+
126
+ module.exports = VariableTemplate;