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 |