// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. #library("json"); // Pure Dart implementation of JSON protocol. /** * Utility class to parse JSON and serialize objects to JSON. */ class JSON { /** * Parses [:json:] and build the corresponding object. */ static parse(String json) { return JsonParser.parse(json); } /** * Serializes [:object:] into JSON string. */ static String stringify(Object object) { return JsonStringifier.stringify(object); } } //// Implementation /////////////////////////////////////////////////////////// /** * Union-like class for JSON tokens. */ class JsonToken { static final int STRING = 0; static final int NUMBER = 1; static final int NULL = 2; static final int FALSE = 3; static final int TRUE = 4; static final int RBRACKET = 5; static final int LBRACKET = 6; static final int RBRACE = 7; static final int LBRACE = 8; static final int COLON = 9; static final int COMMA = 10; final int kind; final String _s; final num _n; String get str() { assert(kind == STRING); return _s; } num get number() { assert(kind == NUMBER); return _n; } const JsonToken._internal(this.kind, this._s, this._n); factory JsonToken.string(String s) { return new JsonToken._internal(STRING, s, 0); } factory JsonToken.number(num n) { return new JsonToken._internal(NUMBER, '', n); } factory JsonToken.atom(int kind) { return new JsonToken._internal(kind, '', 0); } String toString() { switch (kind) { case STRING: return 'STRING(${str})'; case NUMBER: return 'NUMBER(${number})'; case NULL: return 'ATOM(null)'; case FALSE: return 'ATOM(false)'; case TRUE: return 'ATOM(true)'; case RBRACKET: return 'ATOM(])'; case LBRACKET: return 'ATOM([)'; case RBRACE: return 'ATOM(})'; case LBRACE: return 'ATOM({)'; case COLON: return 'ATOM(:)'; case COMMA: return 'ATOM(,)'; } } } typedef bool Predicate(int c); class JsonTokenizer { static final int BACKSPACE = 8; // '\b'.charCodeAt(0) static final int TAB = 9; // '\t'.charCodeAt(0) static final int NEW_LINE = 10; // '\n'.charCodeAt(0) static final int FORM_FEED = 12; // '\f'.charCodeAt(0) static final int CARRIAGE_RETURN = 13; // '\r'.charCodeAt(0) static final int SPACE = 32; // ' '.charCodeAt(0) static final int QUOTE = 34; // '"'.charCodeAt(0) static final int PLUS = 43; // '+'.charCodeAt(0) static final int COMMA = 44; // ','.charCodeAt(0) static final int MINUS = 45; // '-'.charCodeAt(0) static final int DOT = 46; // '.'.charCodeAt(0) static final int SLASH = 47; // '/'.charCodeAt(0) static final int ZERO = 48; // '0'.charCodeAt(0) static final int NINE = 57; // '9'.charCodeAt(0) static final int COLON = 58; // ':'.charCodeAt(0) static final int A_BIG = 65; // 'A'.charCodeAt(0) static final int E_BIG = 69; // 'E'.charCodeAt(0) static final int Z_BIG = 90; // 'Z'.charCodeAt(0) static final int LBRACKET = 91; // '['.charCodeAt(0) static final int BACKSLASH = 92; // '\\'.charCodeAt(0) static final int RBRACKET = 93; // ']'.charCodeAt(0) static final int A_SMALL = 97; // 'a'.charCodeAt(0) static final int B_SMALL = 98; // 'b'.charCodeAt(0) static final int E_SMALL = 101; // 'e'.charCodeAt(0) static final int F_SMALL = 102; // 'f'.charCodeAt(0) static final int N_SMALL = 110; // 'n'.charCodeAt(0) static final int R_SMALL = 114; // 'r'.charCodeAt(0) static final int T_SMALL = 116; // 't'.charCodeAt(0) static final int U_SMALL = 117; // 'u'.charCodeAt(0) static final int Z_SMALL = 122; // 'z'.charCodeAt(0) static final int LBRACE = 123; // '{'.charCodeAt(0) static final int RBRACE = 125; // '}'.charCodeAt(0) JsonTokenizer(String s) : _s = s + ' ', _pos = 0, _len = s.length + 1 {} /** * Fetches next token or [:null:] if the stream has been exhausted. */ JsonToken next() { while (_pos < _len && isWhitespace(_s.charCodeAt(_pos))) { _pos++; } if (_pos == _len) { return null; } final int cur = _s.charCodeAt(_pos); switch (true) { case cur == QUOTE: _pos++; List charCodes = new List(); while (_pos < _len) { int c = _s.charCodeAt(_pos); if (c == QUOTE) { break; } if (c == BACKSLASH) { _pos++; if (_pos == _len) { throw '\\ at the end'; } switch (_s[_pos]) { case '"': c = QUOTE; break; case '\\': c = BACKSLASH; break; case '/': c = SLASH; break; case 'b': c = BACKSPACE; break; case 'n': c = NEW_LINE; break; case 'r': c = CARRIAGE_RETURN; break; case 'f': c = FORM_FEED; break; case 't': c = TAB; break; case 'u': if (_pos + 5 > _len) { throw 'Invalid unicode esacape sequence: \\' + _s.substring(_pos, _len); } final codeString = _s.substring(_pos + 1, _pos + 5); c = Math.parseInt('0x' + codeString); if (c >= 128) { // TODO(jmessery): the VM doesn't support 2-byte strings yet // see runtime/lib/string.cc:49 // So instead we replace these characters with '?' c = '?'.charCodeAt(0); } _pos += 4; break; default: throw 'Invalid esacape sequence: \\' + _s[_pos]; } } charCodes.add(c); _pos++; } if (_pos == _len) { throw 'Unmatched quote'; } final String body = new String.fromCharCodes(charCodes); _pos++; return new JsonToken.string(body); case cur == MINUS || isDigit(cur): skipDigits() { _scanWhile((int c) => isDigit(c), 'Invalid number'); } int c = cur; final int startPos = _pos; int value = 0; bool isNegative = false; if (c == MINUS) { isNegative = true; _pos++; c = _s.charCodeAt(_pos); } while (isDigit(c)) { value = value * 10 + c - ZERO; _pos++; c = _s.charCodeAt(_pos); } if (c != DOT) { if (c != E_SMALL && cur != E_BIG) { if (isNegative) value = -value; return new JsonToken.number(value); } } else { _pos++; skipDigits(); c = _s.charCodeAt(_pos); } if (c == E_SMALL || c == E_BIG) { // TODO: consider keeping E+ as an integer. _pos++; c = _s.charCodeAt(_pos); if (c == PLUS || c == MINUS) { _pos++; } skipDigits(); } final String body = _s.substring(startPos, _pos); return new JsonToken.number(Math.parseDouble(body)); case cur == LBRACE: _pos++; return new JsonToken.atom(JsonToken.LBRACE); case cur == RBRACE: _pos++; return new JsonToken.atom(JsonToken.RBRACE); case cur == LBRACKET: _pos++; return new JsonToken.atom(JsonToken.LBRACKET); case cur == RBRACKET: _pos++; return new JsonToken.atom(JsonToken.RBRACKET); case cur == COMMA: _pos++; return new JsonToken.atom(JsonToken.COMMA); case cur == COLON: _pos++; return new JsonToken.atom(JsonToken.COLON); case isLetter(cur): final int startPos = _pos; _pos++; while (_pos < _len && isLetter(_s.charCodeAt(_pos))) { _pos++; } final String body = _s.substring(startPos, _pos); switch (body) { case 'null': return new JsonToken.atom(JsonToken.NULL); case 'false': return new JsonToken.atom(JsonToken.FALSE); case 'true': return new JsonToken.atom(JsonToken.TRUE); default: throw 'Unexpected sequence ${body}'; } // TODO: Bogous, to please DartVM. return null; default: throw 'Invalid token'; } } final String _s; int _pos; final int _len; void _scanWhile(Predicate predicate, String errorMsg) { while (_pos < _len && predicate(_s.charCodeAt(_pos))) { _pos++; } if (_pos == _len) { throw errorMsg; } } // TODO other kind of whitespace. static bool isWhitespace(int c) { return c == SPACE || c == TAB || c == NEW_LINE || c == CARRIAGE_RETURN; } static bool isDigit(int c) { return (ZERO <= c) && (c <= NINE); } static bool isLetter(int c) { return ((A_SMALL <= c) && (c <= Z_SMALL)) || ((A_BIG <= c) && (c <= Z_BIG)); } } class JsonParser { static parse(String json) { return new JsonParser._internal(json)._parseToplevel(); } final JsonTokenizer _tokenizer; JsonParser._internal(String json) : _tokenizer = new JsonTokenizer(json) {} _parseToplevel() { JsonToken token = _tokenizer.next(); final result = _parseValue(token); token = _tokenizer.next(); if (token !== null) { throw 'Junk at the end'; } return result; } _parseValue(final JsonToken token) { if (token === null) { throw 'Nothing to parse'; } switch (token.kind) { case JsonToken.STRING: return token.str; case JsonToken.NUMBER: return token.number; case JsonToken.NULL: return null; case JsonToken.FALSE: return false; case JsonToken.TRUE: return true; case JsonToken.LBRACE: return _parseObject(); case JsonToken.LBRACKET: return _parseList(); default: throw 'Unexpected token: ${token}'; } } _parseObject() { final object = {}; _parseSequence(JsonToken.RBRACE, (JsonToken token) { _assertTokenKind(token, JsonToken.STRING); final String key = token.str; token = _tokenizer.next(); _assertTokenKind(token, JsonToken.COLON); token = _tokenizer.next(); final value = _parseValue(token); object[key] = value; }); return object; } _parseList() { final list = []; _parseSequence(JsonToken.RBRACKET, (JsonToken token) { final value = _parseValue(token); list.add(value); }); return list; } void _parseSequence(int endTokenKind, void parseElement(JsonToken token)) { JsonToken token = _tokenizer.next(); if (token === null) { throw 'Unexpected end of stream'; } if (token.kind == endTokenKind) { return; } parseElement(token); token = _tokenizer.next(); if (token === null) { throw 'Expected either comma or terminator'; } while (token.kind != endTokenKind) { _assertTokenKind(token, JsonToken.COMMA); token = _tokenizer.next(); parseElement(token); token = _tokenizer.next(); } } void _assertTokenKind(JsonToken token, int kind) { if (token === null || token.kind != kind) { throw 'Unexpected token kind: token = ${token}, expected kind = ${kind}'; } } // TODO: consider factor out error throwing code and build more complicated // data structure to provide more info for a caller. } // TODO: proper base class. class JsonUnsupportedObjectType { const JsonUnsupportedObjectType(); } class JsonStringifier { static String stringify(final object) { JsonStringifier stringifier = new JsonStringifier._internal(); stringifier._stringify(object); return stringifier._result; } JsonStringifier._internal() : _sb = new StringBuffer(), _seen = new List() {} StringBuffer _sb; List _seen; // TODO: that should be identity set. String get _result() { return _sb.toString(); } static String _numberToString(num x) { // TODO: need some more investigation what to do with precision // of double values. switch (true) { case x is int: return x.toString(); case x is double: return x.toString(); default: return x.toDouble().toString(); } } // ('0' + x) or ('a' + x - 10) static int _hexDigit(int x) => x < 10 ? 48 + x : 87 + x; static void _escape(StringBuffer sb, String s) { final int length = s.length; bool needsEscape = false; final charCodes = new List(); for (int i = 0; i < length; i++) { int charCode = s.charCodeAt(i); if (charCode < 32) { needsEscape = true; charCodes.add(JsonTokenizer.BACKSLASH); switch (charCode) { case JsonTokenizer.BACKSPACE: charCodes.add(JsonTokenizer.B_SMALL); break; case JsonTokenizer.TAB: charCodes.add(JsonTokenizer.T_SMALL); break; case JsonTokenizer.NEW_LINE: charCodes.add(JsonTokenizer.N_SMALL); break; case JsonTokenizer.FORM_FEED: charCodes.add(JsonTokenizer.F_SMALL); break; case JsonTokenizer.CARRIAGE_RETURN: charCodes.add(JsonTokenizer.R_SMALL); break; default: charCodes.add(JsonTokenizer.U_SMALL); charCodes.add(_hexDigit((charCode >> 12) & 0xf)); charCodes.add(_hexDigit((charCode >> 8) & 0xf)); charCodes.add(_hexDigit((charCode >> 4) & 0xf)); charCodes.add(_hexDigit(charCode & 0xf)); break; } } else if (charCode == JsonTokenizer.QUOTE || charCode == JsonTokenizer.BACKSLASH) { needsEscape = true; charCodes.add(JsonTokenizer.BACKSLASH); charCodes.add(charCode); } else { charCodes.add(charCode); } } sb.add(needsEscape ? new String.fromCharCodes(charCodes) : s); } void _checkCycle(final object) { // TODO: use Iterables. for (int i = 0; i < _seen.length; i++) { if (_seen[i] === object) { throw 'Cyclic structure'; } } _seen.add(object); } void _stringify(final object) { switch (true) { case object is num: // TODO: use writeOn. _sb.add(_numberToString(object)); return; case object === true: _sb.add('true'); return; case object === false: _sb.add('false'); return; case object === null: _sb.add('null'); return; case object is String: _sb.add('"'); _escape(_sb, object); _sb.add('"'); return; case object is List: _checkCycle(object); List a = object; _sb.add('['); if (a.length > 0) { _stringify(a[0]); // TODO: switch to Iterables. for (int i = 1; i < a.length; i++) { _sb.add(','); _stringify(a[i]); } } _sb.add(']'); _seen.removeLast(); return; case object is Map: _checkCycle(object); Map m = object; _sb.add('{'); bool first = true; m.forEach((String key, Object value) { if (!first) { _sb.add(',"'); } else { _sb.add('"'); } _escape(_sb, key); _sb.add('":'); _stringify(value); first = false; }); _sb.add('}'); _seen.removeLast(); return; default: throw const JsonUnsupportedObjectType(); } } }