JsonParserByLeo/Test/Other/toyjson3.mqh
2026-06-29 14:22:15 -05:00

1263 行
37 KiB
MQL5

//+------------------------------------------------------------------+
//| toyjson3.mqh |
//| Copyright 2022-2025, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#include "..\\..\\..\\MQL5Book\\Include\\MapArray.mqh"
#include "..\\..\\..\\MQL5Book\\Include\\AutoPtr.mqh"
#include "..\\..\\..\\MQL5Book\\Include\\Defines.mqh"
#include "..\\..\\..\\MQL5Book\\Include\\StringUtils.mqh"
enum JS_TYPE
{
JS_OBJECT,
JS_ARRAY,
JS_PRIMITIVE,
JS_NULL, // singleton object stub for undefined props
JS_ERROR
};
enum JS_AFFINITY // for primitive type
{
JS_NONE = 0,
JS_CONST = 1, // true, false, null
JS_INT = 2,
JS_FLOAT = 4,
JS_STRING = 8
};
#define JS_EOF 26 // Ctrl-Z
#ifndef JS_STACK_LIMIT
#define JS_STACK_LIMIT 50
#endif
#ifndef ArrayContains
template<typename T>
int ArrayIndexOf(const T &a[], const T v)
{
for(int i = 0; i < ArraySize(a); i++)
{
if(a[i] == v) return i;
}
return -1;
}
#define ArrayContains(A,V) (ArrayIndexOf(A,(V)) > -1)
#endif
//+------------------------------------------------------------------+
//| Main class to store single value (primitive, object, or array) |
//+------------------------------------------------------------------+
class JsValue
{
MapArray<string, int> properties;
JsValue *objects[];
public:
const static JsValue null;
const string s; // value is assigned once at construction
const JS_TYPE t; // type and
const JS_AFFINITY a; // affinity as detected
JsValue(const string _s): s(_s), t(JS_PRIMITIVE), a(detect()) { }
JsValue(const JS_TYPE _t = JS_OBJECT): t(_t), s(NULL), a(JS_NONE) { }
JsValue(JsValue *that): s(that.s), t(that.t), a(that.a)
{
for(int i = 0; i < that.size(); i++)
{
if(that[i].t >= JS_PRIMITIVE)
{
this.put(t == JS_OBJECT ? that.props().getKey(i) : NULL, that[i].s);
}
else
{
this.put(t == JS_OBJECT ? that.props().getKey(i) : NULL, new JsValue(that[i]));
}
}
}
~JsValue()
{
for(int i = 0; i < ArraySize(objects); i++)
{
FREE(objects[i]);
}
}
bool merge(JsValue *that, const bool move = false)
{
if(this.t != that.t) return false;
if(this.t >= JS_PRIMITIVE) return false;
for(int i = 0; i < that.size(); i++)
{
if(that[i].t >= JS_PRIMITIVE)
{
if(move)
{
this.put(that.t == JS_OBJECT ? that.props().getKey(i) : NULL, that[i]);
}
else
{
this.put(that.t == JS_OBJECT ? that.props().getKey(i) : NULL, that[i].s);
}
}
else
{
if(move)
{
this.put(that.t == JS_OBJECT ? that.props().getKey(i) : NULL, that[i]);
}
else
{
this.put(that.t == JS_OBJECT ? that.props().getKey(i) : NULL, move ? that[i] : new JsValue(that[i]));
}
}
if(move) that.drop(i);
}
return true;
}
int length() const
{
return ArraySize(objects);
}
const MapArray<string, int> * const props() const
{
return &properties;
}
// simplistic state-machine approach
static JS_AFFINITY isNumeric(const ushort c, const JS_AFFINITY state = 0)
{
if(c == '+' || c == '-')
{
if(state == JS_NONE || ((state & JS_FLOAT) == JS_FLOAT)) return state; // leading sign or in exponent
}
else if(c >= '0' && c <= '9') return JS_INT;
else if(c == '.')
{
if(state < JS_FLOAT) return JS_FLOAT; // one point is a valid number
}
else if(c == 'e' || c == 'E') // PRB: multiple E's aren't catched as string
{
if(state >= JS_INT) return JS_FLOAT; // exponent is allowed provided digits exist
}
else if(c == ' ' || c == '\r' || c == '\n' || c == '\t') return JS_NONE;
return JS_STRING; // also used for datetime, not existing in JSON
}
JS_AFFINITY detect() const
{
if(s == "true" || s == "false" || s == "null") return JS_CONST;
if(s == "") return JS_STRING;
JS_AFFINITY result = 0;
for(int i = 0; i < StringLen(s) && result < JS_STRING; ++i)
{
result |= isNumeric(s[i], result);
}
return result;
}
string stringify(const uint allocate = 0, const int pretty = -1) const
{
if(t < JS_PRIMITIVE)
{
string buffer;
if(allocate) StringReserve(buffer, allocate);
stringify(buffer, pretty);
return buffer;
}
if(a >= JS_STRING)
{
string ss = s;
StringReplace(ss, "\"", "\\\"");
return "\"" + ss + "\"";
}
if(a >= JS_FLOAT) return s; // FIXME: apply specific accuracy
if(a >= JS_INT) return s;
return s != NULL ? s : "null";
}
template<typename T>
T get(const bool typechecks = true, T def = (T)NULL) const
{
if(typechecks)
{
if(t != JS_PRIMITIVE) return def;
if(a >= JS_STRING && typename(T) != "string") return def;
if(a == JS_CONST && typename(T) != "bool") return def;
}
// return (T)s;
T r;
cast(s, r);
return r;
}
template<typename T>
void cast(const string &in, T &out) const
{
out = (T)s;
}
void cast(const string &in, bool &out) const
{
out = !this.operator!();
}
bool operator!() const
{
if(t != JS_PRIMITIVE) return !(t < JS_PRIMITIVE); // NULL object is falsy
if(a >= JS_STRING) return !(StringLen(s) > 0);
if(a == JS_CONST) return !(s == "true");
return !(double)(s);
}
template<typename T>
bool operator==(const T value) const
{
if(t < JS_PRIMITIVE) return false;
if(a >= JS_STRING && typename(T) != "string") return false;
if(a == JS_CONST && typename(T) != "bool") return false;
T temp = this.get<T>(false);
return temp == value;
}
// check if 'that' is a subset of 'this',
// 'fast' false means deep look for detailed changes and strict comparison
bool match(const JsValue *that, const bool fast = true, JsValue *details = NULL)
{
bool result = true;
if(this.t != that.t)
{
if(details)
{
details.put("(Type change)", StringFormat("(%s->%s)", EnumToString(this.t), EnumToString(that.t)));
}
return false;
}
if(this.t == JS_PRIMITIVE)
{
if(details && this.s != that.s)
{
details.put("(Value change)", StringFormat("(%s;%s->%s)", EnumToString(this.a), this.s, that.s));
}
return this.s == that.s;
}
if(this.t == JS_OBJECT)
{
if(fast && this.size() != that.size())
{
if(details)
{
details.put("(Object size change)", StringFormat("(%d->%d)", this.size(), that.size()));
}
return false;
}
static string keys1[], keys2[];
that.getProperties(keys2);
if(fast)
{
this.getProperties(keys1);
if(ArrayCompare(keys1, keys2))
{
if(details)
{
details.put("(Object keys differ)", StringFormat("`[[%s],[%s]]", SubArrayCombine(keys1, ","), SubArrayCombine(keys2, ",")));
}
return false;
}
}
else
{
for(int i = 0; i < ArraySize(keys2); i++)
{
if(this[keys2[i]].t == JS_NULL)
{
//if(details)
//{
// details.put("Key is missing", keys2[i]); // will be logged below
//}
result = false;
}
}
}
}
else
{
if(fast && this.length() != that.length())
{
if(details)
{
details.put("(Array length change)", StringFormat("(%d->%d)", this.length(), that.length()));
}
return false;
}
}
if(fast)
{
for(int i = 0; i < ArraySize(objects); i++)
{
JsValue *sub = details ? new JsValue() : NULL;
if(!objects[i].match(that[i], fast, sub))
{
if(details)
{
if(sub.size()) details.put(this.t == JS_OBJECT ? properties.getKey(i) : StringFormat("%d", i), sub);
else delete sub;
}
result = false;
if(!details) return false;
// for details keep looping and comparing
}
if(sub && !sub.size()) delete sub;
}
}
else
{
int matches = 0;
int foundinthis[];
int foundinthat[];
// collect changes preliminary
for(int i = 0; i < that.length() /*&& matches < that.length()*/; i++)
{
const JsValue *other = that[i];
//if(ArrayContains(foundinthat, i)) continue; // fast but incomplete
for(int j = 0; j < ArraySize(objects); j++)
{
//if(ArrayContains(foundinthis, j)) continue; // fast but incomplete
if(objects[j].match(other, fast, NULL))
{
//PUSH(foundinthis, j); // fast but incomplete
//PUSH(foundinthat, i); // fast but incomplete
bool update = true;
int i1 = ArrayIndexOf(foundinthis, j);
int i2 = ArrayIndexOf(foundinthat, i);
// look at corresponding counterparts
int ip = -1, jp = -1;
if(i1 > -1) jp = foundinthat[i1];
if(i2 > -1) ip = foundinthis[i2];
if(jp > -1 && ip > -1 && fabs(ip - jp) <= fabs(i - j)) // speculation on closer changes
{
update = false;
}
if(update)
{
if(i1 > -1) foundinthis[i1] = -1;
PUSH(foundinthis, j);
if(i2 > -1) foundinthat[i2] = -1;
PUSH(foundinthat, i);
}
matches++;
}
}
}
matches = 0; // that.length();
for(int i = 0; i < ArraySize(foundinthat); i++)
{
if(foundinthat[i] != -1 && foundinthat[i] == foundinthis[i]) matches++;
}
if(matches < that.length())
{
if(details)
{
// collect details about differences
for(int i = 0; i < that.length(); i++)
{
const int p = ArrayIndexOf(foundinthis, i); // ArrayIndexOf(foundinthis, i);
if(p != -1 && foundinthat[p] == i) continue;
int k = ArrayIndexOf(foundinthat, i);
if(k > -1) k = foundinthis[k];
if(k == -1 && i < this.length()) k = i; // speculation about change at the same i, modification
if(k == -1)
{
details.put(that.t == JS_OBJECT ? that.props().getKey(i) : StringFormat("%d", i), "(new, missing in old)");
continue;
}
// here we have 'that' element missing in 'this'
JsValue *sub = new JsValue();
objects[k].match(that[i], fast, sub);
if(sub.size())
{
details.put(that.t == JS_OBJECT ? that.props().getKey(i) : StringFormat("%d", i), sub);
}
else
{
delete sub;
matches++;
}
}
#ifdef TOYJSON_SHOW_STATS
if(matches < that.length())
details.put("(Incomplete pattern match)", StringFormat("(%d of %d)", matches, that.length()));
#endif
}
if(matches < that.length())
{
result = false;
if(!details) return false;
}
}
// uncomment this to make comparison symmetrical between 'this' and 'that'
#ifdef TOYJSON_MIRROR_MATCH
matches = 0; // that.length();
for(int i = 0; i < ArraySize(foundinthis); i++)
{
if(foundinthis[i] != -1 && foundinthat[i] == foundinthis[i]) matches++;
}
if(matches < this.length())
{
if(details)
{
for(int j = 0; j < ArraySize(objects); j++)
{
const int p = ArrayIndexOf(foundinthis, j);
if(p != -1 && foundinthat[p] == j) continue;
int k = ArrayIndexOf(foundinthis, j);
if(k > -1) k = foundinthat[k];
if(k == -1 && j < that.length()) k = j; // speculation about change at the same j, modification
if(k == -1)
{
details.put(this.t == JS_OBJECT ? this.props().getKey(j) : StringFormat("%d", j), "(old, missing in new)");
continue;
}
// here we have 'this' element missing in 'that' (which can be normal if 'that' is a subset, "search pattern")
JsValue *sub = new JsValue();
objects[j].match(that[k], fast, sub);
if(sub.size())
{
details.put(this.t == JS_OBJECT ? properties.getKey(j) : StringFormat("%d", j), sub);
}
else
{
delete sub;
matches++;
}
}
#ifdef TOYJSON_SHOW_STATS
if(matches < this.length())
details.put("(Coverage)", StringFormat("(%d of %d)", matches, this.length()));
#endif
}
// we don't return false here, because 'that' is considered a search pattern (subset) for 'this',
// hence some parts existing in 'this' but missing in 'that' does still mean that 'this' fulfills all props of 'that'
// result = false;
}
#endif
}
return result;
}
int size() const
{
if(t == JS_OBJECT) return properties.getSize();
else if(t == JS_ARRAY) return ArraySize(objects);
return 0;
}
// object putters
template<typename T>
bool try_parse(const string key, const T value)
{
return false; // all except strings are non-parseable
}
bool try_parse(const string key, const string value)
{
if(value[0] == '`')
{
put(key, JsParser::jsonify(StringSubstr(value, 1)));
return true;
}
return false;
}
template<typename T>
void put(const string key, const T value)
{
if(try_parse(key, value)) return;
put(key, new JsValue((string)value));
}
void put(const string key, const double value, const int digits = 8)
{
put(key, new JsValue(DoubleToString(value, digits)));
}
void put(const string key, JsValue *value)
{
if(t == JS_NULL) return; // show bulk errors?
if(t != JS_OBJECT)
{
if(key != NULL)
{
PrintFormat("WARNING: Setting property '%s' for non-object", key);
}
}
else if(key == NULL)
{
Print("WARNING: Setting NULL property for object");
}
int p = key == NULL ? -1 : properties.find(key);
if(p == -1)
{
p = EXPAND(objects);
if(key != NULL) properties.put(key, p);
}
else
{
FREE(objects[p]);
}
objects[p] = value;
}
// array putters
template<typename T>
void put(const T value)
{
put(NULL, value);
}
template<typename T>
void replace(const int i, const T value)
{
if(t >= JS_PRIMITIVE) return;
if(i < 0 || i >= ArraySize(objects)) return;
FREE(objects[i]);
objects[i] = new JsValue((string)value);
}
template<typename T>
void put(const T &values[])
{
for(int i = 0; i < ArraySize(values); i++)
{
put(NULL, new JsValue((string)values[i]));
}
}
void put(const double value, const int digits = 8)
{
put(NULL, new JsValue(DoubleToString(value, digits)));
}
void put(JsValue *value)
{
if(t != JS_ARRAY)
{
Print("WARNING: Setting indexed element for non-array: ", value.stringify());
}
put(NULL, value);
}
void replace(const int i, JsValue *value)
{
if(t >= JS_PRIMITIVE) return;
/*
if(t != JS_ARRAY)
{
PrintFormat("WARNING: Setting %d-th element for non-array (%s): %s", i, EnumToString(t), value.stringify());
}
*/
if(i < 0 || i >= ArraySize(objects)) return;
FREE(objects[i]);
objects[i] = value;
}
void drop(const int i)
{
if(t >= JS_PRIMITIVE) return;
/*
if(t != JS_ARRAY)
{
PrintFormat("WARNING: Dropping %d-th element of non-array (%s): %s", i, EnumToString(t), s);
}
*/
if(i < 0 || i >= ArraySize(objects)) return;
objects[i] = (JsValue *)&null;
}
// indexed access
JsValue *operator[](const string name) // const
{
const int p = properties.find(name);
if(p == -1) return (JsValue *)&null;
return objects[p];
}
JsValue *operator[](const int i) const
{
if(i >= ArraySize(objects)) return (JsValue *)&null;
if(i < 0)
{
if(-i <= ArraySize(objects)) return objects[ArraySize(objects) + i];
return (JsValue *)&null;
}
return objects[i];
}
bool getProperties(string &keys[]) const
{
if(t != JS_OBJECT) return false;
ArrayResize(keys, properties.getSize());
for(int i = 0; i < properties.getSize(); i++)
{
keys[i] = properties.getKey(i);
}
return true;
}
int indexOf(const string value)
{
if(t != JS_ARRAY) return -1;
for(int i = 0; i < ArraySize(objects); i++)
{
if(objects[i].s == value) return i;
}
return -1;
}
// aux stuff
void print(const int maxlevel = INT_MAX) const
{
int level = 0;
print(level, maxlevel);
}
void print(int &level, const int maxlevel) const
{
const static string open[] = {"{", "[", "", "null", "ERROR: "};
const static string close[] = {"}", "]", "", "", "!"};
Print(StringFormat("%*s", level * 2, ""), open[t], s);
++level;
if(level < maxlevel)
{
const string padding = StringFormat("%*s", level * 2, "");
if(properties.getSize() >= ArraySize(objects))
{
for(int i = 0; i < properties.getSize(); ++i)
{
if(objects[i])
{
if(objects[i].s != NULL)
{
Print(padding, properties.getKey(i), " = ", objects[i].s);
}
else
{
Print(padding, properties.getKey(i), " = ");
objects[i].print(level, maxlevel);
}
}
else Print(padding, "<N/A>");
}
}
else // if(t == JS_ARRAY)
{
for(int i = 0; i < ArraySize(objects); ++i)
{
if(objects[i])
{
if(objects[i].s != NULL)
{
Print(padding, objects[i].s);
}
else
{
objects[i].print(level, maxlevel);
}
}
else Print(padding, "<N/A>");
}
}
}
--level;
if(t < JS_PRIMITIVE) Print(StringFormat("%*s", level * 2, ""), close[t]);
}
void stringify(string &buffer, const int pretty = 0) const
{
const static string open[] = {"{", "[", "", "null", "ERROR: "};
const static string close[] = {"}", "]", "", "", "!"};
const string padding = pretty && t <= JS_ARRAY ? StringFormat("\n%*s", fmax(pretty, 0) * 2, "") : "";
const string inner = pretty && t <= JS_ARRAY ? " " : "";
const bool inprocess = StringLen(buffer);
if(StringLen(buffer) << 1 > (int)StringBufferLen(buffer)) StringReserve(buffer, StringBufferLen(buffer) << 1);
StringAdd(buffer, (inprocess && size() ? padding : "") + open[t]);
for(int i = 0; i < ArraySize(objects); ++i)
{
if(i > 0) StringAdd(buffer, ", ");
if(t != JS_ARRAY) StringAdd(buffer, padding + inner + "\"" + properties.getKey(i) + "\" : ");
if(objects[i].s != NULL)
{
StringAdd(buffer, (!i && t == JS_ARRAY ? (padding + inner) : "") + objects[i].stringify());
}
else
{
objects[i].stringify(buffer, pretty ? fmax(pretty, 0) + 1 : pretty);
}
}
// TODO: single value json produces empty output!
if(s != NULL) StringAdd(buffer, padding + inner + s);
StringAdd(buffer, (ArraySize(objects) ? padding : "") + close[t]);
}
string flatten(const bool compact = true, const string prefix = "@")
{
string buffer;
flatten(&this, buffer, compact, prefix);
return buffer;
}
static void flatten(JsValue *element, string &buffer, const bool compact = true, const string prefix = "@")
{
if(element.t == JS_OBJECT)
{
if(!compact)
buffer += prefix + " = {..." + "};\n"; // 0x2026
}
else if(element.t == JS_ARRAY)
{
if(!compact)
buffer += prefix + " = [..." + "];\n";
}
else
{
buffer += prefix + "=" + element.s + ";\n";
return;
}
for(int i = 0; i < element.size(); i++)
{
string key = (element.t == JS_OBJECT) ? element.props().getKey(i) : "";
if(!StringLen(key)) key = "[" + (string)i + "]";
else key = "[" + key + "]";
flatten(element[i], buffer, compact, prefix + key);
}
}
static JsValue *toUniformCase(JsValue *source, const bool upper = false)
{
if(source.t == JS_PRIMITIVE)
{
if(source.a == JS_STRING)
{
string s = source.s;
if(upper) StringToUpper(s);
else StringToLower(s);
return new JsValue(s);
}
return new JsValue(source);
}
else
{
JsValue *result = new JsValue(source.t);
if(source.t == JS_OBJECT)
{
string keys[];
source.getProperties(keys);
for(int i = 0; i < source.size(); i++)
{
result.put(keys[i], toUniformCase(source[i], upper));
}
}
else
{
for(int i = 0; i < source.length(); i++)
{
result.put(toUniformCase(source[i], upper));
}
}
return result;
}
}
};
/* Can't do this due to MQL5 limitation (pointers vs values mixture)
class JsObject: public JsValue
{
public:
JsObject() : JsValue(JS_OBJECT) { }
};
class JsArray: public JsValue
{
public:
JsArray() : JsValue(JS_ARRAY) { }
};
*/
//+------------------------------------------------------------------+
//| Helper classes for stack overflow control |
//+------------------------------------------------------------------+
class JsCallController
{
public:
int counter;
JsCallController(): counter(0) { };
};
class JsCallCounter
{
JsCallController *owner;
public:
JsCallCounter(JsCallController *controller): owner(controller)
{
owner.counter++;
}
~JsCallCounter()
{
owner.counter--;
}
};
//+------------------------------------------------------------------+
//| Lightweight pseudo-string proxies on top of entire input text |
//+------------------------------------------------------------------+
struct JsToken
{
int offset; // points to data, control delimiter is at -1 (if not left trimmed)
int length;
ushort c;
};
//+------------------------------------------------------------------+
//| JSON parser class |
//+------------------------------------------------------------------+
class JsParser
{
public:
int cursor;
JsToken token2[];
int offsets[][2]; // [offset][line No]
bool error;
int error_cursor;
JsCallController stack;
string copy; // input document as is
bool isWhitespace(const ushort c)
{
return c == ' ' || c == '\r' || c == '\n' || c == '\t';
}
void tokenTrimLeft(const int i)
{
while(isWhitespace(copy[token2[i].offset]) && token2[i].offset < (i < ArraySize(token2) - 1 ? token2[i + 1].offset : StringLen(copy)))
{
token2[i].offset++;
token2[i].length--;
}
}
void tokenTrimRight(const int i)
{
while(isWhitespace(copy[token2[i].offset + token2[i].length - 1]) && token2[i].length > 0)
token2[i].length--;
}
string token(const int i) const // show token with leading delimiter
{
return ((token2[i].c != 1 && token2[i].c != JS_EOF) || token2[i].length ? ShortToString(token2[i].c) + StringSubstr(copy, token2[i].offset, token2[i].length) : "");
}
void tokenize()
{
//copy = context;
// lookup table for syntax chars
static const bool tbl[256] = {0,0,0,0,0,0,0,0,0,0,/*1*/0,0,0,/*1*/0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
// static const ushort ctrl[] = {'{', '}', '[', ']', ',', ':', '\r', '\n'};
bool quotes = false;
JsToken _t = {0, 0, (ushort)1}; // start with optional whitespace
PUSH(token2, _t);
int m = EXPAND(offsets);
offsets[m][0] = 0;
for(int i = 0; i < StringLen(copy); i++)
{
if(copy[i] == '"')
{
quotes = !quotes;
m = EXPAND(offsets);
offsets[m][0] = i;
JsToken t = {i + 1, 0, '"'};
PUSH(token2, t);
token2[m - 1].length = i - token2[m - 1].offset;
}
else
{
if(quotes) // inside quotes
{
if(copy[i] == '\\' && copy[i + 1] == '"')
{
i++; // skip next escaped quote
}
}
else
{
if(copy[i] < 256 && tbl[copy[i]])
{
m = EXPAND(offsets);
offsets[m][0] = i;
JsToken t = {i + 1, 0, copy[i]};
PUSH(token2, t);
token2[m - 1].length = i - token2[m - 1].offset;
}
}
}
}
if(ArraySize(token2))
{
token2[ArraySize(token2) - 1].length = StringLen(copy) - token2[ArraySize(token2) - 1].offset;
}
for(int i = 0; i < ArraySize(token2); i++)
{
if(token2[i].c != '"') tokenTrimLeft(i);
if(token2[i].c != '"') tokenTrimRight(i);
}
int lineno = 1;
for(int i = 0; i < ArrayRange(offsets, 0); ++i)
{
int position = offsets[i][0];
offsets[i][1] = lineno;
// CRLF \r\n - Windows
// LF \n - Linux/Unix
// CR \r - Mac
if(copy[position] == '\r') lineno++;
else if(copy[position] == '\n' && position && copy[position - 1] != '\r') lineno++;
}
// end of file
JsToken t = {StringLen(copy), 0, (ushort)(JS_EOF)};
PUSH(token2, t);
m = EXPAND(offsets);
offsets[m][0] = StringLen(copy);
offsets[m][1] = lineno;
}
int next() // pre-increment safeguard
{
if(cursor < ArraySize(token2) - 1) ++cursor;
return cursor;
}
int post() // post-increment safeguard
{
const int prev = cursor;
next();
return prev;
}
string parse_key()
{
if(token2[cursor].c == '"')
{
const JsToken t = token2[post()];
const string result = StringSubstr(copy, t.offset, t.length);
if(token2[cursor].c != '"')
{
if(!error) {PrintFormat("Closing '\"' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
return NULL;
}
next();
return result;
}
else
{
if(!error) {PrintFormat("Opening '\"' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
return NULL;
}
}
bool stack_guard()
{
if(stack.counter > JS_STACK_LIMIT)
{
if(!error) { Print("Too many nested structures, the limit exceeded: ", JS_STACK_LIMIT); error_cursor = cursor; }
error = true;
return false;
}
return true;
}
JsValue *parse_value(const bool skip = false)
{
JsCallCounter cnt(&stack);
if(!stack_guard()) return NULL;
if(token2[cursor].c == '"')
{
const JsToken t = token2[post()];
string v = StringSubstr(copy, t.offset, t.length);
while((token2[cursor].c != '"'
|| (copy[ token2[cursor - 1].offset + token2[cursor - 1].length - 1 ] == '\\'))
&& token2[cursor].c != JS_EOF)
{
const JsToken w = token2[post()];
v += StringSubstr(copy, w.offset, w.length);
}
StringReplace(v, "\\\"", "\"");
// TODO: unescape more chars? \u0000
// static const ushort specs[] = {'"', '\\', '\/', '\r', '\n', '\t', '\b', '\f'};
if(token2[cursor].c != '"')
{
if(!error) { PrintFormat("Unclosed string: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
}
else if(token2[cursor].length > 0)
{
tokenTrimRight(cursor);
if(token2[cursor].length > 0)
{
if(!error) { PrintFormat("Garbage after closing quote: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
}
}
next();
return new JsValue(v);
}
else if(token2[cursor].c == '{')
{
return parse_object();
}
else if(token2[cursor].c == '[' && !skip)
{
return parse_array();
}
else // primitive, should be ":123.456", ":true", ",123", "\"text\"", "[null"
{
JsValue *result = token2[cursor].length ? new JsValue(StringSubstr(copy, token2[cursor].offset, token2[cursor].length)) : new JsValue(NULL);
if(result.a == JS_NONE)
{
if(!error) { PrintFormat("Value expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
}
else if(result.a == JS_STRING && token2[cursor].c != '"')
{
if(!error) { PrintFormat("Unexpected string, no quotes: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
}
next();
return result;
}
}
JsValue *parse_array()
{
JsCallCounter cnt(&stack);
if(!stack_guard()) return NULL;
JsValue *current = NULL;
if(token2[cursor].c == '[')
{
current = new JsValue(JS_ARRAY);
const int i = cursor; // prevent reentrancy from parse_value
do
{
if((token2[cursor].c == '[' || token2[cursor].c == ',') && !token2[cursor].length) // skip leading '[' or iterative ','
{
next();
if(token2[cursor].c == ',')
{
if(!error) { PrintFormat("Extra comma: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
break;
}
}
if(token2[cursor].c == ']')
{
if(token2[cursor].length == 0) break; // valid array close-up, including empty one
if(!error) { PrintFormat("Delimiter after array expected, got some data: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
break;
}
current.put(parse_value(i == cursor)); // parse_value increments cursor internally
if(!(token2[cursor].c == ']' && !token2[cursor].length) && token2[cursor].c != ',')
{
if(!error) { PrintFormat("'],' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
return current;
}
}
while(token2[cursor].c != ']' && token2[cursor].c != JS_EOF);
next();
}
else
{
if(!error) { PrintFormat("'[' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
return NULL;
}
return current;
}
JsValue *parse_object()
{
JsCallCounter cnt(&stack);
if(!stack_guard()) return NULL;
JsValue *current = NULL;
if(token2[cursor].c == '{')
{
current = new JsValue(JS_OBJECT);
const int i = cursor;
do
{
if((token2[cursor].c == '{' || token2[cursor].c == ',') && !token2[cursor].length) // equivalent mod of toyjson2 ?
{
next();
}
if(token2[cursor].c == '}' && !token2[cursor].length && i == cursor - 1)
{
break; // empty object
}
string key = parse_key(); // parse_key increments cursor internally
if(token2[cursor].c != ':')
{
if(!error) { PrintFormat("':' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
return current;
}
if(token2[cursor].length == 0) next(); // can be ":<whitespace>" (if a string or object is next) or ":<whitespace>value"
current.put(key, parse_value()); // parse_value increments cursor internally
if(!(token2[cursor].c == '}' && !token2[cursor].length) && token2[cursor].c != ',')
{
if(!error) { PrintFormat("'},' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
return current;
}
}
while(token2[cursor].c != '}' && token2[cursor].c != JS_EOF);
next();
}
else
{
if(!error) { PrintFormat("'{' expected, got: '%s' @ %d", token(cursor), cursor); error_cursor = cursor; }
error = true;
return NULL;
}
return current;
}
public:
JsParser(): cursor(0), error_cursor(0) { }
JsValue *parse(const string &text)
{
tokenize();
// printTokens();
cursor = 0;
if(token2[cursor].c == (ushort)(1) && token2[cursor].length == 0) next(); // skip single start token (0x1) and whitespace, if present
error = false;
JsValue *result = parse_value();
if(!error)
{
if(cursor != ArraySize(token2) - 1)
{
Print("Trailing garbage detected at the end: @", cursor);
error_cursor = cursor;
error = true;
}
}
return result;
}
void printTokens(const bool context = false) const
{
if(context)
{
PrintFormat("Line No: %d, Offset: 0x%X/%d bytes", offsets[error_cursor][1], offsets[error_cursor][0], offsets[error_cursor][0]);
string line = "";
for(int i = error_cursor - 10; i <= error_cursor + 10; ++i)
{
if(i >= 0 && i < ArraySize(token2))
{
line += "[" + (string)i + "]:`" + token(i) + "` ";
}
}
Print(line);
}
else
{
ArrayPrint(token2);
}
}
static JsValue *jsonify(const string text)
{
JsParser parser;
JsValue *result = parser.parse(text);
if(parser.error)
{
parser.printTokens(true);
JsValue *error = new JsValue(JS_ERROR);
error.put("Bad JSON", result);
return error;
}
return result;
}
bool isValid() const
{
return !error;
}
};
const static JsValue JsValue::null(JS_NULL);
//+------------------------------------------------------------------+