diff --git a/docs/source/Rules/Rules.rst b/docs/source/Rules/Rules.rst index 192692923f..7a53b64ccd 100644 --- a/docs/source/Rules/Rules.rst +++ b/docs/source/Rules/Rules.rst @@ -1100,6 +1100,196 @@ Usage: ``{lookup:::}`` ````: The combined string with all lookup values padded to have the same length, f.e. ``"Off.Fan HeatCool"``, where all lookup values are of length 4. The index to retrieve the word ``Off.`` is 0, ``Fan`` (including a space) is index 1, etc. This string should be wrapped in quotes if it contains space or colon ``:`` character(s). +UnEscape and Escape +^^^^^^^^^^^^^^^^^^^ + +(Added: 2026/08/01, only available when String Variables feature is included in the build) + +With ``UnEscape`` any ``\`` escape characters added before ``%``, ``[``, ``]``, ``{``, ``}``, ``(``, ``)`` and/or ``:`` is removed so the intended string value is made available, f.e. when sending to an external destination. + +Usage: ``{unescape:}`` + +As P037, and possibly other plugins, can generate events that include escaped JSON content, this helps to pass that on to other destinations. + +With ``Escape`` in a string containing ``%``, ``[``, ``]``, ``{``, ``}``, ``(``, ``)`` and/or ``:``, any of these characters is prefixed with a ``\``, so it isn't interpreted as possible rules variables or functions. + +Usage: ``{escape:}`` + +For passing received JSON or similar data as an argument to an event function. + +Example: + +.. code-block:: none + + On testEscape Do + LetStr,j1,'\{"method"\:"setDo0","params"\:true\}' + LetStr,j2,`{unescape:[str#j1]}` + LogEntry,'j1=[str#j1], unescape=[str#j2], escape={escape:[str#j2]}' + Endon + +Output: + +.. code-block:: none + + 00:04:25.248 : (86376) Info | ACT : LetStr,j1,'\{"method"\:"setDo0","params"\:true\}' + 00:04:25.257 : (86380) Info | ACT : LetStr,j2,`{"method":"setDo0","params":true}` + 00:04:25.267 : (86216) Info | ACT : LogEntry,'j1=\{"method"\:"setDo0","params"\:true\}, unescape={"method":"setDo0","params":true}, escape=\{"method"\:"setDo0","params"\:true\}' + 00:04:25.272 : > LogEntry,'j1=\{"method"\:"setDo0","params"\:true\}, unescape={"method":"setDo0","params":true}, escape=\{"method"\:"setDo0","params"\:true\}' + 00:04:25.274 : (85772) Info | j1=\{"method"\:"setDo0","params"\:true\}, unescape={"method":"setDo0","params":true}, escape=\{"method"\:"setDo0","params"\:true\} + 00:04:25.276 : > j1=\{"method"\:"setDo0","params"\:true\}, unescape={"method":"setDo0","params":true}, escape=\{"method"\:"setDo0","params"\:true\} + 00:04:25.280 : > OK + + +Parse +^^^^^ + +(Added: 2026/08/02, only available when String Variables feature is included in the build) + +Helps to 'parse out' parts of a string, based on a provided separator, based on the internal ``parseString()`` function. All input for the string processing functions is un-escaped before processing. + +Usage: ``{parse::[]:}`` + +``param``: the nth option in the ``string-to-parse`` provided. n = 1-based + +``separator``: choose the desired separator for parsing out the desired value. Defaults to ``,`` (comma) when left empty. + +Example: + +.. code-block:: none + + On testParse Do + LetStr,p1,"!RFLink#AcuriteV2;ID=feb1;TEMP=80d5;HUM=65;BAT=OK;^^" // an input like received from RFlink + LetStr,RFDevice,{parse:2:#:{parse:1:;:[str#p1]}} // Split on ';', 1st element, then split on '#' > AcuriteV2 + LetStr,RFID,{parse:2:=:{parse:2:;:[str#p1]}} // Split on ';', 2nd element, then split on '=' > feb1 + LetStr,p2,{parse:2:=:{parse:3:;:'[str#p1]'}} // Split on ';', 3rd element, then split on '=' > 80d5 + Let,temp,{and:0x[str#p2]:0x3ff}/10 // Calculate (nb. partially correct) + LetStr,p3,{parse:2:=:{parse:4:;:'[str#p1]'}} // Split on ';', 4th element, then split on '=' > 65 + Let,hum,[str#p3] + LetStr,bat,{parse:2:=:{parse:5:;:'[str#p1]'}} // Split on ';', 5th element, then split on '=' > OK + LogEntry,'p2=[str#p2], p3=[str#p3]' // Check values + LogEntry,'[str#RFDevice]-[str#RFID],{"temp":[var#temp],"hum":%v_hum%,"bat":"[str#bat]"}' + Endon + +Output: + +.. code-block:: none + + 00:09:21.204 : (89356) Info | ACT : LetStr,p1,"!RFLink#AcuriteV2;ID=feb1;TEMP=80d5;HUM=65;BAT=OK;^^" + 00:09:21.212 : (89392) Info | ACT : LetStr,RFDevice,{parse:2:#:!RFLink#AcuriteV2} + 00:09:21.221 : (89436) Info | ACT : LetStr,RFID,{parse:2:=:ID=feb1} + 00:09:21.229 : (89436) Info | ACT : LetStr,p2,{parse:2:=:TEMP=80d5} + 00:09:21.236 : (89444) Info | ACT : Let,temp,213/10 + 00:09:21.244 : (89436) Info | ACT : LetStr,p3,{parse:2:=:HUM=65} + 00:09:21.251 : (89524) Info | ACT : Let,hum,65 + 00:09:21.258 : (89436) Info | ACT : LetStr,bat,{parse:2:=:BAT=OK} + 00:09:21.265 : (89436) Info | ACT : LogEntry,'p2=80d5, p3=65' + 00:09:21.268 : > LogEntry,'p2=80d5, p3=65' + 00:09:21.269 : (89324) Info | p2=80d5, p3=65 + 00:09:21.270 : > p2=80d5, p3=65 + 00:09:21.278 : (89308) Info | ACT : LogEntry,'AcuriteV2-feb1,{"temp":21.3,"hum":65,"bat":"OK"}' + 00:09:21.283 : > LogEntry,'AcuriteV2-feb1,{"temp":21.3,"hum":65,"bat":"OK"}' + 00:09:21.284 : (89092) Info | AcuriteV2-feb1,{"temp":21.3,"hum":65,"bat":"OK"} + 00:09:21.286 : > AcuriteV2-feb1,{"temp":21.3,"hum":65,"bat":"OK"} + 00:09:21.288 : > OK + + +Json +^^^^ + +(Added: 2026/08/02, only available when JSON Parse feature is included in the build) + +Extract a value (numeric, text, bool) from a (valid) JSON object (string). All input for the string processing functions is un-escaped before processing. + +Usage: ``{json::[]:}`` + +``attribute-to-retrieve`` can use this format: ``level.attr[n].subattr``, multi-level supported, arrays[] are 0-based + +``asJson``: Set to 1 (true) to return the value in JSON format (quoted strings, bool = true/false), default: 0 (false), some examples included below. + +Returns 0/1 for a false/true ``bool`` value. + +Returns a comma-separated list for array values without an index, see example. Returns ``unknown`` for unsupported value types, and a comma-separated name/value list for objects. + +Example: + +.. code-block:: none + + On testJson Do + LetStr,j1,'\{"method"\:"setDo0","params"\:true,"values":\[1,2,3\],"args":\{"bla"\:"that","blip"\:false\},"array"\:\[\{"a":1,"b":2\},\{"a":10,"b":20\}\],"sub":\{"array":\[\{"c":1,"d":2\},\{"c":10,"d":20\}\]\}\}' + LetStr,j2,'{json:params::`[str#j1]`}' + LetStr,j3,'{json:values::[str#j1]}' + LetStr,j4,'{json:values[1]::[str#j1]}' + LogEntry,'json: {unescape:[str#j1]}' + LogEntry,'attributes: params=[str#j2], values=[str#j3], values[1]=[str#j4], bla={json:args.bla::[str#j1]},{json:args.bla:1:[str#j1]}, blip={json:args.blip::[str#j1]},{json:args.blip:1:[str#j1]}, array[1].b={json:array[1].b::[str#j1]}, sub.array[0].c={json:sub.array[0].c::[str#j1]}' + LogEntry,'array={json:array::[str#j1]}, sub={json:sub::[str#j1]}, (JSON)sub={json:sub:1:[str#j1]}' + Endon + +The input, as assigned to ``j1``, is like it can be received from MQTT Import plugin. You can also use an incoming JSON payload, ``escape`` that (see above), and parse it. As JSON contains at least some ``{``, ``}`` and ``:`` characters, escaping is *required*, as these string functions use ``:`` as the parameter separator, and ``}`` as the function terminator. + +Formatted JSON as used in the above example: (values marked with ``//`` are retrieved in the example) + +.. code-block:: json + + { + "method": "setDo0", + "params": true, // + "values": [ // + 1, + 2, // + 3 + ], + "args": { + "bla": "that", // + "blip": false // + }, + "array": [ // + { + "a": 1, + "b": 2 + }, + { + "a": 10, // + "b": 20 + } + ], + "sub": { // + "array": [ + { + "c": 1, // + "d": 2 + }, + { + "c": 10, + "d": 20 + } + ] + } + } + + +Output: + +.. code-block:: none + + 00:01:22.983 : (87572) Info | ACT : LetStr,j1,'\{"method"\:"setDo0","params"\:true,"values":\[1,2,3\],"args":\{"bla"\:"that","blip"\:false\},"array"\:\[\{"a":1,"b":2\},\{"a":10,"b":20\}\],"sub":\{"array":\[\{"c":1,"d":2\},\{"c":10,"d":20\}\]\}\}' + 00:01:22.996 : (87712) Info | ACT : LetStr,j2,'1' + 00:01:23.005 : (87572) Info | ACT : LetStr,j3,'1,2,3' + 00:01:23.014 : (87600) Info | ACT : LetStr,j4,'2' + 00:01:23.023 : (87116) Info | ACT : LogEntry,'json: {"method":"setDo0","params":true,"values":[1,2,3],"args":{"bla":"that","blip":false},"array":[{"a":1,"b":2},{"a":10,"b":20}],"sub":{"array":[{"c":1,"d":2},{"c":10,"d":20}]}}' + 00:01:23.034 : > LogEntry,'json: {"method":"setDo0","params":true,"values":[1,2,3],"args":{"bla":"that","blip":false},"array":[{"a":1,"b":2},{"a":10,"b":20}],"sub":{"array":[{"c":1,"d":2},{"c":10,"d":20}]}}' + 00:01:23.036 : (86528) Info | json: {"method":"setDo0","params":true,"values":[1,2,3],"args":{"bla":"that","blip":false},"array":[{"a":1,"b":2},{"a":10,"b":20}],"sub":{"array":[{"c":1,"d":2},{"c":10,"d":20}]}} + 00:01:23.038 : > json: {"method":"setDo0","params":true,"values":[1,2,3],"args":{"bla":"that","blip":false},"array":[{"a":1,"b":2},{"a":10,"b":20}],"sub":{"array":[{"c":1,"d":2},{"c":10,"d":20}]}} + 00:01:23.063 : (87132) Info | ACT : LogEntry,'attributes: params=1, values=1,2,3, values[1]=2, bla=that,"that", blip=0,false, array[1].b=20, sub.array[0].c=1' + 00:01:23.067 : > LogEntry,'attributes: params=1, values=1,2,3, values[1]=2, bla=that,"that", blip=0,false, array[1].b=20, sub.array[0].c=1' + 00:01:23.077 : (86724) Info | attributes: params=1, values=1,2,3, values[1]=2, bla=that,"that", blip=0,false, array[1].b=20, sub.array[0].c=1 + 00:01:23.096 : > attributes: params=1, values=1,2,3, values[1]=2, bla=that,"that", blip=0,false, array[1].b=20, sub.array[0].c=1 + 00:01:23.110 : (86748) Info | ACT : LogEntry,'array=a,1,b,2,a,10,b,20, sub=array,c,1,d,2,c,10,d,20, (JSON)sub={"array",[{"c",1},{"d",2},{"c",10},{"d",20}]}' + 00:01:23.116 : > LogEntry,'array=a,1,b,2,a,10,b,20, sub=array,c,1,d,2,c,10,d,20, (JSON)sub={"array",[{"c",1},{"d",2},{"c",10},{"d",20}]}' + 00:01:23.124 : (86356) Info | array=a,1,b,2,a,10,b,20, sub=array,c,1,d,2,c,10,d,20, (JSON)sub={"array",[{"c",1},{"d",2},{"c",10},{"d",20}]} + 00:01:23.135 : > array=a,1,b,2,a,10,b,20, sub=array,c,1,d,2,c,10,d,20, (JSON)sub={"array",[{"c",1},{"d",2},{"c",10},{"d",20}]} + 00:01:23.139 : > OK + + IndexOf and IndexOf_ci ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/Custom-sample.h b/src/Custom-sample.h index 97e6daad1f..451b79a30b 100644 --- a/src/Custom-sample.h +++ b/src/Custom-sample.h @@ -30,6 +30,8 @@ #define FEATURE_JSON_EVENT 0 // Generates an event with the values of a JSON repsonse of an HTTP call. Keys are stored in json.keys one key per line (e.g.: Body.Data.DAY_ENERGY.Values.1) // #define FEATURE_SD 1 // Enable SD card support // #define FEATURE_DOWNLOAD 1 // Enable downloading a file from an url +// #define FEATURE_JSON_PARSE 1 // Enable extended JSON parsing, disabled by default on ESP8266 +// #define FEATURE_EXTENDED_STRING_FUNCTIONS 1 // Enable extra Rules string functions. Also required to enable FEATURE_JSON_PARSE on ESP8266, as it's disabled by default for ESP8266 #ifdef BUILD_GIT # undef BUILD_GIT diff --git a/src/_P037_MQTTImport.ino b/src/_P037_MQTTImport.ino index d355fe267e..f381df58df 100644 --- a/src/_P037_MQTTImport.ino +++ b/src/_P037_MQTTImport.ino @@ -11,6 +11,7 @@ // This task reads data from the MQTT Import input stream and saves the value /** + * 2026-08-02 tonhuisman: Add better support for JSON parsing, that handles multiple levels and arrays * 2025-08-20 tonhuisman: Generate events with numeric values using the configured decimals setting. * 2025-06-14 tonhuisman: Add support for Custom Value Type per task value * 2025-01-12 tonhuisman: Add support for MQTT AutoDiscovery (not supported for MQTT Import) @@ -58,10 +59,11 @@ # define P037_MAX_QUEUEDEPTH 150 -bool MQTT_unsubscribe_037(struct EventStruct *event); -bool MQTTSubscribe_037(struct EventStruct *event); +bool MQTT_unsubscribe_037(struct EventStruct *event); +bool MQTTSubscribe_037(struct EventStruct *event); # if P037_MAPPING_SUPPORT || P037_JSON_SUPPORT + String P037_getMQTTLastTopicPart(const String& topic) { const int16_t lastSlash = topic.lastIndexOf('/'); @@ -470,7 +472,8 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) // json filter check if (checkJson && P037_data->hasFilters()) { // See if we pass the filters for all json attributes - do { + do + { key = P037_data->iter->key().c_str(); Payload = P037_data->iter->value().as(); # if P037_MAPPING_SUPPORT @@ -521,7 +524,8 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) if (checkJson && P037_data->hasFilters()) { // See if we pass the filters for all json attributes P037_data->iter = P037_data->doc.begin(); - do { + do + { key = P037_data->iter->key().c_str(); Payload = P037_data->iter->value().as(); # if P037_MAPPING_SUPPORT @@ -541,7 +545,8 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) # endif // P037_FILTER_PER_TOPIC # endif // if P037_JSON_SUPPORT { - do { + do + { # if P037_JSON_SUPPORT if (checkJson && (P037_data->iter != P037_data->doc.end())) { @@ -552,6 +557,10 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) if (!jsonAttribute.isEmpty()) { key = jsonAttribute; + # if FEATURE_JSON_PARSE + Payload = getJsonValue(P037_data->root, key, false); + # else + if (key.indexOf('.') > -1) { String part1 = parseStringKeepCase(key, 1, '.'); String part2 = parseStringKeepCase(key, 2, '.'); @@ -559,6 +568,7 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) } else { Payload = P037_data->doc[key].as(); } + # endif // if FEATURE_JSON_PARSE unparsedPayload = Payload; int8_t jIndex = jsonIndex.toInt(); @@ -627,10 +637,10 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) success = false; break; } - numericPayload = false; // No, it isn't numeric - doublePayload = NAN; // Invalid value + numericPayload = false; // No, it isn't numeric + doublePayload = NAN; // Invalid value } - UserVar.setFloat(event->TaskIndex, x, doublePayload); // Save the new value + UserVar.setFloat(event->TaskIndex, x, doublePayload); // Save the new value // Generate event for rules processing - proposed by TridentTD @@ -665,9 +675,9 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) addEscapeCharacters(tmp); // Add escape characters to avoid problems with rules processing if a JSON message is received String RuleEvent = strformat(F("%s#%s=%s"), - getTaskDeviceName(event->TaskIndex).c_str(), - event->String1.c_str(), - wrapWithQuotesIfContainsParameterSeparatorChar(tmp).c_str()); + getTaskDeviceName(event->TaskIndex).c_str(), + event->String1.c_str(), + wrapWithQuotesIfContainsParameterSeparatorChar(tmp).c_str()); P037_addEventToQueue(event, RuleEvent); } @@ -676,9 +686,9 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLog(LOG_LEVEL_INFO, strformat(F("IMPT : [%s#%s] : %s"), - getTaskDeviceName(event->TaskIndex).c_str(), - checkJson ? key.c_str() : getTaskValueName(event->TaskIndex, x).c_str(), - toString(doublePayload, ExtraTaskSettings.TaskDeviceValueDecimals[x]).c_str())); + getTaskDeviceName(event->TaskIndex).c_str(), + checkJson ? key.c_str() : getTaskValueName(event->TaskIndex, x).c_str(), + toString(doublePayload, ExtraTaskSettings.TaskDeviceValueDecimals[x]).c_str())); } # endif // if !defined(P037_LIMIT_BUILD_SIZE) || defined(P037_OVERRIDE) @@ -695,7 +705,7 @@ boolean Plugin_037(uint8_t function, struct EventStruct *event, String& string) } else { String tmp = Payload; addEscapeCharacters(tmp); // Add escape characters to avoid problems with rules processing if a JSON message is received - + RuleEvent += wrapWithQuotesIfContainsParameterSeparatorChar(tmp); } P037_addEventToQueue(event, RuleEvent); diff --git a/src/src/CustomBuild/define_plugin_sets.h b/src/src/CustomBuild/define_plugin_sets.h index 0ddd3935bd..f901eb489d 100644 --- a/src/src/CustomBuild/define_plugin_sets.h +++ b/src/src/CustomBuild/define_plugin_sets.h @@ -4394,6 +4394,27 @@ To create/register a plugin, you have to : #endif //-------------------End of HTTPResponseParser Section---------- +#ifndef FEATURE_JSON_PARSE + #if defined(ESP32) && (defined(USES_P037) || FEATURE_JSON_EVENT) // and other JSON-parsing features + #define FEATURE_JSON_PARSE 1 // By default only on ESP32 + #else + #define FEATURE_JSON_PARSE 0 + #endif // if defined(USES_P037) || FEATURE_JSON_EVENT +#endif // ifndef FEATURE_JSON_PARSE + +#ifndef FEATURE_EXTENDED_STRING_FUNCTIONS + #ifdef ESP32 + #define FEATURE_EXTENDED_STRING_FUNCTIONS 1 + #endif // ifdef ESP32 + #ifdef ESP8266 + #define FEATURE_EXTENDED_STRING_FUNCTIONS 0 + #endif // ifdef ESP8266 +#endif // ifndef FEATURE_EXTENDED_STRING_FUNCTIONS +#if defined(ESP8266) && FEATURE_JSON_PARSE && !FEATURE_EXTENDED_STRING_FUNCTIONS + #undef FEATURE_JSON_PARSE + #define FEATURE_JSON_PARSE 0 +#endif // if FEATURE_JSON_PARSE && !FEATURE_EXTENDED_STRING_FUNCTIONS + #if !(defined(SOC_DAC_SUPPORTED) && SOC_DAC_SUPPORTED) #ifdef USES_P152 #undef USES_P152 @@ -4414,7 +4435,7 @@ To create/register a plugin, you have to : #if FEATURE_WIFI #ifndef USES_NW001 #define USES_NW001 - #endif + #endif #ifndef USES_NW002 #define USES_NW002 #endif diff --git a/src/src/ESPEasyCore/ESPEasyRules.cpp b/src/src/ESPEasyCore/ESPEasyRules.cpp index f7197b7b11..176dac9016 100644 --- a/src/src/ESPEasyCore/ESPEasyRules.cpp +++ b/src/src/ESPEasyCore/ESPEasyRules.cpp @@ -29,6 +29,11 @@ #include #include +#if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS +#include +#include "../Helpers/JSON_helper.h" +#endif // if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS + #ifdef WEBSERVER_NEW_RULES String EventToFileName(const String& eventName) { int size = eventName.length(); @@ -466,7 +471,14 @@ bool parse_math_functions(const String& cmd_s_lower, const String& arg1, const S const char string_commands[] PROGMEM = "substring|indexof|indexof_ci|equals|equals_ci|timetomin|timetosec|strtol|tobin|tohex|ord|urlencode" #if FEATURE_STRING_VARIABLES "|lookup" + #if FEATURE_EXTENDED_STRING_FUNCTIONS + "|unescape|escape" + "|parse" + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS #endif // if FEATURE_STRING_VARIABLES + #if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS + "|json" + #endif // if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS ; enum class string_commands_e { substring, @@ -483,19 +495,122 @@ enum class string_commands_e { urlencode, #if FEATURE_STRING_VARIABLES lookup, + #if FEATURE_EXTENDED_STRING_FUNCTIONS + unescape, + escape, + parse, + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS #endif // if FEATURE_STRING_VARIABLES + #if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS + json, + #endif // if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS }; + #if FEATURE_EXTENDED_STRING_FUNCTIONS +void hideEscaped(String &line, bool &mustReplaceEscapedBracket, bool &mustReplaceEscapedCurlyBracket, bool &mustReplaceEscapedColon) { + String MaskEscapedBracket; + + if (hasEscapedCharacter(line,'(') || hasEscapedCharacter(line,')')) { + // replace the \( and \) with other characters to mask the escaped brackets so we can continue parsing. + // We have to unmask then after we're finished. + MaskEscapedBracket = static_cast(0x11); // ASCII 0x11 = Device control 1 + line.replace(F("\\("), MaskEscapedBracket); + MaskEscapedBracket = static_cast(0x12); // ASCII 0x12 = Device control 2 + line.replace(F("\\)"), MaskEscapedBracket); + mustReplaceEscapedBracket = true; + } + + if (hasEscapedCharacter(line,'{') || hasEscapedCharacter(line,'}')) { + // replace the \{ and \} with other characters to mask the escaped curly brackets so we can continue parsing. + // We have to unmask then after we're finished. + MaskEscapedBracket = static_cast(0x13); // ASCII 0x13 = Device control 3 + line.replace(F("\\{"), MaskEscapedBracket); + MaskEscapedBracket = static_cast(0x14); // ASCII 0x14 = Device control 4 + line.replace(F("\\}"), MaskEscapedBracket); + mustReplaceEscapedCurlyBracket = true; + } + + if (hasEscapedCharacter(line, ':')) { + // replace the \: with another character to mask the escaped colon so we can continue parsing. + // We have to unmask then after we're finished. + MaskEscapedBracket = static_cast(0x15); // ASCII 0x15 = NAK + line.replace(F("\\:"), MaskEscapedBracket); + mustReplaceEscapedColon = true; + } +} + +void hideMasked(String &line, bool mustReplaceMaskedChars) { + if (mustReplaceMaskedChars) { + line.replace('{', static_cast(0x02)); + line.replace('}', static_cast(0x03)); + } +} + +void restoreMaskedAndEscaped(String &line, bool mustReplaceMaskedChars, bool mustReplaceEscapedBracket, bool mustReplaceEscapedCurlyBracket, bool mustReplaceEscapedColon) { + String MaskEscapedBracket; + + if (mustReplaceMaskedChars) { + // We now have to check if we did mask some parts and unmask them. + // Let's hope we don't mess up any Unicode here. + line.replace(static_cast(0x02), '{'); + line.replace(static_cast(0x03), '}'); + } + + if (mustReplaceEscapedBracket) { + // We now have to check if we did mask some escaped bracket and unmask them. + // Let's hope we don't mess up any Unicode here. + MaskEscapedBracket = static_cast(0x11); // ASCII 0x11 = Device control 1 + line.replace(MaskEscapedBracket, F("\\(")); + MaskEscapedBracket = static_cast(0x12); // ASCII 0x12 = Device control 2 + line.replace(MaskEscapedBracket, F("\\)")); + } + + if (mustReplaceEscapedCurlyBracket) { + // We now have to check if we did mask some escaped curly bracket and unmask them. + // Let's hope we don't mess up any Unicode here. + MaskEscapedBracket = static_cast(0x13); // ASCII 0x13 = Device control 3 + line.replace(MaskEscapedBracket, F("\\{")); + MaskEscapedBracket = static_cast(0x14); // ASCII 0x14 = Device control 4 + line.replace(MaskEscapedBracket, F("\\}")); + } + + if (mustReplaceEscapedColon) { + // We now have to check if we did mask some escaped colon and unmask them. + // Let's hope we don't mess up any Unicode here. + MaskEscapedBracket = static_cast(0x15); // ASCII 0x15 = NAK + line.replace(MaskEscapedBracket, F("\\:")); + } +} +#endif // if FEATURE_EXTENDED_STRING_FUNCTIONS + void parse_string_commands(String& line) { unsigned int startIndex = 0; int closingIndex; + #if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS + DynamicJsonDocument*root = nullptr; + uint16_t lastJsonMessageLength = 512; + + // Cleanup lambda to deallocate resources + auto cleanupJSON = [&root]() { + if (root != nullptr) { + root->clear(); + delete root; + root = nullptr; + } + }; + #endif // if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS bool mustReplaceMaskedChars = false; bool mustReplaceEscapedBracket = false; bool mustReplaceEscapedCurlyBracket = false; String MaskEscapedBracket; + + #if FEATURE_EXTENDED_STRING_FUNCTIONS + bool mustReplaceEscapedColon = false; + hideEscaped(line, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); + #else // if FEATURE_EXTENDED_STRING_FUNCTIONS if (hasEscapedCharacter(line,'(') || hasEscapedCharacter(line,')')) { // replace the \( and \) with other characters to mask the escaped brackets so we can continue parsing. // We have to unmask then after we're finished. @@ -505,6 +620,7 @@ void parse_string_commands(String& line) { line.replace(F("\\)"), MaskEscapedBracket); mustReplaceEscapedBracket = true; } + if (hasEscapedCharacter(line,'{') || hasEscapedCharacter(line,'}')) { // replace the \{ and \} with other characters to mask the escaped curly brackets so we can continue parsing. // We have to unmask then after we're finished. @@ -514,6 +630,7 @@ void parse_string_commands(String& line) { line.replace(F("\\}"), MaskEscapedBracket); mustReplaceEscapedCurlyBracket = true; } + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS while (get_next_inner_bracket(line, startIndex, closingIndex, '}')) { // Command without opening and closing brackets. @@ -569,6 +686,24 @@ void parse_string_commands(String& line) { replacement = arg3.substring(startpos * endpos, (startpos + 1) * endpos); } break; + #if FEATURE_EXTENDED_STRING_FUNCTIONS + case string_commands_e::unescape: + case string_commands_e::escape: + replacement = parseStringToEndKeepCaseNoTrim(fullCommand, 2, ':'); + // Undo escape masks + restoreMaskedAndEscaped(replacement, mustReplaceMaskedChars, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); + + stripEscapeCharacters(replacement); // Always strip first to avoid double-escaping + + if (string_commands_e::escape == command) { + addEscapeCharacters(replacement); + } + // Redo escape masks and masked {} characters + hideEscaped(replacement, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); + hideMasked(replacement, mustReplaceMaskedChars); + + break; + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS #endif // if FEATURE_STRING_VARIABLES case string_commands_e::indexof: case string_commands_e::indexof_ci: @@ -666,6 +801,75 @@ void parse_string_commands(String& line) { replacement = URLEncode(arg1); } break; + #if FEATURE_STRING_VARIABLES && FEATURE_EXTENDED_STRING_FUNCTIONS + case string_commands_e::parse: + // parse: a function to retrieve nth param (1-based), with optional separator, default: comma + // {parse::[]:} + if (arg1valid) { + String de_escaped = parseStringToEndKeepCaseNoTrim(fullCommand, 4, ':'); + restoreMaskedAndEscaped(de_escaped, mustReplaceMaskedChars, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); + stripEscapeCharacters(de_escaped); + + replacement = parseStringKeepCase(de_escaped, startpos, arg2.isEmpty() ? ',' : arg2[0]); + + addEscapeCharacters(replacement); + hideEscaped(replacement, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); + hideMasked(replacement, mustReplaceMaskedChars); // re-apply + } + break; + #endif // if FEATURE_STRING_VARIABLES && FEATURE_EXTENDED_STRING_FUNCTIONS + #if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS + case string_commands_e::json: + // json: get a value from a (valid) JSON string + // {json::[]:} + // attribute-to-retrieve can use: level.attr[n].subattr, multi-level supported, arrays[] are 0-based + // asJson: 1 (true), return a JSON compatible value, useful when retrieving a part of the input for passing on, default: 0 (false) + { + String jsonInput = parseStringToEndKeepCase(fullCommand, 4, ':'); + if (arg1.isEmpty() || jsonInput.isEmpty()) { + break; + } + restoreMaskedAndEscaped(jsonInput, mustReplaceMaskedChars, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); + stripEscapeCharacters(jsonInput); + String arg1_ = arg1; + stripEscapeCharacters(arg1_); + + if ((nullptr != root) && (jsonInput.length() * 2.5 > lastJsonMessageLength)) { + cleanupJSON(); + } + + // Resize lastJsonMessageLength if needed + if (jsonInput.length() * 2 > lastJsonMessageLength) { + lastJsonMessageLength = jsonInput.length() * 2; + } + + // Allocate memory for root if needed + if (nullptr == root) { + // Try to allocate in PSRAM or 2nd heap if possible + constexpr unsigned size = sizeof(DynamicJsonDocument); + void *ptr = special_calloc(1, size); + if (ptr) { + root = new (ptr) DynamicJsonDocument(lastJsonMessageLength); // Dynamic allocation + } + } + + if (nullptr != root) { + // if (loglevelActiveFor(LOG_LEVEL_INFO)) { // for development debugging + // addLog(LOG_LEVEL_INFO, strformat(F("parse json: %s"), jsonInput.c_str())); + // } + // Parse the JSON + DeserializationError error = deserializeJson(*root, jsonInput); + + if (!error) { + // if (loglevelActiveFor(LOG_LEVEL_INFO)) { // for development debugging + // addLog(LOG_LEVEL_INFO, strformat(F("parse json for %s"), arg1.c_str())); + // } + replacement = getJsonValue(root, arg1_, arg2valid && 1 == endpos); + } + } + break; + } + #endif // if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS } } } @@ -676,9 +880,13 @@ void parse_string_commands(String& line) { // We have to unmask then after we're finished. // See: https://github.com/letscontrolit/ESPEasy/issues/2932#issuecomment-596139096 replacement = line.substring(startIndex, closingIndex + 1); - replacement.replace('{', static_cast(0x02)); - replacement.replace('}', static_cast(0x03)); mustReplaceMaskedChars = true; + #if FEATURE_EXTENDED_STRING_FUNCTIONS + hideMasked(replacement, mustReplaceMaskedChars); // apply + #else // if FEATURE_EXTENDED_STRING_FUNCTIONS + line.replace('{', static_cast(0x02)); + line.replace('}', static_cast(0x03)); + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS } // Replace the full command including opening and closing brackets. @@ -692,6 +900,16 @@ void parse_string_commands(String& line) { } } + #if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS + + if (nullptr != root) { + cleanupJSON(); + } + #endif // if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS + + #if FEATURE_EXTENDED_STRING_FUNCTIONS + restoreMaskedAndEscaped(line, mustReplaceMaskedChars, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); + #else // if FEATURE_EXTENDED_STRING_FUNCTIONS if (mustReplaceMaskedChars) { // We now have to check if we did mask some parts and unmask them. // Let's hope we don't mess up any Unicode here. @@ -716,6 +934,7 @@ void parse_string_commands(String& line) { MaskEscapedBracket = static_cast(0x14); // ASCII 0x14 = Device control 4 line.replace(MaskEscapedBracket, F("\\}")); } + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS } void substitute_eventvalue(String& line, const String& event) { diff --git a/src/src/Helpers/HTTPResponseParser.cpp b/src/src/Helpers/HTTPResponseParser.cpp index 8dfb6d739f..30cc93215f 100644 --- a/src/src/Helpers/HTTPResponseParser.cpp +++ b/src/src/Helpers/HTTPResponseParser.cpp @@ -10,13 +10,13 @@ # if FEATURE_JSON_EVENT # include "../Helpers/ESPEasy_Storage.h" +# include "../Helpers/JSON_helper.h" # include "../WebServer/LoadFromFS.h" # include # endif // if FEATURE_JSON_EVENT - void eventFromResponse(const String& host, const int& httpCode, const String& uri, ESPEasy_HTTPClient& http, const int& parseJson) { if ((httpCode == 200)) { if (parseJson == -1) { @@ -236,10 +236,13 @@ void eventFromResponse(const String& host, const int& httpCode, const String& ur // Allocate memory for root if needed if (root == nullptr) { - # ifdef USE_SECOND_HEAP - HeapSelectIram ephemeral; - # endif // ifdef USE_SECOND_HEAP - root = new (std::nothrow) DynamicJsonDocument(lastJsonMessageLength); + // Try to allocate in PSRAM or 2nd heap if possible + constexpr unsigned size = sizeof(DynamicJsonDocument); + void *ptr = special_calloc(1, size); + + if (ptr) { + root = new (ptr) DynamicJsonDocument(lastJsonMessageLength); // Dynamic allocation + } } if (root != nullptr) { @@ -319,6 +322,15 @@ void readAndProcessJsonKeys(DynamicJsonDocument *root, int numJson) { } } + # if FEATURE_JSON_PARSE + const String val = getJsonValue(root, key, false); // Return arrays and objects as csv, _not_ JSON formatted + + if (!val.isEmpty()) { + successfullyProcessedCount++; + csvOutput += val; + csvOutput += ','; + } + # else // if FEATURE_JSON_PARSE // Process the key and navigate the JSON JsonVariant value = *root; size_t start = 0, end; @@ -414,8 +426,11 @@ void readAndProcessJsonKeys(DynamicJsonDocument *root, int numJson) { } } else { csvOutput += F("null"); // Indicate missing value + csvOutput += val; + csvOutput += ','; } csvOutput += ','; + # endif // if FEATURE_JSON_PARSE } keyFile.close(); @@ -429,11 +444,11 @@ void readAndProcessJsonKeys(DynamicJsonDocument *root, int numJson) { // Log the results if (loglevelActiveFor(LOG_LEVEL_INFO)) { addLog(LOG_LEVEL_INFO, strformat(F("Successfully processed %d out of %d keys"), successfullyProcessedCount, keyCount)); - eventQueue.addMove(strformat(F("JsonReply%s%s=%s"), - numJson != 0 ? "#" : "", - toStringNoZero(numJson).c_str(), - csvOutput.c_str())); } + eventQueue.addMove(strformat(F("JsonReply%s%s=%s"), + numJson != 0 ? "#" : "", + toStringNoZero(numJson).c_str(), + csvOutput.c_str())); } } diff --git a/src/src/Helpers/JSON_helper.cpp b/src/src/Helpers/JSON_helper.cpp new file mode 100644 index 0000000000..3ca188aadb --- /dev/null +++ b/src/src/Helpers/JSON_helper.cpp @@ -0,0 +1,173 @@ +#include "../Helpers/JSON_helper.h" + +/** Changelog: + * 2026-08-04 tonhuisman: Parse JsonObject as name,value pairs, + * Add recursive processing to retrieve the value + * Add asJson parameter to get JSON-compliant values (quoted strings, bool as true/false, + * arrays with [] and objects with {}) + * 2026-08-01 tonhuisman: Extracted JSON value parser from HTTPResponseParser by @chromoxdor + * Support bool type by returning 1/0 for true/false + */ + +#if FEATURE_JSON_PARSE +# include "../Helpers/StringConverter.h" + +// Private for now +String getJsonValue(JsonVariant element, + bool asJson); + +String getJsonValue(DynamicJsonDocument *root, + String key, + bool asJson) { + // Process the key and navigate the JSON + JsonVariant value = *root; + size_t start = 0; + size_t end; + String result; + + while ((end = key.indexOf('.', start)) != -1) { + const String part = key.substring(start, end); + start = end + 1; + + // Look for an array e.g., "result[0]" → object "result", index 0 + const int bracketStart = part.indexOf('['); + + if (bracketStart != -1) { + const String objectName = part.substring(0, bracketStart); + const String indexStr = part.substring(bracketStart + 1, part.indexOf(']', bracketStart)); + + if (objectName.length() > 0) { + value = value[objectName]; // Access the object + } + + if (value.is()) { + value = value[indexStr.toInt()]; + } else { + value = value[indexStr]; // fallback if not actually array + } + } else { + // Normal object access without array + value = value[part]; + } + + if (value.isNull()) { + break; // Key path is invalid + } + } + + if (!value.isNull()) { + const String lastPart = key.substring(start); + const int bracketStart = lastPart.indexOf('['); + + if (bracketStart != -1) { + const String objectName = lastPart.substring(0, bracketStart); + const String indexStr = lastPart.substring(bracketStart + 1, lastPart.indexOf(']', bracketStart)); + + if (objectName.length() > 0) { + value = value[objectName]; + } + + if (value.is()) { + value = value[indexStr.toInt()]; + } else { + value = value[indexStr]; + } + } else { + value = value[lastPart]; + } + } + + // Append the value to the CSV string if it exists + if (!value.isNull()) { + result += getJsonValue(value, asJson); + } else { + result += F("null"); + } + + return result; +} + +String getJsonValue(JsonVariant value, bool asJson) { + # if FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE + const int nr_decimals = ESPEASY_DOUBLE_NR_DECIMALS; + # else // ifdef FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE + const int nr_decimals = ESPEASY_FLOAT_NR_DECIMALS; + # endif // ifdef FEATURE_USE_DOUBLE_AS_ESPEASY_RULES_FLOAT_TYPE + + String result; + + if (value.is()) { + result += String(value.as()); + } else if (value.is()) { + result += doubleToString(value.as(), nr_decimals, true); + } else if (value.is()) { + if (asJson) { + result += wrap_String(value.as(), '\"'); // JSON quotes are always " + } else { + result += String(value.as()); + } + } else if (value.is()) { + if (asJson) { + result += value.as() ? F("true") : F("false"); + } else { + result += String(value.as() ? 1 : 0); + } + } else if (value.is()) { + // If the value is an array, iterate over its elements + JsonArray array = value.as(); + size_t arraySize = array.size(); // Get the total number of elements in the array + size_t currentIndex = 0; // Track the current index + + if (asJson) { + result += '['; + } + + for (JsonVariant element : array) { + result += getJsonValue(element, asJson); + + // Add a comma unless it's the last element + ++currentIndex; + + if (currentIndex < arraySize) { + result += ','; + } + } + + if (asJson) { + result += ']'; + } + } else if (value.is()) { + // if the value is a JSON Object, iterate over the attributes and return , pair(s), recursive + auto it = value.as().begin(); + size_t objectSize = value.as().size(); + size_t currentIndex = 0; + + while (it != value.as().end()) { + if (asJson) { + result += '{'; + result += wrap_String(it->key().c_str(), '\"'); // JSON quotes are always " + } else { + result += String(it->key().c_str()); + } + result += ','; + result += getJsonValue(it->value(), asJson); + + if (asJson) { + result += '}'; + } + ++currentIndex; + + if (currentIndex < objectSize) { + result += ','; + } + ++it; + } + + } else { + result += F("unknown"); + } + + return result; +} + +#endif // if FEATURE_JSON_PARSE diff --git a/src/src/Helpers/JSON_helper.h b/src/src/Helpers/JSON_helper.h new file mode 100644 index 0000000000..a6b8e6520b --- /dev/null +++ b/src/src/Helpers/JSON_helper.h @@ -0,0 +1,12 @@ +#pragma once + +/** See JSON_helper.cpp for changelog */ +#if FEATURE_JSON_PARSE +# include +# include "../Helpers/StringConverter_Numerical.h" + +String getJsonValue(DynamicJsonDocument *root, + String key, + bool asJson); // Format objects and arrays as JSON with {} and [] wrappers + +#endif // if FEATURE_JSON_PARSE diff --git a/src/src/Helpers/StringParser.cpp b/src/src/Helpers/StringParser.cpp index ccf985cf7d..533870176c 100644 --- a/src/src/Helpers/StringParser.cpp +++ b/src/src/Helpers/StringParser.cpp @@ -37,7 +37,11 @@ bool hasEscapedCharacter(String& str, const char EscapeChar) void stripEscapeCharacters(String& str) { - const char braces[] = { '%', '[', ']', '{', '}', '(', ')' }; + const char braces[] = { '%', '[', ']', '{', '}', '(', ')' + #if FEATURE_EXTENDED_STRING_FUNCTIONS + , ':' + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS + }; constexpr uint8_t nrbraces = NR_ELEMENTS(braces); for (uint8_t i = 0; i < nrbraces; ++i) { @@ -48,7 +52,11 @@ void stripEscapeCharacters(String& str) void addEscapeCharacters(String& str) { - const char braces[] = { '%', '[', ']', '{', '}', '(', ')' }; + const char braces[] = { '%', '[', ']', '{', '}', '(', ')' + #if FEATURE_EXTENDED_STRING_FUNCTIONS + , ':' + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS + }; constexpr uint8_t nrbraces = NR_ELEMENTS(braces); for (uint8_t i = 0; i < nrbraces; ++i) { diff --git a/src/src/Helpers/StringParser.h b/src/src/Helpers/StringParser.h index 25406e881f..807f325e78 100644 --- a/src/src/Helpers/StringParser.h +++ b/src/src/Helpers/StringParser.h @@ -12,9 +12,11 @@ bool hasEscapedCharacter(String& str, const char EscapeChar); // Cleans str from escaped characters -// So far \\% \\[ \\] \\{ \\} \\( and \\) are used (all with single backslash!) +// So far \\% \\[ \\] \\{ \\} \\( \\) and \\: are used (all with single backslash!) void stripEscapeCharacters(String& str); +// Applies str with escaped characters +// So far % [ ] { } ( ) and : are escaped (all with single backslash!) void addEscapeCharacters(String& str); #if FEATURE_STRING_VARIABLES diff --git a/src/src/PluginStructs/P037_data_struct.h b/src/src/PluginStructs/P037_data_struct.h index 974abca08f..5dc10c6d84 100644 --- a/src/src/PluginStructs/P037_data_struct.h +++ b/src/src/PluginStructs/P037_data_struct.h @@ -11,8 +11,6 @@ # include "../Helpers/StringParser.h" # include "../Globals/MQTT.h" -# include - // # define PLUGIN_037_DEBUG // Additional debugging information # if (defined(PLUGIN_BUILD_CUSTOM) || defined(PLUGIN_BUILD_MAX_ESP32) || defined(ESP32) \ @@ -84,6 +82,12 @@ # endif // ifndef P037_FILTER_PER_TOPIC # endif // if P037_FILTER_SUPPORT && P037_MAX_FILTERS == VARS_PER_TASK +#if P037_JSON_SUPPORT +# include "../Helpers/JSON_helper.h" + +# include +#endif // if P037_JSON_SUPPORT + # define P037_ARRAY_SIZE (P037_MAX_MAPPINGS + P037_MAX_FILTERS) // Storage layout definitions # define P037_START_MAPPINGS 0 # define P037_END_MAPPINGS (P037_MAX_MAPPINGS - 1) @@ -197,9 +201,13 @@ struct P037_data_struct : public PluginTaskData_base String _filterListItem; # endif // if P037_FILTER_SUPPORT # if P037_JSON_SUPPORT - DynamicJsonDocument *root = nullptr; - uint16_t lastJsonMessageLength = 512; + uint16_t lastJsonMessageLength = 512; + +public: + + DynamicJsonDocument *root = nullptr; # endif // if P037_JSON_SUPPORT + }; #endif // ifdef USED_P037 diff --git a/static/espeasy.js b/static/espeasy.js index 3bf7a53d44..edd03f9130 100644 --- a/static/espeasy.js +++ b/static/espeasy.js @@ -174,7 +174,7 @@ var AnythingElse = [ "%dns%", "%dns1%", "%dns2%", "%flash_freq%", "%flash_size%", "%flash_chip_vendor%", "%flash_chip_model%", "%fs_free%", "%fs_size%", "%cpu_id%", "%cpu_freq%", "%cpu_model%", "%cpu_rev%", "%cpu_cores%", "%board_name%", "%inttemp%", "%islimited_build%", "%isvar_double%", //String Functions - "substring", "lookup", "indexOf", "indexOf_ci", "equals", "equals_ci", "strtol", "timeToMin", "timeToSec", + "substring", "lookup", "indexOf", "indexOf_ci", "equals", "equals_ci", "strtol", "timeToMin", "timeToSec", "unescape", "escape", "parse", "json", //Ethernet "%ethwifimode%", "%ethconnected%", "%ethduplex%", "%ethspeed%", "%ethstate%", "%ethspeedstate%", //Standard Conversions diff --git a/static/espeasy.min.js b/static/espeasy.min.js index 63a16731ae..398428c45c 100644 --- a/static/espeasy.min.js +++ b/static/espeasy.min.js @@ -1 +1 @@ -var commonAtoms=["And","Or"],commonKeywords=["If","Else","Elseif","Endif"],commonCommands=["AccessInfo","Background","Build","ClearAccessBlock","ClearRTCam","Config","ControllerDisable","ControllerEnable","DateTime","Debug","Dec","DeepSleep","DisablePriorityTask","DNS","DST","EraseSDKWiFi","ExecuteRules","FactoryReset","Gateway","I2Cscanner","Inc","IP","Latitude","Let","LetStr","Load","LogEntry","LogPortStatus","Longitude","LoopTimerSet","LoopTimerSet_ms","LoopTimerSetAndRun","LoopTimerSetAndRun_ms","MemInfo","MemInfoDetail","Name","NetworkDisable","NetworkEnable","Password","PostToHTTP","PostToHTTPS","Publish","PublishR","PutToHTTP","PutToHTTPS","Reboot","Save","SendTo","SendToHTTP","SendToHTTPS","SendToUDP","SendToUDPMix","Settings","Subnet","Subscribe","TaskClear","TaskClearAll","TaskDisable","TaskEnable","TaskRun","TaskValueSet","TaskValueSetAndRun","TaskValueSetDerived","TaskValueSetPresentation","TimerPause","TimerResume","TimerSet","TimerSet_ms","TimeZone","UdpPort","UdpTest","Unit","UseNTP","WdConfig","WdRead","WiFi","WiFiAllowAP","WiFiAPMode","WiFiConnect","WiFiDisconnect","WiFiKey","WiFiKey2","WiFiMode","WiFiScan","WiFiSSID","WiFiSSID2","WiFiSTAMode","Event","AsyncEvent","GPIO","GPIOToggle","LongPulse","LongPulse_mS","Monitor","Pulse","PWM","Servo","Status","Tone","RTTTL","UnMonitor","Provision","Provision,Config","Provision,Security","Provision,Notification","Provision,Provision","Provision,Rules","Provision,CustomCdnUrl","Provision,Firmware"],commonEvents=["Clock#Time","JsonReply","JsonReply#","Login#Failed","MQTT#Connected","MQTT#Disconnected","MQTTimport#Connected","MQTTimport#Disconnected","OpenMeteo#current","OpenMeteo#daily","OpenMeteo#hourly","Rules#Timer","System#Boot","System#BootMode","System#Sleep","System#Wake","TaskExit#","TaskInit#","ThingspeakReply","Time#Initialized","Time#Set","WiFi#APmodeDisabled","WiFi#APmodeEnabled","WiFi#ChangedAccesspoint","WiFi#ChangedWiFichannel","WiFi#Connected","WiFi#Disconnected"],commonPlugins=["ResetPulseCounter","SetPulseCounterTotal","LogPulseStatistic","analogout","MCPGPIO","MCPGPIOToggle","MCPLongPulse","MCPLongPulse_ms","MCPPulse","Status,MCP","Monitor,MCP","MonitorRange,MCP","UnMonitorRange,MCP","UnMonitor,MCP","MCPGPIORange","MCPGPIOPattern","MCPMode","MCPModeRange","ExtGpio","ExtPwm","ExtPulse","ExtLongPulse","Status,EXT,","LCDCmd","LCD","PCFGPIO","PCFGPIOToggle","PCFLongPulse","PCFLongPulse_ms","PCFPulse","Status,PCF","Monitor,PCF","MonitorRange,PCF","UnMonitorRange,PCF","UnMonitor,PCF","PCFGPIORange","PCFGPIOpattern","PCFMode","PCFmodeRange","SerialSend","SerialSendMix","Ser2NetClientSend","SerialSend_test","pcapwm","pcafrq","mode2","OLED","OLEDCMD","OLEDCMD,on","OLEDCMD,off","OLEDCMD,clear","IRSEND","IRSENDAC","OledFramedCmd","OledFramedCmd,Display","OledFramedCmd,low","OledFramedCmd,med","OledFramedCmd,high","OledFramedCmd,Frame","OledFramedCmd,linecount","OledFramedCmd,leftalign","OledFramedCmd,align","OledFramedCmd,userDef1","OledFramedCmd,userDef2","NeoPixel","NeoPixelAll","NeoPixelLine","NeoPixelHSV","NeoPixelAllHSV","NeoPixelLineHSV","NeoPixelBright","MotorShieldCmd,DCMotor","MotorShieldCmd,Stepper","MHZCalibrateZero","MHZReset","MHZABCEnable","MHZABCDisable","Sensair_SetRelay","PMSX003","PMSX003,Wake","PMSX003,Sleep","PMSX003,Reset","encwrite","Play","Vol","Eq","Mode","Repeat","tareChanA","tareChanB","7dn","7dst","7dsd","7dtext","7ddt","7dt","7dtfont","7dtbin","7don","7doff","7output","HLWCalibrate","HLWReset","csecalibrate","cseclearpulses","csereset","WemosMotorShieldCMD","LolinMotorShieldCMD","GPS","GPS,Sleep","GPS,Wake","GPS#GotFix","GPS#LostFix","GPS#Travelled","homieValueSet","SerialProxy_Write","SerialProxy_WriteMix","SerialProxy_Test","HeatPumpir","MitsubishiHP","MitsubishiHP,temperature","MitsubishiHP,power","MitsubishiHP,mode","MitsubishiHP,fan","MitsubishiHP,vane","MitsubishiHP,widevane","Culreader_Write","Touch","Touch,Rot","Touch,Flip","Touch,Enable","Touch,Disable","Touch,On","Touch,Off","Touch,Toggle","Touch,Setgrp","Touch,Incgrp","Touch,Decgrp","Touch,Incpage","Touch,Decpage","Touch,Updatebutton","WakeOnLan","DotMatrix","DotMatrix,clear","DotMatrix,update","DotMatrix,size","DotMatrix,txt","DotMatrix,settxt","DotMatrix,content","DotMatrix,alignment","DotMatrix,anim.in","DotMatrix,anim.out","DotMatrix,speed","DotMatrix,pause","DotMatrix,font","DotMatrix,layout","DotMatrix,inverted","DotMatrix,specialeffect","DotMatrix,offset","DotMatrix,brightness","DotMatrix,repeat","DotMatrix,setbar","DotMatrix,bar","Thermo","Thermo,Up","Thermo,Down","Thermo,Mode","Thermo,ModeBtn","Thermo,Setpoint","Max1704xclearalert","scdgetabc","scdgetalt","scdgettmp","scdsetcalibration","scdsetfrc","scdgetinterval","multirelay","multirelay,on","multirelay,off","multirelay,set","multirelay,get","multirelay,loop","ShiftOut","ShiftOut,Set","ShiftOut,SetNoUpdate","ShiftOut,Update","ShiftOut,SetAll","ShiftOut,SetAllNoUpdate","ShiftOut,SetAllLow","ShiftOut,SetAllHigh","ShiftOut,SetChipCount","ShiftOut,SetHexBin","cdmrst","nfx","nfx,off","nfx,on","nfx,dim","nfx,line,","nfx,hsvline,","nfx,one,","nfx,hsvone,","nfx,all,","nfx,rgb,","nfx,fade,","nfx,hsv,","nfx,colorfade,","nfx,rainbow","nfx,kitt,","nfx,comet,","nfx,theatre,","nfx,scan,","nfx,dualscan,","nfx,twinkle,","nfx,twinklefade,","nfx,sparkle,","nfx,wipe,","nfx,dualwipe","nfx,fire","nfx,fireflicker","nfx,faketv","nfx,simpleclock","nfx,stop","nfx,statusrequest","nfx,fadetime,","nfx,fadedelay,","nfx,speed,","nfx,count,","nfx,bgcolor","ShiftIn","ShiftIn,PinEvent","ShiftIn,ChipEvent","ShiftIn,SetChipCount","ShiftIn,SampleFrequency","ShiftIn,EventPerPin","scd4x","scd4x,storesettings","scd4x,facoryreset","scd4x,selftest","scd4x,setfrc,","axp","axp,ldo2","axp,ldo3","axp,ldoio","axp,gpio0","axp,gpio1","axp,gpio2","axp,gpio3","axp,gpio4","axp,dcdc2","axp,dcdc3","axp,ldo2map","axp,ldo3map","axp,ldoiomap","axp,dcdc2map","axp,dcdc3map","axp,ldo2perc","axp,ldo3perc","axp,ldoioperc","axp,dcdc2perc","axp,dcdc3perc","I2CEncoder","I2CEncoder,bright","I2CEncoder,led1","I2CEncoder,led2","I2CEncoder,gain","I2CEncoder,set","cachereader","cachereader,readpos","cachereader,sendtaskinfo","cachereader,flush","tm1621","tm1621,write,","tm1621,writerow,","tm1621,voltamp,","tm1621,energy,","tm1621,celcius,","tm1621,fahrenheit,","tm1621,humidity,","tm1621,raw,","dac","dac,1","dac,2","sht4x","sht4x,startup","ld2410","ld2410,factoryreset","ld2410,logall","digipot","digipot,reset","digipot,shutdown","digipot,","7dextra","7dbefore","7dgroup","7digit","7color","7digitcolor","7groupcolor","gp8403","gp8403,volt,","gp8403,mvolt,","gp8403,range,","gp8403,preset,","gp8403,init,","sen5x","sen5x,startclean","sen5x,techlog,","as3935","as3935,clearstats","as3935,calibrate","as3935,setgain,","as3935,setnf,","as3935,setwd,","as3925,setsrej,","lu9685","lu9685,servo,","lu9685,enable,","lu9685,disable,","lu9685,setrange,","geni2c","geni2c,cmd,","geni2c,exec,","geni2c,log,"],pluginDispKind=["tft","ili9341","ili9342","ili9481","ili9486","ili9488","epd","eink","epaper","il3897","uc8151d","ssd1680","ws2in7","ws1in54","st77xx","st7735","st7789","st7796","neomatrix","neo","pcd8544"],pluginDispCmd=["cmd,on","cmd,off","cmd,clear","cmd,backlight","cmd,bright","cmd,deepsleep","cmd,seq_start","cmd,seq_end","cmd,inv","cmd,rot",",clear",",rot",",tpm",",txt",",txp",",txz",",txc",",txs",",txtfull",",asciitable",",font",",l",",lh",",lv",",lm",",lmr",",r",",rf",",c",",cf",",rf",",t",",tf",",rr",",rrf",",px",",pxh",",pxv",",bmp",",btn",",win",",defwin",",delwin"],commonTag=["On","Do","Endon"],commonNumber=["toBin","toHex","Constrain","XOR","AND:","OR:","Ord","bitRead","bitSet","bitClear","bitWrite","urlencode"],commonMath=["Log","Ln","Abs","Exp","Sqrt","Sq","Round","Sin","Cos","Tan","aSin","aCos","aTan","aTan2","Sin_d","Cos_d","Tan_d","aSin_d","aCos_d","aTan_d","aTan2_d","map","mapc","fmod"],commonWarning=["delay","Delay","ResetFlashWriteCounter"],taskSpecifics=["settings.Enabled","settings.Interval","settings.ValueCount","settings.Controller1.Enabled","settings.Controller2.Enabled","settings.Controller3.Enabled","settings.Controller1.Idx","settings.Controller2.Idx","settings.Controller3.Idx"],AnythingElse=["%eventvalue%","%eventpar%","%eventname%","%sysname%","%bootcause%","%systime%","%systm_hm%","%systm_hm_0%","%systm_hm_sp%","%systime_am%","%systime_am_0%","%systime_am_sp%","%systm_hm_am%","%systm_hm_am_0%","%systm_hm_am_sp%","%lcltime%","%sunrise%","%s_sunrise%","%m_sunrise%","%sunset%","%s_sunset%","%m_sunset%","%lcltime_am%","%latitude%","%longitude%","%syshour%","%syshour_0%","%sysmin%","%sysmin_0%","%syssec%","%syssec_0%","%sysday%","%sysday_0%","%sysmonth%","%sysmonth_0%","%systzoffset%","%systzoffset_s%","%sysyear%","%sysyear_0%","%sysyears%","%sysweekday%","%sysweekday_s%","%unixtime%","%unixtime_lcl%","%uptime%","%uptime_ms%","%rssi%","%ip%","%unit%","%unit_0%","%ssid%","%bssid%","%wi_ch%","%iswifi%","%vcc%","%mac%","%mac_int%","%isntp%","%ismqtt%","%dns%","%dns1%","%dns2%","%flash_freq%","%flash_size%","%flash_chip_vendor%","%flash_chip_model%","%fs_free%","%fs_size%","%cpu_id%","%cpu_freq%","%cpu_model%","%cpu_rev%","%cpu_cores%","%board_name%","%inttemp%","%islimited_build%","%isvar_double%","substring","lookup","indexOf","indexOf_ci","equals","equals_ci","strtol","timeToMin","timeToSec","%ethwifimode%","%ethconnected%","%ethduplex%","%ethspeed%","%ethstate%","%ethspeedstate%","%c_w_dir%","%c_c2f%","%c_ms2Bft%","%c_dew_th%","%c_alt_pres_sea%","%c_sea_pres_alt%","%c_cm2imp%","%c_isnum%","%c_mm2imp%","%c_m2day%","%c_m2dh%","%c_m2dhm%","%c_s2dhms%","%c_ts2date%","%c_ts2isodate%","%c_ts2wday%","%c_random%","%c_2hex%","%c_u2ip%","%c_uname%","%c_uage%","%c_ubuild%","%c_ubuildstr%","%c_uload%","%c_utype%","%c_utypestr%","%c_strf%","%c_d2r%","%c_r2d%","%SP%","%CR%","%LF%","%N%","%R%","%e%","%pi%","var","int","str","length"];for(const e of pluginDispKind)commonPlugins=commonPlugins.concat(e);for(const e of pluginDispKind)for(const t of pluginDispCmd){let n=e+t;commonPlugins=commonPlugins.concat(n)}var rEdit,EXTRAWORDS=commonAtoms.concat(commonPlugins,commonKeywords,commonCommands,commonEvents,commonTag,commonNumber,commonMath,commonWarning,taskSpecifics,AnythingElse),confirmR=!0,android=/Android/.test(navigator.userAgent);function initCM(){function e(e){}android&&(confirmR=!!confirm("Do you want to enable colored rules on your Android device?\nThis feature hasn't been fully tested yet and may still have some issues.\nIt is currently expected to work with Chrome, Firefox, and Vivaldi.\nPlease report any problems you encounter.")),confirmR&&(CodeMirror.commands.autocomplete=function(e){e.showHint({hint:CodeMirror.hint.anyword})},(rEdit=CodeMirror.fromTextArea(document.getElementById("rules"),{tabSize:2,indentWithTabs:!1,lineNumbers:!0,autoCloseBrackets:!0,extraKeys:{"Ctrl-Space":"autocomplete",Tab:e=>{"null"===e.getMode().name?e.execCommand("insertTab"):e.somethingSelected()?e.execCommand("indentMore"):e.execCommand("insertSoftTab")},"Shift-Tab":e=>e.execCommand("indentLess")}})).on("change",(function(){rEdit.save()})),android||rEdit.on("inputRead",(function(e,t){var n=e.getCursor(),o=e.getTokenAt(n);/[\w%,.]/.test(t.text)&&"comment"!=o.type&&e.showHint({completeSingle:!1})})),CodeMirror.keyMap.default["Ctrl-F"]=function(e){openFind()},CodeMirror.keyMap.default["Cmd-F"]=function(e){openFind()},CodeMirror.keyMap.default["Ctrl-G"]=e,CodeMirror.keyMap.default["Cmd-G"]=e,CodeMirror.keyMap.default["Shift-Ctrl-G"]=e,CodeMirror.keyMap.default["Shift-Cmd-G"]=e,CodeMirror.keyMap.default["Ctrl-H"]=e,CodeMirror.keyMap.default["Cmd-H"]=e,CodeMirror.keyMap.default["Shift-Ctrl-F"]=e,CodeMirror.keyMap.default["Shift-Cmd-F"]=e,CodeMirror.keyMap.default["Ctrl-Shift-R"]=e,CodeMirror.keyMap.default["Cmd-Shift-R"]=e)}function closeSearchDialog(){const e=document.querySelectorAll(".CodeMirror-dialog");e.length>0&&(e.forEach((e=>e.remove())),document.body.classList.remove("dialog-opened")),rEdit.execCommand("clearSearch")}function removeHighlight(){requestAnimationFrame((()=>{document.querySelectorAll(".search-next-highlight").forEach((e=>e.classList.remove("search-next-highlight")))}))}let findDialogObserver=null;function openFind(){findDialogObserver&&(document.querySelectorAll(".CodeMirror-dialog").forEach((e=>e.remove())),findDialogObserver.disconnect(),findDialogObserver=null),findDialogObserver=new MutationObserver((()=>{document.querySelector(".CodeMirror-dialog")||(removeHighlight(),findDialogObserver.disconnect(),findDialogObserver=null)})),findDialogObserver.observe(document.body,{childList:!0,subtree:!0}),clearSearchNextHighlight(rEdit),rEdit.execCommand("findPersistent"),addFindButtons()}function clearSearchNextHighlight(e){removeHighlight(),e.__searchNextHighlight&&(e.__searchNextHighlight.clear(),e.__searchNextHighlight=null)}function addFindButtons(){document.querySelector(".CodeMirror-selected");const e=document.querySelector(".CodeMirror-dialog");if(!e||e.querySelector(".search-button-group"))return;[{title:"Find Previous",symbol:"▲",action:()=>rEdit.execCommand("findPersistentPrev")},{title:"Find Next",symbol:"▼",action:()=>rEdit.execCommand("findPersistentNext")},{title:"Replace",symbol:"Replace",action:()=>{closeSearchDialog(),rEdit.execCommand("replace"),addFindButtons()}},{title:"Close",symbol:"❌",action:closeSearchDialog},{title:"Help",symbol:"?",action:()=>{alert("Available shortcuts:\n• Ctrl+F / Cmd+F: Open search\n• Enter: Find next\n• Shift+Enter: Find previous\n• Use /re/ syntax for regex search")}}].forEach((({title:t,symbol:n,action:o})=>{const i=document.createElement("span");i.title=t,i.className="help"===t.toLowerCase()?"button help":"button",i.innerHTML=n,i.style.cssText="\n cursor: pointer;\n user-select: none;\n ",i.addEventListener("click",(e=>{e.preventDefault(),o()})),e.appendChild(i)}))}function triggerFormatting(){let e,t,n,o,i;if(confirmR){const r=rEdit.getDoc();e=rEdit.getScrollInfo(),t=r.getCursor(),n=0===t.ch?t.line-1:t.line,o=rEdit.getLine(n)||"",i=rEdit.getValue()}else i=document.getElementById("rules").value;if(i=initalAutocorrection(i),i=formatLogic(i),confirmR){rEdit.setValue(i);const r=n,s=0===t.ch&&o.length>0?o.length:t.ch;rEdit.setCursor({line:r,ch:s}),setTimeout((()=>{rEdit.scrollTo(e.left,e.top),rEdit.focus()}),0),rEdit.save()}else document.getElementById("rules").value=i}function initalAutocorrection(e){for(const t of EXTRAWORDS)if("Do"===t){const t=/(^|\s)(do)(\s*)(\/\/.*)?$/gim;e=e.replace(t,((e,t,n,o,i)=>`${t}Do${o}${i??""}`))}else{const n=new RegExp(`^\\s*\\b${t}\\b`,"gmi");e=e.replace(n,(e=>e.replace(new RegExp(t,"i"),t)))}return e}function formatLogic(e){const t=" ",n=e.split("\n").map((e=>{const t=e.trimStart();return t.startsWith("//")?e:t})),o=[],i=[];let r=!1,s=null,a=[],l=[];function c(e){return e.trim().startsWith("//")}function d(e){return""===e.trim()}function m(e){return e.trim().toLowerCase().startsWith("on")}function u(e){return e.trim().toLowerCase().endsWith("do")}function f(e){return"endon"===e.trim().toLowerCase()}function h(e){return e.trim().toLowerCase().startsWith("if")}function p(e){return"else"===e.trim().toLowerCase()}function g(e){return e.trim().toLowerCase().startsWith("elseif")}function C(e){return"endif"===e.trim().toLowerCase()}let x=0;function S(){a.length>0&&(i.push(`• Missing ${a.length} Endif(s):`),i.push(` - Unclosed If block(s) starting at line(s): ${l.join(", ")}`)),a=[],l=[]}for(let e=0;e0){const e=extractFirstErrorLine(i);if(alert("Errors found:\n"+i.join("\n")),!isNaN(e))if(confirmR)setTimeout((()=>{jumpToLine(e)}),50);else{const t=document.getElementById("rules");setTimeout((()=>{jumpToLineInTextarea(t,e)}),50)}}return o.join("\n")}function jumpToLine(e){const t=Math.max(0,e-1);rEdit.setCursor({line:t,ch:0}),rEdit.focus(),rEdit.scrollIntoView({line:t,ch:0},100)}function extractFirstErrorLine(e){for(const t of e){let e=t.match(/• Line (\d+)/);if(e)return parseInt(e[1]);if(e=t.match(/starting at line (\d+)/),e)return parseInt(e[1]);if(e=t.match(/starting at line\(s\):\s*(\d+)/),e)return parseInt(e[1])}return null}function jumpToLineInTextarea(e,t){const n=e.value.split("\n"),o=Math.max(1,Math.min(t,n.length));let i=0;for(let e=0;e{const e=document.getElementById("rulesselect");if(e){if(confirmR){const t=document.createElement("button");t.type="button",t.id="searchBtn",t.innerHTML="🔎︎",t.style.padding="2px 5px",t.className="button help",e.appendChild(t),t.addEventListener("click",(()=>{void 0!==rEdit&&openFind()}))}const t=document.createElement("button");t.type="button",t.id="formatBtn",t.textContent="Format",t.className="button",e.appendChild(t),t.addEventListener("click",(()=>{triggerFormatting()}))}let t="";if(document.addEventListener("keydown",(function(e){const n=e.key;(["Backspace","Delete","ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Enter","Tab","Escape","Shift","Control","Alt","Meta"].includes(n)||1!==n.length)&&(t="")})),android){var n=!1;rEdit.on("keydown",((e,o)=>{["Enter","Backspace"," "].includes(o.key)&&(t=""),n=!0}));let e="";const i=rEdit.getInputField();function o(o,i=!1){if(!(rEdit.hasFocus()&&rEdit&&o.data&&n))return;n=!1;const r=o.data,s=rEdit.getDoc(),a=s.getCursor(),l=rEdit.getTokenAt(a);if(" "===r)return t="",void(e="");if(!(r===e&&t.length>0))if(/[\w%,.]/.test(r)&&"comment"!==l.type){const n=a.ch<=1?r.slice(-1):r;t+=n,e=t,t.startsWith(String(a.line+1))&&0===a.ch&&(t=t.slice(String(a.line).length));const o={line:a.line,ch:a.ch-t.length+1},l=()=>{s.replaceRange(t,o,a),rEdit.setCursor({line:o.line,ch:o.ch+t.length}),rEdit.showHint({completeSingle:!1})};i?l():setTimeout(l,0)}else t=""}const r=navigator.userAgent.toLowerCase(),s=/firefox/.test(r),a=/chrome/.test(r)&&!s;s?i.addEventListener("beforeinput",(e=>{e.preventDefault(),o(e,!0)})):a&&document.addEventListener("input",(e=>{o(e,!1)})),rEdit.on("endCompletion",(function(){setTimeout((()=>{!function(){const e=document.createElement("input");e.type="text",e.style.position="absolute",e.style.opacity="0",e.style.height="0",e.style.width="0",e.style.border="none",e.style.top="0",e.style.left="-9999",e.style.padding="0",e.style.zIndex="-1",e.style.fontSize="16px",document.body.appendChild(e),e.focus(),setTimeout((()=>{e.remove(),rEdit.focus()}),10)}()}),100)}))}})),function(e){"object"==typeof exports&&"object"==typeof module?e(require("codemirror")):"function"==typeof define&&define.amd?define(["codemirror"],e):e(CodeMirror)}((function(e){"use strict";e.defineMode("espeasy",(function(){var e={};function t(t,n){for(var o=0;oe.toLowerCase()));commonCommands=commonCommands.concat(n);var o=commonEvents.map((e=>e.toLowerCase()));commonEvents=commonEvents.concat(o);var i=commonPlugins.map((e=>e.toLowerCase()));commonPlugins=commonPlugins.concat(i);var r=commonAtoms.map((e=>e.toLowerCase()));commonAtoms=commonAtoms.concat(r);var s=commonKeywords.map((e=>e.toLowerCase()));commonKeywords=commonKeywords.concat(s);var a=commonTag.map((e=>e.toLowerCase()));commonTag=commonTag.concat(a);var l=commonNumber.map((e=>e.toLowerCase()));commonNumber=commonNumber.concat(l);var c=commonMath.map((e=>e.toLowerCase()));commonMath=commonMath.concat(c);var d=AnythingElse.map((e=>e.toLowerCase()));AnythingElse=AnythingElse.concat(d);var m=taskSpecifics.map((e=>e.toLowerCase()));function u(t,n){if(t.eatSpace())return null;t.sol();var o=t.next();if(/\d/.test(o)){if("0"==o)return"x"===t.next()?(t.eatWhile(/\w/),"number"):(t.eatWhile(/\d|\./),"number");if(t.eatWhile(/\d|\./),!t.match("d")&&!t.match("output")&&(t.eol()||/\D/.test(t.peek())))return"number"}if(/\w/.test(o))for(const e of EXTRAWORDS){let n=e.substring(1);(e.includes(":")||e.includes(",")||e.includes("."))&&t.match(n)}if(/\w/.test(o)&&(t.eatWhile(/[\w]/),t.match(".gpio")||t.match(".pulse")||t.match(".frq")||t.match(".pwm")))return"def";if("\\"===o)return t.next(),null;if("("===o||")"===o)return"bracket";if("{"===o||"}"===o||":"===o)return"number";if("/"==o)return/\//.test(t.peek())?(t.skipToEnd(),"comment"):"operator";if("'"==o&&(t.eatWhile(/[^']/),t.match("'")))return"attribute";if("+"===o||"="===o||"<"===o||">"===o||"-"===o||","===o||"*"===o||"!"===o)return"operator";if("%"==o){if(/\d/.test(t.next()))return"number";if(t.eatWhile(/[^\s\%]/),t.match("%"))return"hr"}if("["==o&&(t.eatWhile(/[^\s\]]/),t.eat("]")))return"hr";t.eatWhile(/\w/);var i=t.current();return/\w/.test(o)&&t.match("#")?(t.eatWhile(/[\w.#]/),"events"):"#"===o?(t.eatWhile(/\w/),"number"):e.hasOwnProperty(i)?e[i]:null}function f(e,t){return(t.tokens[0]||u)(e,t)}return taskSpecifics=taskSpecifics.concat(m),t("atom",commonAtoms),t("keyword",commonKeywords),t("builtin",commonCommands),t("events",commonEvents),t("def",commonPlugins),t("tag",commonTag),t("number",commonNumber),t("bracket",commonMath),t("warning",commonWarning),t("hr",AnythingElse),t("comment",taskSpecifics),{startState:function(){return{tokens:[]}},token:function(e,t){return f(e,t)},closeBrackets:"[]{}''\"\"``()",lineComment:"//",fold:"brace"}}))})),function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],mod):e(CodeMirror)}((function(e){var t={pairs:"()[]{}''\"\"",closeBefore:")]}'\":;>",triples:"",explode:"[]{}"},n=e.Pos;function o(e,n){return"pairs"==n&&"string"==typeof e?e:"object"==typeof e&&null!=e[n]?e[n]:t[n]}e.defineOption("autoCloseBrackets",!1,(function(t,n,s){s&&s!=e.Init&&(t.removeKeyMap(i),t.state.closeBrackets=null),n&&(r(o(n,"pairs")),t.state.closeBrackets=n,t.addKeyMap(i))}));var i={Backspace:function(t){var i=a(t);if(!i||t.getOption("disableInput"))return e.Pass;for(var r=o(i,"pairs"),s=t.listSelections(),l=0;l=0;l--){var m=s[l].head;t.replaceRange("",n(m.line,m.ch-1),n(m.line,m.ch+1),"+delete")}},Enter:function(t){var n=a(t),i=n&&o(n,"explode");if(!i||t.getOption("disableInput"))return e.Pass;for(var r=t.listSelections(),s=0;s1&&h.indexOf(i)>=0&&t.getRange(n(M.line,M.ch-2),M)==i+i){if(M.ch>2&&/\bstring/.test(t.getTokenTypeAt(n(M.line,M.ch-2))))return e.Pass;S="addFour"}else if(p){var b=0==M.ch?" ":t.getRange(n(M.line,M.ch-1),M);if(e.isWordChar(P)||b==i||e.isWordChar(b))return e.Pass;S="both"}else{if(!C||!(0===P.length||/\s/.test(P)||f.indexOf(P)>-1))return e.Pass;S="both"}else S=p&&m(t,M)?"both":h.indexOf(i)>=0&&t.getRange(M,n(M.line,M.ch+3))==i+i+i?"skipThree":"skip";if(u){if(u!=S)return e.Pass}else u=S}var T=d%2?s.charAt(d-1):i,v=d%2?i:s.charAt(d+1);t.operation((function(){if("skip"==u)l(t,1);else if("skipThree"==u)l(t,3);else if("surround"==u){for(var e=t.getSelections(),n=0;n0?{line:s.head.line,ch:s.head.ch+t}:{line:s.head.line-1};n.push({anchor:a,head:a})}e.setSelections(n,i)}function c(t){var o=e.cmpPos(t.anchor,t.head)>0;return{anchor:new n(t.anchor.line,t.anchor.ch+(o?-1:1)),head:new n(t.head.line,t.head.ch+(o?1:-1))}}function d(e,t){var o=e.getRange(n(t.line,t.ch-1),n(t.line,t.ch+1));return 2==o.length?o:null}function m(e,t){var o=e.getTokenAt(n(t.line,t.ch+1));return/\bstring/.test(o.type)&&o.start==t.ch&&(0==t.ch||!/\bstring/.test(e.getTokenTypeAt(t)))}r(t.pairs+"`")})); \ No newline at end of file +var commonAtoms=["And","Or"],commonKeywords=["If","Else","Elseif","Endif"],commonCommands=["AccessInfo","Background","Build","ClearAccessBlock","ClearRTCam","Config","ControllerDisable","ControllerEnable","DateTime","Debug","Dec","DeepSleep","DisablePriorityTask","DNS","DST","EraseSDKWiFi","ExecuteRules","FactoryReset","Gateway","I2Cscanner","Inc","IP","Latitude","Let","LetStr","Load","LogEntry","LogPortStatus","Longitude","LoopTimerSet","LoopTimerSet_ms","LoopTimerSetAndRun","LoopTimerSetAndRun_ms","MemInfo","MemInfoDetail","Name","NetworkDisable","NetworkEnable","Password","PostToHTTP","PostToHTTPS","Publish","PublishR","PutToHTTP","PutToHTTPS","Reboot","Save","SendTo","SendToHTTP","SendToHTTPS","SendToUDP","SendToUDPMix","Settings","Subnet","Subscribe","TaskClear","TaskClearAll","TaskDisable","TaskEnable","TaskRun","TaskValueSet","TaskValueSetAndRun","TaskValueSetDerived","TaskValueSetPresentation","TimerPause","TimerResume","TimerSet","TimerSet_ms","TimeZone","UdpPort","UdpTest","Unit","UseNTP","WdConfig","WdRead","WiFi","WiFiAllowAP","WiFiAPMode","WiFiConnect","WiFiDisconnect","WiFiKey","WiFiKey2","WiFiMode","WiFiScan","WiFiSSID","WiFiSSID2","WiFiSTAMode","Event","AsyncEvent","GPIO","GPIOToggle","LongPulse","LongPulse_mS","Monitor","Pulse","PWM","Servo","Status","Tone","RTTTL","UnMonitor","Provision","Provision,Config","Provision,Security","Provision,Notification","Provision,Provision","Provision,Rules","Provision,CustomCdnUrl","Provision,Firmware"],commonEvents=["Clock#Time","JsonReply","JsonReply#","Login#Failed","MQTT#Connected","MQTT#Disconnected","MQTTimport#Connected","MQTTimport#Disconnected","OpenMeteo#current","OpenMeteo#daily","OpenMeteo#hourly","Rules#Timer","System#Boot","System#BootMode","System#Sleep","System#Wake","TaskExit#","TaskInit#","ThingspeakReply","Time#Initialized","Time#Set","WiFi#APmodeDisabled","WiFi#APmodeEnabled","WiFi#ChangedAccesspoint","WiFi#ChangedWiFichannel","WiFi#Connected","WiFi#Disconnected"],commonPlugins=["ResetPulseCounter","SetPulseCounterTotal","LogPulseStatistic","analogout","MCPGPIO","MCPGPIOToggle","MCPLongPulse","MCPLongPulse_ms","MCPPulse","Status,MCP","Monitor,MCP","MonitorRange,MCP","UnMonitorRange,MCP","UnMonitor,MCP","MCPGPIORange","MCPGPIOPattern","MCPMode","MCPModeRange","ExtGpio","ExtPwm","ExtPulse","ExtLongPulse","Status,EXT,","LCDCmd","LCD","PCFGPIO","PCFGPIOToggle","PCFLongPulse","PCFLongPulse_ms","PCFPulse","Status,PCF","Monitor,PCF","MonitorRange,PCF","UnMonitorRange,PCF","UnMonitor,PCF","PCFGPIORange","PCFGPIOpattern","PCFMode","PCFmodeRange","SerialSend","SerialSendMix","Ser2NetClientSend","SerialSend_test","pcapwm","pcafrq","mode2","OLED","OLEDCMD","OLEDCMD,on","OLEDCMD,off","OLEDCMD,clear","IRSEND","IRSENDAC","OledFramedCmd","OledFramedCmd,Display","OledFramedCmd,low","OledFramedCmd,med","OledFramedCmd,high","OledFramedCmd,Frame","OledFramedCmd,linecount","OledFramedCmd,leftalign","OledFramedCmd,align","OledFramedCmd,userDef1","OledFramedCmd,userDef2","NeoPixel","NeoPixelAll","NeoPixelLine","NeoPixelHSV","NeoPixelAllHSV","NeoPixelLineHSV","NeoPixelBright","MotorShieldCmd,DCMotor","MotorShieldCmd,Stepper","MHZCalibrateZero","MHZReset","MHZABCEnable","MHZABCDisable","Sensair_SetRelay","PMSX003","PMSX003,Wake","PMSX003,Sleep","PMSX003,Reset","encwrite","Play","Vol","Eq","Mode","Repeat","tareChanA","tareChanB","7dn","7dst","7dsd","7dtext","7ddt","7dt","7dtfont","7dtbin","7don","7doff","7output","HLWCalibrate","HLWReset","csecalibrate","cseclearpulses","csereset","WemosMotorShieldCMD","LolinMotorShieldCMD","GPS","GPS,Sleep","GPS,Wake","GPS#GotFix","GPS#LostFix","GPS#Travelled","homieValueSet","SerialProxy_Write","SerialProxy_WriteMix","SerialProxy_Test","HeatPumpir","MitsubishiHP","MitsubishiHP,temperature","MitsubishiHP,power","MitsubishiHP,mode","MitsubishiHP,fan","MitsubishiHP,vane","MitsubishiHP,widevane","Culreader_Write","Touch","Touch,Rot","Touch,Flip","Touch,Enable","Touch,Disable","Touch,On","Touch,Off","Touch,Toggle","Touch,Setgrp","Touch,Incgrp","Touch,Decgrp","Touch,Incpage","Touch,Decpage","Touch,Updatebutton","WakeOnLan","DotMatrix","DotMatrix,clear","DotMatrix,update","DotMatrix,size","DotMatrix,txt","DotMatrix,settxt","DotMatrix,content","DotMatrix,alignment","DotMatrix,anim.in","DotMatrix,anim.out","DotMatrix,speed","DotMatrix,pause","DotMatrix,font","DotMatrix,layout","DotMatrix,inverted","DotMatrix,specialeffect","DotMatrix,offset","DotMatrix,brightness","DotMatrix,repeat","DotMatrix,setbar","DotMatrix,bar","Thermo","Thermo,Up","Thermo,Down","Thermo,Mode","Thermo,ModeBtn","Thermo,Setpoint","Max1704xclearalert","scdgetabc","scdgetalt","scdgettmp","scdsetcalibration","scdsetfrc","scdgetinterval","multirelay","multirelay,on","multirelay,off","multirelay,set","multirelay,get","multirelay,loop","ShiftOut","ShiftOut,Set","ShiftOut,SetNoUpdate","ShiftOut,Update","ShiftOut,SetAll","ShiftOut,SetAllNoUpdate","ShiftOut,SetAllLow","ShiftOut,SetAllHigh","ShiftOut,SetChipCount","ShiftOut,SetHexBin","cdmrst","nfx","nfx,off","nfx,on","nfx,dim","nfx,line,","nfx,hsvline,","nfx,one,","nfx,hsvone,","nfx,all,","nfx,rgb,","nfx,fade,","nfx,hsv,","nfx,colorfade,","nfx,rainbow","nfx,kitt,","nfx,comet,","nfx,theatre,","nfx,scan,","nfx,dualscan,","nfx,twinkle,","nfx,twinklefade,","nfx,sparkle,","nfx,wipe,","nfx,dualwipe","nfx,fire","nfx,fireflicker","nfx,faketv","nfx,simpleclock","nfx,stop","nfx,statusrequest","nfx,fadetime,","nfx,fadedelay,","nfx,speed,","nfx,count,","nfx,bgcolor","ShiftIn","ShiftIn,PinEvent","ShiftIn,ChipEvent","ShiftIn,SetChipCount","ShiftIn,SampleFrequency","ShiftIn,EventPerPin","scd4x","scd4x,storesettings","scd4x,facoryreset","scd4x,selftest","scd4x,setfrc,","axp","axp,ldo2","axp,ldo3","axp,ldoio","axp,gpio0","axp,gpio1","axp,gpio2","axp,gpio3","axp,gpio4","axp,dcdc2","axp,dcdc3","axp,ldo2map","axp,ldo3map","axp,ldoiomap","axp,dcdc2map","axp,dcdc3map","axp,ldo2perc","axp,ldo3perc","axp,ldoioperc","axp,dcdc2perc","axp,dcdc3perc","I2CEncoder","I2CEncoder,bright","I2CEncoder,led1","I2CEncoder,led2","I2CEncoder,gain","I2CEncoder,set","cachereader","cachereader,readpos","cachereader,sendtaskinfo","cachereader,flush","tm1621","tm1621,write,","tm1621,writerow,","tm1621,voltamp,","tm1621,energy,","tm1621,celcius,","tm1621,fahrenheit,","tm1621,humidity,","tm1621,raw,","dac","dac,1","dac,2","sht4x","sht4x,startup","ld2410","ld2410,factoryreset","ld2410,logall","digipot","digipot,reset","digipot,shutdown","digipot,","7dextra","7dbefore","7dgroup","7digit","7color","7digitcolor","7groupcolor","gp8403","gp8403,volt,","gp8403,mvolt,","gp8403,range,","gp8403,preset,","gp8403,init,","sen5x","sen5x,startclean","sen5x,techlog,","as3935","as3935,clearstats","as3935,calibrate","as3935,setgain,","as3935,setnf,","as3935,setwd,","as3925,setsrej,","lu9685","lu9685,servo,","lu9685,enable,","lu9685,disable,","lu9685,setrange,","geni2c","geni2c,cmd,","geni2c,exec,","geni2c,log,"],pluginDispKind=["tft","ili9341","ili9342","ili9481","ili9486","ili9488","epd","eink","epaper","il3897","uc8151d","ssd1680","ws2in7","ws1in54","st77xx","st7735","st7789","st7796","neomatrix","neo","pcd8544"],pluginDispCmd=["cmd,on","cmd,off","cmd,clear","cmd,backlight","cmd,bright","cmd,deepsleep","cmd,seq_start","cmd,seq_end","cmd,inv","cmd,rot",",clear",",rot",",tpm",",txt",",txp",",txz",",txc",",txs",",txtfull",",asciitable",",font",",l",",lh",",lv",",lm",",lmr",",r",",rf",",c",",cf",",rf",",t",",tf",",rr",",rrf",",px",",pxh",",pxv",",bmp",",btn",",win",",defwin",",delwin"],commonTag=["On","Do","Endon"],commonNumber=["toBin","toHex","Constrain","XOR","AND:","OR:","Ord","bitRead","bitSet","bitClear","bitWrite","urlencode"],commonMath=["Log","Ln","Abs","Exp","Sqrt","Sq","Round","Sin","Cos","Tan","aSin","aCos","aTan","aTan2","Sin_d","Cos_d","Tan_d","aSin_d","aCos_d","aTan_d","aTan2_d","map","mapc","fmod"],commonWarning=["delay","Delay","ResetFlashWriteCounter"],taskSpecifics=["settings.Enabled","settings.Interval","settings.ValueCount","settings.Controller1.Enabled","settings.Controller2.Enabled","settings.Controller3.Enabled","settings.Controller1.Idx","settings.Controller2.Idx","settings.Controller3.Idx"],AnythingElse=["%eventvalue%","%eventpar%","%eventname%","%sysname%","%bootcause%","%systime%","%systm_hm%","%systm_hm_0%","%systm_hm_sp%","%systime_am%","%systime_am_0%","%systime_am_sp%","%systm_hm_am%","%systm_hm_am_0%","%systm_hm_am_sp%","%lcltime%","%sunrise%","%s_sunrise%","%m_sunrise%","%sunset%","%s_sunset%","%m_sunset%","%lcltime_am%","%latitude%","%longitude%","%syshour%","%syshour_0%","%sysmin%","%sysmin_0%","%syssec%","%syssec_0%","%sysday%","%sysday_0%","%sysmonth%","%sysmonth_0%","%systzoffset%","%systzoffset_s%","%sysyear%","%sysyear_0%","%sysyears%","%sysweekday%","%sysweekday_s%","%unixtime%","%unixtime_lcl%","%uptime%","%uptime_ms%","%rssi%","%ip%","%unit%","%unit_0%","%ssid%","%bssid%","%wi_ch%","%iswifi%","%vcc%","%mac%","%mac_int%","%isntp%","%ismqtt%","%dns%","%dns1%","%dns2%","%flash_freq%","%flash_size%","%flash_chip_vendor%","%flash_chip_model%","%fs_free%","%fs_size%","%cpu_id%","%cpu_freq%","%cpu_model%","%cpu_rev%","%cpu_cores%","%board_name%","%inttemp%","%islimited_build%","%isvar_double%","substring","lookup","indexOf","indexOf_ci","equals","equals_ci","strtol","timeToMin","timeToSec","unescape","escape","parse","json","%ethwifimode%","%ethconnected%","%ethduplex%","%ethspeed%","%ethstate%","%ethspeedstate%","%c_w_dir%","%c_c2f%","%c_ms2Bft%","%c_dew_th%","%c_alt_pres_sea%","%c_sea_pres_alt%","%c_cm2imp%","%c_isnum%","%c_mm2imp%","%c_m2day%","%c_m2dh%","%c_m2dhm%","%c_s2dhms%","%c_ts2date%","%c_ts2isodate%","%c_ts2wday%","%c_random%","%c_2hex%","%c_u2ip%","%c_uname%","%c_uage%","%c_ubuild%","%c_ubuildstr%","%c_uload%","%c_utype%","%c_utypestr%","%c_strf%","%c_d2r%","%c_r2d%","%SP%","%CR%","%LF%","%N%","%R%","%e%","%pi%","var","int","str","length"];for(const e of pluginDispKind)commonPlugins=commonPlugins.concat(e);for(const e of pluginDispKind)for(const t of pluginDispCmd){let n=e+t;commonPlugins=commonPlugins.concat(n)}var rEdit,EXTRAWORDS=commonAtoms.concat(commonPlugins,commonKeywords,commonCommands,commonEvents,commonTag,commonNumber,commonMath,commonWarning,taskSpecifics,AnythingElse),confirmR=!0,android=/Android/.test(navigator.userAgent);function initCM(){function e(e){}android&&(confirmR=!!confirm("Do you want to enable colored rules on your Android device?\nThis feature hasn't been fully tested yet and may still have some issues.\nIt is currently expected to work with Chrome, Firefox, and Vivaldi.\nPlease report any problems you encounter.")),confirmR&&(CodeMirror.commands.autocomplete=function(e){e.showHint({hint:CodeMirror.hint.anyword})},(rEdit=CodeMirror.fromTextArea(document.getElementById("rules"),{tabSize:2,indentWithTabs:!1,lineNumbers:!0,autoCloseBrackets:!0,extraKeys:{"Ctrl-Space":"autocomplete",Tab:e=>{"null"===e.getMode().name?e.execCommand("insertTab"):e.somethingSelected()?e.execCommand("indentMore"):e.execCommand("insertSoftTab")},"Shift-Tab":e=>e.execCommand("indentLess")}})).on("change",(function(){rEdit.save()})),android||rEdit.on("inputRead",(function(e,t){var n=e.getCursor(),o=e.getTokenAt(n);/[\w%,.]/.test(t.text)&&"comment"!=o.type&&e.showHint({completeSingle:!1})})),CodeMirror.keyMap.default["Ctrl-F"]=function(e){openFind()},CodeMirror.keyMap.default["Cmd-F"]=function(e){openFind()},CodeMirror.keyMap.default["Ctrl-G"]=e,CodeMirror.keyMap.default["Cmd-G"]=e,CodeMirror.keyMap.default["Shift-Ctrl-G"]=e,CodeMirror.keyMap.default["Shift-Cmd-G"]=e,CodeMirror.keyMap.default["Ctrl-H"]=e,CodeMirror.keyMap.default["Cmd-H"]=e,CodeMirror.keyMap.default["Shift-Ctrl-F"]=e,CodeMirror.keyMap.default["Shift-Cmd-F"]=e,CodeMirror.keyMap.default["Ctrl-Shift-R"]=e,CodeMirror.keyMap.default["Cmd-Shift-R"]=e)}function closeSearchDialog(){const e=document.querySelectorAll(".CodeMirror-dialog");e.length>0&&(e.forEach((e=>e.remove())),document.body.classList.remove("dialog-opened")),rEdit.execCommand("clearSearch")}function removeHighlight(){requestAnimationFrame((()=>{document.querySelectorAll(".search-next-highlight").forEach((e=>e.classList.remove("search-next-highlight")))}))}let findDialogObserver=null;function openFind(){findDialogObserver&&(document.querySelectorAll(".CodeMirror-dialog").forEach((e=>e.remove())),findDialogObserver.disconnect(),findDialogObserver=null),findDialogObserver=new MutationObserver((()=>{document.querySelector(".CodeMirror-dialog")||(removeHighlight(),findDialogObserver.disconnect(),findDialogObserver=null)})),findDialogObserver.observe(document.body,{childList:!0,subtree:!0}),clearSearchNextHighlight(rEdit),rEdit.execCommand("findPersistent"),addFindButtons()}function clearSearchNextHighlight(e){removeHighlight(),e.__searchNextHighlight&&(e.__searchNextHighlight.clear(),e.__searchNextHighlight=null)}function addFindButtons(){document.querySelector(".CodeMirror-selected");const e=document.querySelector(".CodeMirror-dialog");if(!e||e.querySelector(".search-button-group"))return;[{title:"Find Previous",symbol:"▲",action:()=>rEdit.execCommand("findPersistentPrev")},{title:"Find Next",symbol:"▼",action:()=>rEdit.execCommand("findPersistentNext")},{title:"Replace",symbol:"Replace",action:()=>{closeSearchDialog(),rEdit.execCommand("replace"),addFindButtons()}},{title:"Close",symbol:"❌",action:closeSearchDialog},{title:"Help",symbol:"?",action:()=>{alert("Available shortcuts:\n• Ctrl+F / Cmd+F: Open search\n• Enter: Find next\n• Shift+Enter: Find previous\n• Use /re/ syntax for regex search")}}].forEach((({title:t,symbol:n,action:o})=>{const i=document.createElement("span");i.title=t,i.className="help"===t.toLowerCase()?"button help":"button",i.innerHTML=n,i.style.cssText="\n cursor: pointer;\n user-select: none;\n ",i.addEventListener("click",(e=>{e.preventDefault(),o()})),e.appendChild(i)}))}function triggerFormatting(){let e,t,n,o,i;if(confirmR){const r=rEdit.getDoc();e=rEdit.getScrollInfo(),t=r.getCursor(),n=0===t.ch?t.line-1:t.line,o=rEdit.getLine(n)||"",i=rEdit.getValue()}else i=document.getElementById("rules").value;if(i=initalAutocorrection(i),i=formatLogic(i),confirmR){rEdit.setValue(i);const r=n,s=0===t.ch&&o.length>0?o.length:t.ch;rEdit.setCursor({line:r,ch:s}),setTimeout((()=>{rEdit.scrollTo(e.left,e.top),rEdit.focus()}),0),rEdit.save()}else document.getElementById("rules").value=i}function initalAutocorrection(e){for(const t of EXTRAWORDS)if("Do"===t){const t=/(^|\s)(do)(\s*)(\/\/.*)?$/gim;e=e.replace(t,((e,t,n,o,i)=>`${t}Do${o}${i??""}`))}else{const n=new RegExp(`^\\s*\\b${t}\\b`,"gmi");e=e.replace(n,(e=>e.replace(new RegExp(t,"i"),t)))}return e}function formatLogic(e){const t=" ",n=e.split("\n").map((e=>{const t=e.trimStart();return t.startsWith("//")?e:t})),o=[],i=[];let r=!1,s=null,a=[],l=[];function c(e){return e.trim().startsWith("//")}function d(e){return""===e.trim()}function m(e){return e.trim().toLowerCase().startsWith("on")}function u(e){return e.trim().toLowerCase().endsWith("do")}function f(e){return"endon"===e.trim().toLowerCase()}function h(e){return e.trim().toLowerCase().startsWith("if")}function p(e){return"else"===e.trim().toLowerCase()}function g(e){return e.trim().toLowerCase().startsWith("elseif")}function C(e){return"endif"===e.trim().toLowerCase()}let x=0;function S(){a.length>0&&(i.push(`• Missing ${a.length} Endif(s):`),i.push(` - Unclosed If block(s) starting at line(s): ${l.join(", ")}`)),a=[],l=[]}for(let e=0;e0){const e=extractFirstErrorLine(i);if(alert("Errors found:\n"+i.join("\n")),!isNaN(e))if(confirmR)setTimeout((()=>{jumpToLine(e)}),50);else{const t=document.getElementById("rules");setTimeout((()=>{jumpToLineInTextarea(t,e)}),50)}}return o.join("\n")}function jumpToLine(e){const t=Math.max(0,e-1);rEdit.setCursor({line:t,ch:0}),rEdit.focus(),rEdit.scrollIntoView({line:t,ch:0},100)}function extractFirstErrorLine(e){for(const t of e){let e=t.match(/• Line (\d+)/);if(e)return parseInt(e[1]);if(e=t.match(/starting at line (\d+)/),e)return parseInt(e[1]);if(e=t.match(/starting at line\(s\):\s*(\d+)/),e)return parseInt(e[1])}return null}function jumpToLineInTextarea(e,t){const n=e.value.split("\n"),o=Math.max(1,Math.min(t,n.length));let i=0;for(let e=0;e{const e=document.getElementById("rulesselect");if(e){if(confirmR){const t=document.createElement("button");t.type="button",t.id="searchBtn",t.innerHTML="🔎︎",t.style.padding="2px 5px",t.className="button help",e.appendChild(t),t.addEventListener("click",(()=>{void 0!==rEdit&&openFind()}))}const t=document.createElement("button");t.type="button",t.id="formatBtn",t.textContent="Format",t.className="button",e.appendChild(t),t.addEventListener("click",(()=>{triggerFormatting()}))}let t="";if(document.addEventListener("keydown",(function(e){const n=e.key;(["Backspace","Delete","ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Enter","Tab","Escape","Shift","Control","Alt","Meta"].includes(n)||1!==n.length)&&(t="")})),android){var n=!1;rEdit.on("keydown",((e,o)=>{["Enter","Backspace"," "].includes(o.key)&&(t=""),n=!0}));let e="";const i=rEdit.getInputField();function o(o,i=!1){if(!(rEdit.hasFocus()&&rEdit&&o.data&&n))return;n=!1;const r=o.data,s=rEdit.getDoc(),a=s.getCursor(),l=rEdit.getTokenAt(a);if(" "===r)return t="",void(e="");if(!(r===e&&t.length>0))if(/[\w%,.]/.test(r)&&"comment"!==l.type){const n=a.ch<=1?r.slice(-1):r;t+=n,e=t,t.startsWith(String(a.line+1))&&0===a.ch&&(t=t.slice(String(a.line).length));const o={line:a.line,ch:a.ch-t.length+1},l=()=>{s.replaceRange(t,o,a),rEdit.setCursor({line:o.line,ch:o.ch+t.length}),rEdit.showHint({completeSingle:!1})};i?l():setTimeout(l,0)}else t=""}const r=navigator.userAgent.toLowerCase(),s=/firefox/.test(r),a=/chrome/.test(r)&&!s;s?i.addEventListener("beforeinput",(e=>{e.preventDefault(),o(e,!0)})):a&&document.addEventListener("input",(e=>{o(e,!1)})),rEdit.on("endCompletion",(function(){setTimeout((()=>{!function(){const e=document.createElement("input");e.type="text",e.style.position="absolute",e.style.opacity="0",e.style.height="0",e.style.width="0",e.style.border="none",e.style.top="0",e.style.left="-9999",e.style.padding="0",e.style.zIndex="-1",e.style.fontSize="16px",document.body.appendChild(e),e.focus(),setTimeout((()=>{e.remove(),rEdit.focus()}),10)}()}),100)}))}})),function(e){"object"==typeof exports&&"object"==typeof module?e(require("codemirror")):"function"==typeof define&&define.amd?define(["codemirror"],e):e(CodeMirror)}((function(e){"use strict";e.defineMode("espeasy",(function(){var e={};function t(t,n){for(var o=0;oe.toLowerCase()));commonCommands=commonCommands.concat(n);var o=commonEvents.map((e=>e.toLowerCase()));commonEvents=commonEvents.concat(o);var i=commonPlugins.map((e=>e.toLowerCase()));commonPlugins=commonPlugins.concat(i);var r=commonAtoms.map((e=>e.toLowerCase()));commonAtoms=commonAtoms.concat(r);var s=commonKeywords.map((e=>e.toLowerCase()));commonKeywords=commonKeywords.concat(s);var a=commonTag.map((e=>e.toLowerCase()));commonTag=commonTag.concat(a);var l=commonNumber.map((e=>e.toLowerCase()));commonNumber=commonNumber.concat(l);var c=commonMath.map((e=>e.toLowerCase()));commonMath=commonMath.concat(c);var d=AnythingElse.map((e=>e.toLowerCase()));AnythingElse=AnythingElse.concat(d);var m=taskSpecifics.map((e=>e.toLowerCase()));function u(t,n){if(t.eatSpace())return null;t.sol();var o=t.next();if(/\d/.test(o)){if("0"==o)return"x"===t.next()?(t.eatWhile(/\w/),"number"):(t.eatWhile(/\d|\./),"number");if(t.eatWhile(/\d|\./),!t.match("d")&&!t.match("output")&&(t.eol()||/\D/.test(t.peek())))return"number"}if(/\w/.test(o))for(const e of EXTRAWORDS){let n=e.substring(1);(e.includes(":")||e.includes(",")||e.includes("."))&&t.match(n)}if(/\w/.test(o)&&(t.eatWhile(/[\w]/),t.match(".gpio")||t.match(".pulse")||t.match(".frq")||t.match(".pwm")))return"def";if("\\"===o)return t.next(),null;if("("===o||")"===o)return"bracket";if("{"===o||"}"===o||":"===o)return"number";if("/"==o)return/\//.test(t.peek())?(t.skipToEnd(),"comment"):"operator";if("'"==o&&(t.eatWhile(/[^']/),t.match("'")))return"attribute";if("+"===o||"="===o||"<"===o||">"===o||"-"===o||","===o||"*"===o||"!"===o)return"operator";if("%"==o){if(/\d/.test(t.next()))return"number";if(t.eatWhile(/[^\s\%]/),t.match("%"))return"hr"}if("["==o&&(t.eatWhile(/[^\s\]]/),t.eat("]")))return"hr";t.eatWhile(/\w/);var i=t.current();return/\w/.test(o)&&t.match("#")?(t.eatWhile(/[\w.#]/),"events"):"#"===o?(t.eatWhile(/\w/),"number"):e.hasOwnProperty(i)?e[i]:null}function f(e,t){return(t.tokens[0]||u)(e,t)}return taskSpecifics=taskSpecifics.concat(m),t("atom",commonAtoms),t("keyword",commonKeywords),t("builtin",commonCommands),t("events",commonEvents),t("def",commonPlugins),t("tag",commonTag),t("number",commonNumber),t("bracket",commonMath),t("warning",commonWarning),t("hr",AnythingElse),t("comment",taskSpecifics),{startState:function(){return{tokens:[]}},token:function(e,t){return f(e,t)},closeBrackets:"[]{}''\"\"``()",lineComment:"//",fold:"brace"}}))})),function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],mod):e(CodeMirror)}((function(e){var t={pairs:"()[]{}''\"\"",closeBefore:")]}'\":;>",triples:"",explode:"[]{}"},n=e.Pos;function o(e,n){return"pairs"==n&&"string"==typeof e?e:"object"==typeof e&&null!=e[n]?e[n]:t[n]}e.defineOption("autoCloseBrackets",!1,(function(t,n,s){s&&s!=e.Init&&(t.removeKeyMap(i),t.state.closeBrackets=null),n&&(r(o(n,"pairs")),t.state.closeBrackets=n,t.addKeyMap(i))}));var i={Backspace:function(t){var i=a(t);if(!i||t.getOption("disableInput"))return e.Pass;for(var r=o(i,"pairs"),s=t.listSelections(),l=0;l=0;l--){var m=s[l].head;t.replaceRange("",n(m.line,m.ch-1),n(m.line,m.ch+1),"+delete")}},Enter:function(t){var n=a(t),i=n&&o(n,"explode");if(!i||t.getOption("disableInput"))return e.Pass;for(var r=t.listSelections(),s=0;s1&&h.indexOf(i)>=0&&t.getRange(n(M.line,M.ch-2),M)==i+i){if(M.ch>2&&/\bstring/.test(t.getTokenTypeAt(n(M.line,M.ch-2))))return e.Pass;S="addFour"}else if(p){var b=0==M.ch?" ":t.getRange(n(M.line,M.ch-1),M);if(e.isWordChar(P)||b==i||e.isWordChar(b))return e.Pass;S="both"}else{if(!C||!(0===P.length||/\s/.test(P)||f.indexOf(P)>-1))return e.Pass;S="both"}else S=p&&m(t,M)?"both":h.indexOf(i)>=0&&t.getRange(M,n(M.line,M.ch+3))==i+i+i?"skipThree":"skip";if(u){if(u!=S)return e.Pass}else u=S}var T=d%2?s.charAt(d-1):i,v=d%2?i:s.charAt(d+1);t.operation((function(){if("skip"==u)l(t,1);else if("skipThree"==u)l(t,3);else if("surround"==u){for(var e=t.getSelections(),n=0;n0?{line:s.head.line,ch:s.head.ch+t}:{line:s.head.line-1};n.push({anchor:a,head:a})}e.setSelections(n,i)}function c(t){var o=e.cmpPos(t.anchor,t.head)>0;return{anchor:new n(t.anchor.line,t.anchor.ch+(o?-1:1)),head:new n(t.head.line,t.head.ch+(o?1:-1))}}function d(e,t){var o=e.getRange(n(t.line,t.ch-1),n(t.line,t.ch+1));return 2==o.length?o:null}function m(e,t){var o=e.getTokenAt(n(t.line,t.ch+1));return/\bstring/.test(o.type)&&o.start==t.ch&&(0==t.ch||!/\bstring/.test(e.getTokenTypeAt(t)))}r(t.pairs+"`")})); \ No newline at end of file