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); |