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;
|
||
}
|
||
} |