702 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
			
		
		
	
	
			702 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
| /**
 | ||
|  * JSON Parser
 | ||
|  *
 | ||
|  * @author Mikhail Yurasov <mikhail@electricimp.com>
 | ||
|  * @package JSONParser
 | ||
|  * @version 1.0.1
 | ||
|  */
 | ||
| 
 | ||
| /**
 | ||
|  * JSON Parser
 | ||
|  * @package JSONParser
 | ||
|  */
 | ||
| class JSONParser {
 | ||
| 
 | ||
|     // should be the same for all components within JSONParser package
 | ||
|     static version = "1.0.1";
 | ||
| 
 | ||
|     /**
 | ||
|      * Parse JSON string into data structure
 | ||
|      *
 | ||
|      * @param {string} str
 | ||
|      * @param {function({string} value[, "number"|"string"])|null} converter
 | ||
|      * @return {*}
 | ||
|      */
 | ||
|     function parse(str, converter = null) {
 | ||
| 
 | ||
|         local state;
 | ||
|         local stack = []
 | ||
|         local container;
 | ||
|         local key;
 | ||
|         local value;
 | ||
| 
 | ||
|         // actions for string tokens
 | ||
|         local string = {
 | ||
|             go = function() {
 | ||
|                 state = "ok";
 | ||
|             },
 | ||
|             firstokey = function() {
 | ||
|                 key = value;
 | ||
|                 state = "colon";
 | ||
|             },
 | ||
|             okey = function() {
 | ||
|                 key = value;
 | ||
|                 state = "colon";
 | ||
|             },
 | ||
|             ovalue = function() {
 | ||
|                 value = this._convert(value, "string", converter);
 | ||
|                 state = "ocomma";
 | ||
|             }.bindenv(this),
 | ||
|             firstavalue = function() {
 | ||
|                 value = this._convert(value, "string", converter);
 | ||
|                 state = "acomma";
 | ||
|             }.bindenv(this),
 | ||
|             avalue = function() {
 | ||
|                 value = this._convert(value, "string", converter);
 | ||
|                 state = "acomma";
 | ||
|             }.bindenv(this)
 | ||
|         };
 | ||
| 
 | ||
|         // the actions for number tokens
 | ||
|         local number = {
 | ||
|             go = function() {
 | ||
|                 state = "ok";
 | ||
|             },
 | ||
|             ovalue = function() {
 | ||
|                 value = this._convert(value, "number", converter);
 | ||
|                 state = "ocomma";
 | ||
|             }.bindenv(this),
 | ||
|             firstavalue = function() {
 | ||
|                 value = this._convert(value, "number", converter);
 | ||
|                 state = "acomma";
 | ||
|             }.bindenv(this),
 | ||
|             avalue = function() {
 | ||
|                 value = this._convert(value, "number", converter);
 | ||
|                 state = "acomma";
 | ||
|             }.bindenv(this)
 | ||
|         };
 | ||
| 
 | ||
|         // action table
 | ||
|         // describes where the state machine will go from each given state
 | ||
|         local action = {
 | ||
| 
 | ||
|             "{": {
 | ||
|                 go = function() {
 | ||
|                     stack.push({
 | ||
|                         state = "ok"
 | ||
|                     });
 | ||
|                     container = {};
 | ||
|                     state = "firstokey";
 | ||
|                 },
 | ||
|                 ovalue = function() {
 | ||
|                     stack.push({
 | ||
|                         container = container,
 | ||
|                         state = "ocomma",
 | ||
|                         key = key
 | ||
|                     });
 | ||
|                     container = {};
 | ||
|                     state = "firstokey";
 | ||
|                 },
 | ||
|                 firstavalue = function() {
 | ||
|                     stack.push({
 | ||
|                         container = container,
 | ||
|                         state = "acomma"
 | ||
|                     });
 | ||
|                     container = {};
 | ||
|                     state = "firstokey";
 | ||
|                 },
 | ||
|                 avalue = function() {
 | ||
|                     stack.push({
 | ||
|                         container = container,
 | ||
|                         state = "acomma"
 | ||
|                     });
 | ||
|                     container = {};
 | ||
|                     state = "firstokey";
 | ||
|                 }
 | ||
|             },
 | ||
| 
 | ||
|             "}": {
 | ||
|                 firstokey = function() {
 | ||
|                     local pop = stack.pop();
 | ||
|                     value = container;
 | ||
|                     container = ("container" in pop) ? pop.container : null;
 | ||
|                     key = ("key" in pop) ? pop.key : null;
 | ||
|                     state = pop.state;
 | ||
|                 },
 | ||
|                 ocomma = function() {
 | ||
|                     local pop = stack.pop();
 | ||
|                     container[key] <- value;
 | ||
|                     value = container;
 | ||
|                     container = ("container" in pop) ? pop.container : null;
 | ||
|                     key = ("key" in pop) ? pop.key : null;
 | ||
|                     state = pop.state;
 | ||
|                 }
 | ||
|             },
 | ||
| 
 | ||
|             "[": {
 | ||
|                 go = function() {
 | ||
|                     stack.push({
 | ||
|                         state = "ok"
 | ||
|                     });
 | ||
|                     container = [];
 | ||
|                     state = "firstavalue";
 | ||
|                 },
 | ||
|                 ovalue = function() {
 | ||
|                     stack.push({
 | ||
|                         container = container,
 | ||
|                         state = "ocomma",
 | ||
|                         key = key
 | ||
|                     });
 | ||
|                     container = [];
 | ||
|                     state = "firstavalue";
 | ||
|                 },
 | ||
|                 firstavalue = function() {
 | ||
|                     stack.push({
 | ||
|                         container = container,
 | ||
|                         state = "acomma"
 | ||
|                     });
 | ||
|                     container = [];
 | ||
|                     state = "firstavalue";
 | ||
|                 },
 | ||
|                 avalue = function() {
 | ||
|                     stack.push({
 | ||
|                         container = container,
 | ||
|                         state = "acomma"
 | ||
|                     });
 | ||
|                     container = [];
 | ||
|                     state = "firstavalue";
 | ||
|                 }
 | ||
|             },
 | ||
| 
 | ||
|             "]": {
 | ||
|                 firstavalue = function() {
 | ||
|                     local pop = stack.pop();
 | ||
|                     value = container;
 | ||
|                     container = ("container" in pop) ? pop.container : null;
 | ||
|                     key = ("key" in pop) ? pop.key : null;
 | ||
|                     state = pop.state;
 | ||
|                 },
 | ||
|                 acomma = function() {
 | ||
|                     local pop = stack.pop();
 | ||
|                     container.push(value);
 | ||
|                     value = container;
 | ||
|                     container = ("container" in pop) ? pop.container : null;
 | ||
|                     key = ("key" in pop) ? pop.key : null;
 | ||
|                     state = pop.state;
 | ||
|                 }
 | ||
|             },
 | ||
| 
 | ||
|             ":": {
 | ||
|                 colon = function() {
 | ||
|                     // Check if the key already exists
 | ||
|                     // NOTE previous code used 'if (key in container)...'
 | ||
|                     //      but this finds table ('container') member methods too
 | ||
|                     local err = false;
 | ||
|                     foreach(akey, avalue in container) {
 | ||
|                         if (akey == key) err = true;
 | ||
|                         break
 | ||
|                     }
 | ||
|                     if (err) throw "Duplicate key \"" + key + "\"";
 | ||
|                     state = "ovalue";
 | ||
|                 }
 | ||
|             },
 | ||
| 
 | ||
|             ",": {
 | ||
|                 ocomma = function() {
 | ||
|                     container[key] <- value;
 | ||
|                     state = "okey";
 | ||
|                 },
 | ||
|                 acomma = function() {
 | ||
|                     container.push(value);
 | ||
|                     state = "avalue";
 | ||
|                 }
 | ||
|             },
 | ||
| 
 | ||
|             "true": {
 | ||
|                 go = function() {
 | ||
|                     value = true;
 | ||
|                     state = "ok";
 | ||
|                 },
 | ||
|                 ovalue = function() {
 | ||
|                     value = true;
 | ||
|                     state = "ocomma";
 | ||
|                 },
 | ||
|                 firstavalue = function() {
 | ||
|                     value = true;
 | ||
|                     state = "acomma";
 | ||
|                 },
 | ||
|                 avalue = function() {
 | ||
|                     value = true;
 | ||
|                     state = "acomma";
 | ||
|                 }
 | ||
|             },
 | ||
| 
 | ||
|             "false": {
 | ||
|                 go = function() {
 | ||
|                     value = false;
 | ||
|                     state = "ok";
 | ||
|                 },
 | ||
|                 ovalue = function() {
 | ||
|                     value = false;
 | ||
|                     state = "ocomma";
 | ||
|                 },
 | ||
|                 firstavalue = function() {
 | ||
|                     value = false;
 | ||
|                     state = "acomma";
 | ||
|                 },
 | ||
|                 avalue = function() {
 | ||
|                     value = false;
 | ||
|                     state = "acomma";
 | ||
|                 }
 | ||
|             },
 | ||
| 
 | ||
|             "null": {
 | ||
|                 go = function() {
 | ||
|                     value = null;
 | ||
|                     state = "ok";
 | ||
|                 },
 | ||
|                 ovalue = function() {
 | ||
|                     value = null;
 | ||
|                     state = "ocomma";
 | ||
|                 },
 | ||
|                 firstavalue = function() {
 | ||
|                     value = null;
 | ||
|                     state = "acomma";
 | ||
|                 },
 | ||
|                 avalue = function() {
 | ||
|                     value = null;
 | ||
|                     state = "acomma";
 | ||
|                 }
 | ||
|             }
 | ||
|         };
 | ||
| 
 | ||
|         //
 | ||
| 
 | ||
|         state = "go";
 | ||
|         stack = [];
 | ||
| 
 | ||
|         // current tokenizeing position
 | ||
|         local start = 0;
 | ||
| 
 | ||
|         try {
 | ||
| 
 | ||
|             local
 | ||
|             result,
 | ||
|             token,
 | ||
|             tokenizer = _JSONTokenizer();
 | ||
| 
 | ||
|             while (token = tokenizer.nextToken(str, start)) {
 | ||
| 
 | ||
|                 if ("ptfn" == token.type) {
 | ||
|                     // punctuation/true/false/null
 | ||
|                     action[token.value][state]();
 | ||
|                 } else if ("number" == token.type) {
 | ||
|                     // number
 | ||
|                     value = token.value;
 | ||
|                     number[state]();
 | ||
|                 } else if ("string" == token.type) {
 | ||
|                     // string
 | ||
|                     value = tokenizer.unescape(token.value);
 | ||
|                     string[state]();
 | ||
|                 }
 | ||
| 
 | ||
|                 start += token.length;
 | ||
|             }
 | ||
| 
 | ||
|         } catch (e) {
 | ||
|             state = e;
 | ||
|         }
 | ||
| 
 | ||
|         // check is the final state is not ok
 | ||
|         // or if there is somethign left in the str
 | ||
|         if (state != "ok" || regexp("[^\\s]").capture(str, start)) {
 | ||
|             local min = @(a, b) a< b ? a : b;
 | ||
|             local near = str.slice(start, min(str.len(), start + 10));
 | ||
|             throw "JSON Syntax Error near `" + near + "`";
 | ||
|         }
 | ||
| 
 | ||
|         return value;
 | ||
|     }
 | ||
| 
 | ||
|     /**
 | ||
|      * Convert strings/numbers
 | ||
|      * Uses custom converter function
 | ||
|      *
 | ||
|      * @param {string} value
 | ||
|      * @param {string} type
 | ||
|      * @param {function|null} converter
 | ||
|      */
 | ||
|     function _convert(value, type, converter) {
 | ||
|         if ("function" == typeof converter) {
 | ||
| 
 | ||
|             // # of params for converter function
 | ||
| 
 | ||
|             local parametercCount = 2;
 | ||
| 
 | ||
|             // .getinfos() is missing on ei platform
 | ||
|             if ("getinfos" in converter) {
 | ||
|                 parametercCount = converter.getinfos().parameters.len() -
 | ||
|                     1 /* "this" is also included */ ;
 | ||
|             }
 | ||
| 
 | ||
|             if (parametercCount == 1) {
 | ||
|                 return converter(value);
 | ||
|             } else if (parametercCount == 2) {
 | ||
|                 return converter(value, type);
 | ||
|             } else {
 | ||
|                 throw "Error: converter function must take 1 or 2 parameters"
 | ||
|             }
 | ||
| 
 | ||
|         } else if ("number" == type) {
 | ||
|             return (value.find(".") == null && value.find("e") == null && value.find("E") == null) ? value.tointeger() : value.tofloat();
 | ||
|         } else {
 | ||
|             return value;
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * JSON Tokenizer
 | ||
|  * @package JSONParser
 | ||
|  */
 | ||
| class _JSONTokenizer {
 | ||
| 
 | ||
|     _ptfnRegex = null;
 | ||
|     _numberRegex = null;
 | ||
|     _stringRegex = null;
 | ||
|     _ltrimRegex = null;
 | ||
|     _unescapeRegex = null;
 | ||
| 
 | ||
|     constructor() {
 | ||
|         // punctuation/true/false/null
 | ||
|         this._ptfnRegex = regexp("^(?:\\,|\\:|\\[|\\]|\\{|\\}|true|false|null)");
 | ||
| 
 | ||
|         // numbers
 | ||
|         this._numberRegex = regexp("^(?:\\-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)");
 | ||
| 
 | ||
|         // strings
 | ||
|         this._stringRegex = regexp("^(?:\\\"((?:[^\\r\\n\\t\\\\\\\"]|\\\\(?:[\"\\\\\\/trnfb]|u[0-9a-fA-F]{4}))*)\\\")");
 | ||
| 
 | ||
|         // ltrim pattern
 | ||
|         this._ltrimRegex = regexp("^[\\s\\t\\n\\r]*");
 | ||
| 
 | ||
|         // string unescaper tokenizer pattern
 | ||
|         this._unescapeRegex = regexp("\\\\(?:(?:u\\d{4})|[\\\"\\\\/bfnrt])");
 | ||
|     }
 | ||
| 
 | ||
|     /**
 | ||
|      * Get next available token
 | ||
|      * @param {string} str
 | ||
|      * @param {integer} start
 | ||
|      * @return {{type,value,length}|null}
 | ||
|      */
 | ||
|     function nextToken(str, start = 0) {
 | ||
| 
 | ||
|         local
 | ||
|         m,
 | ||
|         type,
 | ||
|         token,
 | ||
|         value,
 | ||
|         length,
 | ||
|         whitespaces;
 | ||
| 
 | ||
|         // count # of left-side whitespace chars
 | ||
|         whitespaces = this._leadingWhitespaces(str, start);
 | ||
|         start += whitespaces;
 | ||
| 
 | ||
|         if (m = this._ptfnRegex.capture(str, start)) {
 | ||
|             // punctuation/true/false/null
 | ||
|             value = str.slice(m[0].begin, m[0].end);
 | ||
|             type = "ptfn";
 | ||
|         } else if (m = this._numberRegex.capture(str, start)) {
 | ||
|             // number
 | ||
|             value = str.slice(m[0].begin, m[0].end);
 | ||
|             type = "number";
 | ||
|         } else if (m = this._stringRegex.capture(str, start)) {
 | ||
|             // string
 | ||
|             value = str.slice(m[1].begin, m[1].end);
 | ||
|             type = "string";
 | ||
|         } else {
 | ||
|             return null;
 | ||
|         }
 | ||
| 
 | ||
|         token = {
 | ||
|             type = type,
 | ||
|             value = value,
 | ||
|             length = m[0].end - m[0].begin + whitespaces
 | ||
|         };
 | ||
| 
 | ||
|         return token;
 | ||
|     }
 | ||
| 
 | ||
|     /**
 | ||
|      * Count # of left-side whitespace chars
 | ||
|      * @param {string} str
 | ||
|      * @param {integer} start
 | ||
|      * @return {integer} number of leading spaces
 | ||
|      */
 | ||
|     function _leadingWhitespaces(str, start) {
 | ||
|         local r = this._ltrimRegex.capture(str, start);
 | ||
| 
 | ||
|         if (r) {
 | ||
|             return r[0].end - r[0].begin;
 | ||
|         } else {
 | ||
|             return 0;
 | ||
|         }
 | ||
|     }
 | ||
| 
 | ||
|     // unesacape() replacements table
 | ||
|     _unescapeReplacements = {
 | ||
|         "b": "\b",
 | ||
|         "f": "\f",
 | ||
|         "n": "\n",
 | ||
|         "r": "\r",
 | ||
|         "t": "\t"
 | ||
|     };
 | ||
| 
 | ||
|     /**
 | ||
|      * Unesacape string escaped per JSON standard
 | ||
|      * @param {string} str
 | ||
|      * @return {string}
 | ||
|      */
 | ||
|     function unescape(str) {
 | ||
|         local start = 0;
 | ||
|         local res = "";
 | ||
| 
 | ||
|         while (start< str.len()) {
 | ||
|             local m = this._unescapeRegex.capture(str, start);
 | ||
| 
 | ||
|             if (m) {
 | ||
|                 local token = str.slice(m[0].begin, m[0].end);
 | ||
| 
 | ||
|                 // append chars before match
 | ||
|                 local pre = str.slice(start, m[0].begin);
 | ||
|                 res += pre;
 | ||
| 
 | ||
|                 if (token.len() == 6) {
 | ||
|                     // unicode char in format \uhhhh, where hhhh is hex char code
 | ||
|                     // todo: convert \uhhhh chars
 | ||
|                     res += token;
 | ||
|                 } else {
 | ||
|                     // escaped char
 | ||
|                     // @see http://www.json.org/
 | ||
|                     local char = token.slice(1);
 | ||
| 
 | ||
|                     if (char in this._unescapeReplacements) {
 | ||
|                         res += this._unescapeReplacements[char];
 | ||
|                     } else {
 | ||
|                         res += char;
 | ||
|                     }
 | ||
|                 }
 | ||
| 
 | ||
|             } else {
 | ||
|                 // append the rest of the source string
 | ||
|                 res += str.slice(start);
 | ||
|                 break;
 | ||
|             }
 | ||
| 
 | ||
|             start = m[0].end;
 | ||
|         }
 | ||
| 
 | ||
|         return res;
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| 
 | ||
| // Copyright (c) 2017 Electric Imp
 | ||
| // This file is licensed under the MIT License
 | ||
| // http://opensource.org/licenses/MIT
 | ||
| 
 | ||
| class JSONEncoder {
 | ||
| 
 | ||
|     static VERSION = "2.0.0";
 | ||
| 
 | ||
|     // max structure depth
 | ||
|     // anything above probably has a cyclic ref
 | ||
|     static _maxDepth = 32;
 | ||
| 
 | ||
|     /**
 | ||
|      * Encode value to JSON
 | ||
|      * @param {table|array|*} value
 | ||
|      * @returns {string}
 | ||
|      */
 | ||
|     function encode(value) {
 | ||
|         return this._encode(value);
 | ||
|     }
 | ||
| 
 | ||
|     /**
 | ||
|      * @param {table|array} val
 | ||
|      * @param {integer=0} depth – current depth level
 | ||
|      * @private
 | ||
|      */
 | ||
|     function _encode(val, depth = 0) {
 | ||
| 
 | ||
|         // detect cyclic reference
 | ||
|         if (depth > this._maxDepth) {
 | ||
|             throw "Possible cyclic reference";
 | ||
|         }
 | ||
|         local
 | ||
|         r = "",
 | ||
|             s = "",
 | ||
|             i = 0;
 | ||
| 
 | ||
|         switch (typeof val) {
 | ||
| 
 | ||
|             case "table":
 | ||
|             case "class":
 | ||
|                 s = "";
 | ||
| 
 | ||
|                 // serialize properties, but not functions
 | ||
|                 foreach(k, v in val) {
 | ||
|                     if (typeof v != "function") {
 | ||
|                         s += ",\"" + k + "\":" + this._encode(v, depth + 1);
 | ||
|                     }
 | ||
|                 }
 | ||
| 
 | ||
|                 s = s.len() > 0 ? s.slice(1) : s;
 | ||
|                 r += "{" + s + "}";
 | ||
|                 break;
 | ||
| 
 | ||
|             case "array":
 | ||
|                 s = "";
 | ||
| 
 | ||
|                 for (i = 0; i< val.len(); i++) {
 | ||
|                     s += "," + this._encode(val[i], depth + 1);
 | ||
|                 }
 | ||
| 
 | ||
|                 s = (i > 0) ? s.slice(1) : s;
 | ||
|                 r += "[" + s + "]";
 | ||
|                 break;
 | ||
| 
 | ||
|             case "integer":
 | ||
|             case "float":
 | ||
|             case "bool":
 | ||
|                 r += val;
 | ||
|                 break;
 | ||
| 
 | ||
|             case "null":
 | ||
|                 r += "null";
 | ||
|                 break;
 | ||
| 
 | ||
|             case "instance":
 | ||
| 
 | ||
|                 if ("_serializeRaw" in val && typeof val._serializeRaw == "function") {
 | ||
| 
 | ||
|                     // include value produced by _serializeRaw()
 | ||
|                     r += val._serializeRaw().tostring();
 | ||
| 
 | ||
|                 } else if ("_serialize" in val && typeof val._serialize == "function") {
 | ||
| 
 | ||
|                     // serialize instances by calling _serialize method
 | ||
|                     r += this._encode(val._serialize(), depth + 1);
 | ||
| 
 | ||
|                 } else {
 | ||
| 
 | ||
|                     s = "";
 | ||
| 
 | ||
|                     try {
 | ||
| 
 | ||
|                         // iterate through instances which implement _nexti meta-method
 | ||
|                         foreach(k, v in val) {
 | ||
|                             s += ",\"" + k + "\":" + this._encode(v, depth + 1);
 | ||
|                         }
 | ||
| 
 | ||
|                     } catch (e) {
 | ||
| 
 | ||
|                         // iterate through instances w/o _nexti
 | ||
|                         // serialize properties, but not functions
 | ||
|                         foreach(k, v in val.getclass()) {
 | ||
|                             if (typeof v != "function") {
 | ||
|                                 s += ",\"" + k + "\":" + this._encode(val[k], depth + 1);
 | ||
|                             }
 | ||
|                         }
 | ||
| 
 | ||
|                     }
 | ||
| 
 | ||
|                     s = s.len() > 0 ? s.slice(1) : s;
 | ||
|                     r += "{" + s + "}";
 | ||
|                 }
 | ||
| 
 | ||
|                 break;
 | ||
| 
 | ||
|             case "blob":
 | ||
|                 // This is a workaround for a known bug:
 | ||
|                 // on device side Blob.tostring() returns null
 | ||
|                 // (instaead of an empty string)
 | ||
|                 r += "\"" + (val.len() ? this._escape(val.tostring()) : "") + "\"";
 | ||
|                 break;
 | ||
| 
 | ||
|                 // strings and all other
 | ||
|             case "string":
 | ||
|                 r += "\"" + this._escape(val) + "\"";
 | ||
|                 break;
 | ||
|             default:
 | ||
|                 r += "\"" + this._escape(val.tostring()) + "\"";
 | ||
|                 break;
 | ||
|         }
 | ||
| 
 | ||
|         return r;
 | ||
|     }
 | ||
| 
 | ||
|     /**
 | ||
|      * Escape strings according to http://www.json.org/ spec
 | ||
|      * @param {string} str
 | ||
|      */
 | ||
|     function _escape(str) {
 | ||
|         local res = "";
 | ||
| 
 | ||
|         for (local i = 0; i< str.len(); i++) {
 | ||
| 
 | ||
|             local ch1 = (str[i] & 0xFF);
 | ||
| 
 | ||
|             if ((ch1 & 0x80) == 0x00) {
 | ||
|                 // 7-bit Ascii
 | ||
| 
 | ||
|                 ch1 = format("%c", ch1);
 | ||
| 
 | ||
|                 if (ch1 == "\"") {
 | ||
|                     res += "\\\"";
 | ||
|                 } else if (ch1 == "\\") {
 | ||
|                     res += "\\\\";
 | ||
|                 } else if (ch1 == "/") {
 | ||
|                     res += "\\/";
 | ||
|                 } else if (ch1 == "\b") {
 | ||
|                     res += "\\b";
 | ||
|                 } else if (ch1 == "\f") {
 | ||
|                     res += "\\f";
 | ||
|                 } else if (ch1 == "\n") {
 | ||
|                     res += "\\n";
 | ||
|                 } else if (ch1 == "\r") {
 | ||
|                     res += "\\r";
 | ||
|                 } else if (ch1 == "\t") {
 | ||
|                     res += "\\t";
 | ||
|                 } else if (ch1 == "\0") {
 | ||
|                     res += "\\u0000";
 | ||
|                 } else {
 | ||
|                     res += ch1;
 | ||
|                 }
 | ||
|             } else {
 | ||
| 
 | ||
|                 if ((ch1 & 0xE0) == 0xC0) {
 | ||
|                     // 110xxxxx = 2-byte unicode
 | ||
|                     local ch2 = (str[++i] & 0xFF);
 | ||
|                     res += format("%c%c", ch1, ch2);
 | ||
|                 } else if ((ch1 & 0xF0) == 0xE0) {
 | ||
|                     // 1110xxxx = 3-byte unicode
 | ||
|                     local ch2 = (str[++i] & 0xFF);
 | ||
|                     local ch3 = (str[++i] & 0xFF);
 | ||
|                     res += format("%c%c%c", ch1, ch2, ch3);
 | ||
|                     // return str;
 | ||
|                 } else if ((ch1 & 0xF8) == 0xF0) {
 | ||
|                     // 11110xxx = 4 byte unicode
 | ||
|                     local ch2 = (str[++i] & 0xFF);
 | ||
|                     local ch3 = (str[++i] & 0xFF);
 | ||
|                     local ch4 = (str[++i] & 0xFF);
 | ||
|                     res += format("%c%c%c%c", ch1, ch2, ch3, ch4);
 | ||
|                 }
 | ||
| 
 | ||
|             }
 | ||
|         }
 | ||
|         return res;
 | ||
|     }
 | ||
| } |