@halleyassist/rule-templater 0.0.6 → 0.0.8

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.
@@ -1,120 +1,1691 @@
1
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){
2
- const {Grammars} = require('ebnf');
3
-
4
- const grammar = `
5
- statement_main ::= statement EOF
6
- logical_operator ||= AND | OR
7
- statement ::= expression (logical_operator expression)*
8
- expression ::= not_expression | standard_expression | parenthesis_expression
9
- parenthesis_expression ::= BEGIN_PARENTHESIS WS* statement WS* END_PARENTHESIS
10
- not_expression ||= NOT (result | parenthesis_expression)
11
- standard_expression ||= result ((WS* eq_approx) | (WS* basic_rhs) | ((WS+ IS)? WS+ between) | (WS+ in_expr))?
12
- basic_rhs ::= operator WS* result
13
- eq_approx ::= eq_operator WS* "~" WS* result
14
-
15
- PLUS ::= "+"
16
- MINUS ::= "-"
17
- MULTIPLY ::= "*"
18
- DIVIDE ::= "/"
19
- MODULUS ::= "%"
20
- DEFAULT_VAL ::= "??"
21
- arithmetic_operator ::= PLUS | MINUS | MULTIPLY | DIVIDE | MODULUS | DEFAULT_VAL
22
-
23
- number_atom ::= number
24
- number_time_atom ::= number_time
25
- tod_atom ::= number_tod
26
- dow_atom ::= dow
27
-
28
- arithmetic_operand ::= fcall | number_time_atom | number_atom
29
- arithmetic_result ::= arithmetic_operand WS* arithmetic_operator WS* (arithmetic_result | arithmetic_operand)
30
-
31
- simple_result ::= fcall | value
32
- result ::= arithmetic_result | simple_result
33
-
34
- value_atom ::= false | true | array | time_period | number_time_atom | number_atom | tod_atom | string
35
- value ::= value_atom
36
-
37
- BEGIN_ARRAY ::= WS* #x5B WS* /* [ */
38
- BEGIN_OBJECT ::= WS* #x7B WS* /* { */
39
- END_ARRAY ::= WS* #x5D WS* /* ] */
40
- END_OBJECT ::= WS* #x7D WS* /* } */
41
- NAME_SEPARATOR ::= WS* #x3A WS* /* : */
42
- VALUE_SEPARATOR ::= WS* #x2C WS* /* , */
43
- WS ::= [#x20#x09#x0A#x0D]
44
-
45
- operator ::= GTE | LTE | GT | LT | EQ | NEQ
46
- eq_operator ::= EQ | NEQ
47
-
48
- BEGIN_ARGUMENT ::= "("
49
- END_ARGUMENT ::= ")"
50
- BEGIN_PARENTHESIS ::= "("
51
- END_PARENTHESIS ::= ")"
52
-
53
- BEGIN_IN ||= "IN"
54
- in_expr ::= BEGIN_IN WS* BEGIN_PARENTHESIS WS* arguments END_PARENTHESIS
55
-
56
- argument ::= statement WS*
57
- arguments ::= argument (WS* "," WS* argument)*
58
- fname ::= [A-Za-z0-9]+
59
- fcall ::= fname WS* BEGIN_ARGUMENT WS* arguments? END_ARGUMENT
60
-
61
- between_dash_or_and ||= (WS+ "AND" WS+) | (WS* "-" WS*)
62
-
63
- between_number_inner ::= number_atom | number_time_atom
64
- between_number ||= between_number_inner between_dash_or_and between_number_inner
65
-
66
- between_number_time_inner ::= number_time_atom
67
- between_number_time ||= between_number_time_inner between_dash_or_and between_number_time_inner (WS+ dow_range)?
68
-
69
- between_tod_inner ::= tod_atom
70
- between_tod ||= between_tod_inner (WS+ "AND" WS+) between_tod_inner (WS+ dow_range)?
71
-
72
- between ||= "BETWEEN" WS+ (between_number | between_tod)
73
-
74
- dow ||= "MONDAY" | "MON" | "TUESDAY" | "TUE" | "WEDNESDAY" | "WED" | "THURSDAY" | "THU" | "THUR" | "FRIDAY" | "FRI" | "SATURDAY" | "SAT" | "SUNDAY" | "SUN"
75
-
76
- dow_range_inner ::= dow_atom
77
- dow_range ||= "ON" WS+ dow_range_inner (WS+ "TO" WS+ dow_range_inner)?
78
-
79
- between_time_only ||= "BETWEEN" WS+ between_number_time
80
- between_tod_only ||= "BETWEEN" WS+ between_tod
81
-
82
- AND ||= (WS* "&&" WS*) | (WS+ "AND" WS+)
83
- OR ||= (WS* "||" WS*) | (WS+ "OR" WS+)
84
- AGO ||= "AGO"
85
- GT ::= ">"
86
- LT ::= "<"
87
- GTE ::= ">="
88
- LTE ::= "<="
89
- IS ||= "is"
90
- EQ ::= "==" | "="
91
- NEQ ::= "!="
92
- NOT ||= ("!" WS*) | ("not" WS+)
93
-
94
- false ||= "FALSE"
95
- null ||= "null"
96
- true ||= "TRUE"
97
-
98
- array ::= BEGIN_ARRAY (value (VALUE_SEPARATOR value)*)? END_ARRAY
99
-
100
- unit ||= "seconds" | "minutes" | "hours" | "weeks" | "days" | "second" | "minute" | "week" | "hour" | "day" | "mins" | "min"
101
-
102
- number ::= "-"? ([0-9]+) ("." [0-9]+)? ("e" ("-" | "+")? ("0" | [1-9] [0-9]*))?
103
- number_time ::= number WS+ unit
104
- number_tod ::= ([0-9]+) ":" ([0-9]+)
105
-
106
- time_period_ago ||= number_time_atom (WS+ number_time_atom)* WS+ AGO
107
- time_period_ago_between ||= number_time_atom (WS+ number_time_atom)* WS+ AGO WS+ between_tod_only
108
- time_period_const ||= "today" | time_period_ago
109
- time_period ::= time_period_ago_between | time_period_const | between_tod_only | between_time_only
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
+ },{}],2:[function(require,module,exports){
4
+ const {Parser} = require('ebnf/dist/Parser.js'),
5
+ {ParsingError} = require('ebnf'),
6
+ assert = require('assert'),
7
+ RuleParseError = require('./errors/RuleParseError')
110
8
 
111
- string ::= '"' (([#x20-#x21] | [#x23-#x5B] | [#x5D-#xFFFF]) | #x5C (#x22 | #x5C | #x2F | #x62 | #x66 | #x6E | #x72 | #x74 | #x75 HEXDIG HEXDIG HEXDIG HEXDIG))* '"'
112
- HEXDIG ::= [a-fA-F0-9]
113
- `
9
+ let ParserRules = require('./RuleParser.production.ebnf.js')
10
+ let ParserCache;
114
11
 
115
- module.exports = Grammars.W3C.getRules(grammar);
12
+ const { ErrorAnalyzer } = require('./errors/ErrorAnalyzer');
116
13
 
117
- },{"ebnf":10}],2:[function(require,module,exports){
14
+ const ArithmeticOperators = {
15
+ "+": 'MathAdd',
16
+ "-": 'MathSub',
17
+ "/": 'MathDiv',
18
+ "*": 'MathMul',
19
+ "%": 'MathMod',
20
+ "??": "Default"
21
+ }
22
+
23
+ const OperatorFn = {
24
+ ">": "Gt",
25
+ "<": "Lt",
26
+ ">=": "Gte",
27
+ "<=": "Lte",
28
+ "==": "Eq",
29
+ "=": "Eq",
30
+ "!=": "Neq"
31
+ }
32
+
33
+ const LogicalOperators = {
34
+ "&&": 'And',
35
+ "AND": 'And',
36
+ "||": 'Or',
37
+ "OR": 'Or',
38
+ }
39
+
40
+ // Map abbreviations to canonical uppercase full form
41
+ const DOW_MAP = {
42
+ 'MON': 'MONDAY',
43
+ 'TUE': 'TUESDAY',
44
+ 'WED': 'WEDNESDAY',
45
+ 'THU': 'THURSDAY',
46
+ 'THUR': 'THURSDAY',
47
+ 'FRI': 'FRIDAY',
48
+ 'SAT': 'SATURDAY',
49
+ 'SUN': 'SUNDAY',
50
+ };
51
+
52
+ // Valid full day names
53
+ const VALID_DAYS = new Set(['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY']);
54
+
55
+ const normalizeDow = (text) => {
56
+ const upper = text.toUpperCase();
57
+ // Check if it's an abbreviation first
58
+ if (upper in DOW_MAP) {
59
+ return DOW_MAP[upper];
60
+ }
61
+ // Otherwise, check if it's a valid full name
62
+ if (VALID_DAYS.has(upper)) {
63
+ return upper;
64
+ }
65
+ throw new Error(`Invalid day of week: ${text}`);
66
+ };
67
+
68
+ const Epsilon = 0.01
69
+
70
+ class RuleParser {
71
+ static toAst(txt, parser = null){
72
+ let ret
73
+
74
+ if(!parser) {
75
+ if(!ParserCache){
76
+ ParserCache = new Parser(ParserRules, {debug: false})
77
+ }
78
+ parser = ParserCache
79
+ }
80
+
81
+ try {
82
+ ret = parser.getAST(txt.trim(), 'statement_main');
83
+ } catch (e) {
84
+ // If ebnf throws ParsingError, convert it to RuleParseError with helpful error code
85
+ if (e instanceof ParsingError) {
86
+ throw ErrorAnalyzer.analyzeParseFailure(txt, e);
87
+ }
88
+ throw e;
89
+ }
90
+
91
+ if(ret){
92
+ return ret.children[0]
93
+ }
94
+
95
+ // If parsing failed without throwing (shouldn't happen with new ebnf), throw error
96
+ throw ErrorAnalyzer.analyzeParseFailure(txt);
97
+ }
98
+ static _parseArgument(argument){
99
+ assert(argument.type === 'argument')
100
+ const child = argument.children[0]
101
+ return RuleParser._buildExpressionGroup(child)
102
+ }
103
+ static _parseFcall(fcall){
104
+ const fname = fcall.children[0]
105
+ const ret = [fname.text]
106
+ if(fcall.children.length != 1){
107
+ const args = fcall.children[1]
108
+ for(const a of args.children){
109
+ ret.push(RuleParser._parseArgument(a))
110
+ }
111
+ }
112
+ return ret
113
+ }
114
+ static _parseDowRange(dowRange) {
115
+ // dow_range can have 1 or 2 children (single day or range)
116
+ if (dowRange.children.length === 1) {
117
+ // Single day: ON MONDAY - return just the day string
118
+ // New structure: dow_range -> dow_range_inner -> dow_atom -> dow
119
+ const dayText = RuleParser.__parseValue(dowRange.children[0])
120
+ return { start: dayText, end: dayText };
121
+ } else if (dowRange.children.length === 2) {
122
+ // Range: ON MONDAY TO FRIDAY - return both start and end days
123
+ const startDay = RuleParser.__parseValue(dowRange.children[0])
124
+ const endDay = RuleParser.__parseValue(dowRange.children[1])
125
+ return { start: startDay, end: endDay };
126
+ } else {
127
+ throw new Error(`Invalid dow_range with ${dowRange.children.length} children`);
128
+ }
129
+ }
130
+ static _addDowToTods(startTod, endTod, dowRange) {
131
+ if (dowRange && dowRange.type === 'dow_range') {
132
+ const dow = RuleParser._parseDowRange(dowRange)
133
+ startTod.dow = dow.start
134
+ endTod.dow = dow.end
135
+ }
136
+ }
137
+ static _parseTimePeriod(tp){
138
+ switch(tp.type){
139
+ case 'time_period_const':
140
+ // Check if this is a time_period_ago (has children)
141
+ if (tp.children && tp.children.length > 0 && tp.children[0].type === 'time_period_ago') {
142
+ const timePeriodAgo = tp.children[0]
143
+ // Extract all number_time children and sum them up
144
+ let totalSeconds = 0
145
+ const components = []
146
+ for (const child of timePeriodAgo.children) {
147
+ if (child.type === 'number_time_atom') {
148
+ // New structure: number_time_atom -> number_time -> number, unit
149
+ const numberTime = child.children[0]
150
+ const number = parseFloat(numberTime.children[0].text)
151
+ const unit = numberTime.children[1].text.toUpperCase()
152
+ components.push([number, unit])
153
+ // Parse the value to get seconds
154
+ totalSeconds += RuleParser.__parseValue(numberTime)
155
+ }
156
+ }
157
+ // If there's only one component, use its number and unit
158
+ // Otherwise, use the total in seconds and "SECONDS" as the unit
159
+ if (components.length === 1) {
160
+ return ["TimePeriodConstAgo", components[0][0], components[0][1]]
161
+ } else {
162
+ return ["TimePeriodConstAgo", totalSeconds, "SECONDS"]
163
+ }
164
+ }
165
+ return ["TimePeriodConst", tp.text]
166
+ case 'time_period_ago_between': {
167
+ // time_period_ago_between has: number_time_atom (WS+ number_time_atom)* WS+ AGO WS+ (between_time_only | between_tod_only)
168
+ // We need to extract all number_time children and sum them up, then return TimePeriodBetweenAgo
169
+ let totalSeconds = 0
170
+ let betweenNode = null
171
+ let isBetweenTod = false
172
+
173
+ // Find all number_time_atom children and the between node (either between_time_only or between_tod_only)
174
+ for (let i = 0; i < tp.children.length; i++) {
175
+ if (tp.children[i].type === 'number_time_atom') {
176
+ // New structure: number_time_atom -> number_time
177
+ totalSeconds += RuleParser.__parseValue(tp.children[i].children[0])
178
+ } else if (tp.children[i].type === 'between_time_only' || tp.children[i].type === 'between_time_only_atom') {
179
+ betweenNode = tp.children[i].type === 'between_time_only_atom' ? tp.children[i].children[0] : tp.children[i]
180
+ isBetweenTod = false
181
+ } else if (tp.children[i].type === 'between_tod_only' || tp.children[i].type === 'between_tod_only_atom') {
182
+ betweenNode = tp.children[i].type === 'between_tod_only_atom' ? tp.children[i].children[0] : tp.children[i]
183
+ isBetweenTod = true
184
+ }
185
+ }
186
+
187
+ // This should always be present based on the grammar, but check defensively
188
+ if (!betweenNode) {
189
+ throw new Error('time_period_ago_between requires between_time_only or between_tod_only child')
190
+ }
191
+
192
+ if (isBetweenTod) {
193
+ const betweenTod = betweenNode.children[0]
194
+ // between_tod has inline separator, so: children[0] = first tod_inner, children[1] = second tod_inner, children[2] = optional dow_range
195
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
196
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
197
+
198
+ // Check if there's a dow_range at betweenTod.children[2]
199
+ // Note: startTod and endTod should always be objects from number_tod parsing
200
+ if (betweenTod.children.length > 2) {
201
+ RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
202
+ }
203
+
204
+ return ["TimePeriodBetweenAgo", totalSeconds, startTod, endTod]
205
+ } else {
206
+ const betweenNumberTime = betweenNode.children[0]
207
+ // between_number_time has: children[0] = first time_inner, children[1] = separator, children[2] = second time_inner, children[3] = optional dow_range
208
+ const startValue = RuleParser.__parseValue(betweenNumberTime.children[0])
209
+ const endValue = RuleParser.__parseValue(betweenNumberTime.children[2])
210
+
211
+ // Check if there's a dow_range at betweenNumberTime.children[3]
212
+ if (betweenNumberTime.children.length > 3 && betweenNumberTime.children[3].type === 'dow_range') {
213
+ const dow = RuleParser._parseDowRange(betweenNumberTime.children[3])
214
+ if (dow.start === dow.end) {
215
+ // Single day: ["TimePeriodBetweenAgo", totalSeconds, start, end, "MONDAY"]
216
+ return ["TimePeriodBetweenAgo", totalSeconds, startValue, endValue, dow.start]
217
+ } else {
218
+ // Range: ["TimePeriodBetweenAgo", totalSeconds, start, end, "MONDAY", "FRIDAY"]
219
+ return ["TimePeriodBetweenAgo", totalSeconds, startValue, endValue, dow.start, dow.end]
220
+ }
221
+ }
222
+
223
+ return ["TimePeriodBetweenAgo", totalSeconds, startValue, endValue]
224
+ }
225
+ }
226
+ case 'between_tod_only': {
227
+ // between_tod_only has children[0] = between_tod node
228
+ const betweenTod = tp.children[0]
229
+ // between_tod has inline separator, so: children[0] = first tod_inner, children[1] = second tod_inner, children[2] = optional dow_range
230
+ let startTod = RuleParser.__parseValue(betweenTod.children[0])
231
+ let endTod = RuleParser.__parseValue(betweenTod.children[1])
232
+
233
+ // Check if there's a dow_range at betweenTod.children[2]
234
+ if (betweenTod.children.length > 2) {
235
+ if(typeof startTod === 'number') startTod = {seconds: startTod, dow: null}
236
+ if(typeof endTod === 'number') endTod = {seconds: endTod, dow: null}
237
+ RuleParser._addDowToTods(startTod, endTod, betweenTod.children[2])
238
+ }
239
+
240
+ return ["TimePeriodBetween", startTod, endTod]
241
+ }
242
+ case 'between_time_only': {
243
+ // between_time_only has children[0] = between_number_time node
244
+ const betweenNumberTime = tp.children[0]
245
+ // between_number_time has: children[0] = first time_inner, children[1] = separator, children[2] = second time_inner, children[3] = optional dow_range
246
+ const startValue = RuleParser.__parseValue(betweenNumberTime.children[0])
247
+ const endValue = RuleParser.__parseValue(betweenNumberTime.children[2])
248
+
249
+ // Check if there's a dow_range at betweenNumberTime.children[3]
250
+ // If DOW filters are provided, append them as additional parameters
251
+ if (betweenNumberTime.children.length > 3 && betweenNumberTime.children[3].type === 'dow_range') {
252
+ const dow = RuleParser._parseDowRange(betweenNumberTime.children[3])
253
+ if (dow.start === dow.end) {
254
+ // Single day: ["TimePeriodBetween", start, end, "MONDAY"]
255
+ return ["TimePeriodBetween", startValue, endValue, dow.start]
256
+ } else {
257
+ // Range: ["TimePeriodBetween", start, end, "MONDAY", "FRIDAY"]
258
+ return ["TimePeriodBetween", startValue, endValue, dow.start, dow.end]
259
+ }
260
+ }
261
+
262
+ return ["TimePeriodBetween", startValue, endValue]
263
+ }
264
+ }
265
+ }
266
+ static __parseValue(child){
267
+ const type = child.type
268
+ switch(type){
269
+ case 'value': {
270
+ // Arrays have value nodes as children - unwrap to get value_atom
271
+ return RuleParser.__parseValue(child.children[0])
272
+ }
273
+ case 'value_atom': {
274
+ // New layer: unwrap value_atom to get the actual atomic type
275
+ return RuleParser.__parseValue(child.children[0])
276
+ }
277
+ case 'number_atom':
278
+ case 'number_time_atom':
279
+ case 'tod_atom':
280
+ case 'dow_atom':
281
+ case 'between_tod_inner':
282
+ case 'between_number_inner':
283
+ case 'between_number_time_inner':
284
+ case 'dow_range_inner': {
285
+ // New layer: unwrap atom wrappers to get the actual leaf nodes
286
+ return RuleParser.__parseValue(child.children[0])
287
+ }
288
+ case 'string': {
289
+ const str = child.text
290
+ return str.slice(1, -1)
291
+ }
292
+ case 'number':
293
+ return parseFloat(child.text)
294
+ case 'number_tod': {
295
+ const tokens = child.text.split(':')
296
+ if (tokens.length !== 2) {
297
+ throw new Error(`Invalid time of day, ${child.text} should be ##:##`)
298
+ }
299
+ const hours = parseInt(tokens[0])
300
+ const minutes = parseInt(tokens[1])
301
+ const tod = hours * 100 + minutes
302
+ const ret = { hours, minutes, tod }
303
+ if (!isNaN(tod) && ret.hours >= 0 && ret.hours < 24 && ret.minutes >= 0 && ret.minutes < 60) {
304
+ return ret
305
+ }
306
+ throw new Error(`Invalid time of day, ${child.text} -> [${tokens.join(', ')}] -> ${hours}h${minutes}m -> ${tod}`)
307
+ }
308
+ case 'number_time': {
309
+ const nt = child
310
+ const mult = parseFloat(nt.children[0].text)
311
+ switch(nt.children[1].text.toUpperCase()){
312
+ case 'SECONDS':
313
+ case 'SECOND':
314
+ return mult
315
+ case 'MINUTES':
316
+ case 'MINUTE':
317
+ case 'MINS':
318
+ case 'MIN':
319
+ return mult * 60
320
+ case 'HOURS':
321
+ case 'HOUR':
322
+ return mult * 60 * 60
323
+ case 'DAYS':
324
+ case 'DAY':
325
+ return mult * 60 * 60 * 24
326
+ case 'WEEKS':
327
+ case 'WEEK':
328
+ return mult * 60 * 60 * 24 * 7
329
+ }
330
+ throw new Error(`Invalid exponent ${nt.children[1].text}`)
331
+ }
332
+ case 'true':
333
+ return true
334
+ case 'false':
335
+ return false
336
+ case 'array': {
337
+ const ret = []
338
+ for(const c of child.children){
339
+ ret.push(RuleParser.__parseValue(c))
340
+ }
341
+ return ret;
342
+ }
343
+ case 'dow': {
344
+ return normalizeDow(child.text)
345
+ }
346
+ default:
347
+ throw new Error(`Unknown value type ${type}`)
348
+ }
349
+ }
350
+ static _parseValue(value){
351
+ const child = value.children[0]
352
+
353
+ const type = child.type
354
+ switch(type){
355
+ case 'value_atom': {
356
+ // New layer: unwrap value_atom to get the actual atomic type
357
+ const atomChild = child.children[0]
358
+ if (atomChild.type === 'time_period') {
359
+ const tp = atomChild.children[0]
360
+ return RuleParser._parseTimePeriod(tp)
361
+ }
362
+ return ['Value', RuleParser.__parseValue(atomChild)]
363
+ }
364
+ case 'time_period': {
365
+ // Old structure (shouldn't happen with new EBNF)
366
+ const tp = child.children[0]
367
+ return RuleParser._parseTimePeriod(tp)
368
+ }
369
+ default:
370
+ return ['Value', RuleParser.__parseValue(child)]
371
+ }
372
+ }
373
+ static _parseSimpleResult(result){
374
+ assert(result.children.length == 1)
375
+ const child = result.children[0]
376
+ const type = child.type
377
+ switch(type){
378
+ case 'fcall':
379
+ return RuleParser._parseFcall(child)
380
+ case 'value':
381
+ return RuleParser._parseValue(child)
382
+ }
383
+ return null
384
+ }
385
+ static _parseArithmeticOperand(operand){
386
+ assert(operand.children.length == 1)
387
+ const child = operand.children[0]
388
+ const type = child.type
389
+ switch(type){
390
+ case 'fcall':
391
+ return RuleParser._parseFcall(child)
392
+ case 'number_atom':
393
+ case 'number_time_atom': {
394
+ // New layer: unwrap atom wrappers to get the actual leaf nodes
395
+ return ['Value', RuleParser.__parseValue(child.children[0])]
396
+ }
397
+ case 'number':
398
+ return ['Value', parseFloat(child.text)]
399
+ case 'number_time':
400
+ return ['Value', RuleParser.__parseValue(child)]
401
+ }
402
+ throw new Error(`Unknown arithmetic operand type ${type}`)
403
+ }
404
+ static _isConstantValue(expr){
405
+ // Check if an expression is a constant value (not just numbers, but any literal)
406
+ return Array.isArray(expr) && expr.length === 2 && expr[0] === 'Value'
407
+ }
408
+
409
+ static _isConstantNumberValue(expr){
410
+ // Check if an expression is a constant numeric value
411
+ return Array.isArray(expr) && expr.length === 2 && expr[0] === 'Value' && typeof expr[1] === 'number'
412
+ }
413
+
414
+ static _evaluateConstantArithmetic(operator, leftValue, rightValue){
415
+ // Evaluate constant arithmetic operations at parse time
416
+ switch(operator){
417
+ case 'MathAdd':
418
+ return leftValue + rightValue
419
+ case 'MathSub':
420
+ return leftValue - rightValue
421
+ case 'MathMul':
422
+ return leftValue * rightValue
423
+ case 'MathDiv':
424
+ return leftValue / rightValue
425
+ case 'MathMod':
426
+ return leftValue % rightValue
427
+ default:
428
+ return null
429
+ }
430
+ }
431
+
432
+ static _parseArithmeticResult(result){
433
+ assert(result.children.length == 3)
434
+ const partA = RuleParser._parseArithmeticOperand(result.children[0])
435
+ const operatorFn = ArithmeticOperators[result.children[1].text]
436
+ const partB = RuleParser.__parseArithmeticResult(result, 2)
437
+
438
+ // Compile out constant expressions
439
+ if (RuleParser._isConstantNumberValue(partA) && RuleParser._isConstantNumberValue(partB)) {
440
+ const result = RuleParser._evaluateConstantArithmetic(operatorFn, partA[1], partB[1])
441
+ if (result !== null) {
442
+ return ['Value', result]
443
+ }
444
+ }
445
+
446
+ return [operatorFn, partA, partB]
447
+ }
448
+
449
+ static __parseArithmeticResult(result, idx){
450
+ const child = result.children[idx]
451
+ const type = child.type
452
+ switch(type){
453
+ case 'arithmetic_operand':
454
+ return RuleParser._parseArithmeticOperand(child)
455
+ case 'arithmetic_result':
456
+ return RuleParser._parseArithmeticResult(child)
457
+ }
458
+
459
+ throw new Error(`Unknown arithmetic result node ${type}`)
460
+ }
461
+
462
+ static __parseResult(result, idx){
463
+ const child = result.children[idx]
464
+ const type = child.type
465
+ switch(type){
466
+ case 'simple_result':
467
+ return RuleParser._parseSimpleResult(child)
468
+ case 'arithmetic_result':
469
+ return RuleParser._parseArithmeticResult(child)
470
+ }
471
+
472
+ throw new Error(`Unknown result node ${type}`)
473
+ }
474
+ static _parseResult(result){
475
+ assert(result.children.length == 1)
476
+
477
+ return RuleParser.__parseResult(result, 0)
478
+ }
479
+ static _parseStdExpression(expr){
480
+ assert(expr.type === 'standard_expression')
481
+ switch(expr.children.length){
482
+ case 1:
483
+ return RuleParser._parseResult(expr.children[0])
484
+ case 2: {
485
+ const rhs = expr.children[1]
486
+ switch(rhs.type){
487
+ case 'between_tod': {
488
+ // Direct between_tod (without wrapping between node)
489
+ // between_tod has inline separator, so: children[0] = first tod_inner, children[1] = second tod_inner, children[2] = optional dow_range
490
+ const startTod = RuleParser.__parseValue(rhs.children[0])
491
+ const endTod = RuleParser.__parseValue(rhs.children[1])
492
+
493
+ // Check if there's a dow_range (children[2])
494
+ if (rhs.children.length > 2) {
495
+ RuleParser._addDowToTods(startTod, endTod, rhs.children[2])
496
+ }
497
+
498
+ return ['Between', RuleParser._parseResult(expr.children[0]), ['Value', startTod], ['Value', endTod]]
499
+ }
500
+ case 'between': {
501
+ // between wraps either between_number or between_tod
502
+ const betweenChild = rhs.children[0]
503
+ if (betweenChild.type === 'between_tod') {
504
+ // between_tod has inline separator, so: children[0] = first tod_inner, children[1] = second tod_inner, children[2] = optional dow_range
505
+ const startTod = RuleParser.__parseValue(betweenChild.children[0])
506
+ const endTod = RuleParser.__parseValue(betweenChild.children[1])
507
+
508
+ // Check if there's a dow_range (children[2])
509
+ if (betweenChild.children.length > 2) {
510
+ RuleParser._addDowToTods(startTod, endTod, betweenChild.children[2])
511
+ }
512
+
513
+ return ['Between', RuleParser._parseResult(expr.children[0]), ['Value', startTod], ['Value', endTod]]
514
+ } else {
515
+ // between_number has: children[0] = first number_inner, children[1] = separator, children[2] = second number_inner
516
+ return ['Between', RuleParser._parseResult(expr.children[0]), ['Value', RuleParser.__parseValue(betweenChild.children[0])], ['Value', RuleParser.__parseValue(betweenChild.children[2])]]
517
+ }
518
+ }
519
+ case 'between_number':
520
+ // between_number has: children[0] = first number_inner, children[1] = separator, children[2] = second number_inner
521
+ return ['Between', RuleParser._parseResult(expr.children[0]), ['Value', RuleParser.__parseValue(rhs.children[0].children[0])], ['Value', RuleParser.__parseValue(rhs.children[0].children[2])]]
522
+ case 'basic_rhs':
523
+ return [OperatorFn[rhs.children[0].text], RuleParser._parseResult(expr.children[0]), RuleParser._parseResult(rhs.children[1])]
524
+ case 'eq_approx': {
525
+ const rhsValue = RuleParser._parseResult(rhs.children[1])
526
+ assert(rhsValue[0] === 'Value')
527
+ const ret = ['Between', RuleParser._parseResult(expr.children[0]), ['Value', rhsValue[1] - Epsilon], ['Value', rhsValue[1] + Epsilon]]
528
+ if(rhs.children[0].text === '!='){
529
+ return ['Not', ret]
530
+ }
531
+ return ret
532
+ }
533
+ case 'in_expr': {
534
+ // in_expr has children[0] = arguments node
535
+ // ArrayIn now takes 2 parameters:
536
+ // Parameter 1: haystack array (the array to be searched)
537
+ // Parameter 2: the needle (the value to search for)
538
+ const args = rhs.children[0]
539
+ const haystack = []
540
+ for(const a of args.children){
541
+ haystack.push(RuleParser._parseArgument(a))
542
+ }
543
+ const needle = RuleParser._parseResult(expr.children[0])
544
+
545
+ // Check if all haystack values are constants
546
+ const allConstants = haystack.every(item => RuleParser._isConstantValue(item))
547
+
548
+ let haystackExpr
549
+ if (allConstants) {
550
+ // All constants: use ["Value", [array of constants]]
551
+ const constantArray = haystack.map(item => item[1])
552
+ haystackExpr = ['Value', constantArray]
553
+ } else {
554
+ // Has non-constants: use ["Array", arg1, arg2, ...]
555
+ haystackExpr = ['Array', ...haystack]
556
+ }
557
+
558
+ return ['ArrayIn', haystackExpr, needle]
559
+ }
560
+
561
+ default:
562
+ throw new Error(`unable to parse std expression, unknown rhs type ${rhs.type}`)
563
+ }
564
+ }
565
+
566
+ default:
567
+ throw new Error(`unable to parse std expression, unknown number of children ${expr.children.length}`)
568
+ }
569
+ }
570
+ static buildLogical(members, fn){
571
+ return [fn, ...members]
572
+ }
573
+ static _buildExpressionGroup(ast){
574
+ let ret = []
575
+ let currentLogical = null
576
+ for(const expr of ast.children){
577
+ if(expr.type == 'logical_operator') {
578
+ const logicalOperator = expr.text.trim()
579
+ const operatorFn = LogicalOperators[logicalOperator.toUpperCase()]
580
+ assert(operatorFn, `Unknown logical operator ${logicalOperator}`)
581
+ if(currentLogical === null || currentLogical !== operatorFn){
582
+ if(ret.length > 1){
583
+ ret = [RuleParser.buildLogical(ret, currentLogical)]
584
+ }
585
+ currentLogical = operatorFn
586
+ }
587
+ }else{
588
+ ret.push(RuleParser._exprToIL(expr))
589
+ }
590
+ }
591
+ if(ret.length == 0){
592
+ throw new Error('invalid rule')
593
+ }
594
+ if(ret.length == 1){
595
+ return ret[0]
596
+ }
597
+ return RuleParser.buildLogical(ret, currentLogical)
598
+ }
599
+ static _parseParenthesisExpression(expr){
600
+ return RuleParser._buildExpressionGroup(expr.children[0])
601
+ }
602
+ static _exprToIL(expr){
603
+ assert(expr.type === 'expression')
604
+ assert(expr.children.length === 1)
605
+ const eInner = expr.children[0]
606
+ switch(eInner.type){
607
+ case 'standard_expression':
608
+ return RuleParser._parseStdExpression(eInner)
609
+ case 'not_expression': {
610
+ const child = eInner.children[0]
611
+ let result
612
+ switch(child.type){
613
+ case 'parenthesis_expression':
614
+ result = RuleParser._parseParenthesisExpression(child)
615
+ break;
616
+ default:
617
+ result = RuleParser._parseResult(child)
618
+ }
619
+ return ['Not', result]
620
+ }
621
+ case 'parenthesis_expression':
622
+ return RuleParser._parseParenthesisExpression(eInner)
623
+ default:
624
+ throw new Error(`unknown type of expression ${eInner.type}`)
625
+ }
626
+ }
627
+ static toIL(txt){
628
+ try {
629
+ const ast = RuleParser.toAst(txt)
630
+ if(!ast) throw new Error(`failed to parse ${txt}`)
631
+ return RuleParser._buildExpressionGroup(ast)
632
+ } catch (e) {
633
+ // If it's already a RuleParseError, just re-throw it
634
+ if (e.name === 'RuleParseError') {
635
+ throw e;
636
+ }
637
+
638
+ // Check if it's a validation error we can map to a specific code
639
+ if (e.message && e.message.includes('Invalid time of day')) {
640
+ // Extract the invalid time from the error message
641
+ const match = e.message.match(/Invalid time of day[,:]?\s*([0-9:]+)/);
642
+ const badTod = match ? match[1] : 'invalid';
643
+
644
+ // Calculate position (simplified - at end of input)
645
+ const lines = txt.trim().split('\n');
646
+ const position = {
647
+ line: lines.length,
648
+ column: lines[lines.length - 1].length + 1,
649
+ offset: txt.trim().length
650
+ };
651
+
652
+ throw new RuleParseError(
653
+ "BAD_TOD",
654
+ `Invalid time of day: ${badTod}`,
655
+ "Time of day must be in HH:MM format with hours 0-23 and minutes 0-59, e.g. 08:30, 14:00, 23:59.",
656
+ position,
657
+ badTod,
658
+ ["HH:MM"],
659
+ txt.trim().substring(Math.max(0, txt.trim().length - 50))
660
+ );
661
+ }
662
+
663
+ // Check if it's a day of week error
664
+ if (e.message && e.message.includes('Invalid day of week')) {
665
+ const match = e.message.match(/Invalid day of week[,:]?\s*(\w+)/);
666
+ const badDow = match ? match[1] : 'invalid';
667
+
668
+ const lines = txt.trim().split('\n');
669
+ const position = {
670
+ line: lines.length,
671
+ column: lines[lines.length - 1].length + 1,
672
+ offset: txt.trim().length
673
+ };
674
+
675
+ throw new RuleParseError(
676
+ "BAD_DOW",
677
+ `Invalid day of week: ${badDow}`,
678
+ "Valid days are: MONDAY/MON, TUESDAY/TUE, WEDNESDAY/WED, THURSDAY/THU, FRIDAY/FRI, SATURDAY/SAT, SUNDAY/SUN.",
679
+ position,
680
+ badDow,
681
+ ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"],
682
+ txt.trim().substring(Math.max(0, txt.trim().length - 50))
683
+ );
684
+ }
685
+
686
+ // For other errors, re-throw
687
+ throw e;
688
+ }
689
+ }
690
+ }
691
+ module.exports = RuleParser
692
+ module.exports.ParserRules = ParserRules
693
+ module.exports.ParsingError = ParsingError
694
+ module.exports.RuleParseError = RuleParseError
695
+
696
+ },{"./RuleParser.production.ebnf.js":1,"./errors/ErrorAnalyzer":3,"./errors/RuleParseError":4,"assert":undefined,"ebnf":13,"ebnf/dist/Parser.js":9}],3:[function(require,module,exports){
697
+ const { ParsingError } = require('ebnf');
698
+ const RuleParseError = require('./RuleParseError');
699
+
700
+ /**
701
+ * Analyzes parsing errors and maps them to user-friendly error codes
702
+ */
703
+ class ErrorAnalyzer {
704
+ /**
705
+ * Analyze a ParsingError and return a user-friendly RuleParseError
706
+ * @param {string} input - The input string that failed to parse
707
+ * @param {ParsingError} parsingError - The ParsingError from the parser (optional)
708
+ * @returns {RuleParseError} - A user-friendly error with error code
709
+ */
710
+ static analyzeParseFailure(input, parsingError = null) {
711
+ const trimmedInput = input.trim();
712
+
713
+ // Check for empty or missing expression first (fast path)
714
+ if (!trimmedInput) {
715
+ const position = { line: 1, column: 1, offset: 0 };
716
+ return new RuleParseError(
717
+ "MISSING_EXPRESSION",
718
+ "Empty or missing expression.",
719
+ "Provide a valid rule expression.",
720
+ position,
721
+ "empty input",
722
+ ["expression"],
723
+ ""
724
+ );
725
+ }
726
+
727
+ // If we have a ParsingError, use its information
728
+ if (parsingError && parsingError instanceof ParsingError) {
729
+ return this._analyzeFromParsingError(input, parsingError);
730
+ }
731
+
732
+ // Fallback to string-based analysis (for validation errors)
733
+ const lines = trimmedInput.split('\n');
734
+ const line = lines.length;
735
+ const lastLine = lines[lines.length - 1] || '';
736
+ const column = lastLine.length + 1;
737
+ const offset = trimmedInput.length;
738
+
739
+ const position = { line, column, offset };
740
+
741
+ // Get snippet (last 50 chars or the whole input if shorter)
742
+ const snippetStart = Math.max(0, trimmedInput.length - 50);
743
+ const snippet = (snippetStart > 0 ? '...' : '') + trimmedInput.substring(snippetStart);
744
+
745
+ // Analyze the error pattern
746
+ const errorInfo = this._detectErrorPattern(trimmedInput, position, snippet);
747
+
748
+ return new RuleParseError(
749
+ errorInfo.code,
750
+ errorInfo.message,
751
+ errorInfo.hint,
752
+ position,
753
+ errorInfo.found,
754
+ errorInfo.expected,
755
+ snippet
756
+ );
757
+ }
758
+
759
+ /**
760
+ * Analyze error using ParsingError exception data
761
+ * @private
762
+ */
763
+ static _analyzeFromParsingError(input, err) {
764
+ const position = err.position;
765
+ const expected = err.expected || [];
766
+ const found = err.found || '';
767
+ const failureTree = err.failureTree || [];
768
+
769
+ // Get snippet around error position
770
+ const snippetStart = Math.max(0, position.offset - 20);
771
+ const snippetEnd = Math.min(input.length, position.offset + 30);
772
+ const snippet = (snippetStart > 0 ? '...' : '') +
773
+ input.substring(snippetStart, snippetEnd) +
774
+ (snippetEnd < input.length ? '...' : '');
775
+
776
+ // Analyze what was expected to determine error type using failureTree
777
+ const errorInfo = this._detectErrorFromFailureTree(input, position, expected, found, failureTree);
778
+
779
+ return new RuleParseError(
780
+ errorInfo.code,
781
+ errorInfo.message,
782
+ errorInfo.hint,
783
+ position,
784
+ found,
785
+ expected,
786
+ snippet
787
+ );
788
+ }
789
+
790
+ /**
791
+ * Detect error type from failureTree
792
+ * @private
793
+ */
794
+ static _detectErrorFromFailureTree(input, position, expected, found, failureTree) {
795
+ const beforeError = input.substring(0, position.offset);
796
+
797
+ // Helper to check if a name exists in the failure tree
798
+ const hasFailure = (name) => {
799
+ const search = (nodes) => {
800
+ if (!Array.isArray(nodes)) return false;
801
+ for (const node of nodes) {
802
+ if (node.name === name) return true;
803
+ if (node.children && search(node.children)) return true;
804
+ }
805
+ return false;
806
+ };
807
+ return search(failureTree);
808
+ };
809
+
810
+ // Helper to check if a name exists at top level only
811
+ const hasTopLevelFailure = (name) => {
812
+ if (!Array.isArray(failureTree)) return false;
813
+ return failureTree.some(node => node.name === name);
814
+ };
815
+
816
+ // PRIORITY 1: Check for unterminated string FIRST (before any balance checks)
817
+ // This must be checked early because unclosed strings can confuse other detections
818
+ if (this._hasUnterminatedString(input)) {
819
+ return {
820
+ code: "UNTERMINATED_STRING",
821
+ message: "Unterminated string literal.",
822
+ hint: "String literals must be enclosed in double quotes, e.g. \"hello world\".",
823
+ };
824
+ }
825
+
826
+ // PRIORITY 2: Check for bad number format
827
+ const badNum = this._findBadNumber(input);
828
+ if (badNum) {
829
+ return {
830
+ code: "BAD_NUMBER",
831
+ message: `Invalid number format: ${badNum}`,
832
+ hint: "Numbers must be valid integers or decimals, e.g. 42, 3.14, -5, 1.5e10.",
833
+ };
834
+ }
835
+
836
+ // PRIORITY 3: Check for token-level issues (extra characters after valid tokens)
837
+ // This must be checked before END_ARGUMENT/END_ARRAY to catch cases like WEDNESDAYY
838
+ const tokenError = this._detectExtraCharactersAfterToken(input, position, beforeError, found);
839
+ if (tokenError) {
840
+ return tokenError;
841
+ }
842
+
843
+ // PRIORITY 4: Check for specific parser contexts (function calls, arrays)
844
+ // But first check if we have bracket/paren imbalance that's more specific
845
+
846
+ // Check bracket balance first for extra brackets
847
+ const bracketBalance = this._checkBracketBalance(input);
848
+ if (bracketBalance < 0) {
849
+ // Extra closing bracket - this is more specific than function call context
850
+ return {
851
+ code: "UNMATCHED_BRACKET",
852
+ message: "Extra closing bracket detected.",
853
+ hint: "Every closing ']' must have a matching opening '['. Remove the extra closing bracket.",
854
+ };
855
+ }
856
+
857
+ // Check paren balance for certain cases
858
+ const parenBalance = this._checkParenBalance(input);
859
+ if (parenBalance < 0) {
860
+ // Extra closing paren - this is more specific than function call context
861
+ return {
862
+ code: "UNMATCHED_PAREN",
863
+ message: "Extra closing parenthesis detected.",
864
+ hint: "Every closing ')' must have a matching opening '('. Remove the extra closing parenthesis.",
865
+ };
866
+ }
867
+
868
+ // Check if parser was expecting END_ARGUMENT (closing paren for function)
869
+ // This indicates we're inside a function call context
870
+ if (hasFailure('END_ARGUMENT')) {
871
+ return {
872
+ code: "BAD_FUNCTION_CALL",
873
+ message: "Invalid function call syntax.",
874
+ hint: "Function calls must have matching parentheses, e.g. func() or func(arg1, arg2).",
875
+ };
876
+ }
877
+
878
+ // Check if parser was expecting END_ARRAY (closing bracket)
879
+ // This indicates we're inside an array context
880
+ if (hasFailure('END_ARRAY')) {
881
+ return {
882
+ code: "BAD_ARRAY_SYNTAX",
883
+ message: "Invalid array syntax.",
884
+ hint: "Arrays must be enclosed in brackets with comma-separated values, e.g. [1, 2, 3].",
885
+ };
886
+ }
887
+
888
+ // PRIORITY 5: Check for dangling operators
889
+
890
+ // Check for dangling logical operator (check input pattern directly)
891
+ if (found === "end of input") {
892
+ const trimmed = input.trim();
893
+ if (/&&\s*$/.test(trimmed)) {
894
+ return {
895
+ code: "DANGLING_LOGICAL_OPERATOR",
896
+ message: "Logical operator '&&' at end of expression.",
897
+ hint: "Logical operators (&&, ||, AND, OR) must be followed by an expression.",
898
+ };
899
+ }
900
+ if (/\|\|\s*$/.test(trimmed)) {
901
+ return {
902
+ code: "DANGLING_LOGICAL_OPERATOR",
903
+ message: "Logical operator '||' at end of expression.",
904
+ hint: "Logical operators (&&, ||, AND, OR) must be followed by an expression.",
905
+ };
906
+ }
907
+ if (/\bAND\b\s*$/i.test(trimmed)) {
908
+ return {
909
+ code: "DANGLING_LOGICAL_OPERATOR",
910
+ message: "Logical operator 'AND' at end of expression.",
911
+ hint: "Logical operators (&&, ||, AND, OR) must be followed by an expression.",
912
+ };
913
+ }
914
+ if (/\bOR\b\s*$/i.test(trimmed)) {
915
+ return {
916
+ code: "DANGLING_LOGICAL_OPERATOR",
917
+ message: "Logical operator 'OR' at end of expression.",
918
+ hint: "Logical operators (&&, ||, AND, OR) must be followed by an expression.",
919
+ };
920
+ }
921
+ }
922
+
923
+ // Check for BETWEEN - if we see BETWEEN in input with incomplete syntax
924
+ if (/\bBETWEEN\b/i.test(input)) {
925
+ // Check if BETWEEN is incomplete or missing AND
926
+ if (found === "end of input" || /\bBETWEEN\s+\d+\s*$/i.test(beforeError.trim())) {
927
+ return {
928
+ code: "BAD_BETWEEN_SYNTAX",
929
+ message: "Invalid BETWEEN syntax.",
930
+ hint: "BETWEEN requires two values: 'expr BETWEEN value1 AND value2' or 'expr BETWEEN value1-value2'.",
931
+ };
932
+ }
933
+ }
934
+
935
+ // Check if parser expected BEGIN_ARGUMENT - could be missing RHS after operator
936
+ if (hasTopLevelFailure('BEGIN_ARGUMENT')) {
937
+ const trimmed = beforeError.trim();
938
+ // If found is a comparison operator, this is likely missing RHS
939
+ if (found.match(/^[><=!]/)) {
940
+ return {
941
+ code: "MISSING_RHS_AFTER_OPERATOR",
942
+ message: `Expected a value after '${found}'.`,
943
+ hint: "Comparison operators must be followed by a value, e.g. temp > 25, name == \"bob\".",
944
+ };
945
+ }
946
+
947
+ // Also check if beforeError ends with comparison operator
948
+ const compOps = ['>=', '<=', '==', '!=', '>', '<', '=', '==~', '!=~'];
949
+ for (const op of compOps) {
950
+ if (trimmed.endsWith(op)) {
951
+ return {
952
+ code: "MISSING_RHS_AFTER_OPERATOR",
953
+ message: `Expected a value after '${op}'.`,
954
+ hint: "Comparison operators must be followed by a value, e.g. temp > 25, name == \"bob\".",
955
+ };
956
+ }
957
+ }
958
+ }
959
+
960
+ // Check for value/result expected (could be missing RHS)
961
+ if ((hasFailure('value') || hasFailure('result') || hasFailure('simple_result')) && found === "end of input") {
962
+ // Check for comparison operator
963
+ const trimmed = beforeError.trim();
964
+ const compOps = ['>=', '<=', '==', '!=', '>', '<', '=', '==~', '!=~'];
965
+
966
+ for (const op of compOps) {
967
+ if (trimmed.endsWith(op)) {
968
+ return {
969
+ code: "MISSING_RHS_AFTER_OPERATOR",
970
+ message: `Expected a value after '${op}'.`,
971
+ hint: "Comparison operators must be followed by a value, e.g. temp > 25, name == \"bob\".",
972
+ };
973
+ }
974
+ }
975
+ }
976
+
977
+ // PRIORITY 6: Final fallback - general balance checks for unclosed parens/brackets
978
+ // (Note: extra closing parens/brackets were already checked in PRIORITY 4)
979
+
980
+ // Check for unclosed parentheses (only if not already handled)
981
+ if (parenBalance > 0 && found === "end of input") {
982
+ // Check if this looks like a function call context
983
+ const looksLikeFunction = /[a-zA-Z_][a-zA-Z0-9_]*\s*\([^)]*$/.test(input);
984
+ const message = looksLikeFunction
985
+ ? "Unclosed parenthesis in function call."
986
+ : "Unclosed parenthesis detected.";
987
+
988
+ return {
989
+ code: "UNMATCHED_PAREN",
990
+ message: message,
991
+ hint: "Every opening '(' must have a matching closing ')'. Check your expression for missing closing parentheses.",
992
+ };
993
+ }
994
+
995
+ // Check for unclosed brackets (only if not already handled)
996
+ if (bracketBalance > 0 && found === "end of input") {
997
+ // Check if this looks like an array context
998
+ const looksLikeArray = /\[[^\]]*$/.test(input);
999
+ const message = looksLikeArray
1000
+ ? "Unclosed bracket in array."
1001
+ : "Unclosed bracket detected.";
1002
+
1003
+ return {
1004
+ code: "UNMATCHED_BRACKET",
1005
+ message: message,
1006
+ hint: "Every opening '[' must have a matching closing ']'. Check your array syntax.",
1007
+ };
1008
+ }
1009
+
1010
+ // Default to unexpected token
1011
+ const foundDesc = found === "end of input" ? "end of input" : `'${found}'`;
1012
+ return {
1013
+ code: "UNEXPECTED_TOKEN",
1014
+ message: `Unexpected ${foundDesc} at position ${position.offset}.`,
1015
+ hint: "Check your syntax. Common issues include missing operators, invalid characters, or malformed expressions.",
1016
+ };
1017
+ }
1018
+
1019
+ /**
1020
+ * Detect extra characters after a valid token (e.g., WEDNESDAYY has extra Y after WEDNESDAY)
1021
+ * @private
1022
+ */
1023
+ static _detectExtraCharactersAfterToken(input, position, beforeError, found) {
1024
+ // First, check for invalid day of week patterns
1025
+ // Pattern: "ON" followed by partial day name
1026
+ const onMatch = /\bON\s+(\w+)$/i.exec(beforeError);
1027
+ if (onMatch) {
1028
+ const partial = onMatch[1].toUpperCase();
1029
+ // Check if this looks like a partial day name or invalid day
1030
+ // Note: THUR is not included because it's not actually supported by the parser
1031
+ const validDays = ['MONDAY', 'MON', 'TUESDAY', 'TUE', 'WEDNESDAY', 'WED',
1032
+ 'THURSDAY', 'THU', 'FRIDAY', 'FRI', 'SATURDAY',
1033
+ 'SAT', 'SUNDAY', 'SUN'];
1034
+
1035
+ if (!validDays.includes(partial)) {
1036
+ // This might be part of an invalid day name
1037
+ // Combine with found to get the full attempted day name if found is alphabetic
1038
+ const fullAttempt = partial + (found && found !== "end of input" && found.match(/^[A-Z]/i) ? found : "");
1039
+
1040
+ // Check if it looks like a misspelled day
1041
+ const possibleMisspelling = validDays.some(day => {
1042
+ // Check for similarity (starts with same letter, length close)
1043
+ return day.startsWith(partial.charAt(0)) &&
1044
+ Math.abs(day.length - fullAttempt.length) <= 3;
1045
+ });
1046
+
1047
+ if (possibleMisspelling || fullAttempt.length >= 3) {
1048
+ return {
1049
+ code: "BAD_DOW",
1050
+ message: `Invalid day of week: ${fullAttempt}`,
1051
+ hint: "Valid days are: MONDAY/MON, TUESDAY/TUE, WEDNESDAY/WED, THURSDAY/THU, FRIDAY/FRI, SATURDAY/SAT, SUNDAY/SUN.",
1052
+ };
1053
+ }
1054
+ }
1055
+ }
1056
+
1057
+ // Check for time of day patterns right before error
1058
+ const todMatch = /(\d{1,2}):(-?\d{1,2})?$/.exec(beforeError);
1059
+ if (todMatch || (beforeError.match(/\d+:$/) && found && found.match(/^-?\d+/))) {
1060
+ // Looks like we're in a time of day context
1061
+ // Extract the full time attempt
1062
+ let fullTime = todMatch ? todMatch[0] : '';
1063
+ if (!todMatch && beforeError.match(/\d+:$/)) {
1064
+ // We have a number: before and digits after
1065
+ const num = beforeError.match(/\d+:$/)[0];
1066
+ const minutes = found.match(/^-?\d+/)[0];
1067
+ fullTime = num + minutes;
1068
+ }
1069
+
1070
+ // Check if it's a bad time
1071
+ const timePattern = /(\d{1,2}):(-?\d{1,2})/;
1072
+ const timeMatch = timePattern.exec(fullTime);
1073
+ if (timeMatch) {
1074
+ const hours = parseInt(timeMatch[1], 10);
1075
+ const minutes = parseInt(timeMatch[2], 10);
1076
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
1077
+ return {
1078
+ code: "BAD_TOD",
1079
+ message: `Invalid time of day: ${timeMatch[0]}`,
1080
+ hint: "Time of day must be in HH:MM format with hours 0-23 and minutes 0-59, e.g. 08:30, 14:00, 23:59.",
1081
+ };
1082
+ }
1083
+ }
1084
+ }
1085
+
1086
+ // List of known valid tokens (keywords, days of week, etc.)
1087
+ const validTokens = [
1088
+ // Days of week
1089
+ 'MONDAY', 'MON', 'TUESDAY', 'TUE', 'WEDNESDAY', 'WED',
1090
+ 'THURSDAY', 'THU', 'THUR', 'FRIDAY', 'FRI', 'SATURDAY',
1091
+ 'SAT', 'SUNDAY', 'SUN',
1092
+ // Keywords
1093
+ 'BETWEEN', 'AND', 'OR', 'NOT', 'IN', 'ON'
1094
+ ];
1095
+
1096
+ // Check if any valid token appears right before the error position
1097
+ for (const token of validTokens) {
1098
+ if (beforeError.toUpperCase().endsWith(token)) {
1099
+ // Found a valid token right before error - check if we have extra characters
1100
+ if (found && found.length > 0 && found !== "end of input" && found.match(/^[A-Z]/i)) {
1101
+ // We have alphabetic characters after a valid token
1102
+ const extraChars = found.charAt(0);
1103
+ return {
1104
+ code: "UNEXPECTED_TOKEN",
1105
+ message: `Unexpected characters after '${token}'. Remove the extra '${extraChars}'.`,
1106
+ hint: `'${token}' is a valid keyword. Remove any extra characters that follow it.`,
1107
+ };
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ return null;
1113
+ }
1114
+
1115
+ /**
1116
+ * Detect the error pattern in the input (fallback for non-ParsingError cases)
1117
+ * @private
1118
+ * @param {string} input - The input to analyze
1119
+ * @param {Object} position - Position information
1120
+ * @param {string} snippet - Code snippet
1121
+ * @returns {Object} Error information
1122
+ */
1123
+ static _detectErrorPattern(input, position, snippet) {
1124
+ // Check for unterminated string first (this affects other checks)
1125
+ if (this._hasUnterminatedString(input)) {
1126
+ return {
1127
+ code: "UNTERMINATED_STRING",
1128
+ message: "Unterminated string literal.",
1129
+ hint: "String literals must be enclosed in double quotes, e.g. \"hello world\".",
1130
+ found: "end of input",
1131
+ expected: ["\""]
1132
+ };
1133
+ }
1134
+
1135
+ // Check for dangling logical operators
1136
+ const danglingOp = this._findDanglingLogicalOperator(input);
1137
+ if (danglingOp) {
1138
+ return {
1139
+ code: "DANGLING_LOGICAL_OPERATOR",
1140
+ message: `Logical operator '${danglingOp}' at end of expression.`,
1141
+ hint: "Logical operators (&&, ||, AND, OR) must be followed by an expression.",
1142
+ found: danglingOp,
1143
+ expected: ["expression", "function call", "value"]
1144
+ };
1145
+ }
1146
+
1147
+ // Check for missing RHS after comparison operator
1148
+ const danglingCompOp = this._findDanglingComparisonOperator(input);
1149
+ if (danglingCompOp) {
1150
+ return {
1151
+ code: "MISSING_RHS_AFTER_OPERATOR",
1152
+ message: `Expected a value after '${danglingCompOp}'.`,
1153
+ hint: "Comparison operators must be followed by a value, e.g. temp > 25, name == \"bob\".",
1154
+ found: "end of input",
1155
+ expected: ["value", "function call", "number", "string"]
1156
+ };
1157
+ }
1158
+
1159
+ // Check for bad BETWEEN syntax
1160
+ if (this._hasBadBetweenSyntax(input)) {
1161
+ return {
1162
+ code: "BAD_BETWEEN_SYNTAX",
1163
+ message: "Invalid BETWEEN syntax.",
1164
+ hint: "BETWEEN requires two values: 'expr BETWEEN value1 AND value2' or 'expr BETWEEN value1-value2'.",
1165
+ found: "incomplete BETWEEN expression",
1166
+ expected: ["BETWEEN value1 AND value2"]
1167
+ };
1168
+ }
1169
+
1170
+ // Check for bad time of day (TOD) format
1171
+ const badTod = this._findBadTimeOfDay(input);
1172
+ if (badTod) {
1173
+ return {
1174
+ code: "BAD_TOD",
1175
+ message: `Invalid time of day: ${badTod.value}`,
1176
+ hint: "Time of day must be in HH:MM format with hours 0-23 and minutes 0-59, e.g. 08:30, 14:00, 23:59.",
1177
+ found: badTod.value,
1178
+ expected: ["HH:MM"]
1179
+ };
1180
+ }
1181
+
1182
+ // Check for bad day of week (DOW)
1183
+ const badDow = this._findBadDayOfWeek(input);
1184
+ if (badDow) {
1185
+ return {
1186
+ code: "BAD_DOW",
1187
+ message: `Invalid day of week: ${badDow}`,
1188
+ hint: "Valid days are: MONDAY/MON, TUESDAY/TUE, WEDNESDAY/WED, THURSDAY/THU, FRIDAY/FRI, SATURDAY/SAT, SUNDAY/SUN.",
1189
+ found: badDow,
1190
+ expected: ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"]
1191
+ };
1192
+ }
1193
+
1194
+ // Check for bad number format
1195
+ const badNumber = this._findBadNumber(input);
1196
+ if (badNumber) {
1197
+ return {
1198
+ code: "BAD_NUMBER",
1199
+ message: `Invalid number format: ${badNumber}`,
1200
+ hint: "Numbers must be valid integers or decimals, e.g. 42, 3.14, -5, 1.5e10.",
1201
+ found: badNumber,
1202
+ expected: ["valid number"]
1203
+ };
1204
+ }
1205
+
1206
+ // Check for bad function call syntax (more specific than general paren check)
1207
+ if (this._hasBadFunctionCall(input)) {
1208
+ return {
1209
+ code: "BAD_FUNCTION_CALL",
1210
+ message: "Invalid function call syntax.",
1211
+ hint: "Function calls must have matching parentheses, e.g. func() or func(arg1, arg2).",
1212
+ found: "incomplete function call",
1213
+ expected: [")"]
1214
+ };
1215
+ }
1216
+
1217
+ // Check for bad array syntax (more specific than general bracket check)
1218
+ if (this._hasBadArraySyntax(input)) {
1219
+ return {
1220
+ code: "BAD_ARRAY_SYNTAX",
1221
+ message: "Invalid array syntax.",
1222
+ hint: "Arrays must be enclosed in brackets with comma-separated values, e.g. [1, 2, 3].",
1223
+ found: "incomplete array",
1224
+ expected: ["]", ","]
1225
+ };
1226
+ }
1227
+
1228
+ // Check for unmatched parentheses (general check)
1229
+ const parenBalance = this._checkParenBalance(input);
1230
+ if (parenBalance > 0) {
1231
+ return {
1232
+ code: "UNMATCHED_PAREN",
1233
+ message: "Unclosed parenthesis detected.",
1234
+ hint: "Every opening '(' must have a matching closing ')'. Check your expression for missing closing parentheses.",
1235
+ found: "end of input",
1236
+ expected: [")"]
1237
+ };
1238
+ } else if (parenBalance < 0) {
1239
+ const closeIndex = this._findExtraCloseParen(input);
1240
+ return {
1241
+ code: "UNMATCHED_PAREN",
1242
+ message: "Extra closing parenthesis detected.",
1243
+ hint: "Every closing ')' must have a matching opening '('. Remove the extra closing parenthesis.",
1244
+ found: ")",
1245
+ expected: ["expression", "operator"]
1246
+ };
1247
+ }
1248
+
1249
+ // Check for unmatched brackets (general check)
1250
+ const bracketBalance = this._checkBracketBalance(input);
1251
+ if (bracketBalance > 0) {
1252
+ return {
1253
+ code: "UNMATCHED_BRACKET",
1254
+ message: "Unclosed bracket detected.",
1255
+ hint: "Every opening '[' must have a matching closing ']'. Check your array syntax.",
1256
+ found: "end of input",
1257
+ expected: ["]"]
1258
+ };
1259
+ } else if (bracketBalance < 0) {
1260
+ return {
1261
+ code: "UNMATCHED_BRACKET",
1262
+ message: "Extra closing bracket detected.",
1263
+ hint: "Every closing ']' must have a matching opening '['. Remove the extra closing bracket.",
1264
+ found: "]",
1265
+ expected: ["expression", "operator"]
1266
+ };
1267
+ }
1268
+
1269
+ // Check for multiple expressions without logical operators
1270
+ if (this._hasMultipleExpressionsWithoutOperator(input)) {
1271
+ return {
1272
+ code: "UNEXPECTED_TOKEN",
1273
+ message: "Multiple expressions without logical operator.",
1274
+ hint: "Use logical operators (&&, ||, AND, OR) to combine multiple expressions.",
1275
+ found: "expression",
1276
+ expected: ["&&", "||", "AND", "OR"]
1277
+ };
1278
+ }
1279
+
1280
+ // Default to unexpected token
1281
+ const found = this._getLastToken(input) || "unknown token";
1282
+ return {
1283
+ code: "UNEXPECTED_TOKEN",
1284
+ message: `Unexpected token: ${found}`,
1285
+ hint: "Check your syntax. Common issues include missing operators, invalid characters, or malformed expressions.",
1286
+ found: found,
1287
+ expected: ["valid expression"]
1288
+ };
1289
+ }
1290
+
1291
+ /**
1292
+ * Check if string is properly terminated
1293
+ * @private
1294
+ * @param {string} input - Input to check
1295
+ * @returns {boolean} True if string is unterminated
1296
+ */
1297
+ static _hasUnterminatedString(input) {
1298
+ let inString = false;
1299
+ let i = 0;
1300
+ while (i < input.length) {
1301
+ const char = input[i];
1302
+
1303
+ if (char === '"') {
1304
+ if (!inString) {
1305
+ inString = true;
1306
+ } else {
1307
+ // Check if this quote is escaped by counting preceding backslashes
1308
+ let backslashCount = 0;
1309
+ let j = i - 1;
1310
+ while (j >= 0 && input[j] === '\\') {
1311
+ backslashCount++;
1312
+ j--;
1313
+ }
1314
+ // If even number of backslashes (including 0), the quote is not escaped
1315
+ if (backslashCount % 2 === 0) {
1316
+ inString = false;
1317
+ }
1318
+ }
1319
+ }
1320
+ i++;
1321
+ }
1322
+ return inString;
1323
+ }
1324
+
1325
+ /**
1326
+ * Check parenthesis balance
1327
+ * @private
1328
+ * @param {string} input - Input to check
1329
+ * @returns {number} Balance (positive = unclosed, negative = extra closing)
1330
+ */
1331
+ static _checkParenBalance(input) {
1332
+ let balance = 0;
1333
+ let inString = false;
1334
+ let i = 0;
1335
+ while (i < input.length) {
1336
+ const char = input[i];
1337
+
1338
+ if (char === '"') {
1339
+ if (!inString) {
1340
+ inString = true;
1341
+ } else {
1342
+ // Check if this quote is escaped
1343
+ let backslashCount = 0;
1344
+ let j = i - 1;
1345
+ while (j >= 0 && input[j] === '\\') {
1346
+ backslashCount++;
1347
+ j--;
1348
+ }
1349
+ if (backslashCount % 2 === 0) {
1350
+ inString = false;
1351
+ }
1352
+ }
1353
+ }
1354
+
1355
+ // Only count parentheses outside of strings
1356
+ if (!inString) {
1357
+ if (char === '(') balance++;
1358
+ else if (char === ')') balance--;
1359
+ }
1360
+ i++;
1361
+ }
1362
+ return balance;
1363
+ }
1364
+
1365
+ /**
1366
+ * Find position of extra closing paren
1367
+ * @private
1368
+ */
1369
+ static _findExtraCloseParen(input) {
1370
+ let balance = 0;
1371
+ let inString = false;
1372
+ let i = 0;
1373
+ while (i < input.length) {
1374
+ const char = input[i];
1375
+
1376
+ if (char === '"') {
1377
+ if (!inString) {
1378
+ inString = true;
1379
+ } else {
1380
+ // Check if this quote is escaped
1381
+ let backslashCount = 0;
1382
+ let j = i - 1;
1383
+ while (j >= 0 && input[j] === '\\') {
1384
+ backslashCount++;
1385
+ j--;
1386
+ }
1387
+ if (backslashCount % 2 === 0) {
1388
+ inString = false;
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ if (!inString) {
1394
+ if (char === '(') balance++;
1395
+ else if (char === ')') {
1396
+ balance--;
1397
+ if (balance < 0) return i;
1398
+ }
1399
+ }
1400
+ i++;
1401
+ }
1402
+ return -1;
1403
+ }
1404
+
1405
+ /**
1406
+ * Check bracket balance
1407
+ * @private
1408
+ * @param {string} input - Input to check
1409
+ * @returns {number} Balance (positive = unclosed, negative = extra closing)
1410
+ */
1411
+ static _checkBracketBalance(input) {
1412
+ let balance = 0;
1413
+ let inString = false;
1414
+ let i = 0;
1415
+ while (i < input.length) {
1416
+ const char = input[i];
1417
+
1418
+ if (char === '"') {
1419
+ if (!inString) {
1420
+ inString = true;
1421
+ } else {
1422
+ // Check if this quote is escaped
1423
+ let backslashCount = 0;
1424
+ let j = i - 1;
1425
+ while (j >= 0 && input[j] === '\\') {
1426
+ backslashCount++;
1427
+ j--;
1428
+ }
1429
+ if (backslashCount % 2 === 0) {
1430
+ inString = false;
1431
+ }
1432
+ }
1433
+ }
1434
+
1435
+ if (!inString) {
1436
+ if (char === '[') balance++;
1437
+ else if (char === ']') balance--;
1438
+ }
1439
+ i++;
1440
+ }
1441
+ return balance;
1442
+ }
1443
+
1444
+ static _findDanglingLogicalOperator(input) {
1445
+ const trimmed = input.trim();
1446
+ const logicalOps = [
1447
+ { pattern: /&&\s*$/, op: '&&' },
1448
+ { pattern: /\|\|\s*$/, op: '||' },
1449
+ { pattern: /\bAND\b\s*$/i, op: 'AND' },
1450
+ { pattern: /\bOR\b\s*$/i, op: 'OR' }
1451
+ ];
1452
+
1453
+ for (const { pattern, op } of logicalOps) {
1454
+ if (pattern.test(trimmed)) {
1455
+ return op;
1456
+ }
1457
+ }
1458
+ return null;
1459
+ }
1460
+
1461
+ static _findDanglingComparisonOperator(input) {
1462
+ const trimmed = input.trim();
1463
+ const compOps = ['>=', '<=', '==', '!=', '>', '<', '=', '==~', '!=~'];
1464
+
1465
+ for (const op of compOps) {
1466
+ const pattern = new RegExp(`${this._escapeRegex(op)}\\s*$`);
1467
+ if (pattern.test(trimmed)) {
1468
+ return op;
1469
+ }
1470
+ }
1471
+ return null;
1472
+ }
1473
+
1474
+ static _hasBadBetweenSyntax(input) {
1475
+ // Check if BETWEEN keyword exists but not followed by proper syntax
1476
+ if (/\bBETWEEN\b/i.test(input)) {
1477
+ // Check for valid BETWEEN patterns
1478
+ const hasValidBetweenAnd = /\bBETWEEN\s+\S+\s+AND\s+\S+/i.test(input);
1479
+ const hasValidBetweenDash = /\bBETWEEN\s+\S+-\s*\S+/i.test(input);
1480
+ return !hasValidBetweenAnd && !hasValidBetweenDash;
1481
+ }
1482
+ return false;
1483
+ }
1484
+
1485
+ static _hasBadFunctionCall(input) {
1486
+ // Only return true if:
1487
+ // 1. We have a function pattern (identifier followed by open paren)
1488
+ // 2. The parentheses are unbalanced
1489
+ // 3. There are NO array brackets (so we don't confuse arrays with function calls)
1490
+ // 4. The imbalance is specifically in a function call context
1491
+
1492
+ const funcPattern = /[a-zA-Z0-9]+\s*\(/;
1493
+ const hasArrayBracket = /\[/.test(input);
1494
+
1495
+ if (!funcPattern.test(input) || hasArrayBracket) {
1496
+ return false;
1497
+ }
1498
+
1499
+ const parenBalance = this._checkParenBalance(input);
1500
+ if (parenBalance > 0) {
1501
+ // We have unclosed parens in what looks like a function call
1502
+ // Make sure it's not just a general parenthesis expression
1503
+ // by checking if the first occurrence of ( is preceded by an identifier
1504
+ const firstParenMatch = input.match(/^[^(]*([a-zA-Z0-9]+)\s*\(/);
1505
+ if (firstParenMatch) {
1506
+ return true;
1507
+ }
1508
+ }
1509
+
1510
+ return false;
1511
+ }
1512
+
1513
+ static _hasBadArraySyntax(input) {
1514
+ // Only return true if:
1515
+ // 1. We have an array bracket [
1516
+ // 2. The brackets are unbalanced
1517
+ // 3. It's clearly in an array context (inside function args or standalone)
1518
+
1519
+ if (!/\[/.test(input)) {
1520
+ return false;
1521
+ }
1522
+
1523
+ const bracketBalance = this._checkBracketBalance(input);
1524
+ if (bracketBalance > 0) {
1525
+ // We have unclosed brackets - this is likely an array issue
1526
+ return true;
1527
+ }
1528
+
1529
+ return false;
1530
+ }
1531
+
1532
+ static _hasMultipleExpressionsWithoutOperator(input) {
1533
+ // Detect patterns like "A() B()" without && or || between them
1534
+ // Check if there's no logical operator between function calls
1535
+ const betweenPattern = /\)\s+(?!&&|\|\||AND|OR|BETWEEN)/i;
1536
+ if (betweenPattern.test(input)) {
1537
+ // Make sure the next thing after ) is an identifier (suggesting another expression)
1538
+ return /\)\s+[a-zA-Z]/.test(input);
1539
+ }
1540
+ return false;
1541
+ }
1542
+
1543
+ static _getLastToken(input) {
1544
+ const trimmed = input.trim();
1545
+ // Get last 20 characters or the whole string if shorter
1546
+ const lastPart = trimmed.substring(Math.max(0, trimmed.length - 20));
1547
+ return lastPart;
1548
+ }
1549
+
1550
+ /**
1551
+ * Check for invalid time of day format (e.g., 25:00, 12:60)
1552
+ * @private
1553
+ */
1554
+ static _findBadTimeOfDay(input) {
1555
+ // Look for patterns like HH:MM where hours or minutes are out of range or negative
1556
+ const todPattern = /\b(-?\d{1,2}):(-?\d{1,2})\b/g;
1557
+ let match;
1558
+
1559
+ while ((match = todPattern.exec(input)) !== null) {
1560
+ const hours = parseInt(match[1], 10);
1561
+ const minutes = parseInt(match[2], 10);
1562
+ const fullMatch = match[0];
1563
+
1564
+ // Check if hours or minutes are out of valid range or negative
1565
+ if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
1566
+ return { value: fullMatch, hours, minutes };
1567
+ }
1568
+ }
1569
+ return null;
1570
+ }
1571
+
1572
+ /**
1573
+ * Check for invalid day of week
1574
+ * @private
1575
+ */
1576
+ static _findBadDayOfWeek(input) {
1577
+ // Look for ON keyword followed by potential day name
1578
+ const onPattern = /\bON\s+([A-Z]+)/i;
1579
+ const match = onPattern.exec(input);
1580
+
1581
+ if (match) {
1582
+ const dayCandidate = match[1].toUpperCase();
1583
+ const validDays = ['MONDAY', 'MON', 'TUESDAY', 'TUE', 'WEDNESDAY', 'WED',
1584
+ 'THURSDAY', 'THU', 'THUR', 'FRIDAY', 'FRI', 'SATURDAY',
1585
+ 'SAT', 'SUNDAY', 'SUN'];
1586
+
1587
+ if (!validDays.includes(dayCandidate)) {
1588
+ return dayCandidate;
1589
+ }
1590
+ }
1591
+ return null;
1592
+ }
1593
+
1594
+ /**
1595
+ * Check for invalid number format (e.g., 1.2.3, 123abc)
1596
+ * @private
1597
+ */
1598
+ static _findBadNumber(input) {
1599
+ // Look for malformed numbers - multiple decimal points or numbers followed by letters
1600
+ const badNumberPatterns = [
1601
+ /\b\d+\.\d+\.\d+/, // Multiple decimal points like 1.2.3
1602
+ /\b\d+[a-zA-Z]+\d*/, // Numbers with letters mixed in like 123abc
1603
+ /\b\d+\.\d+[a-zA-Z]+/ // Decimals with letters like 1.5abc
1604
+ ];
1605
+
1606
+ for (const pattern of badNumberPatterns) {
1607
+ const match = pattern.exec(input);
1608
+ if (match) {
1609
+ return match[0];
1610
+ }
1611
+ }
1612
+ return null;
1613
+ }
1614
+
1615
+ static _escapeRegex(str) {
1616
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1617
+ }
1618
+ }
1619
+
1620
+ module.exports = { ErrorAnalyzer };
1621
+
1622
+ },{"./RuleParseError":4,"ebnf":13}],4:[function(require,module,exports){
1623
+ /**
1624
+ * Represents a user-friendly rule parsing error with error codes
1625
+ */
1626
+ class RuleParseError extends Error {
1627
+ /**
1628
+ * Create a RuleParseError
1629
+ * @param {string} code - Error code for programmatic handling
1630
+ * @param {string} message - Human-readable error message
1631
+ * @param {string} hint - Helpful suggestion for fixing the error
1632
+ * @param {Object} position - Position information with line, column, and offset
1633
+ * @param {string} found - What was found at error position
1634
+ * @param {Array<string>} expected - What was expected
1635
+ * @param {string} snippet - Code snippet showing error location
1636
+ */
1637
+ constructor(code, message, hint, position, found, expected, snippet) {
1638
+ super(message);
1639
+ this.name = 'RuleParseError';
1640
+ this.code = code;
1641
+ this.hint = hint;
1642
+ this.line = position.line;
1643
+ this.column = position.column;
1644
+ this.offset = position.offset;
1645
+ this.found = found;
1646
+ this.expected = expected;
1647
+ this.snippet = snippet;
1648
+
1649
+ // Maintain proper prototype chain for instanceof checks
1650
+ Object.setPrototypeOf(this, RuleParseError.prototype);
1651
+ }
1652
+
1653
+ toString() {
1654
+ let msg = `${this.name} [${this.code}]: ${this.message}\n`;
1655
+ msg += ` at line ${this.line}, column ${this.column} (offset ${this.offset})\n`;
1656
+ if (this.snippet) {
1657
+ msg += ` ${this.snippet}\n`;
1658
+ }
1659
+ if (this.hint) {
1660
+ msg += ` Hint: ${this.hint}\n`;
1661
+ }
1662
+ if (this.found) {
1663
+ msg += ` Found: ${this.found}\n`;
1664
+ }
1665
+ if (this.expected && this.expected.length) {
1666
+ msg += ` Expected: ${this.expected.join(', ')}`;
1667
+ }
1668
+ return msg;
1669
+ }
1670
+
1671
+ toJSON() {
1672
+ return {
1673
+ code: this.code,
1674
+ message: this.message,
1675
+ hint: this.hint,
1676
+ line: this.line,
1677
+ column: this.column,
1678
+ offset: this.offset,
1679
+ found: this.found,
1680
+ expected: this.expected,
1681
+ snippet: this.snippet
1682
+ };
1683
+ }
1684
+ }
1685
+
1686
+ module.exports = RuleParseError;
1687
+
1688
+ },{}],5:[function(require,module,exports){
118
1689
  "use strict";
119
1690
  // https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form
120
1691
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -363,7 +1934,7 @@ var BNF;
363
1934
  })(BNF || (BNF = {}));
364
1935
  exports.default = BNF;
365
1936
 
366
- },{"../Parser":6,"../SemanticHelpers":8}],3:[function(require,module,exports){
1937
+ },{"../Parser":9,"../SemanticHelpers":11}],6:[function(require,module,exports){
367
1938
  "use strict";
368
1939
  // https://www.w3.org/TR/REC-xml/#NT-Name
369
1940
  // http://www.bottlecaps.de/rr/ui
@@ -818,7 +2389,7 @@ var BNF;
818
2389
  })(BNF || (BNF = {}));
819
2390
  exports.default = BNF;
820
2391
 
821
- },{"../Parser":6,"../TokenError":9}],4:[function(require,module,exports){
2392
+ },{"../Parser":9,"../TokenError":12}],7:[function(require,module,exports){
822
2393
  "use strict";
823
2394
  // https://www.w3.org/TR/REC-xml/#NT-Name
824
2395
  // http://www.bottlecaps.de/rr/ui
@@ -1199,7 +2770,7 @@ var BNF;
1199
2770
  })(BNF || (BNF = {}));
1200
2771
  exports.default = BNF;
1201
2772
 
1202
- },{"../Parser":6}],5:[function(require,module,exports){
2773
+ },{"../Parser":9}],8:[function(require,module,exports){
1203
2774
  "use strict";
1204
2775
  Object.defineProperty(exports, "__esModule", { value: true });
1205
2776
  exports.Custom = exports.W3C = exports.BNF = void 0;
@@ -1210,7 +2781,7 @@ Object.defineProperty(exports, "W3C", { enumerable: true, get: function () { ret
1210
2781
  var Custom_1 = require("./Custom");
1211
2782
  Object.defineProperty(exports, "Custom", { enumerable: true, get: function () { return Custom_1.default; } });
1212
2783
 
1213
- },{"./BNF":2,"./Custom":3,"./W3CEBNF":4}],6:[function(require,module,exports){
2784
+ },{"./BNF":5,"./Custom":6,"./W3CEBNF":7}],9:[function(require,module,exports){
1214
2785
  "use strict";
1215
2786
  // https://www.ics.uci.edu/~pattis/ICS-33/lectures/ebnf.pdf
1216
2787
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -1962,7 +3533,7 @@ class Parser {
1962
3533
  exports.Parser = Parser;
1963
3534
  exports.default = Parser;
1964
3535
 
1965
- },{"./ParsingError":7,"./TokenError":9}],7:[function(require,module,exports){
3536
+ },{"./ParsingError":10,"./TokenError":12}],10:[function(require,module,exports){
1966
3537
  "use strict";
1967
3538
  Object.defineProperty(exports, "__esModule", { value: true });
1968
3539
  exports.ParsingError = void 0;
@@ -1988,7 +3559,7 @@ class ParsingError extends Error {
1988
3559
  }
1989
3560
  exports.ParsingError = ParsingError;
1990
3561
 
1991
- },{}],8:[function(require,module,exports){
3562
+ },{}],11:[function(require,module,exports){
1992
3563
  "use strict";
1993
3564
  Object.defineProperty(exports, "__esModule", { value: true });
1994
3565
  exports.findChildrenByType = findChildrenByType;
@@ -1999,7 +3570,7 @@ function findChildrenByType(token, type) {
1999
3570
  return token.children ? token.children.filter(x => x.type == type) : [];
2000
3571
  }
2001
3572
 
2002
- },{}],9:[function(require,module,exports){
3573
+ },{}],12:[function(require,module,exports){
2003
3574
  "use strict";
2004
3575
  Object.defineProperty(exports, "__esModule", { value: true });
2005
3576
  exports.TokenError = void 0;
@@ -2019,7 +3590,7 @@ class TokenError extends Error {
2019
3590
  }
2020
3591
  exports.TokenError = TokenError;
2021
3592
 
2022
- },{}],10:[function(require,module,exports){
3593
+ },{}],13:[function(require,module,exports){
2023
3594
  "use strict";
2024
3595
  Object.defineProperty(exports, "__esModule", { value: true });
2025
3596
  exports.ParsingError = exports.TokenError = exports.Parser = void 0;
@@ -2031,17 +3602,17 @@ var ParsingError_1 = require("./ParsingError");
2031
3602
  Object.defineProperty(exports, "ParsingError", { enumerable: true, get: function () { return ParsingError_1.ParsingError; } });
2032
3603
  exports.Grammars = require("./Grammars");
2033
3604
 
2034
- },{"./Grammars":5,"./Parser":6,"./ParsingError":7,"./TokenError":9}],11:[function(require,module,exports){
2035
- 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"]]},{"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"]]}]
2036
- },{}],12:[function(require,module,exports){
3605
+ },{"./Grammars":8,"./Parser":9,"./ParsingError":10,"./TokenError":12}],14:[function(require,module,exports){
3606
+ 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"]]}]
3607
+ },{}],15:[function(require,module,exports){
2037
3608
  module.exports = require('./RuleTemplater.production.js');
2038
- },{"./RuleTemplater.production.js":13}],13:[function(require,module,exports){
2039
- // Note: We import the internal RuleParser.ebnf to extend the grammar with template rules.
2040
- // This creates coupling to the internal structure of @halleyassist/rule-parser.
2041
- const RuleParserRules = require('@halleyassist/rule-parser/src/RuleParser.ebnf'),
2042
- TemplateGrammar = require('./RuleTemplate.production.ebnf.js'),
3609
+ },{"./RuleTemplater.production.js":16}],16:[function(require,module,exports){
3610
+ // Note: We are coupled closely with the ebnf grammar structure of rule-parser
3611
+ const TemplateGrammar = require('./RuleTemplate.production.ebnf.js'),
2043
3612
  TemplateFilters = require('./TemplateFilters'),
2044
- {Parser} = require('ebnf');
3613
+ RuleParser = require('@halleyassist/rule-parser'),
3614
+ RuleParserRules = RuleParser.ParserRules,
3615
+ {Parser} = require('ebnf');
2045
3616
 
2046
3617
  let ParserCache = null;
2047
3618
 
@@ -2107,7 +3678,7 @@ class RuleTemplate {
2107
3678
  ParserCache = new Parser(ParserRules, {debug: false})
2108
3679
  }
2109
3680
 
2110
- const ast = ParserCache.getAST(ruleTemplate.trim(), 'statement_main');
3681
+ const ast = RuleParser.toAst(ruleTemplate.trim(), ParserCache);
2111
3682
  return new RuleTemplate(ruleTemplate, ast);
2112
3683
  }
2113
3684
 
@@ -2443,7 +4014,7 @@ module.exports = RuleTemplate;
2443
4014
  module.exports.ParserRules = ParserRules;
2444
4015
  module.exports.VariableTypes = VariableTypes;
2445
4016
  module.exports.TemplateFilters = TemplateFilters;
2446
- },{"./RuleTemplate.production.ebnf.js":11,"./TemplateFilters":14,"@halleyassist/rule-parser/src/RuleParser.ebnf":1,"ebnf":10}],14:[function(require,module,exports){
4017
+ },{"./RuleTemplate.production.ebnf.js":14,"./TemplateFilters":17,"@halleyassist/rule-parser":2,"ebnf":13}],17:[function(require,module,exports){
2447
4018
  /*
2448
4019
  Template filters are functions that transform variable values.
2449
4020
  They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
@@ -2507,5 +4078,5 @@ const TemplateFilters = {
2507
4078
  }
2508
4079
 
2509
4080
  module.exports = TemplateFilters;
2510
- },{}]},{},[12])(12)
4081
+ },{}]},{},[15])(15)
2511
4082
  });