extended json
The snippet can be accessed without any authentication.
Authored by
Frying☆Pan
Edited
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 / `/* like this */`.
**/
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;
}
Please register or sign in to comment