Skip to content
Snippets Groups Projects

extended json

  • Clone with SSH
  • Clone with HTTPS
  • Embed
  • Share
    The snippet can be accessed without any authentication.
    Authored by Frying☆Pan
    Edited
    ExtendedJson.hx 11.64 KiB
    package;
    
    using StringTools;
    
    /**
       A JSON parser that supports slightly extended JSON syntax, including
       trailing commas and comments
    
       // TODO : make this a haxelib?
    **/
    class ExtendedJson {
       /**
          Parses a JSON with Comments string. Equivalent to `haxe.Json.parse(str)`.
       **/
       public static function parseJson(jsonString:String, ?options:ParserOptions):Dynamic
          return new ExtendedJson(options ?? {}).parse(jsonString);
    
       /**
          Stringify an object. Identical to `haxe.Json.stringify(object)`.
       **/
       public static inline function stringify(object:Dynamic):String
          return haxe.Json.stringify(object);
    
       final options:ParserOptions;
    
       public var allowMultiLineComments(get, set):Bool;
       public var allowSingleLineComments(get, set):Bool;
       public var allowTrailingCommas(get, set):Bool;
    
       var str:String = '';
       var pos:Int = 0;
    
       inline function next():Int
          #if EXTJSON_USE_FAST
          return str.fastCodeAt(pos++);
          #else
          return str.charCodeAt(pos++);
          #end
    
       inline function peek(offset = 0):Int
          #if EXTJSON_USE_FAST
          return str.fastCodeAt(pos + offset);
          #else
          return str.charCodeAt(pos + offset);
          #end
    
       function skip(offset = 1):Bool {
          pos += offset;
          return true;
       }
    
       inline function error(text:String)
          throw 'Parse error at $pos: $text';
    
       inline function invalidChar() {
          error('Invalid character `${str.charAt(--pos)}`');
       }
    
       #if EXTJSON_USE_FAST
       var isEof = StringTools.isEof;
       #else
       inline function isEof(char:Null<Int>):Bool
          return char == null;
       #end
    
       public function new(options:ParserOptions) {
          this.options = options;
       }
    
       /**
          Actually parse a string into an object.
       **/
       public function parse(string:String):Dynamic {
          // init
          str = string;
          pos = 0;
    
          var result = parseRec();
    
          // allow trailing whitespace and comments
          var c:Int;
          while (!isEof(c = next()))
             switch c {
                case ' '.code, '\r'.code, '\n'.code, '\t'.code:
                case '/'.code:
                   parseComment();
                default:
                   invalidChar();
             }
    
          // reset these
          str = '';
          pos = 0;
    
          // return the result
          return result;
       }
    
       function parseRec():Dynamic {
          var c:Int;
          while (true) {
             c = next();
             switch c {
                case ' '.code, '\r'.code, '\n'.code, '\t'.code: // whitespace
                case '/'.code:
                   parseComment();
                   return parseRec();
    
                case '{'.code:
                   return parseObject();
    
                case '['.code:
                   return parseArray();
    
                case 't'.code: // true
                   if (peek(0) == 'r'.code && peek(1) == 'u'.code && peek(2) == 'e'.code) {
                      skip(3);
                      return true;
                   }
                   invalidChar();
    
                case 'f'.code: // false
                   if (peek(0) == 'a'.code && peek(1) == 'l'.code && peek(2) == 's'.code && peek(3) == 'e'.code) {
                      skip(4);
                      return false;
                   }
                   invalidChar();
    
                case 'n'.code: // true
                   if (peek(0) == 'u'.code && peek(1) == 'l'.code && peek(2) == 'l'.code) {
                      skip(3);
                      return null;
                   }
                   invalidChar();
    
                case '"'.code:
                   return parseString();
    
                case '0'.code | '1'.code | '2'.code | '3'.code | '4'.code | '5'.code | '6'.code | '7'.code | '8'.code | '9'.code | '-'.code:
                   return parseNumber(c);
    
                default:
                   invalidChar();
             }
          }
       }
    
       function parseComment() {
          // trace('parse comment');
          var c:Int = peek();
          switch c {
             case '/'.code if (allowSingleLineComments):
                while (!isEof(c = next()))
                   if (c == '\r'.code || c == '\n'.code)
                      break;
    
             case '*'.code if (allowMultiLineComments):
                pos++;
                var start = pos - 2;
                var closed = false;
                while (!isEof(c = next()))
                   if (c == '*'.code && peek() == '/'.code) {
                      skip();
                      closed = true;
                      break;
                   }
    
                if (!closed) {
                   pos = start;
                   error('Unclosed comment');
                }
    
             default:
                invalidChar();
          }
       }
    
       function parseObject():Dynamic {
          // trace('parse object');
          var object = {};
          var field:Null<String> = null;
          var hasComma:Null<Bool> = null;
    
          while (true) {
             var c = next();
    
             switch c {
                case ' '.code, '\r'.code, '\n'.code, '\t'.code: // whitespace
                case '/'.code:
                   parseComment();
                case '}'.code if (field == null && (allowTrailingCommas || hasComma != true)):
                   return object;
                case ','.code if (hasComma != true):
                   hasComma = true;
                case ':'.code if (field != null):
                   Reflect.setField(object, field, parseRec());
                   field = null;
                   hasComma = false;
                case '"'.code if (field == null && hasComma != false):
                   field = parseString();
                default:
                   invalidChar();
             }
          }
       }
    
       function parseArray():Array<Dynamic> {
          // trace('parse array');
          var c:Int;
          var array = [];
          var hasComma:Null<Bool> = null;
    
          while (!isEof(c = next())) {
             switch c {
                case ' '.code, '\r'.code, '\n'.code, '\t'.code: // whitespace
                case '/'.code:
                   parseComment();
                case ']'.code if (allowTrailingCommas || !hasComma):
                   return array;
                case ','.code if (!hasComma):
                   hasComma = true;
                case _ if (hasComma != false):
                   pos--;
    
                   array.push(parseRec());
                   hasComma = false;
                default:
                   invalidChar();
             }
          }
          throw "Reached end of file";
       }
    
       function parseString():String {
          var start = pos;
          var buffer:Null<StringBuf> = null;
          var c:Int;
          var closed = false;
    
          // honestly, copied most of this from https://github.com/HaxeFoundation/haxe/blob/development/std/haxe/format/JsonParser.hx#L152-L254
    
          #if target.unicode
          var prev = -1;
          inline function cancel() {
             buffer.addChar(0xFFFD);
             prev = -1;
          }
          #end
    
          while (!isEof(c = next())) {
             if (c == '"'.code) {
                closed = true;
                break;
             }
    
             if (c == '\\'.code) {
                buffer ??= new StringBuf();
                buffer.addSub(str, start, pos - start - 1);
    
                c = next();
    
                #if target.unicode
                if (c != 'u'.code && prev != -1)
                   cancel();
                #end
    
                switch c {
                   case 'r'.code:
                      buffer.addChar('\r'.code);
                   case 'n'.code:
                      buffer.addChar('\n'.code);
                   case 't'.code:
                      buffer.addChar('\t'.code);
                   case 'b'.code:
                      buffer.addChar(8);
                   case 'f'.code:
                      buffer.addChar(12);
                   case '/'.code | '\\'.code | '"'.code:
                      buffer.addChar(c);
                   case 'u'.code:
                      var uc = Std.parseInt('0x${str.substring(pos, 4)}');
                      skip(4);
                      #if target.unicode
                      if (prev != -1) {
                         if (uc < 0xDC00 || uc > 0xDFFF) {
                            cancel();
                            continue;
                         }
    
                         buffer.addChar(((prev - 0xD800) << 10) + (uc - 0xDC00) + 0x10000);
                         prev = -1;
                      }
    
                      if (uc >= 0xD800 && uc <= 0xDBFF) {
                         prev = uc;
                         continue;
                      }
    
                      buffer.addChar(uc);
                      #else
                      if (uc <= 0x7F) {
                         buffer.addChar(uc);
                         continue;
                      }
    
                      if (uc <= 0x7FF) {
                         buffer.addChar(0xC0 | (uc >> 6));
                         buffer.addChar(0x80 | (uc & 63));
                         continue;
                      }
    
                      if (uc <= 0xFFFF) {
                         buffer.addChar(0xE0 | (uc >> 12));
                         buffer.addChar(0x80 | ((uc >> 6) & 63));
                         buffer.addChar(0x80 | (uc & 63));
                         continue;
                      }
    
                      buffer.addChar(0xF0 | (uc >> 18));
                      buffer.addChar(0x80 | ((uc >> 12) & 63));
                      buffer.addChar(0x80 | ((uc >> 6) & 63));
                      buffer.addChar(0x80 | (uc & 63));
                      #end
                   default:
                      throw 'Invalid escape sequence `\\${String.fromCharCode(c)}`';
                }
             }
          }
    
          if (!closed)
             throw "Unclosed string!";
    
          if (buffer == null)
             return str.substr(start, pos - start - 1);
    
          buffer.addSub(str, start, pos - start - 1);
          return buffer.toString();
       }
    
       function parseNumber(c:Int):Dynamic {
          // trace('parse number');
          var start = pos - 1;
          var digit = true;
    
          inline function invalid()
             error('Invalid number `${str.substr(start, pos - start)}`');
    
          // any valid positive number is also valid as a negative
          // ...i think...
          if (c == '-'.code) {
             digit = false;
          }
    
          // reject leading zeroes
          if (c == '0'.code && ('9'.code >= peek() && peek() >= '0'.code))
             invalid();
    
          var decimal = false;
          var scientific = false;
          var end = false;
    
          while (!end) {
             c = next();
    
             switch c {
                case '.'.code if (!decimal && !scientific):
                   decimal = true;
                   digit = false;
                case 'e'.code | 'E'.code if (!scientific):
                   scientific = true;
                   digit = false;
    
                   var n = peek();
                   if (n == '+'.code || n == '-'.code)
                      skip();
                default:
                   if ('9'.code >= c && c >= '0'.code) {
                      digit = true;
                      continue;
                   }
    
                   if (!digit)
                      invalid();
                   pos--;
                   end = true;
             }
          }
    
          var float = Std.parseFloat(str.substr(start, pos - start));
    
          if (decimal)
             return float;
    
          var int = Std.int(float);
    
          if (int == float)
             return int;
    
          return float;
       }
    
       function get_allowMultiLineComments():Bool
          return options.allowMultiLineComments ?? true;
    
       function set_allowMultiLineComments(value:Bool):Bool
          return options.allowMultiLineComments = value;
    
       function get_allowSingleLineComments():Bool
          return options.allowSingleLineComments ?? true;
    
       function set_allowSingleLineComments(value:Bool):Bool
          return options.allowSingleLineComments = value;
    
       function get_allowTrailingCommas():Bool
          return options.allowTrailingCommas ?? true;
    
       function set_allowTrailingCommas(value:Bool):Bool
          return options.allowTrailingCommas = value;
    }
    
    /**
       options for the parser
    **/
    typedef ParserOptions = {
       /**
          If this parser should strip multi-line comments &#47; `/* like this *&#47;`.
       **/
       var ?allowMultiLineComments:Bool;
    
       /**
          If this parser should strip single-line comments `// like this`.
       **/
       var ?allowSingleLineComments:Bool;
    
       /**
          If this parser should strip trailing commas `{"like": "this",}`.
       **/
       var ?allowTrailingCommas:Bool;
    }
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment