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