//+------------------------------------------------------------------+ //| 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 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 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 * 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 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 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 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(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 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 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 void put(const T value) { put(NULL, value); } template 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 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, ""); } } 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, ""); } } } --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 ":" (if a string or object is next) or ":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); //+------------------------------------------------------------------+