@halleyassist/rule-templater 0.0.12 → 0.0.14

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
@@ -167,6 +167,26 @@ const prepared = parsed.prepare({
167
167
  // If a door is opened between 08:00 AND 12:00
168
168
  ```
169
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
+
170
190
  ## API
171
191
 
172
192
  ### `RuleTemplate.parse(ruleTemplate)`
@@ -301,6 +321,75 @@ The following variable types are supported:
301
321
  - `boolean array`
302
322
  - `object array`
303
323
 
324
+ ### Time Type Formats
325
+
326
+ The time-related variable types use the following structures when passed to `prepare()` or `validate()`:
327
+
328
+ #### `time value`
329
+
330
+ Use a time-of-day string such as `08:00`.
331
+
332
+ ```javascript
333
+ {
334
+ value: '08:00',
335
+ type: 'time value'
336
+ }
337
+ ```
338
+
339
+ This is useful when a rule expects a single time value, for example:
340
+
341
+ ```javascript
342
+ const prepared = parsed.prepare({
343
+ START_TIME: { value: '08:00', type: 'time value' }
344
+ });
345
+ ```
346
+
347
+ #### `time period`
348
+
349
+ Use an object with `from` and `to` properties, both using the same time-of-day string format as `time value`.
350
+
351
+ ```javascript
352
+ {
353
+ value: {
354
+ from: '08:00',
355
+ to: '12:00'
356
+ },
357
+ type: 'time period'
358
+ }
359
+ ```
360
+
361
+ When rendered into a rule template, this becomes:
362
+
363
+ ```text
364
+ 08:00 TO 12:00
365
+ ```
366
+
367
+ #### `time period ago`
368
+
369
+ Use the same structure as `time period`, plus an `ago` tuple containing:
370
+
371
+ - the numeric offset
372
+ - the rule time unit token, for example `HOURS`
373
+
374
+ ```javascript
375
+ {
376
+ value: {
377
+ from: '08:00',
378
+ to: '12:00',
379
+ ago: [2, 'HOURS']
380
+ },
381
+ type: 'time period ago'
382
+ }
383
+ ```
384
+
385
+ When rendered into a rule template, this becomes:
386
+
387
+ ```text
388
+ 08:00 TO 12:00 AGO 2 HOURS
389
+ ```
390
+
391
+ The `time_start` and `time_end` filters can extract `from` and `to` from either `time period` or `time period ago` and convert them to `time value`.
392
+
304
393
  ## License
305
394
 
306
395
  ISC
@@ -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'),
@@ -3665,6 +3665,7 @@ const VariableTypes = [
3665
3665
  'time period',
3666
3666
  'time period ago',
3667
3667
  'time value',
3668
+ 'number time',
3668
3669
  'string array',
3669
3670
  'number array',
3670
3671
  'boolean array',
@@ -3678,6 +3679,7 @@ const AllowedTypeMapping = {
3678
3679
  'time period': ['time_period_atom'],
3679
3680
  'time period ago': ['time_period_atom'],
3680
3681
  'time value': ['time_value_atom', 'tod_atom'],
3682
+ 'number time': ['number_atom'],
3681
3683
  'string array': ['string_array'],
3682
3684
  'number array': ['number_array'],
3683
3685
  'boolean array': ['boolean_array'],
@@ -4074,7 +4076,7 @@ RuleTemplate.TemplateFilters = TemplateFilters;
4074
4076
 
4075
4077
  module.exports = RuleTemplate;
4076
4078
 
4077
- },{"./RuleTemplate.production.ebnf.js":14,"./TemplateFilters":17,"@halleyassist/rule-parser":2,"ebnf":13}],17:[function(require,module,exports){
4079
+ },{"./RuleTemplate.production.ebnf.js":15,"./TemplateFilters":17,"@halleyassist/rule-parser":2,"ebnf":13}],17:[function(require,module,exports){
4078
4080
  /*
4079
4081
  Template filters are functions that transform variable values.
4080
4082
  They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
@@ -4129,7 +4131,9 @@ const TemplateFilters = {
4129
4131
  number: varData => {
4130
4132
  varData.value = Number(varData.value);
4131
4133
  varData.type = 'number';
4132
-
4134
+ if(isNaN(varData.value)){
4135
+ throw new Error(`Value "${varData.value}" cannot be converted to a number`);
4136
+ }
4133
4137
  },
4134
4138
 
4135
4139
  // Convert to boolean
@@ -4232,5 +4236,5 @@ const TemplateFilters = {
4232
4236
  }
4233
4237
 
4234
4238
  module.exports = TemplateFilters;
4235
- },{}]},{},[15])(15)
4239
+ },{}]},{},[14])(14)
4236
4240
  });
package/index.d.ts CHANGED
@@ -119,6 +119,26 @@ export class GeneralTemplate {
119
119
  prepare(variables: Variables): string;
120
120
  }
121
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
+
122
142
  export const ParserRules: any[];
123
143
  export const VariableTypes: string[];
124
144
  export const TemplateFilters: TemplateFiltersType;
package/index.js CHANGED
@@ -1,8 +1,10 @@
1
- const RuleTemplate = require('./src/RuleTemplater');
1
+ const RuleTemplate = require('./src/RuleTemplate');
2
2
  const GeneralTemplate = require('./src/GeneralTemplate');
3
+ const VariableTemplate = require('./src/VariableTemplate');
3
4
 
4
5
  module.exports.RuleTemplate = RuleTemplate;
5
6
  module.exports.ParserRules = RuleTemplate.ParserRules;
6
7
  module.exports.VariableTypes = RuleTemplate.VariableTypes;
7
8
  module.exports.TemplateFilters = RuleTemplate.TemplateFilters;
8
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.12",
3
+ "version": "0.0.14",
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 @@
1
+ module.exports = require('./RuleTemplate.production.js');
@@ -15,6 +15,7 @@ const VariableTypes = [
15
15
  'time period',
16
16
  'time period ago',
17
17
  'time value',
18
+ 'number time',
18
19
  'string array',
19
20
  'number array',
20
21
  'boolean array',
@@ -28,6 +29,7 @@ const AllowedTypeMapping = {
28
29
  'time period': ['time_period_atom'],
29
30
  'time period ago': ['time_period_atom'],
30
31
  'time value': ['time_value_atom', 'tod_atom'],
32
+ 'number time': ['number_atom'],
31
33
  'string array': ['string_array'],
32
34
  'number array': ['number_array'],
33
35
  'boolean array': ['boolean_array'],
@@ -0,0 +1,427 @@
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
+ 'number time',
19
+ 'string array',
20
+ 'number array',
21
+ 'boolean array',
22
+ 'object array'
23
+ ]
24
+
25
+ const AllowedTypeMapping = {
26
+ 'string': ['string_atom', 'string_concat'],
27
+ 'number': ['number_atom', 'math_expr'],
28
+ 'boolean': ['boolean_atom', 'boolean_expr'],
29
+ 'time period': ['time_period_atom'],
30
+ 'time period ago': ['time_period_atom'],
31
+ 'time value': ['time_value_atom', 'tod_atom'],
32
+ 'number time': ['number_atom'],
33
+ 'string array': ['string_array'],
34
+ 'number array': ['number_array'],
35
+ 'boolean array': ['boolean_array'],
36
+ 'object': ['object_atom'],
37
+ 'object array': ['object_array']
38
+ };
39
+
40
+ // Merge the base grammar with template-specific grammar rules
41
+ const extendedGrammar = [...RuleParserRules]
42
+ for(const rule of TemplateGrammar){
43
+ const idx = extendedGrammar.findIndex(r => r.name === rule.name);
44
+ if(idx !== -1){
45
+ extendedGrammar[idx] = rule;
46
+ } else {
47
+ extendedGrammar.push(rule);
48
+ }
49
+ }
50
+
51
+ // Add template_value as an alternative to value_atom so templates can be parsed
52
+ const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
53
+ if (valueAtomIdx !== -1) {
54
+ extendedGrammar[valueAtomIdx] = Object.assign({}, extendedGrammar[valueAtomIdx]);
55
+ extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
56
+ }
57
+
58
+ // Export the parser rules for potential external use
59
+ const ParserRules = extendedGrammar;
60
+
61
+ class RuleTemplate {
62
+ constructor(ruleTemplateText, ast) {
63
+ this.ruleTemplateText = ruleTemplateText;
64
+ this.ast = ast;
65
+ }
66
+
67
+ /**
68
+ * Parse a rule template string and return a RuleTemplate instance
69
+ * @param {string} ruleTemplate - The template string to parse
70
+ * @returns {RuleTemplate} Instance with AST and template text
71
+ */
72
+ static parse(ruleTemplate){
73
+ if(!ParserCache){
74
+ ParserCache = new Parser(ParserRules, {debug: false})
75
+ }
76
+
77
+ const ast = RuleParser.toAst(ruleTemplate.trim(), ParserCache);
78
+ return new RuleTemplate(ruleTemplate, ast);
79
+ }
80
+
81
+ /**
82
+ * Extract variables from the template using the AST
83
+ * @returns {Array} Array of {name, filters: [], positions: [{start, end}]} objects
84
+ */
85
+ extractVariables(){
86
+ const variables = [];
87
+ const variableMap = new Map();
88
+
89
+ const traverse = (node) => {
90
+ if (!node) return;
91
+
92
+ // Check if this is a template_value node
93
+ if (node.type === 'template_value') {
94
+ // Extract the variable information
95
+ const varInfo = this._extractVariableFromNode(node);
96
+ if (varInfo) {
97
+ // Add position to existing variable or create new entry
98
+ if (variableMap.has(varInfo.name)) {
99
+ const existing = variableMap.get(varInfo.name);
100
+ existing.positions.push({
101
+ start: varInfo.start,
102
+ end: varInfo.end
103
+ });
104
+ } else {
105
+ variableMap.set(varInfo.name, {
106
+ name: varInfo.name,
107
+ filters: varInfo.filters,
108
+ positions: [{
109
+ start: varInfo.start,
110
+ end: varInfo.end
111
+ }]
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ // Traverse children
118
+ if (node.children) {
119
+ for (const child of node.children) {
120
+ traverse(child);
121
+ }
122
+ }
123
+ };
124
+
125
+ traverse(this.ast);
126
+
127
+ // Convert map to array
128
+ for (const variable of variableMap.values()) {
129
+ variables.push(variable);
130
+ }
131
+
132
+ return variables;
133
+ }
134
+
135
+ /**
136
+ * Extract function calls from the template using the AST
137
+ * @returns {Array} Array of unique function names used in the template
138
+ */
139
+ extractFunctions(){
140
+ const functions = new Set();
141
+
142
+ const traverse = (node) => {
143
+ if (!node) return;
144
+
145
+ // Check if this is a function call node
146
+ if (node.type === 'fcall') {
147
+ // Find the function name in children
148
+ const fnameNode = node.children?.find(c => c.type === 'fname');
149
+ if (fnameNode && fnameNode.text) {
150
+ functions.add(fnameNode.text.trim());
151
+ }
152
+ }
153
+
154
+ // Traverse children
155
+ if (node.children) {
156
+ for (const child of node.children) {
157
+ traverse(child);
158
+ }
159
+ }
160
+ };
161
+
162
+ traverse(this.ast);
163
+
164
+ // Convert set to sorted array for consistent output
165
+ return Array.from(functions).sort();
166
+ }
167
+
168
+ /**
169
+ * Extract variable name and filters from a template_value AST node
170
+ * @private
171
+ */
172
+ _extractVariableFromNode(node) {
173
+ if (node.type !== 'template_value') return null;
174
+
175
+ // Find the template_expr child
176
+ const templateExpr = node.children?.find(c => c.type === 'template_expr');
177
+ if (!templateExpr) return null;
178
+
179
+ // Extract the path (variable name)
180
+ const templatePath = templateExpr.children?.find(c => c.type === 'template_path');
181
+ if (!templatePath || !templatePath.text) return null;
182
+
183
+ const name = templatePath.text.trim();
184
+
185
+ // Extract filters
186
+ const filters = [];
187
+ for (const child of templateExpr.children || []) {
188
+ if (child.type === 'template_filter_call') {
189
+ const filterName = this._extractFilterName(child);
190
+ if (filterName) {
191
+ filters.push(filterName);
192
+ }
193
+ }
194
+ }
195
+
196
+ // Extract position information from the node
197
+ const start = node.start;
198
+ const end = node.end;
199
+
200
+ return { name, filters, start, end };
201
+ }
202
+
203
+ /**
204
+ * Extract filter name from template_filter_call node
205
+ * @private
206
+ */
207
+ _extractFilterName(node) {
208
+ const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
209
+ if (!filterNameNode || !filterNameNode.text) return null;
210
+
211
+ return filterNameNode.text.trim();
212
+ }
213
+
214
+ /**
215
+ * Validate variable types against the AST
216
+ * @param {Object} variables - Object mapping variable names to {type} objects
217
+ * @returns {Object} Object with validation results: {valid: boolean, errors: []}
218
+ */
219
+ validate(variables) {
220
+ if (!variables || typeof variables !== 'object') {
221
+ return {
222
+ valid: false,
223
+ errors: ['Variables must be provided as an object']
224
+ };
225
+ }
226
+
227
+ const errors = [];
228
+ const extractedVars = this.extractVariables();
229
+
230
+ for (const varInfo of extractedVars) {
231
+ const varName = varInfo.name;
232
+
233
+ // Check if variable is provided
234
+ if (!variables.hasOwnProperty(varName)) {
235
+ errors.push(`Variable '${varName}' not provided in variables object`);
236
+ continue;
237
+ }
238
+
239
+ const varData = variables[varName];
240
+ if (typeof varData !== 'object') {
241
+ errors.push(`Variable '${varName}' must be an object`);
242
+ continue;
243
+ }
244
+
245
+ const { type } = varData;
246
+
247
+ // Validate type if provided
248
+ if (type && !VariableTypes.includes(type)) {
249
+ errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
250
+ }
251
+ }
252
+
253
+ return {
254
+ valid: errors.length === 0,
255
+ errors
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Prepare the template by replacing variables with their values
261
+ * Rebuilds from AST by iterating through children
262
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
263
+ * @returns {string} The prepared rule string
264
+ */
265
+ prepare(variables){
266
+ if (!variables || typeof variables !== 'object') {
267
+ throw new Error('Variables must be provided as an object');
268
+ }
269
+
270
+ // Rebuild the rule string from AST
271
+ return this._rebuildFromAST(this.ast, variables);
272
+ }
273
+
274
+ /**
275
+ * Rebuild rule string from AST node, replacing template_value nodes with variable values
276
+ * @private
277
+ * @param {Object} node - AST node
278
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
279
+ * @returns {string} Rebuilt string
280
+ */
281
+ _rebuildFromAST(node, variables) {
282
+ if (!node) return '';
283
+
284
+ // If this is a template_value node, replace it with the computed value
285
+ if (node.type === 'template_value') {
286
+ return this._computeTemplateReplacement(node, variables);
287
+ }
288
+
289
+ // If node has no children, it's a leaf - return its text
290
+ if (!node.children || node.children.length === 0) {
291
+ return node.text || '';
292
+ }
293
+
294
+ // Node has children - rebuild by iterating through children and preserving gaps
295
+ let result = '';
296
+ const originalText = node.text || '';
297
+ let lastEnd = node.start || 0;
298
+
299
+ for (const child of node.children) {
300
+ // Add any text between the last child's end and this child's start (gaps/syntax)
301
+ if (child.start !== undefined && child.start > lastEnd) {
302
+ result += originalText.substring(lastEnd - (node.start || 0), child.start - (node.start || 0));
303
+ }
304
+
305
+ // Add the child's rebuilt text
306
+ result += this._rebuildFromAST(child, variables);
307
+
308
+ // Update lastEnd to this child's end position
309
+ if (child.end !== undefined) {
310
+ lastEnd = child.end;
311
+ }
312
+ }
313
+
314
+ // Add any remaining text after the last child
315
+ if (node.end !== undefined && lastEnd < node.end) {
316
+ result += originalText.substring(lastEnd - (node.start || 0), node.end - (node.start || 0));
317
+ }
318
+
319
+ return result;
320
+ }
321
+
322
+ /**
323
+ * Compute the replacement value for a template_value node
324
+ * @private
325
+ * @param {Object} node - template_value AST node
326
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
327
+ * @returns {string} Replacement string
328
+ */
329
+ _computeTemplateReplacement(node, variables) {
330
+ const templateInfo = this._extractVariableFromNode(node);
331
+ if (!templateInfo) {
332
+ throw new Error(`Failed to extract variable information from template node`);
333
+ }
334
+
335
+ const varName = templateInfo.name;
336
+
337
+ // Validate variable is provided
338
+ if (!variables.hasOwnProperty(varName)) {
339
+ throw new Error(`Variable '${varName}' not provided in variables object`);
340
+ }
341
+
342
+ let varData = variables[varName];
343
+ if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
344
+ throw new Error(`Variable '${varName}' must be an object with 'value' property`);
345
+ }
346
+
347
+ varData = Object.assign({}, varData);
348
+
349
+ // Require type property for all variables
350
+ if (!varData.hasOwnProperty('type')) {
351
+ throw new Error(`Variable '${varName}' must have a 'type' property`);
352
+ }
353
+
354
+ // Validate type
355
+ if (!VariableTypes.includes(varData.type)) {
356
+ throw new Error(`Invalid variable type '${varData.type}' for variable '${varName}'`);
357
+ }
358
+
359
+ // Apply filters if present
360
+ if (templateInfo.filters && templateInfo.filters.length > 0) {
361
+ for (const filterName of templateInfo.filters) {
362
+ if (!TemplateFilters[filterName]) {
363
+ throw new Error(`Unknown filter '${filterName}'`);
364
+ }
365
+
366
+ TemplateFilters[filterName](varData);
367
+ }
368
+ }
369
+
370
+ return this._serializeVarData(varData, varName);
371
+ }
372
+
373
+ _serializeVarData(varData, varName) {
374
+ const { value, type } = varData;
375
+
376
+ if (!VariableTypes.includes(type)) {
377
+ throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
378
+ }
379
+
380
+ if (type === 'string') {
381
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
382
+ }
383
+
384
+ if (type === 'number') {
385
+ return String(value);
386
+ }
387
+
388
+ if (type === 'boolean') {
389
+ return value ? 'true' : 'false';
390
+ }
391
+
392
+ if (type === 'time period' || type === 'time period ago') {
393
+ let ret = `${value.from} TO ${value.to}`;
394
+ if(value.ago) {
395
+ ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
396
+ }
397
+ return ret;
398
+ }
399
+
400
+ return String(value);
401
+ }
402
+
403
+ /**
404
+ * Helper method to validate if an AST node matches a variable type
405
+ * @param {Object} astNode - The AST node to validate
406
+ * @param {string} variableType - The expected variable type
407
+ * @returns {boolean} True if valid, false otherwise
408
+ */
409
+ static validateVariableNode(astNode, variableType) {
410
+ if (!astNode || !astNode.type) {
411
+ return false;
412
+ }
413
+
414
+ const allowedTypes = AllowedTypeMapping[variableType];
415
+ if (!allowedTypes) {
416
+ return false;
417
+ }
418
+
419
+ return allowedTypes.includes(astNode.type);
420
+ }
421
+ }
422
+
423
+ RuleTemplate.ParserRules = ParserRules;
424
+ RuleTemplate.VariableTypes = VariableTypes;
425
+ RuleTemplate.TemplateFilters = TemplateFilters;
426
+
427
+ 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
  /**
@@ -418,8 +400,8 @@ class RuleTemplate {
418
400
  }
419
401
  }
420
402
 
421
- RuleTemplate.ParserRules = ParserRules;
422
- RuleTemplate.VariableTypes = VariableTypes;
423
- RuleTemplate.TemplateFilters = TemplateFilters;
424
-
403
+ // Export the class and parser rules
425
404
  module.exports = RuleTemplate;
405
+ module.exports.ParserRules = ParserRules;
406
+ module.exports.VariableTypes = VariableTypes;
407
+ module.exports.TemplateFilters = TemplateFilters;
@@ -52,7 +52,9 @@ const TemplateFilters = {
52
52
  number: varData => {
53
53
  varData.value = Number(varData.value);
54
54
  varData.type = 'number';
55
-
55
+ if(isNaN(varData.value)){
56
+ throw new Error(`Value "${varData.value}" cannot be converted to a number`);
57
+ }
56
58
  },
57
59
 
58
60
  // Convert to boolean
@@ -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;