| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library dart.json; | 5 library dart.json; |
| 6 | 6 |
| 7 import 'dart:math'; | |
| 8 | |
| 9 // JSON parsing and serialization. | 7 // JSON parsing and serialization. |
| 10 | 8 |
| 11 /** | 9 /** |
| 12 * Error thrown by JSON serialization if an object cannot be serialized. | 10 * Error thrown by JSON serialization if an object cannot be serialized. |
| 13 * | 11 * |
| 14 * The [unsupportedObject] field holds that object that failed to be serialized. | 12 * The [unsupportedObject] field holds that object that failed to be serialized. |
| 15 * | 13 * |
| 16 * If an isn't directly serializable, the serializer calls the 'toJson' method | 14 * If an object isn't directly serializable, the serializer calls the 'toJson' |
| 17 * on the object. If that call fails, the error will be stored in the [cause] | 15 * method on the object. If that call fails, the error will be stored in the |
| 18 * field. If the call returns an object that isn't directly serializable, | 16 * [cause] field. If the call returns an object that isn't directly |
| 19 * the [cause] will be null. | 17 * serializable, the [cause] will be null. |
| 20 */ | 18 */ |
| 21 class JsonUnsupportedObjectError { | 19 class JsonUnsupportedObjectError implements Error { |
| 22 // TODO: proper base class. | |
| 23 /** The object that could not be serialized. */ | 20 /** The object that could not be serialized. */ |
| 24 final unsupportedObject; | 21 final unsupportedObject; |
| 25 /** The exception thrown by object's [:toJson:] method, if any. */ | 22 /** The exception thrown by object's [:toJson:] method, if any. */ |
| 26 final cause; | 23 final cause; |
| 27 JsonUnsupportedObjectError(this.unsupportedObject) : cause = null; | 24 JsonUnsupportedObjectError(this.unsupportedObject) : cause = null; |
| 28 JsonUnsupportedObjectError.withCause(this.unsupportedObject, this.cause); | 25 JsonUnsupportedObjectError.withCause(this.unsupportedObject, this.cause); |
| 29 | 26 |
| 30 String toString() { | 27 String toString() { |
| 31 if (cause != null) { | 28 if (cause != null) { |
| 32 return "Calling toJson method on object failed."; | 29 return "Calling toJson method on object failed."; |
| 33 } else { | 30 } else { |
| 34 return "Object toJson method returns non-serializable value."; | 31 return "Object toJson method returns non-serializable value."; |
| 35 } | 32 } |
| 36 } | 33 } |
| 37 } | 34 } |
| 38 | 35 |
| 39 | 36 |
| 40 /** | 37 /** |
| 41 * Utility class to parse JSON and serialize objects to JSON. | 38 * Parses [json] and build the corresponding parsed JSON value. |
| 39 * |
| 40 * Parsed JSON values are of the types [num], [String], [bool], [Null], |
| 41 * [List]s of parsed JSON values or [Map]s from [String] to parsed |
| 42 * JSON values. |
| 43 * |
| 44 * Throws [FormatException] if the input is not valid JSON text. |
| 42 */ | 45 */ |
| 43 class JSON { | 46 parse(String json, [reviver(var key, var value)]) { |
| 47 BuildJsonListener listener; |
| 48 if (reviver == null) { |
| 49 listener = new BuildJsonListener(); |
| 50 } else { |
| 51 listener = new ReviverJsonListener(reviver); |
| 52 } |
| 53 new JsonParser(json, listener).parse(); |
| 54 return listener.result; |
| 55 } |
| 56 |
| 57 /** |
| 58 * Serializes [object] into a JSON string. |
| 59 * |
| 60 * Directly serializable types are [num], [String], [bool], [Null], [List] |
| 61 * and [Map]. |
| 62 * For [List], the elements must all be serializable. |
| 63 * For [Map], the keys must be [String] and the values must be serializable. |
| 64 * If a value is any other type is attempted serialized, a "toJson()" method |
| 65 * is invoked on the object and the result, which must be a directly |
| 66 * serializable type, is serialized instead of the original value. |
| 67 * If the object does not support this method, throws, or returns a |
| 68 * value that is not directly serializable, a [JsonUnsupportedObjectError] |
| 69 * exception is thrown. If the call throws (including the case where there |
| 70 * is no nullary "toJson" method, the error is caught and stored in the |
| 71 * [JsonUnsupportedObjectError]'s [:cause:] field. |
| 72 *Json |
| 73 * Objects should not change during serialization. |
| 74 * If an object is serialized more than once, [stringify] is allowed to cache |
| 75 * the JSON text for it. I.e., if an object changes after it is first |
| 76 * serialized, the new values may or may not be reflected in the result. |
| 77 */ |
| 78 String stringify(Object object) { |
| 79 return _JsonStringifier.stringify(object); |
| 80 } |
| 81 |
| 82 /** |
| 83 * Serializes [object] into [output] stream. |
| 84 * |
| 85 * Performs the same operations as [stringify] but outputs the resulting |
| 86 * string to an existing [StringBuffer] instead of creating a new [String]. |
| 87 * |
| 88 * If serialization fails by throwing, some data might have been added to |
| 89 * [output], but it won't contain valid JSON text. |
| 90 */ |
| 91 void printOn(Object object, StringBuffer output) { |
| 92 return _JsonStringifier.printOn(object, output); |
| 93 } |
| 94 |
| 95 //// Implementation /////////////////////////////////////////////////////////// |
| 96 |
| 97 // Simple API for JSON parsing. |
| 98 |
| 99 abstract class JsonListener { |
| 100 void handleString(String value) {} |
| 101 void handleNumber(num value) {} |
| 102 void handleBool(bool value) {} |
| 103 void handleNull() {} |
| 104 void beginObject() {} |
| 105 void propertyName() {} |
| 106 void propertyValue() {} |
| 107 void endObject() {} |
| 108 void beginArray() {} |
| 109 void arrayElement() {} |
| 110 void endArray() {} |
| 111 /** Called on failure to parse [source]. */ |
| 112 void fail(String source, int position, String message) {} |
| 113 } |
| 114 |
| 115 /** |
| 116 * A [JsonListener] that builds data objects from the parser events. |
| 117 * |
| 118 * This is a simple stack-based object builder. It keeps the most recently |
| 119 * seen value in a variable, and uses it depending on the following event. |
| 120 */ |
| 121 class BuildJsonListener extends JsonListener { |
| 44 /** | 122 /** |
| 45 * Parses [json] and build the corresponding parsed JSON value. | 123 * Stack used to handle nested containers. |
| 46 * | 124 * |
| 47 * Parsed JSON values are of the types [num], [String], [bool], [Null], | 125 * The current container is pushed on the stack when a new one is |
| 48 * [List]s of parsed JSON values or [Map]s from [String] to parsed | 126 * started. If the container is a [Map], there is also a current [key] |
| 49 * JSON values. | 127 * which is also stored on the stack. |
| 128 */ |
| 129 List stack = []; |
| 130 /** The current [Map] or [List] being built. */ |
| 131 var currentContainer; |
| 132 /** The most recently read property key. */ |
| 133 String key; |
| 134 /** The most recently read value. */ |
| 135 var value; |
| 136 |
| 137 /** Pushes the currently active container (and key, if a [Map]). */ |
| 138 void pushContainer() { |
| 139 if (currentContainer is Map) stack.add(key); |
| 140 stack.add(currentContainer); |
| 141 } |
| 142 |
| 143 /** Pops the top container from the [stack], including a key if applicable. */ |
| 144 void popContainer() { |
| 145 value = currentContainer; |
| 146 currentContainer = stack.removeLast(); |
| 147 if (currentContainer is Map) key = stack.removeLast(); |
| 148 } |
| 149 |
| 150 void handleString(String value) { this.value = value; } |
| 151 void handleNumber(num value) { this.value = value; } |
| 152 void handleBool(bool value) { this.value = value; } |
| 153 void handleNull() { this.value = value; } |
| 154 |
| 155 void beginObject() { |
| 156 pushContainer(); |
| 157 currentContainer = {}; |
| 158 } |
| 159 |
| 160 void propertyName() { |
| 161 key = value; |
| 162 value = null; |
| 163 } |
| 164 |
| 165 void propertyValue() { |
| 166 Map map = currentContainer; |
| 167 map[key] = value; |
| 168 key = value = null; |
| 169 } |
| 170 |
| 171 void endObject() { |
| 172 popContainer(); |
| 173 } |
| 174 |
| 175 void beginArray() { |
| 176 pushContainer(); |
| 177 currentContainer = []; |
| 178 } |
| 179 |
| 180 void arrayElement() { |
| 181 List list = currentContainer; |
| 182 currentContainer.add(value); |
| 183 value = null; |
| 184 } |
| 185 |
| 186 void endArray() { |
| 187 popContainer(); |
| 188 } |
| 189 |
| 190 /** Read out the final result of parsing a JSON string. */ |
| 191 get result { |
| 192 assert(currentContainer == null); |
| 193 return value; |
| 194 } |
| 195 } |
| 196 |
| 197 typedef _Reviver(var key, var value); |
| 198 |
| 199 class ReviverJsonListener extends BuildJsonListener { |
| 200 final _Reviver reviver; |
| 201 ReviverJsonListener(reviver(key, value)) : this.reviver = reviver; |
| 202 |
| 203 void arrayElement() { |
| 204 List list = currentContainer; |
| 205 value = reviver(list.length, value); |
| 206 super.arrayElement(); |
| 207 } |
| 208 |
| 209 void propertyValue() { |
| 210 value = reviver(key, value); |
| 211 super.propertyValue(); |
| 212 } |
| 213 |
| 214 get result { |
| 215 return reviver("", value); |
| 216 } |
| 217 } |
| 218 |
| 219 class JsonParser { |
| 220 // A simple non-recursive state-based parser for JSON. |
| 221 // |
| 222 // Literal values accepted in states ARRAY_EMPTY, ARRAY_COMMA, OBJECT_COLON |
| 223 // and strings also in OBJECT_EMPTY, OBJECT_COMMA. |
| 224 // VALUE STRING : , } ] Transitions to |
| 225 // EMPTY X X -> END |
| 226 // ARRAY_EMPTY X X @ -> ARRAY_VALUE / pop |
| 227 // ARRAY_VALUE @ @ -> ARRAY_COMMA / pop |
| 228 // ARRAY_COMMA X X -> ARRAY_VALUE |
| 229 // OBJECT_EMPTY X @ -> OBJECT_KEY / pop |
| 230 // OBJECT_KEY @ -> OBJECT_COLON |
| 231 // OBJECT_COLON X X -> OBJECT_VALUE |
| 232 // OBJECT_VALUE @ @ -> OBJECT_COMMA / pop |
| 233 // OBJECT_COMMA X -> OBJECT_KEY |
| 234 // END |
| 235 // Starting a new array or object will push the current state. The "pop" |
| 236 // above means restoring this state and then marking it as an ended value. |
| 237 // X means generic handling, @ means special handling for just that |
| 238 // state - that is, values are handled generically, only punctuation |
| 239 // cares about the current state. |
| 240 // Values for states are chosen so bits 0 and 1 tell whether |
| 241 // a string/value is allowed, and setting bits 0 through 2 after a value |
| 242 // gets to the next state (not empty, doesn't allow a value). |
| 243 |
| 244 // State building-block constants. |
| 245 static const int INSIDE_ARRAY = 1; |
| 246 static const int INSIDE_OBJECT = 2; |
| 247 static const int AFTER_COLON = 3; // Always inside object. |
| 248 |
| 249 static const int ALLOW_STRING_MASK = 8; // Allowed if zero. |
| 250 static const int ALLOW_VALUE_MASK = 4; // Allowed if zero. |
| 251 static const int ALLOW_VALUE = 0; |
| 252 static const int STRING_ONLY = 4; |
| 253 static const int NO_VALUES = 12; |
| 254 |
| 255 // Objects and arrays are "empty" until their first property/element. |
| 256 static const int EMPTY = 0; |
| 257 static const int NON_EMPTY = 16; |
| 258 static const int EMPTY_MASK = 16; // Empty if zero. |
| 259 |
| 260 |
| 261 static const int VALUE_READ_BITS = NO_VALUES | NON_EMPTY; |
| 262 |
| 263 // Actual states. |
| 264 static const int STATE_INITIAL = EMPTY | ALLOW_VALUE; |
| 265 static const int STATE_END = NON_EMPTY | NO_VALUES; |
| 266 |
| 267 static const int STATE_ARRAY_EMPTY = INSIDE_ARRAY | EMPTY | ALLOW_VALUE; |
| 268 static const int STATE_ARRAY_VALUE = INSIDE_ARRAY | NON_EMPTY | NO_VALUES; |
| 269 static const int STATE_ARRAY_COMMA = INSIDE_ARRAY | NON_EMPTY | ALLOW_VALUE; |
| 270 |
| 271 static const int STATE_OBJECT_EMPTY = INSIDE_OBJECT | EMPTY | STRING_ONLY; |
| 272 static const int STATE_OBJECT_KEY = INSIDE_OBJECT | NON_EMPTY | NO_VALUES; |
| 273 static const int STATE_OBJECT_COLON = AFTER_COLON | NON_EMPTY | ALLOW_VALUE; |
| 274 static const int STATE_OBJECT_VALUE = AFTER_COLON | NON_EMPTY | NO_VALUES; |
| 275 static const int STATE_OBJECT_COMMA = INSIDE_OBJECT | NON_EMPTY | STRING_ONLY; |
| 276 |
| 277 // Character code constants. |
| 278 static const int BACKSPACE = 0x08; |
| 279 static const int TAB = 0x09; |
| 280 static const int NEWLINE = 0x0a; |
| 281 static const int CARRIAGE_RETURN = 0x0d; |
| 282 static const int FORM_FEED = 0x0c; |
| 283 static const int SPACE = 0x20; |
| 284 static const int QUOTE = 0x22; |
| 285 static const int PLUS = 0x2b; |
| 286 static const int COMMA = 0x2c; |
| 287 static const int MINUS = 0x2d; |
| 288 static const int DECIMALPOINT = 0x2e; |
| 289 static const int SLASH = 0x2f; |
| 290 static const int CHAR_0 = 0x30; |
| 291 static const int CHAR_9 = 0x39; |
| 292 static const int COLON = 0x3a; |
| 293 static const int CHAR_E = 0x45; |
| 294 static const int LBRACKET = 0x5b; |
| 295 static const int BACKSLASH = 0x5c; |
| 296 static const int RBRACKET = 0x5d; |
| 297 static const int CHAR_a = 0x61; |
| 298 static const int CHAR_b = 0x62; |
| 299 static const int CHAR_e = 0x65; |
| 300 static const int CHAR_f = 0x66; |
| 301 static const int CHAR_l = 0x6c; |
| 302 static const int CHAR_n = 0x6e; |
| 303 static const int CHAR_r = 0x72; |
| 304 static const int CHAR_s = 0x73; |
| 305 static const int CHAR_t = 0x74; |
| 306 static const int CHAR_u = 0x75; |
| 307 static const int LBRACE = 0x7b; |
| 308 static const int RBRACE = 0x7d; |
| 309 |
| 310 final String source; |
| 311 final JsonListener listener; |
| 312 JsonParser(this.source, this.listener); |
| 313 |
| 314 /** Parses [source], or throws if it fails. */ |
| 315 void parse() { |
| 316 final List<int> states = <int>[]; |
| 317 int state = STATE_INITIAL; |
| 318 int position = 0; |
| 319 int length = source.length; |
| 320 while (position < length) { |
| 321 int char = source.charCodeAt(position); |
| 322 switch (char) { |
| 323 case SPACE: |
| 324 case CARRIAGE_RETURN: |
| 325 case NEWLINE: |
| 326 case TAB: |
| 327 position++; |
| 328 break; |
| 329 case QUOTE: |
| 330 if ((state & ALLOW_STRING_MASK) != 0) fail(position); |
| 331 position = parseString(position + 1); |
| 332 state |= VALUE_READ_BITS; |
| 333 break; |
| 334 case LBRACKET: |
| 335 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); |
| 336 listener.beginArray(); |
| 337 states.add(state); |
| 338 state = STATE_ARRAY_EMPTY; |
| 339 position++; |
| 340 break; |
| 341 case LBRACE: |
| 342 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); |
| 343 listener.beginObject(); |
| 344 states.add(state); |
| 345 state = STATE_OBJECT_EMPTY; |
| 346 position++; |
| 347 break; |
| 348 case CHAR_n: |
| 349 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); |
| 350 position = parseNull(position); |
| 351 state |= VALUE_READ_BITS; |
| 352 break; |
| 353 case CHAR_f: |
| 354 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); |
| 355 position = parseFalse(position); |
| 356 state |= VALUE_READ_BITS; |
| 357 break; |
| 358 case CHAR_t: |
| 359 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); |
| 360 position = parseTrue(position); |
| 361 state |= VALUE_READ_BITS; |
| 362 break; |
| 363 case COLON: |
| 364 if (state != STATE_OBJECT_KEY) fail(position); |
| 365 listener.propertyName(); |
| 366 state = STATE_OBJECT_COLON; |
| 367 position++; |
| 368 break; |
| 369 case COMMA: |
| 370 if (state == STATE_OBJECT_VALUE) { |
| 371 listener.propertyValue(); |
| 372 state = STATE_OBJECT_COMMA; |
| 373 position++; |
| 374 } else if (state == STATE_ARRAY_VALUE) { |
| 375 listener.arrayElement(); |
| 376 state = STATE_ARRAY_COMMA; |
| 377 position++; |
| 378 } else { |
| 379 fail(position); |
| 380 } |
| 381 break; |
| 382 case RBRACKET: |
| 383 if (state == STATE_ARRAY_EMPTY) { |
| 384 listener.endArray(); |
| 385 } else if (state == STATE_ARRAY_VALUE) { |
| 386 listener.arrayElement(); |
| 387 listener.endArray(); |
| 388 } else { |
| 389 fail(position); |
| 390 } |
| 391 state = states.removeLast() | VALUE_READ_BITS; |
| 392 position++; |
| 393 break; |
| 394 case RBRACE: |
| 395 if (state == STATE_OBJECT_EMPTY) { |
| 396 listener.endObject(); |
| 397 } else if (state == STATE_OBJECT_VALUE) { |
| 398 listener.propertyValue(); |
| 399 listener.endObject(); |
| 400 } else { |
| 401 fail(position); |
| 402 } |
| 403 state = states.removeLast() | VALUE_READ_BITS; |
| 404 position++; |
| 405 break; |
| 406 default: |
| 407 if ((state & ALLOW_VALUE_MASK) != 0) fail(position); |
| 408 position = parseNumber(char, position); |
| 409 state |= VALUE_READ_BITS; |
| 410 break; |
| 411 } |
| 412 } |
| 413 if (state != STATE_END) fail(position); |
| 414 } |
| 415 |
| 416 /** |
| 417 * Parses a "true" literal starting at [position]. |
| 50 * | 418 * |
| 51 * Throws [JSONParseException] if the input is not valid JSON text. | 419 * [:source[position]:] must be "t". |
| 52 */ | 420 */ |
| 53 static parse(String json) { | 421 int parseTrue(int position) { |
| 54 return _JsonParser.parse(json); | 422 assert(source.charCodeAt(position) == CHAR_t); |
| 423 if (source.length < position + 4) fail(position, "Unexpected identifier"); |
| 424 if (source.charCodeAt(position + 1) != CHAR_r || |
| 425 source.charCodeAt(position + 2) != CHAR_u || |
| 426 source.charCodeAt(position + 3) != CHAR_e) { |
| 427 fail(position); |
| 428 } |
| 429 listener.handleBool(true); |
| 430 return position + 4; |
| 55 } | 431 } |
| 56 | 432 |
| 57 /** | 433 /** |
| 58 * Serializes [object] into a JSON string. | 434 * Parses a "false" literal starting at [position]. |
| 59 * | 435 * |
| 60 * Directly serializable types are [num], [String], [bool], [Null], [List] | 436 * [:source[position]:] must be "f". |
| 61 * and [Map]. | 437 */ |
| 62 * For [List], the elements must all be serializable. | 438 int parseFalse(int position) { |
| 63 * For [Map], the keys must be [String] and the values must be serializable. | 439 assert(source.charCodeAt(position) == CHAR_f); |
| 64 * If a value is any other type is attempted serialized, a "toJson()" method | 440 if (source.length < position + 5) fail(position, "Unexpected identifier"); |
| 65 * is invoked on the object and the result, which must be a directly | 441 if (source.charCodeAt(position + 1) != CHAR_a || |
| 66 * serializable type, is serialized instead of the original value. | 442 source.charCodeAt(position + 2) != CHAR_l || |
| 67 * If the object does not support this method, throws, or returns a | 443 source.charCodeAt(position + 3) != CHAR_s || |
| 68 * value that is not directly serializable, a [JsonUnsupportedObjectError] | 444 source.charCodeAt(position + 4) != CHAR_e) { |
| 69 * exception is thrown. If the call throws (including the case where there | 445 fail(position); |
| 70 * is no nullary "toJson" method, the error is caught and stored in the | 446 } |
| 71 * [JsonUnsupportedObjectError]'s [:cause:] field. | 447 listener.handleBool(false); |
| 448 return position + 5; |
| 449 } |
| 450 |
| 451 /** Parses a "null" literal starting at [position]. |
| 72 * | 452 * |
| 73 * Objects should not change during serialization. | 453 * [:source[position]:] must be "n". |
| 74 * If an object is serialized more than once, [stringify] is allowed to cache | |
| 75 * the JSON text for it. I.e., if an object changes after it is first | |
| 76 * serialized, the new values may or may not be reflected in the result. | |
| 77 */ | 454 */ |
| 78 static String stringify(Object object) { | 455 int parseNull(int position) { |
| 79 return _JsonStringifier.stringify(object); | 456 assert(source.charCodeAt(position) == CHAR_n); |
| 80 } | 457 if (source.length < position + 4) fail(position, "Unexpected identifier"); |
| 81 | 458 if (source.charCodeAt(position + 1) != CHAR_u || |
| 82 /** | 459 source.charCodeAt(position + 2) != CHAR_l || |
| 83 * Serializes [object] into [output] stream. | 460 source.charCodeAt(position + 3) != CHAR_l) { |
| 84 * | 461 fail(position); |
| 85 * Performs the same operations as [stringify] but outputs the resulting | 462 } |
| 86 * string to an existing [StringBuffer] instead of creating a new [String]. | 463 listener.handleNull(); |
| 87 * | 464 return position + 4; |
| 88 * If serialization fails by throwing, some data might have been added to | 465 } |
| 89 * [output], but it won't contain valid JSON text. | 466 |
| 90 */ | 467 int parseString(int position) { |
| 91 static void printOn(Object object, StringBuffer output) { | 468 // Format: '"'([^\x00-\x1f\\\"]|'\\'[bfnrt/\\"])*'"' |
| 92 return _JsonStringifier.printOn(object, output); | 469 // Initial position is right after first '"'. |
| 470 int start = position; |
| 471 int char; |
| 472 do { |
| 473 if (position == source.length) { |
| 474 fail(start - 1, "Unterminated string"); |
| 475 } |
| 476 char = source.charCodeAt(position); |
| 477 if (char == QUOTE) { |
| 478 listener.handleString(source.substring(start, position)); |
| 479 return position + 1; |
| 480 } |
| 481 if (char < SPACE) { |
| 482 fail(position, "Control character in string"); |
| 483 } |
| 484 position++; |
| 485 } while (char != BACKSLASH); |
| 486 // Backslash escape detected. Collect character codes for rest of string. |
| 487 int firstEscape = position - 1; |
| 488 List<int> chars = <int>[]; |
| 489 while (true) { |
| 490 if (position == source.length) { |
| 491 fail(start - 1, "Unterminated string"); |
| 492 } |
| 493 char = source.charCodeAt(position); |
| 494 switch (char) { |
| 495 case CHAR_b: char = BACKSPACE; break; |
| 496 case CHAR_f: char = FORM_FEED; break; |
| 497 case CHAR_n: char = NEWLINE; break; |
| 498 case CHAR_r: char = CARRIAGE_RETURN; break; |
| 499 case CHAR_t: char = TAB; break; |
| 500 case SLASH: |
| 501 case BACKSLASH: |
| 502 case QUOTE: |
| 503 break; |
| 504 case CHAR_u: { |
| 505 int hexStart = position - 1; |
| 506 int value = 0; |
| 507 for (int i = 0; i < 4; i++) { |
| 508 position++; |
| 509 if (position == source.length) { |
| 510 fail(start - 1, "Unterminated string"); |
| 511 } |
| 512 char = source.charCodeAt(position); |
| 513 char -= 0x30; |
| 514 if (char < 0) fail(hexStart, "Invalid unicode escape"); |
| 515 if (char < 10) { |
| 516 value = value * 16 + char; |
| 517 } else { |
| 518 char = (char | 0x20) - 0x31; |
| 519 if (char < 0 || char > 5) { |
| 520 fail(hexStart, "Invalid unicode escape"); |
| 521 } |
| 522 value = value * 16 + char + 10; |
| 523 } |
| 524 } |
| 525 char = value; |
| 526 break; |
| 527 } |
| 528 default: |
| 529 if (char < SPACE) fail(position, "Control character in string"); |
| 530 fail(position, "Unrecognized string escape"); |
| 531 } |
| 532 do { |
| 533 chars.add(char); |
| 534 position++; |
| 535 if (position == source.length) fail(start - 1, "Unterminated string"); |
| 536 char = source.charCodeAt(position); |
| 537 if (char == QUOTE) { |
| 538 String result = new String.fromCharCodes(chars); |
| 539 if (start < firstEscape) { |
| 540 result = "${source.substring(start, firstEscape)}$result"; |
| 541 } |
| 542 listener.handleString(result); |
| 543 return position + 1; |
| 544 } |
| 545 if (char < SPACE) { |
| 546 fail(position, "Control character in string"); |
| 547 } |
| 548 } while (char != BACKSLASH); |
| 549 position++; |
| 550 } |
| 551 } |
| 552 |
| 553 int parseNumber(int char, int position) { |
| 554 // Format: |
| 555 // '-'?('0'|[1-9][0-9]*)('.'[0-9]+)?([eE][+-]?[0-9]+)? |
| 556 int start = position; |
| 557 int length = source.length; |
| 558 bool isDouble = false; |
| 559 if (char == MINUS) { |
| 560 position++; |
| 561 if (position == length) fail(position, "Missing expected digit"); |
| 562 char = source.charCodeAt(position); |
| 563 } |
| 564 if (char < CHAR_0 || char > CHAR_9) { |
| 565 fail(position, "Missing expected digit"); |
| 566 } |
| 567 int handleLiteral(position) { |
| 568 String literal = source.substring(start, position); |
| 569 // This correctly creates -0 for doubles. |
| 570 num value = (isDouble ? double.parse(literal) : int.parse(literal)); |
| 571 listener.handleNumber(value); |
| 572 return position; |
| 573 } |
| 574 if (char == CHAR_0) { |
| 575 position++; |
| 576 if (position == length) return handleLiteral(position); |
| 577 char = source.charCodeAt(position); |
| 578 if (CHAR_0 <= char && char <= CHAR_9) { |
| 579 fail(position); |
| 580 } |
| 581 } else { |
| 582 do { |
| 583 position++; |
| 584 if (position == length) return handleLiteral(position); |
| 585 char = source.charCodeAt(position); |
| 586 } while (CHAR_0 <= char && char <= CHAR_9); |
| 587 } |
| 588 if (char == DECIMALPOINT) { |
| 589 isDouble = true; |
| 590 position++; |
| 591 if (position == length) fail(position, "Missing expected digit"); |
| 592 char = source.charCodeAt(position); |
| 593 if (char < CHAR_0 || char > CHAR_9) fail(position); |
| 594 do { |
| 595 position++; |
| 596 if (position == length) return handleLiteral(position); |
| 597 char = source.charCodeAt(position); |
| 598 } while (CHAR_0 <= char && char <= CHAR_9); |
| 599 } |
| 600 if (char == CHAR_e || char == CHAR_E) { |
| 601 isDouble = true; |
| 602 position++; |
| 603 if (position == length) fail(position, "Missing expected digit"); |
| 604 char = source.charCodeAt(position); |
| 605 if (char == PLUS || char == MINUS) { |
| 606 position++; |
| 607 if (position == length) fail(position, "Missing expected digit"); |
| 608 char = source.charCodeAt(position); |
| 609 } |
| 610 if (char < CHAR_0 || char > CHAR_9) { |
| 611 fail(position, "Missing expected digit"); |
| 612 } |
| 613 do { |
| 614 position++; |
| 615 if (position == length) return handleLiteral(position); |
| 616 char = source.charCodeAt(position); |
| 617 } while (CHAR_0 <= char && char <= CHAR_9); |
| 618 } |
| 619 return handleLiteral(position); |
| 620 } |
| 621 |
| 622 void fail(int position, [String message]) { |
| 623 if (message == null) message = "Unexpected character"; |
| 624 listener.fail(source, position, message); |
| 625 // If the listener didn't throw, do it here. |
| 626 String slice; |
| 627 int sliceEnd = position + 20; |
| 628 if (sliceEnd > source.length) { |
| 629 slice = "'${source.substring(position)}'"; |
| 630 } else { |
| 631 slice = "'${source.substring(position, sliceEnd)}...'"; |
| 632 } |
| 633 throw new FormatException("Unexpected character at $position: $slice"); |
| 93 } | 634 } |
| 94 } | 635 } |
| 95 | 636 |
| 96 //// Implementation /////////////////////////////////////////////////////////// | |
| 97 | |
| 98 // TODO(ajohnsen): Introduce when we have a common exception interface for json. | |
| 99 class JSONParseException { | |
| 100 JSONParseException(int position, String message) : | |
| 101 position = position, | |
| 102 message = 'JSONParseException: $message, at offset $position'; | |
| 103 | |
| 104 String toString() => message; | |
| 105 | |
| 106 final String message; | |
| 107 final int position; | |
| 108 } | |
| 109 | |
| 110 class _JsonParser { | |
| 111 static const int BACKSPACE = 8; | |
| 112 static const int TAB = 9; | |
| 113 static const int NEW_LINE = 10; | |
| 114 static const int FORM_FEED = 12; | |
| 115 static const int CARRIAGE_RETURN = 13; | |
| 116 static const int SPACE = 32; | |
| 117 static const int QUOTE = 34; | |
| 118 static const int PLUS = 43; | |
| 119 static const int COMMA = 44; | |
| 120 static const int MINUS = 45; | |
| 121 static const int DOT = 46; | |
| 122 static const int SLASH = 47; | |
| 123 static const int CHAR_0 = 48; | |
| 124 static const int CHAR_1 = 49; | |
| 125 static const int CHAR_2 = 50; | |
| 126 static const int CHAR_3 = 51; | |
| 127 static const int CHAR_4 = 52; | |
| 128 static const int CHAR_5 = 53; | |
| 129 static const int CHAR_6 = 54; | |
| 130 static const int CHAR_7 = 55; | |
| 131 static const int CHAR_8 = 56; | |
| 132 static const int CHAR_9 = 57; | |
| 133 static const int COLON = 58; | |
| 134 static const int CHAR_CAPITAL_E = 69; | |
| 135 static const int LBRACKET = 91; | |
| 136 static const int BACKSLASH = 92; | |
| 137 static const int RBRACKET = 93; | |
| 138 static const int CHAR_B = 98; | |
| 139 static const int CHAR_E = 101; | |
| 140 static const int CHAR_F = 102; | |
| 141 static const int CHAR_N = 110; | |
| 142 static const int CHAR_R = 114; | |
| 143 static const int CHAR_T = 116; | |
| 144 static const int CHAR_U = 117; | |
| 145 static const int LBRACE = 123; | |
| 146 static const int RBRACE = 125; | |
| 147 | |
| 148 static const int STRING_LITERAL = QUOTE; | |
| 149 static const int NUMBER_LITERAL = MINUS; | |
| 150 static const int NULL_LITERAL = CHAR_N; | |
| 151 static const int FALSE_LITERAL = CHAR_F; | |
| 152 static const int TRUE_LITERAL = CHAR_T; | |
| 153 | |
| 154 static const int WHITESPACE = SPACE; | |
| 155 | |
| 156 static const int LAST_ASCII = RBRACE; | |
| 157 | |
| 158 static const String NULL_STRING = "null"; | |
| 159 static const String TRUE_STRING = "true"; | |
| 160 static const String FALSE_STRING = "false"; | |
| 161 | |
| 162 static List<int> tokens; | |
| 163 | |
| 164 final String json; | |
| 165 final int length; | |
| 166 int position = 0; | |
| 167 | |
| 168 static parse(String json) { | |
| 169 return new _JsonParser(json).parseToplevel(); | |
| 170 } | |
| 171 | |
| 172 _JsonParser(String json) | |
| 173 : json = json, | |
| 174 length = json.length { | |
| 175 if (tokens != null) return; | |
| 176 | |
| 177 // Use a list as jump-table. It is faster than switch and if. | |
| 178 tokens = new List<int>(LAST_ASCII + 1); | |
| 179 tokens[TAB] = WHITESPACE; | |
| 180 tokens[NEW_LINE] = WHITESPACE; | |
| 181 tokens[CARRIAGE_RETURN] = WHITESPACE; | |
| 182 tokens[SPACE] = WHITESPACE; | |
| 183 tokens[CHAR_0] = NUMBER_LITERAL; | |
| 184 tokens[CHAR_1] = NUMBER_LITERAL; | |
| 185 tokens[CHAR_2] = NUMBER_LITERAL; | |
| 186 tokens[CHAR_3] = NUMBER_LITERAL; | |
| 187 tokens[CHAR_4] = NUMBER_LITERAL; | |
| 188 tokens[CHAR_5] = NUMBER_LITERAL; | |
| 189 tokens[CHAR_6] = NUMBER_LITERAL; | |
| 190 tokens[CHAR_7] = NUMBER_LITERAL; | |
| 191 tokens[CHAR_8] = NUMBER_LITERAL; | |
| 192 tokens[CHAR_9] = NUMBER_LITERAL; | |
| 193 tokens[MINUS] = NUMBER_LITERAL; | |
| 194 tokens[LBRACE] = LBRACE; | |
| 195 tokens[RBRACE] = RBRACE; | |
| 196 tokens[LBRACKET] = LBRACKET; | |
| 197 tokens[RBRACKET] = RBRACKET; | |
| 198 tokens[QUOTE] = STRING_LITERAL; | |
| 199 tokens[COLON] = COLON; | |
| 200 tokens[COMMA] = COMMA; | |
| 201 tokens[CHAR_N] = NULL_LITERAL; | |
| 202 tokens[CHAR_T] = TRUE_LITERAL; | |
| 203 tokens[CHAR_F] = FALSE_LITERAL; | |
| 204 } | |
| 205 | |
| 206 parseToplevel() { | |
| 207 final result = parseValue(); | |
| 208 if (token() != null) { | |
| 209 error('Junk at the end of JSON input'); | |
| 210 } | |
| 211 return result; | |
| 212 } | |
| 213 | |
| 214 parseValue() { | |
| 215 final int token = token(); | |
| 216 if (token == null) { | |
| 217 error('Nothing to parse'); | |
| 218 } | |
| 219 switch (token) { | |
| 220 case STRING_LITERAL: return parseString(); | |
| 221 case NUMBER_LITERAL: return parseNumber(); | |
| 222 case NULL_LITERAL: return expectKeyword(NULL_STRING, null); | |
| 223 case FALSE_LITERAL: return expectKeyword(FALSE_STRING, false); | |
| 224 case TRUE_LITERAL: return expectKeyword(TRUE_STRING, true); | |
| 225 case LBRACE: return parseObject(); | |
| 226 case LBRACKET: return parseList(); | |
| 227 | |
| 228 default: | |
| 229 error('Unexpected token'); | |
| 230 } | |
| 231 } | |
| 232 | |
| 233 Object expectKeyword(String word, Object value) { | |
| 234 for (int i = 0; i < word.length; i++) { | |
| 235 // Implicit end check in char(). | |
| 236 if (char() != word.charCodeAt(i)) error("Expected keyword '$word'"); | |
| 237 position++; | |
| 238 } | |
| 239 return value; | |
| 240 } | |
| 241 | |
| 242 parseObject() { | |
| 243 final object = {}; | |
| 244 | |
| 245 position++; // Eat '{'. | |
| 246 | |
| 247 if (!isToken(RBRACE)) { | |
| 248 while (true) { | |
| 249 final String key = parseString(); | |
| 250 if (!isToken(COLON)) error("Expected ':' when parsing object"); | |
| 251 position++; | |
| 252 object[key] = parseValue(); | |
| 253 | |
| 254 if (!isToken(COMMA)) break; | |
| 255 position++; // Skip ','. | |
| 256 }; | |
| 257 | |
| 258 if (!isToken(RBRACE)) error("Expected '}' at end of object"); | |
| 259 } | |
| 260 position++; | |
| 261 | |
| 262 return object; | |
| 263 } | |
| 264 | |
| 265 parseList() { | |
| 266 final list = []; | |
| 267 | |
| 268 position++; // Eat '['. | |
| 269 | |
| 270 if (!isToken(RBRACKET)) { | |
| 271 while (true) { | |
| 272 list.add(parseValue()); | |
| 273 | |
| 274 if (!isToken(COMMA)) break; | |
| 275 position++; | |
| 276 }; | |
| 277 | |
| 278 if (!isToken(RBRACKET)) error("Expected ']' at end of list"); | |
| 279 } | |
| 280 position++; | |
| 281 | |
| 282 return list; | |
| 283 } | |
| 284 | |
| 285 String parseString() { | |
| 286 if (!isToken(STRING_LITERAL)) error("Expected string literal"); | |
| 287 | |
| 288 position++; // Eat '"'. | |
| 289 | |
| 290 List<int> charCodes = new List<int>(); | |
| 291 while (true) { | |
| 292 int c = char(); | |
| 293 if (c == QUOTE) { | |
| 294 position++; | |
| 295 break; | |
| 296 } | |
| 297 if (c == BACKSLASH) { | |
| 298 position++; | |
| 299 if (position == length) { | |
| 300 error('\\ at the end of input'); | |
| 301 } | |
| 302 | |
| 303 switch (char()) { | |
| 304 case QUOTE: | |
| 305 c = QUOTE; | |
| 306 break; | |
| 307 case BACKSLASH: | |
| 308 c = BACKSLASH; | |
| 309 break; | |
| 310 case SLASH: | |
| 311 c = SLASH; | |
| 312 break; | |
| 313 case CHAR_B: | |
| 314 c = BACKSPACE; | |
| 315 break; | |
| 316 case CHAR_N: | |
| 317 c = NEW_LINE; | |
| 318 break; | |
| 319 case CHAR_R: | |
| 320 c = CARRIAGE_RETURN; | |
| 321 break; | |
| 322 case CHAR_F: | |
| 323 c = FORM_FEED; | |
| 324 break; | |
| 325 case CHAR_T: | |
| 326 c = TAB; | |
| 327 break; | |
| 328 case CHAR_U: | |
| 329 if (position + 5 > length) { | |
| 330 error('Invalid unicode esacape sequence'); | |
| 331 } | |
| 332 final codeString = json.substring(position + 1, position + 5); | |
| 333 try { | |
| 334 c = int.parse('0x${codeString}'); | |
| 335 } catch (e) { | |
| 336 error('Invalid unicode esacape sequence'); | |
| 337 } | |
| 338 position += 4; | |
| 339 break; | |
| 340 default: | |
| 341 error('Invalid esacape sequence in string literal'); | |
| 342 } | |
| 343 } | |
| 344 charCodes.add(c); | |
| 345 position++; | |
| 346 } | |
| 347 | |
| 348 return new String.fromCharCodes(charCodes); | |
| 349 } | |
| 350 | |
| 351 num parseNumber() { | |
| 352 if (!isToken(NUMBER_LITERAL)) error('Expected number literal'); | |
| 353 | |
| 354 final int startPos = position; | |
| 355 int char = char(); | |
| 356 if (identical(char, MINUS)) char = nextChar(); | |
| 357 if (identical(char, CHAR_0)) { | |
| 358 char = nextChar(); | |
| 359 } else if (isDigit(char)) { | |
| 360 char = nextChar(); | |
| 361 while (isDigit(char)) char = nextChar(); | |
| 362 } else { | |
| 363 error('Expected digit when parsing number'); | |
| 364 } | |
| 365 | |
| 366 bool isInt = true; | |
| 367 if (identical(char, DOT)) { | |
| 368 char = nextChar(); | |
| 369 if (isDigit(char)) { | |
| 370 char = nextChar(); | |
| 371 isInt = false; | |
| 372 while (isDigit(char)) char = nextChar(); | |
| 373 } else { | |
| 374 error('Expected digit following comma'); | |
| 375 } | |
| 376 } | |
| 377 | |
| 378 if (identical(char, CHAR_E) || identical(char, CHAR_CAPITAL_E)) { | |
| 379 char = nextChar(); | |
| 380 if (identical(char, MINUS) || identical(char, PLUS)) char = nextChar(); | |
| 381 if (isDigit(char)) { | |
| 382 char = nextChar(); | |
| 383 isInt = false; | |
| 384 while (isDigit(char)) char = nextChar(); | |
| 385 } else { | |
| 386 error('Expected digit following \'e\' or \'E\''); | |
| 387 } | |
| 388 } | |
| 389 | |
| 390 String number = json.substring(startPos, position); | |
| 391 if (isInt) { | |
| 392 return int.parse(number); | |
| 393 } else { | |
| 394 return double.parse(number); | |
| 395 } | |
| 396 } | |
| 397 | |
| 398 bool isChar(int char) { | |
| 399 if (position >= length) return false; | |
| 400 return json.charCodeAt(position) == char; | |
| 401 } | |
| 402 | |
| 403 bool isDigit(int char) { | |
| 404 return char >= CHAR_0 && char <= CHAR_9; | |
| 405 } | |
| 406 | |
| 407 bool isToken(int tokenKind) => token() == tokenKind; | |
| 408 | |
| 409 int char() { | |
| 410 if (position >= length) { | |
| 411 error('Unexpected end of JSON stream'); | |
| 412 } | |
| 413 return json.charCodeAt(position); | |
| 414 } | |
| 415 | |
| 416 int nextChar() { | |
| 417 position++; | |
| 418 if (position >= length) return 0; | |
| 419 return json.charCodeAt(position); | |
| 420 } | |
| 421 | |
| 422 int token() { | |
| 423 while (true) { | |
| 424 if (position >= length) return null; | |
| 425 int char = json.charCodeAt(position); | |
| 426 int token = tokens[char]; | |
| 427 if (identical(token, WHITESPACE)) { | |
| 428 position++; | |
| 429 continue; | |
| 430 } | |
| 431 if (token == null) return 0; | |
| 432 return token; | |
| 433 } | |
| 434 } | |
| 435 | |
| 436 void error(String message) { | |
| 437 throw message; | |
| 438 } | |
| 439 } | |
| 440 | 637 |
| 441 class _JsonStringifier { | 638 class _JsonStringifier { |
| 442 StringBuffer sb; | 639 StringBuffer sb; |
| 443 List<Object> seen; // TODO: that should be identity set. | 640 List<Object> seen; // TODO: that should be identity set. |
| 444 | 641 |
| 445 _JsonStringifier(this.sb) : seen = []; | 642 _JsonStringifier(this.sb) : seen = []; |
| 446 | 643 |
| 447 static String stringify(final object) { | 644 static String stringify(final object) { |
| 448 StringBuffer output = new StringBuffer(); | 645 StringBuffer output = new StringBuffer(); |
| 449 _JsonStringifier stringifier = new _JsonStringifier(output); | 646 _JsonStringifier stringifier = new _JsonStringifier(output); |
| (...skipping 14 matching lines...) Expand all Loading... |
| 464 static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; | 661 static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; |
| 465 | 662 |
| 466 static void escape(StringBuffer sb, String s) { | 663 static void escape(StringBuffer sb, String s) { |
| 467 final int length = s.length; | 664 final int length = s.length; |
| 468 bool needsEscape = false; | 665 bool needsEscape = false; |
| 469 final charCodes = new List<int>(); | 666 final charCodes = new List<int>(); |
| 470 for (int i = 0; i < length; i++) { | 667 for (int i = 0; i < length; i++) { |
| 471 int charCode = s.charCodeAt(i); | 668 int charCode = s.charCodeAt(i); |
| 472 if (charCode < 32) { | 669 if (charCode < 32) { |
| 473 needsEscape = true; | 670 needsEscape = true; |
| 474 charCodes.add(_JsonParser.BACKSLASH); | 671 charCodes.add(JsonParser.BACKSLASH); |
| 475 switch (charCode) { | 672 switch (charCode) { |
| 476 case _JsonParser.BACKSPACE: | 673 case JsonParser.BACKSPACE: |
| 477 charCodes.add(_JsonParser.CHAR_B); | 674 charCodes.add(JsonParser.CHAR_b); |
| 478 break; | 675 break; |
| 479 case _JsonParser.TAB: | 676 case JsonParser.TAB: |
| 480 charCodes.add(_JsonParser.CHAR_T); | 677 charCodes.add(JsonParser.CHAR_t); |
| 481 break; | 678 break; |
| 482 case _JsonParser.NEW_LINE: | 679 case JsonParser.NEWLINE: |
| 483 charCodes.add(_JsonParser.CHAR_N); | 680 charCodes.add(JsonParser.CHAR_n); |
| 484 break; | 681 break; |
| 485 case _JsonParser.FORM_FEED: | 682 case JsonParser.FORM_FEED: |
| 486 charCodes.add(_JsonParser.CHAR_F); | 683 charCodes.add(JsonParser.CHAR_f); |
| 487 break; | 684 break; |
| 488 case _JsonParser.CARRIAGE_RETURN: | 685 case JsonParser.CARRIAGE_RETURN: |
| 489 charCodes.add(_JsonParser.CHAR_R); | 686 charCodes.add(JsonParser.CHAR_r); |
| 490 break; | 687 break; |
| 491 default: | 688 default: |
| 492 charCodes.add(_JsonParser.CHAR_U); | 689 charCodes.add(JsonParser.CHAR_u); |
| 493 charCodes.add(hexDigit((charCode >> 12) & 0xf)); | 690 charCodes.add(hexDigit((charCode >> 12) & 0xf)); |
| 494 charCodes.add(hexDigit((charCode >> 8) & 0xf)); | 691 charCodes.add(hexDigit((charCode >> 8) & 0xf)); |
| 495 charCodes.add(hexDigit((charCode >> 4) & 0xf)); | 692 charCodes.add(hexDigit((charCode >> 4) & 0xf)); |
| 496 charCodes.add(hexDigit(charCode & 0xf)); | 693 charCodes.add(hexDigit(charCode & 0xf)); |
| 497 break; | 694 break; |
| 498 } | 695 } |
| 499 } else if (charCode == _JsonParser.QUOTE || | 696 } else if (charCode == JsonParser.QUOTE || |
| 500 charCode == _JsonParser.BACKSLASH) { | 697 charCode == JsonParser.BACKSLASH) { |
| 501 needsEscape = true; | 698 needsEscape = true; |
| 502 charCodes.add(_JsonParser.BACKSLASH); | 699 charCodes.add(JsonParser.BACKSLASH); |
| 503 charCodes.add(charCode); | 700 charCodes.add(charCode); |
| 504 } else { | 701 } else { |
| 505 charCodes.add(charCode); | 702 charCodes.add(charCode); |
| 506 } | 703 } |
| 507 } | 704 } |
| 508 sb.add(needsEscape ? new String.fromCharCodes(charCodes) : s); | 705 sb.add(needsEscape ? new String.fromCharCodes(charCodes) : s); |
| 509 } | 706 } |
| 510 | 707 |
| 511 void checkCycle(final object) { | 708 void checkCycle(final object) { |
| 512 // TODO: use Iterables. | 709 // TODO: use Iterables. |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 593 first = false; | 790 first = false; |
| 594 }); | 791 }); |
| 595 sb.add('}'); | 792 sb.add('}'); |
| 596 seen.removeLast(); | 793 seen.removeLast(); |
| 597 return true; | 794 return true; |
| 598 } else { | 795 } else { |
| 599 return false; | 796 return false; |
| 600 } | 797 } |
| 601 } | 798 } |
| 602 } | 799 } |
| OLD | NEW |