@halleyassist/rule-templater 0.0.7 → 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.
- package/dist/rule-templater.browser.js +1703 -134
- package/package.json +3 -3
- package/src/RuleTemplater.js +6 -6
- package/src/RuleTemplater.production.js +6 -6
|
@@ -1,122 +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
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
between_time_only_atom ::= between_time_only
|
|
82
|
-
between_tod_only_atom ::= between_tod_only
|
|
83
|
-
|
|
84
|
-
AND ||= (WS* "&&" WS*) | (WS+ "AND" WS+)
|
|
85
|
-
OR ||= (WS* "||" WS*) | (WS+ "OR" WS+)
|
|
86
|
-
AGO ||= "AGO"
|
|
87
|
-
GT ::= ">"
|
|
88
|
-
LT ::= "<"
|
|
89
|
-
GTE ::= ">="
|
|
90
|
-
LTE ::= "<="
|
|
91
|
-
IS ||= "is"
|
|
92
|
-
EQ ::= "==" | "="
|
|
93
|
-
NEQ ::= "!="
|
|
94
|
-
NOT ||= ("!" WS*) | ("not" WS+)
|
|
95
|
-
|
|
96
|
-
false ||= "FALSE"
|
|
97
|
-
null ||= "null"
|
|
98
|
-
true ||= "TRUE"
|
|
99
|
-
|
|
100
|
-
array ::= BEGIN_ARRAY (value (VALUE_SEPARATOR value)*)? END_ARRAY
|
|
101
|
-
|
|
102
|
-
unit ||= "seconds" | "minutes" | "hours" | "weeks" | "days" | "second" | "minute" | "week" | "hour" | "day" | "mins" | "min"
|
|
103
|
-
|
|
104
|
-
number ::= "-"? ([0-9]+) ("." [0-9]+)? ("e" ("-" | "+")? ("0" | [1-9] [0-9]*))?
|
|
105
|
-
number_time ::= number WS+ unit
|
|
106
|
-
number_tod ::= ([0-9]+) ":" ([0-9]+)
|
|
107
|
-
|
|
108
|
-
time_period_ago ||= number_time_atom (WS+ number_time_atom)* WS+ AGO
|
|
109
|
-
time_period_ago_between ||= number_time_atom (WS+ number_time_atom)* WS+ AGO WS+ (between_time_only_atom | between_tod_only_atom)
|
|
110
|
-
time_period_const ||= "today" | time_period_ago
|
|
111
|
-
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')
|
|
112
8
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
`
|
|
9
|
+
let ParserRules = require('./RuleParser.production.ebnf.js')
|
|
10
|
+
let ParserCache;
|
|
116
11
|
|
|
117
|
-
|
|
12
|
+
const { ErrorAnalyzer } = require('./errors/ErrorAnalyzer');
|
|
118
13
|
|
|
119
|
-
|
|
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){
|
|
120
1689
|
"use strict";
|
|
121
1690
|
// https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form
|
|
122
1691
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -365,7 +1934,7 @@ var BNF;
|
|
|
365
1934
|
})(BNF || (BNF = {}));
|
|
366
1935
|
exports.default = BNF;
|
|
367
1936
|
|
|
368
|
-
},{"../Parser":
|
|
1937
|
+
},{"../Parser":9,"../SemanticHelpers":11}],6:[function(require,module,exports){
|
|
369
1938
|
"use strict";
|
|
370
1939
|
// https://www.w3.org/TR/REC-xml/#NT-Name
|
|
371
1940
|
// http://www.bottlecaps.de/rr/ui
|
|
@@ -820,7 +2389,7 @@ var BNF;
|
|
|
820
2389
|
})(BNF || (BNF = {}));
|
|
821
2390
|
exports.default = BNF;
|
|
822
2391
|
|
|
823
|
-
},{"../Parser":
|
|
2392
|
+
},{"../Parser":9,"../TokenError":12}],7:[function(require,module,exports){
|
|
824
2393
|
"use strict";
|
|
825
2394
|
// https://www.w3.org/TR/REC-xml/#NT-Name
|
|
826
2395
|
// http://www.bottlecaps.de/rr/ui
|
|
@@ -1201,7 +2770,7 @@ var BNF;
|
|
|
1201
2770
|
})(BNF || (BNF = {}));
|
|
1202
2771
|
exports.default = BNF;
|
|
1203
2772
|
|
|
1204
|
-
},{"../Parser":
|
|
2773
|
+
},{"../Parser":9}],8:[function(require,module,exports){
|
|
1205
2774
|
"use strict";
|
|
1206
2775
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1207
2776
|
exports.Custom = exports.W3C = exports.BNF = void 0;
|
|
@@ -1212,7 +2781,7 @@ Object.defineProperty(exports, "W3C", { enumerable: true, get: function () { ret
|
|
|
1212
2781
|
var Custom_1 = require("./Custom");
|
|
1213
2782
|
Object.defineProperty(exports, "Custom", { enumerable: true, get: function () { return Custom_1.default; } });
|
|
1214
2783
|
|
|
1215
|
-
},{"./BNF":
|
|
2784
|
+
},{"./BNF":5,"./Custom":6,"./W3CEBNF":7}],9:[function(require,module,exports){
|
|
1216
2785
|
"use strict";
|
|
1217
2786
|
// https://www.ics.uci.edu/~pattis/ICS-33/lectures/ebnf.pdf
|
|
1218
2787
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -1964,7 +3533,7 @@ class Parser {
|
|
|
1964
3533
|
exports.Parser = Parser;
|
|
1965
3534
|
exports.default = Parser;
|
|
1966
3535
|
|
|
1967
|
-
},{"./ParsingError":
|
|
3536
|
+
},{"./ParsingError":10,"./TokenError":12}],10:[function(require,module,exports){
|
|
1968
3537
|
"use strict";
|
|
1969
3538
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1970
3539
|
exports.ParsingError = void 0;
|
|
@@ -1990,7 +3559,7 @@ class ParsingError extends Error {
|
|
|
1990
3559
|
}
|
|
1991
3560
|
exports.ParsingError = ParsingError;
|
|
1992
3561
|
|
|
1993
|
-
},{}],
|
|
3562
|
+
},{}],11:[function(require,module,exports){
|
|
1994
3563
|
"use strict";
|
|
1995
3564
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1996
3565
|
exports.findChildrenByType = findChildrenByType;
|
|
@@ -2001,7 +3570,7 @@ function findChildrenByType(token, type) {
|
|
|
2001
3570
|
return token.children ? token.children.filter(x => x.type == type) : [];
|
|
2002
3571
|
}
|
|
2003
3572
|
|
|
2004
|
-
},{}],
|
|
3573
|
+
},{}],12:[function(require,module,exports){
|
|
2005
3574
|
"use strict";
|
|
2006
3575
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2007
3576
|
exports.TokenError = void 0;
|
|
@@ -2021,7 +3590,7 @@ class TokenError extends Error {
|
|
|
2021
3590
|
}
|
|
2022
3591
|
exports.TokenError = TokenError;
|
|
2023
3592
|
|
|
2024
|
-
},{}],
|
|
3593
|
+
},{}],13:[function(require,module,exports){
|
|
2025
3594
|
"use strict";
|
|
2026
3595
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2027
3596
|
exports.ParsingError = exports.TokenError = exports.Parser = void 0;
|
|
@@ -2033,17 +3602,17 @@ var ParsingError_1 = require("./ParsingError");
|
|
|
2033
3602
|
Object.defineProperty(exports, "ParsingError", { enumerable: true, get: function () { return ParsingError_1.ParsingError; } });
|
|
2034
3603
|
exports.Grammars = require("./Grammars");
|
|
2035
3604
|
|
|
2036
|
-
},{"./Grammars":
|
|
3605
|
+
},{"./Grammars":8,"./Parser":9,"./ParsingError":10,"./TokenError":12}],14:[function(require,module,exports){
|
|
2037
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"]]}]
|
|
2038
|
-
},{}],
|
|
3607
|
+
},{}],15:[function(require,module,exports){
|
|
2039
3608
|
module.exports = require('./RuleTemplater.production.js');
|
|
2040
|
-
},{"./RuleTemplater.production.js":
|
|
2041
|
-
// Note: We
|
|
2042
|
-
|
|
2043
|
-
const RuleParserRules = require('@halleyassist/rule-parser/src/RuleParser.ebnf'),
|
|
2044
|
-
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'),
|
|
2045
3612
|
TemplateFilters = require('./TemplateFilters'),
|
|
2046
|
-
|
|
3613
|
+
RuleParser = require('@halleyassist/rule-parser'),
|
|
3614
|
+
RuleParserRules = RuleParser.ParserRules,
|
|
3615
|
+
{Parser} = require('ebnf');
|
|
2047
3616
|
|
|
2048
3617
|
let ParserCache = null;
|
|
2049
3618
|
|
|
@@ -2109,7 +3678,7 @@ class RuleTemplate {
|
|
|
2109
3678
|
ParserCache = new Parser(ParserRules, {debug: false})
|
|
2110
3679
|
}
|
|
2111
3680
|
|
|
2112
|
-
const ast =
|
|
3681
|
+
const ast = RuleParser.toAst(ruleTemplate.trim(), ParserCache);
|
|
2113
3682
|
return new RuleTemplate(ruleTemplate, ast);
|
|
2114
3683
|
}
|
|
2115
3684
|
|
|
@@ -2445,7 +4014,7 @@ module.exports = RuleTemplate;
|
|
|
2445
4014
|
module.exports.ParserRules = ParserRules;
|
|
2446
4015
|
module.exports.VariableTypes = VariableTypes;
|
|
2447
4016
|
module.exports.TemplateFilters = TemplateFilters;
|
|
2448
|
-
},{"./RuleTemplate.production.ebnf.js":
|
|
4017
|
+
},{"./RuleTemplate.production.ebnf.js":14,"./TemplateFilters":17,"@halleyassist/rule-parser":2,"ebnf":13}],17:[function(require,module,exports){
|
|
2449
4018
|
/*
|
|
2450
4019
|
Template filters are functions that transform variable values.
|
|
2451
4020
|
They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
|
|
@@ -2509,5 +4078,5 @@ const TemplateFilters = {
|
|
|
2509
4078
|
}
|
|
2510
4079
|
|
|
2511
4080
|
module.exports = TemplateFilters;
|
|
2512
|
-
},{}]},{},[
|
|
4081
|
+
},{}]},{},[15])(15)
|
|
2513
4082
|
});
|