| Index: sdk/lib/json/json.dart
|
| diff --git a/sdk/lib/json/json.dart b/sdk/lib/json/json.dart
|
| index 5ba8a74c62a1f3a897d89fb37b6d87f9c3cc1868..5de1b0e1a136ca8b80b3a1936ca47d37f7eff351 100644
|
| --- a/sdk/lib/json/json.dart
|
| +++ b/sdk/lib/json/json.dart
|
| @@ -4,8 +4,6 @@
|
|
|
| library dart.json;
|
|
|
| -import 'dart:math';
|
| -
|
| // JSON parsing and serialization.
|
|
|
| /**
|
| @@ -13,13 +11,12 @@ import 'dart:math';
|
| *
|
| * The [unsupportedObject] field holds that object that failed to be serialized.
|
| *
|
| - * If an isn't directly serializable, the serializer calls the 'toJson' method
|
| - * on the object. If that call fails, the error will be stored in the [cause]
|
| - * field. If the call returns an object that isn't directly serializable,
|
| - * the [cause] will be null.
|
| + * If an object isn't directly serializable, the serializer calls the 'toJson'
|
| + * method on the object. If that call fails, the error will be stored in the
|
| + * [cause] field. If the call returns an object that isn't directly
|
| + * serializable, the [cause] will be null.
|
| */
|
| -class JsonUnsupportedObjectError {
|
| - // TODO: proper base class.
|
| +class JsonUnsupportedObjectError implements Error {
|
| /** The object that could not be serialized. */
|
| final unsupportedObject;
|
| /** The exception thrown by object's [:toJson:] method, if any. */
|
| @@ -38,406 +35,606 @@ class JsonUnsupportedObjectError {
|
|
|
|
|
| /**
|
| - * Utility class to parse JSON and serialize objects to JSON.
|
| + * Parses [json] and build the corresponding parsed JSON value.
|
| + *
|
| + * Parsed JSON values are of the types [num], [String], [bool], [Null],
|
| + * [List]s of parsed JSON values or [Map]s from [String] to parsed
|
| + * JSON values.
|
| + *
|
| + * Throws [FormatException] if the input is not valid JSON text.
|
| */
|
| -class JSON {
|
| - /**
|
| - * Parses [json] and build the corresponding parsed JSON value.
|
| - *
|
| - * Parsed JSON values are of the types [num], [String], [bool], [Null],
|
| - * [List]s of parsed JSON values or [Map]s from [String] to parsed
|
| - * JSON values.
|
| - *
|
| - * Throws [JSONParseException] if the input is not valid JSON text.
|
| - */
|
| - static parse(String json) {
|
| - return _JsonParser.parse(json);
|
| +parse(String json, [reviver(var key, var value)]) {
|
| + BuildJsonListener listener;
|
| + if (reviver == null) {
|
| + listener = new BuildJsonListener();
|
| + } else {
|
| + listener = new ReviverJsonListener(reviver);
|
| }
|
| + new JsonParser(json, listener).parse();
|
| + return listener.result;
|
| +}
|
|
|
| +/**
|
| + * Serializes [object] into a JSON string.
|
| + *
|
| + * Directly serializable types are [num], [String], [bool], [Null], [List]
|
| + * and [Map].
|
| + * For [List], the elements must all be serializable.
|
| + * For [Map], the keys must be [String] and the values must be serializable.
|
| + * If a value is any other type is attempted serialized, a "toJson()" method
|
| + * is invoked on the object and the result, which must be a directly
|
| + * serializable type, is serialized instead of the original value.
|
| + * If the object does not support this method, throws, or returns a
|
| + * value that is not directly serializable, a [JsonUnsupportedObjectError]
|
| + * exception is thrown. If the call throws (including the case where there
|
| + * is no nullary "toJson" method, the error is caught and stored in the
|
| + * [JsonUnsupportedObjectError]'s [:cause:] field.
|
| + *Json
|
| + * Objects should not change during serialization.
|
| + * If an object is serialized more than once, [stringify] is allowed to cache
|
| + * the JSON text for it. I.e., if an object changes after it is first
|
| + * serialized, the new values may or may not be reflected in the result.
|
| + */
|
| +String stringify(Object object) {
|
| + return _JsonStringifier.stringify(object);
|
| +}
|
| +
|
| +/**
|
| + * Serializes [object] into [output] stream.
|
| + *
|
| + * Performs the same operations as [stringify] but outputs the resulting
|
| + * string to an existing [StringBuffer] instead of creating a new [String].
|
| + *
|
| + * If serialization fails by throwing, some data might have been added to
|
| + * [output], but it won't contain valid JSON text.
|
| + */
|
| +void printOn(Object object, StringBuffer output) {
|
| + return _JsonStringifier.printOn(object, output);
|
| +}
|
| +
|
| +//// Implementation ///////////////////////////////////////////////////////////
|
| +
|
| +// Simple API for JSON parsing.
|
| +
|
| +abstract class JsonListener {
|
| + void handleString(String value) {}
|
| + void handleNumber(num value) {}
|
| + void handleBool(bool value) {}
|
| + void handleNull() {}
|
| + void beginObject() {}
|
| + void propertyName() {}
|
| + void propertyValue() {}
|
| + void endObject() {}
|
| + void beginArray() {}
|
| + void arrayElement() {}
|
| + void endArray() {}
|
| + /** Called on failure to parse [source]. */
|
| + void fail(String source, int position, String message) {}
|
| +}
|
| +
|
| +/**
|
| + * A [JsonListener] that builds data objects from the parser events.
|
| + *
|
| + * This is a simple stack-based object builder. It keeps the most recently
|
| + * seen value in a variable, and uses it depending on the following event.
|
| + */
|
| +class BuildJsonListener extends JsonListener {
|
| /**
|
| - * Serializes [object] into a JSON string.
|
| - *
|
| - * Directly serializable types are [num], [String], [bool], [Null], [List]
|
| - * and [Map].
|
| - * For [List], the elements must all be serializable.
|
| - * For [Map], the keys must be [String] and the values must be serializable.
|
| - * If a value is any other type is attempted serialized, a "toJson()" method
|
| - * is invoked on the object and the result, which must be a directly
|
| - * serializable type, is serialized instead of the original value.
|
| - * If the object does not support this method, throws, or returns a
|
| - * value that is not directly serializable, a [JsonUnsupportedObjectError]
|
| - * exception is thrown. If the call throws (including the case where there
|
| - * is no nullary "toJson" method, the error is caught and stored in the
|
| - * [JsonUnsupportedObjectError]'s [:cause:] field.
|
| + * Stack used to handle nested containers.
|
| *
|
| - * Objects should not change during serialization.
|
| - * If an object is serialized more than once, [stringify] is allowed to cache
|
| - * the JSON text for it. I.e., if an object changes after it is first
|
| - * serialized, the new values may or may not be reflected in the result.
|
| + * The current container is pushed on the stack when a new one is
|
| + * started. If the container is a [Map], there is also a current [key]
|
| + * which is also stored on the stack.
|
| */
|
| - static String stringify(Object object) {
|
| - return _JsonStringifier.stringify(object);
|
| + List stack = [];
|
| + /** The current [Map] or [List] being built. */
|
| + var currentContainer;
|
| + /** The most recently read property key. */
|
| + String key;
|
| + /** The most recently read value. */
|
| + var value;
|
| +
|
| + /** Pushes the currently active container (and key, if a [Map]). */
|
| + void pushContainer() {
|
| + if (currentContainer is Map) stack.add(key);
|
| + stack.add(currentContainer);
|
| }
|
|
|
| - /**
|
| - * Serializes [object] into [output] stream.
|
| - *
|
| - * Performs the same operations as [stringify] but outputs the resulting
|
| - * string to an existing [StringBuffer] instead of creating a new [String].
|
| - *
|
| - * If serialization fails by throwing, some data might have been added to
|
| - * [output], but it won't contain valid JSON text.
|
| - */
|
| - static void printOn(Object object, StringBuffer output) {
|
| - return _JsonStringifier.printOn(object, output);
|
| + /** Pops the top container from the [stack], including a key if applicable. */
|
| + void popContainer() {
|
| + value = currentContainer;
|
| + currentContainer = stack.removeLast();
|
| + if (currentContainer is Map) key = stack.removeLast();
|
| }
|
| -}
|
|
|
| -//// Implementation ///////////////////////////////////////////////////////////
|
| + void handleString(String value) { this.value = value; }
|
| + void handleNumber(num value) { this.value = value; }
|
| + void handleBool(bool value) { this.value = value; }
|
| + void handleNull() { this.value = value; }
|
|
|
| -// TODO(ajohnsen): Introduce when we have a common exception interface for json.
|
| -class JSONParseException {
|
| - JSONParseException(int position, String message) :
|
| - position = position,
|
| - message = 'JSONParseException: $message, at offset $position';
|
| + void beginObject() {
|
| + pushContainer();
|
| + currentContainer = {};
|
| + }
|
|
|
| - String toString() => message;
|
| + void propertyName() {
|
| + key = value;
|
| + value = null;
|
| + }
|
|
|
| - final String message;
|
| - final int position;
|
| -}
|
| + void propertyValue() {
|
| + Map map = currentContainer;
|
| + map[key] = value;
|
| + key = value = null;
|
| + }
|
|
|
| -class _JsonParser {
|
| - static const int BACKSPACE = 8;
|
| - static const int TAB = 9;
|
| - static const int NEW_LINE = 10;
|
| - static const int FORM_FEED = 12;
|
| - static const int CARRIAGE_RETURN = 13;
|
| - static const int SPACE = 32;
|
| - static const int QUOTE = 34;
|
| - static const int PLUS = 43;
|
| - static const int COMMA = 44;
|
| - static const int MINUS = 45;
|
| - static const int DOT = 46;
|
| - static const int SLASH = 47;
|
| - static const int CHAR_0 = 48;
|
| - static const int CHAR_1 = 49;
|
| - static const int CHAR_2 = 50;
|
| - static const int CHAR_3 = 51;
|
| - static const int CHAR_4 = 52;
|
| - static const int CHAR_5 = 53;
|
| - static const int CHAR_6 = 54;
|
| - static const int CHAR_7 = 55;
|
| - static const int CHAR_8 = 56;
|
| - static const int CHAR_9 = 57;
|
| - static const int COLON = 58;
|
| - static const int CHAR_CAPITAL_E = 69;
|
| - static const int LBRACKET = 91;
|
| - static const int BACKSLASH = 92;
|
| - static const int RBRACKET = 93;
|
| - static const int CHAR_B = 98;
|
| - static const int CHAR_E = 101;
|
| - static const int CHAR_F = 102;
|
| - static const int CHAR_N = 110;
|
| - static const int CHAR_R = 114;
|
| - static const int CHAR_T = 116;
|
| - static const int CHAR_U = 117;
|
| - static const int LBRACE = 123;
|
| - static const int RBRACE = 125;
|
| -
|
| - static const int STRING_LITERAL = QUOTE;
|
| - static const int NUMBER_LITERAL = MINUS;
|
| - static const int NULL_LITERAL = CHAR_N;
|
| - static const int FALSE_LITERAL = CHAR_F;
|
| - static const int TRUE_LITERAL = CHAR_T;
|
| -
|
| - static const int WHITESPACE = SPACE;
|
| -
|
| - static const int LAST_ASCII = RBRACE;
|
| -
|
| - static const String NULL_STRING = "null";
|
| - static const String TRUE_STRING = "true";
|
| - static const String FALSE_STRING = "false";
|
| -
|
| - static List<int> tokens;
|
| -
|
| - final String json;
|
| - final int length;
|
| - int position = 0;
|
| -
|
| - static parse(String json) {
|
| - return new _JsonParser(json).parseToplevel();
|
| + void endObject() {
|
| + popContainer();
|
| }
|
|
|
| - _JsonParser(String json)
|
| - : json = json,
|
| - length = json.length {
|
| - if (tokens != null) return;
|
| -
|
| - // Use a list as jump-table. It is faster than switch and if.
|
| - tokens = new List<int>(LAST_ASCII + 1);
|
| - tokens[TAB] = WHITESPACE;
|
| - tokens[NEW_LINE] = WHITESPACE;
|
| - tokens[CARRIAGE_RETURN] = WHITESPACE;
|
| - tokens[SPACE] = WHITESPACE;
|
| - tokens[CHAR_0] = NUMBER_LITERAL;
|
| - tokens[CHAR_1] = NUMBER_LITERAL;
|
| - tokens[CHAR_2] = NUMBER_LITERAL;
|
| - tokens[CHAR_3] = NUMBER_LITERAL;
|
| - tokens[CHAR_4] = NUMBER_LITERAL;
|
| - tokens[CHAR_5] = NUMBER_LITERAL;
|
| - tokens[CHAR_6] = NUMBER_LITERAL;
|
| - tokens[CHAR_7] = NUMBER_LITERAL;
|
| - tokens[CHAR_8] = NUMBER_LITERAL;
|
| - tokens[CHAR_9] = NUMBER_LITERAL;
|
| - tokens[MINUS] = NUMBER_LITERAL;
|
| - tokens[LBRACE] = LBRACE;
|
| - tokens[RBRACE] = RBRACE;
|
| - tokens[LBRACKET] = LBRACKET;
|
| - tokens[RBRACKET] = RBRACKET;
|
| - tokens[QUOTE] = STRING_LITERAL;
|
| - tokens[COLON] = COLON;
|
| - tokens[COMMA] = COMMA;
|
| - tokens[CHAR_N] = NULL_LITERAL;
|
| - tokens[CHAR_T] = TRUE_LITERAL;
|
| - tokens[CHAR_F] = FALSE_LITERAL;
|
| + void beginArray() {
|
| + pushContainer();
|
| + currentContainer = [];
|
| }
|
|
|
| - parseToplevel() {
|
| - final result = parseValue();
|
| - if (token() != null) {
|
| - error('Junk at the end of JSON input');
|
| - }
|
| - return result;
|
| + void arrayElement() {
|
| + List list = currentContainer;
|
| + currentContainer.add(value);
|
| + value = null;
|
| }
|
|
|
| - parseValue() {
|
| - final int token = token();
|
| - if (token == null) {
|
| - error('Nothing to parse');
|
| - }
|
| - switch (token) {
|
| - case STRING_LITERAL: return parseString();
|
| - case NUMBER_LITERAL: return parseNumber();
|
| - case NULL_LITERAL: return expectKeyword(NULL_STRING, null);
|
| - case FALSE_LITERAL: return expectKeyword(FALSE_STRING, false);
|
| - case TRUE_LITERAL: return expectKeyword(TRUE_STRING, true);
|
| - case LBRACE: return parseObject();
|
| - case LBRACKET: return parseList();
|
| -
|
| - default:
|
| - error('Unexpected token');
|
| - }
|
| + void endArray() {
|
| + popContainer();
|
| }
|
|
|
| - Object expectKeyword(String word, Object value) {
|
| - for (int i = 0; i < word.length; i++) {
|
| - // Implicit end check in char().
|
| - if (char() != word.charCodeAt(i)) error("Expected keyword '$word'");
|
| - position++;
|
| - }
|
| + /** Read out the final result of parsing a JSON string. */
|
| + get result {
|
| + assert(currentContainer == null);
|
| return value;
|
| }
|
| +}
|
|
|
| - parseObject() {
|
| - final object = {};
|
| -
|
| - position++; // Eat '{'.
|
| -
|
| - if (!isToken(RBRACE)) {
|
| - while (true) {
|
| - final String key = parseString();
|
| - if (!isToken(COLON)) error("Expected ':' when parsing object");
|
| - position++;
|
| - object[key] = parseValue();
|
| -
|
| - if (!isToken(COMMA)) break;
|
| - position++; // Skip ','.
|
| - };
|
| +typedef _Reviver(var key, var value);
|
|
|
| - if (!isToken(RBRACE)) error("Expected '}' at end of object");
|
| - }
|
| - position++;
|
| +class ReviverJsonListener extends BuildJsonListener {
|
| + final _Reviver reviver;
|
| + ReviverJsonListener(reviver(key, value)) : this.reviver = reviver;
|
|
|
| - return object;
|
| + void arrayElement() {
|
| + List list = currentContainer;
|
| + value = reviver(list.length, value);
|
| + super.arrayElement();
|
| }
|
|
|
| - parseList() {
|
| - final list = [];
|
| -
|
| - position++; // Eat '['.
|
| -
|
| - if (!isToken(RBRACKET)) {
|
| - while (true) {
|
| - list.add(parseValue());
|
| + void propertyValue() {
|
| + value = reviver(key, value);
|
| + super.propertyValue();
|
| + }
|
|
|
| - if (!isToken(COMMA)) break;
|
| - position++;
|
| - };
|
| + get result {
|
| + return reviver("", value);
|
| + }
|
| +}
|
|
|
| - if (!isToken(RBRACKET)) error("Expected ']' at end of list");
|
| +class JsonParser {
|
| + // A simple non-recursive state-based parser for JSON.
|
| + //
|
| + // Literal values accepted in states ARRAY_EMPTY, ARRAY_COMMA, OBJECT_COLON
|
| + // and strings also in OBJECT_EMPTY, OBJECT_COMMA.
|
| + // VALUE STRING : , } ] Transitions to
|
| + // EMPTY X X -> END
|
| + // ARRAY_EMPTY X X @ -> ARRAY_VALUE / pop
|
| + // ARRAY_VALUE @ @ -> ARRAY_COMMA / pop
|
| + // ARRAY_COMMA X X -> ARRAY_VALUE
|
| + // OBJECT_EMPTY X @ -> OBJECT_KEY / pop
|
| + // OBJECT_KEY @ -> OBJECT_COLON
|
| + // OBJECT_COLON X X -> OBJECT_VALUE
|
| + // OBJECT_VALUE @ @ -> OBJECT_COMMA / pop
|
| + // OBJECT_COMMA X -> OBJECT_KEY
|
| + // END
|
| + // Starting a new array or object will push the current state. The "pop"
|
| + // above means restoring this state and then marking it as an ended value.
|
| + // X means generic handling, @ means special handling for just that
|
| + // state - that is, values are handled generically, only punctuation
|
| + // cares about the current state.
|
| + // Values for states are chosen so bits 0 and 1 tell whether
|
| + // a string/value is allowed, and setting bits 0 through 2 after a value
|
| + // gets to the next state (not empty, doesn't allow a value).
|
| +
|
| + // State building-block constants.
|
| + static const int INSIDE_ARRAY = 1;
|
| + static const int INSIDE_OBJECT = 2;
|
| + static const int AFTER_COLON = 3; // Always inside object.
|
| +
|
| + static const int ALLOW_STRING_MASK = 8; // Allowed if zero.
|
| + static const int ALLOW_VALUE_MASK = 4; // Allowed if zero.
|
| + static const int ALLOW_VALUE = 0;
|
| + static const int STRING_ONLY = 4;
|
| + static const int NO_VALUES = 12;
|
| +
|
| + // Objects and arrays are "empty" until their first property/element.
|
| + static const int EMPTY = 0;
|
| + static const int NON_EMPTY = 16;
|
| + static const int EMPTY_MASK = 16; // Empty if zero.
|
| +
|
| +
|
| + static const int VALUE_READ_BITS = NO_VALUES | NON_EMPTY;
|
| +
|
| + // Actual states.
|
| + static const int STATE_INITIAL = EMPTY | ALLOW_VALUE;
|
| + static const int STATE_END = NON_EMPTY | NO_VALUES;
|
| +
|
| + static const int STATE_ARRAY_EMPTY = INSIDE_ARRAY | EMPTY | ALLOW_VALUE;
|
| + static const int STATE_ARRAY_VALUE = INSIDE_ARRAY | NON_EMPTY | NO_VALUES;
|
| + static const int STATE_ARRAY_COMMA = INSIDE_ARRAY | NON_EMPTY | ALLOW_VALUE;
|
| +
|
| + static const int STATE_OBJECT_EMPTY = INSIDE_OBJECT | EMPTY | STRING_ONLY;
|
| + static const int STATE_OBJECT_KEY = INSIDE_OBJECT | NON_EMPTY | NO_VALUES;
|
| + static const int STATE_OBJECT_COLON = AFTER_COLON | NON_EMPTY | ALLOW_VALUE;
|
| + static const int STATE_OBJECT_VALUE = AFTER_COLON | NON_EMPTY | NO_VALUES;
|
| + static const int STATE_OBJECT_COMMA = INSIDE_OBJECT | NON_EMPTY | STRING_ONLY;
|
| +
|
| + // Character code constants.
|
| + static const int BACKSPACE = 0x08;
|
| + static const int TAB = 0x09;
|
| + static const int NEWLINE = 0x0a;
|
| + static const int CARRIAGE_RETURN = 0x0d;
|
| + static const int FORM_FEED = 0x0c;
|
| + static const int SPACE = 0x20;
|
| + static const int QUOTE = 0x22;
|
| + static const int PLUS = 0x2b;
|
| + static const int COMMA = 0x2c;
|
| + static const int MINUS = 0x2d;
|
| + static const int DECIMALPOINT = 0x2e;
|
| + static const int SLASH = 0x2f;
|
| + static const int CHAR_0 = 0x30;
|
| + static const int CHAR_9 = 0x39;
|
| + static const int COLON = 0x3a;
|
| + static const int CHAR_E = 0x45;
|
| + static const int LBRACKET = 0x5b;
|
| + static const int BACKSLASH = 0x5c;
|
| + static const int RBRACKET = 0x5d;
|
| + static const int CHAR_a = 0x61;
|
| + static const int CHAR_b = 0x62;
|
| + static const int CHAR_e = 0x65;
|
| + static const int CHAR_f = 0x66;
|
| + static const int CHAR_l = 0x6c;
|
| + static const int CHAR_n = 0x6e;
|
| + static const int CHAR_r = 0x72;
|
| + static const int CHAR_s = 0x73;
|
| + static const int CHAR_t = 0x74;
|
| + static const int CHAR_u = 0x75;
|
| + static const int LBRACE = 0x7b;
|
| + static const int RBRACE = 0x7d;
|
| +
|
| + final String source;
|
| + final JsonListener listener;
|
| + JsonParser(this.source, this.listener);
|
| +
|
| + /** Parses [source], or throws if it fails. */
|
| + void parse() {
|
| + final List<int> states = <int>[];
|
| + int state = STATE_INITIAL;
|
| + int position = 0;
|
| + int length = source.length;
|
| + while (position < length) {
|
| + int char = source.charCodeAt(position);
|
| + switch (char) {
|
| + case SPACE:
|
| + case CARRIAGE_RETURN:
|
| + case NEWLINE:
|
| + case TAB:
|
| + position++;
|
| + break;
|
| + case QUOTE:
|
| + if ((state & ALLOW_STRING_MASK) != 0) fail(position);
|
| + position = parseString(position + 1);
|
| + state |= VALUE_READ_BITS;
|
| + break;
|
| + case LBRACKET:
|
| + if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
| + listener.beginArray();
|
| + states.add(state);
|
| + state = STATE_ARRAY_EMPTY;
|
| + position++;
|
| + break;
|
| + case LBRACE:
|
| + if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
| + listener.beginObject();
|
| + states.add(state);
|
| + state = STATE_OBJECT_EMPTY;
|
| + position++;
|
| + break;
|
| + case CHAR_n:
|
| + if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
| + position = parseNull(position);
|
| + state |= VALUE_READ_BITS;
|
| + break;
|
| + case CHAR_f:
|
| + if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
| + position = parseFalse(position);
|
| + state |= VALUE_READ_BITS;
|
| + break;
|
| + case CHAR_t:
|
| + if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
| + position = parseTrue(position);
|
| + state |= VALUE_READ_BITS;
|
| + break;
|
| + case COLON:
|
| + if (state != STATE_OBJECT_KEY) fail(position);
|
| + listener.propertyName();
|
| + state = STATE_OBJECT_COLON;
|
| + position++;
|
| + break;
|
| + case COMMA:
|
| + if (state == STATE_OBJECT_VALUE) {
|
| + listener.propertyValue();
|
| + state = STATE_OBJECT_COMMA;
|
| + position++;
|
| + } else if (state == STATE_ARRAY_VALUE) {
|
| + listener.arrayElement();
|
| + state = STATE_ARRAY_COMMA;
|
| + position++;
|
| + } else {
|
| + fail(position);
|
| + }
|
| + break;
|
| + case RBRACKET:
|
| + if (state == STATE_ARRAY_EMPTY) {
|
| + listener.endArray();
|
| + } else if (state == STATE_ARRAY_VALUE) {
|
| + listener.arrayElement();
|
| + listener.endArray();
|
| + } else {
|
| + fail(position);
|
| + }
|
| + state = states.removeLast() | VALUE_READ_BITS;
|
| + position++;
|
| + break;
|
| + case RBRACE:
|
| + if (state == STATE_OBJECT_EMPTY) {
|
| + listener.endObject();
|
| + } else if (state == STATE_OBJECT_VALUE) {
|
| + listener.propertyValue();
|
| + listener.endObject();
|
| + } else {
|
| + fail(position);
|
| + }
|
| + state = states.removeLast() | VALUE_READ_BITS;
|
| + position++;
|
| + break;
|
| + default:
|
| + if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
| + position = parseNumber(char, position);
|
| + state |= VALUE_READ_BITS;
|
| + break;
|
| + }
|
| }
|
| - position++;
|
| + if (state != STATE_END) fail(position);
|
| + }
|
|
|
| - return list;
|
| + /**
|
| + * Parses a "true" literal starting at [position].
|
| + *
|
| + * [:source[position]:] must be "t".
|
| + */
|
| + int parseTrue(int position) {
|
| + assert(source.charCodeAt(position) == CHAR_t);
|
| + if (source.length < position + 4) fail(position, "Unexpected identifier");
|
| + if (source.charCodeAt(position + 1) != CHAR_r ||
|
| + source.charCodeAt(position + 2) != CHAR_u ||
|
| + source.charCodeAt(position + 3) != CHAR_e) {
|
| + fail(position);
|
| + }
|
| + listener.handleBool(true);
|
| + return position + 4;
|
| }
|
|
|
| - String parseString() {
|
| - if (!isToken(STRING_LITERAL)) error("Expected string literal");
|
| + /**
|
| + * Parses a "false" literal starting at [position].
|
| + *
|
| + * [:source[position]:] must be "f".
|
| + */
|
| + int parseFalse(int position) {
|
| + assert(source.charCodeAt(position) == CHAR_f);
|
| + if (source.length < position + 5) fail(position, "Unexpected identifier");
|
| + if (source.charCodeAt(position + 1) != CHAR_a ||
|
| + source.charCodeAt(position + 2) != CHAR_l ||
|
| + source.charCodeAt(position + 3) != CHAR_s ||
|
| + source.charCodeAt(position + 4) != CHAR_e) {
|
| + fail(position);
|
| + }
|
| + listener.handleBool(false);
|
| + return position + 5;
|
| + }
|
|
|
| - position++; // Eat '"'.
|
| + /** Parses a "null" literal starting at [position].
|
| + *
|
| + * [:source[position]:] must be "n".
|
| + */
|
| + int parseNull(int position) {
|
| + assert(source.charCodeAt(position) == CHAR_n);
|
| + if (source.length < position + 4) fail(position, "Unexpected identifier");
|
| + if (source.charCodeAt(position + 1) != CHAR_u ||
|
| + source.charCodeAt(position + 2) != CHAR_l ||
|
| + source.charCodeAt(position + 3) != CHAR_l) {
|
| + fail(position);
|
| + }
|
| + listener.handleNull();
|
| + return position + 4;
|
| + }
|
|
|
| - List<int> charCodes = new List<int>();
|
| + int parseString(int position) {
|
| + // Format: '"'([^\x00-\x1f\\\"]|'\\'[bfnrt/\\"])*'"'
|
| + // Initial position is right after first '"'.
|
| + int start = position;
|
| + int char;
|
| + do {
|
| + if (position == source.length) {
|
| + fail(start - 1, "Unterminated string");
|
| + }
|
| + char = source.charCodeAt(position);
|
| + if (char == QUOTE) {
|
| + listener.handleString(source.substring(start, position));
|
| + return position + 1;
|
| + }
|
| + if (char < SPACE) {
|
| + fail(position, "Control character in string");
|
| + }
|
| + position++;
|
| + } while (char != BACKSLASH);
|
| + // Backslash escape detected. Collect character codes for rest of string.
|
| + int firstEscape = position - 1;
|
| + List<int> chars = <int>[];
|
| while (true) {
|
| - int c = char();
|
| - if (c == QUOTE) {
|
| - position++;
|
| - break;
|
| + if (position == source.length) {
|
| + fail(start - 1, "Unterminated string");
|
| }
|
| - if (c == BACKSLASH) {
|
| - position++;
|
| - if (position == length) {
|
| - error('\\ at the end of input');
|
| - }
|
| -
|
| - switch (char()) {
|
| - case QUOTE:
|
| - c = QUOTE;
|
| - break;
|
| - case BACKSLASH:
|
| - c = BACKSLASH;
|
| - break;
|
| - case SLASH:
|
| - c = SLASH;
|
| - break;
|
| - case CHAR_B:
|
| - c = BACKSPACE;
|
| - break;
|
| - case CHAR_N:
|
| - c = NEW_LINE;
|
| - break;
|
| - case CHAR_R:
|
| - c = CARRIAGE_RETURN;
|
| - break;
|
| - case CHAR_F:
|
| - c = FORM_FEED;
|
| - break;
|
| - case CHAR_T:
|
| - c = TAB;
|
| - break;
|
| - case CHAR_U:
|
| - if (position + 5 > length) {
|
| - error('Invalid unicode esacape sequence');
|
| + char = source.charCodeAt(position);
|
| + switch (char) {
|
| + case CHAR_b: char = BACKSPACE; break;
|
| + case CHAR_f: char = FORM_FEED; break;
|
| + case CHAR_n: char = NEWLINE; break;
|
| + case CHAR_r: char = CARRIAGE_RETURN; break;
|
| + case CHAR_t: char = TAB; break;
|
| + case SLASH:
|
| + case BACKSLASH:
|
| + case QUOTE:
|
| + break;
|
| + case CHAR_u: {
|
| + int hexStart = position - 1;
|
| + int value = 0;
|
| + for (int i = 0; i < 4; i++) {
|
| + position++;
|
| + if (position == source.length) {
|
| + fail(start - 1, "Unterminated string");
|
| }
|
| - final codeString = json.substring(position + 1, position + 5);
|
| - try {
|
| - c = int.parse('0x${codeString}');
|
| - } catch (e) {
|
| - error('Invalid unicode esacape sequence');
|
| + char = source.charCodeAt(position);
|
| + char -= 0x30;
|
| + if (char < 0) fail(hexStart, "Invalid unicode escape");
|
| + if (char < 10) {
|
| + value = value * 16 + char;
|
| + } else {
|
| + char = (char | 0x20) - 0x31;
|
| + if (char < 0 || char > 5) {
|
| + fail(hexStart, "Invalid unicode escape");
|
| + }
|
| + value = value * 16 + char + 10;
|
| }
|
| - position += 4;
|
| - break;
|
| - default:
|
| - error('Invalid esacape sequence in string literal');
|
| + }
|
| + char = value;
|
| + break;
|
| }
|
| + default:
|
| + if (char < SPACE) fail(position, "Control character in string");
|
| + fail(position, "Unrecognized string escape");
|
| }
|
| - charCodes.add(c);
|
| + do {
|
| + chars.add(char);
|
| + position++;
|
| + if (position == source.length) fail(start - 1, "Unterminated string");
|
| + char = source.charCodeAt(position);
|
| + if (char == QUOTE) {
|
| + String result = new String.fromCharCodes(chars);
|
| + if (start < firstEscape) {
|
| + result = "${source.substring(start, firstEscape)}$result";
|
| + }
|
| + listener.handleString(result);
|
| + return position + 1;
|
| + }
|
| + if (char < SPACE) {
|
| + fail(position, "Control character in string");
|
| + }
|
| + } while (char != BACKSLASH);
|
| position++;
|
| }
|
| -
|
| - return new String.fromCharCodes(charCodes);
|
| }
|
|
|
| - num parseNumber() {
|
| - if (!isToken(NUMBER_LITERAL)) error('Expected number literal');
|
| -
|
| - final int startPos = position;
|
| - int char = char();
|
| - if (identical(char, MINUS)) char = nextChar();
|
| - if (identical(char, CHAR_0)) {
|
| - char = nextChar();
|
| - } else if (isDigit(char)) {
|
| - char = nextChar();
|
| - while (isDigit(char)) char = nextChar();
|
| - } else {
|
| - error('Expected digit when parsing number');
|
| + int parseNumber(int char, int position) {
|
| + // Format:
|
| + // '-'?('0'|[1-9][0-9]*)('.'[0-9]+)?([eE][+-]?[0-9]+)?
|
| + int start = position;
|
| + int length = source.length;
|
| + bool isDouble = false;
|
| + if (char == MINUS) {
|
| + position++;
|
| + if (position == length) fail(position, "Missing expected digit");
|
| + char = source.charCodeAt(position);
|
| }
|
| -
|
| - bool isInt = true;
|
| - if (identical(char, DOT)) {
|
| - char = nextChar();
|
| - if (isDigit(char)) {
|
| - char = nextChar();
|
| - isInt = false;
|
| - while (isDigit(char)) char = nextChar();
|
| - } else {
|
| - error('Expected digit following comma');
|
| - }
|
| + if (char < CHAR_0 || char > CHAR_9) {
|
| + fail(position, "Missing expected digit");
|
| }
|
| -
|
| - if (identical(char, CHAR_E) || identical(char, CHAR_CAPITAL_E)) {
|
| - char = nextChar();
|
| - if (identical(char, MINUS) || identical(char, PLUS)) char = nextChar();
|
| - if (isDigit(char)) {
|
| - char = nextChar();
|
| - isInt = false;
|
| - while (isDigit(char)) char = nextChar();
|
| - } else {
|
| - error('Expected digit following \'e\' or \'E\'');
|
| - }
|
| + int handleLiteral(position) {
|
| + String literal = source.substring(start, position);
|
| + // This correctly creates -0 for doubles.
|
| + num value = (isDouble ? double.parse(literal) : int.parse(literal));
|
| + listener.handleNumber(value);
|
| + return position;
|
| }
|
| -
|
| - String number = json.substring(startPos, position);
|
| - if (isInt) {
|
| - return int.parse(number);
|
| + if (char == CHAR_0) {
|
| + position++;
|
| + if (position == length) return handleLiteral(position);
|
| + char = source.charCodeAt(position);
|
| + if (CHAR_0 <= char && char <= CHAR_9) {
|
| + fail(position);
|
| + }
|
| } else {
|
| - return double.parse(number);
|
| + do {
|
| + position++;
|
| + if (position == length) return handleLiteral(position);
|
| + char = source.charCodeAt(position);
|
| + } while (CHAR_0 <= char && char <= CHAR_9);
|
| }
|
| - }
|
| -
|
| - bool isChar(int char) {
|
| - if (position >= length) return false;
|
| - return json.charCodeAt(position) == char;
|
| - }
|
| -
|
| - bool isDigit(int char) {
|
| - return char >= CHAR_0 && char <= CHAR_9;
|
| - }
|
| -
|
| - bool isToken(int tokenKind) => token() == tokenKind;
|
| -
|
| - int char() {
|
| - if (position >= length) {
|
| - error('Unexpected end of JSON stream');
|
| + if (char == DECIMALPOINT) {
|
| + isDouble = true;
|
| + position++;
|
| + if (position == length) fail(position, "Missing expected digit");
|
| + char = source.charCodeAt(position);
|
| + if (char < CHAR_0 || char > CHAR_9) fail(position);
|
| + do {
|
| + position++;
|
| + if (position == length) return handleLiteral(position);
|
| + char = source.charCodeAt(position);
|
| + } while (CHAR_0 <= char && char <= CHAR_9);
|
| }
|
| - return json.charCodeAt(position);
|
| - }
|
| -
|
| - int nextChar() {
|
| - position++;
|
| - if (position >= length) return 0;
|
| - return json.charCodeAt(position);
|
| - }
|
| -
|
| - int token() {
|
| - while (true) {
|
| - if (position >= length) return null;
|
| - int char = json.charCodeAt(position);
|
| - int token = tokens[char];
|
| - if (identical(token, WHITESPACE)) {
|
| + if (char == CHAR_e || char == CHAR_E) {
|
| + isDouble = true;
|
| + position++;
|
| + if (position == length) fail(position, "Missing expected digit");
|
| + char = source.charCodeAt(position);
|
| + if (char == PLUS || char == MINUS) {
|
| position++;
|
| - continue;
|
| + if (position == length) fail(position, "Missing expected digit");
|
| + char = source.charCodeAt(position);
|
| + }
|
| + if (char < CHAR_0 || char > CHAR_9) {
|
| + fail(position, "Missing expected digit");
|
| }
|
| - if (token == null) return 0;
|
| - return token;
|
| + do {
|
| + position++;
|
| + if (position == length) return handleLiteral(position);
|
| + char = source.charCodeAt(position);
|
| + } while (CHAR_0 <= char && char <= CHAR_9);
|
| }
|
| + return handleLiteral(position);
|
| }
|
|
|
| - void error(String message) {
|
| - throw message;
|
| + void fail(int position, [String message]) {
|
| + if (message == null) message = "Unexpected character";
|
| + listener.fail(source, position, message);
|
| + // If the listener didn't throw, do it here.
|
| + String slice;
|
| + int sliceEnd = position + 20;
|
| + if (sliceEnd > source.length) {
|
| + slice = "'${source.substring(position)}'";
|
| + } else {
|
| + slice = "'${source.substring(position, sliceEnd)}...'";
|
| + }
|
| + throw new FormatException("Unexpected character at $position: $slice");
|
| }
|
| }
|
|
|
| +
|
| class _JsonStringifier {
|
| StringBuffer sb;
|
| List<Object> seen; // TODO: that should be identity set.
|
| @@ -471,35 +668,35 @@ class _JsonStringifier {
|
| int charCode = s.charCodeAt(i);
|
| if (charCode < 32) {
|
| needsEscape = true;
|
| - charCodes.add(_JsonParser.BACKSLASH);
|
| + charCodes.add(JsonParser.BACKSLASH);
|
| switch (charCode) {
|
| - case _JsonParser.BACKSPACE:
|
| - charCodes.add(_JsonParser.CHAR_B);
|
| + case JsonParser.BACKSPACE:
|
| + charCodes.add(JsonParser.CHAR_b);
|
| break;
|
| - case _JsonParser.TAB:
|
| - charCodes.add(_JsonParser.CHAR_T);
|
| + case JsonParser.TAB:
|
| + charCodes.add(JsonParser.CHAR_t);
|
| break;
|
| - case _JsonParser.NEW_LINE:
|
| - charCodes.add(_JsonParser.CHAR_N);
|
| + case JsonParser.NEWLINE:
|
| + charCodes.add(JsonParser.CHAR_n);
|
| break;
|
| - case _JsonParser.FORM_FEED:
|
| - charCodes.add(_JsonParser.CHAR_F);
|
| + case JsonParser.FORM_FEED:
|
| + charCodes.add(JsonParser.CHAR_f);
|
| break;
|
| - case _JsonParser.CARRIAGE_RETURN:
|
| - charCodes.add(_JsonParser.CHAR_R);
|
| + case JsonParser.CARRIAGE_RETURN:
|
| + charCodes.add(JsonParser.CHAR_r);
|
| break;
|
| default:
|
| - charCodes.add(_JsonParser.CHAR_U);
|
| + charCodes.add(JsonParser.CHAR_u);
|
| 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 == _JsonParser.QUOTE ||
|
| - charCode == _JsonParser.BACKSLASH) {
|
| + } else if (charCode == JsonParser.QUOTE ||
|
| + charCode == JsonParser.BACKSLASH) {
|
| needsEscape = true;
|
| - charCodes.add(_JsonParser.BACKSLASH);
|
| + charCodes.add(JsonParser.BACKSLASH);
|
| charCodes.add(charCode);
|
| } else {
|
| charCodes.add(charCode);
|
|
|