Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, 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 part of dart.convert; | 5 part of dart.convert; |
| 6 | 6 |
| 7 /** | 7 /** |
| 8 * Error thrown by JSON serialization if an object cannot be serialized. | 8 * Error thrown by JSON serialization if an object cannot be serialized. |
| 9 * | 9 * |
| 10 * The [unsupportedObject] field holds that object that failed to be serialized. | 10 * The [unsupportedObject] field holds that object that failed to be serialized. |
| (...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 160 /** | 160 /** |
| 161 * The string used for indention. | 161 * The string used for indention. |
| 162 * | 162 * |
| 163 * When generating multi-line output, this string is inserted once at the | 163 * When generating multi-line output, this string is inserted once at the |
| 164 * beginning of each indented line for each level of indentation. | 164 * beginning of each indented line for each level of indentation. |
| 165 * | 165 * |
| 166 * If `null`, the output is encoded as a single line. | 166 * If `null`, the output is encoded as a single line. |
| 167 */ | 167 */ |
| 168 final String indent; | 168 final String indent; |
| 169 | 169 |
| 170 final _toEncodableFunction; | 170 /** |
| 171 * Function called on non-encodable objects to return a replacement | |
| 172 * encodable object that will be encoded in the orignal's place. | |
| 173 */ | |
| 174 final Function _toEncodable; | |
| 171 | 175 |
| 172 /** | 176 /** |
| 173 * Creates a JSON encoder. | 177 * Creates a JSON encoder. |
| 174 * | 178 * |
| 175 * The JSON encoder handles numbers, strings, booleans, null, lists and | 179 * The JSON encoder handles numbers, strings, booleans, null, lists and |
| 176 * maps directly. | 180 * maps directly. |
| 177 * | 181 * |
| 178 * Any other object is attempted converted by [toEncodable] to an | 182 * Any other object is attempted converted by [toEncodable] to an |
| 179 * object that is of one of the convertible types. | 183 * object that is of one of the convertible types. |
| 180 * | 184 * |
| 181 * If [toEncodable] is omitted, it defaults to calling `.toJson()` on | 185 * If [toEncodable] is omitted, it defaults to calling `.toJson()` on |
| 182 * the object. | 186 * the object. |
| 183 */ | 187 */ |
| 184 const JsonEncoder([Object toEncodable(Object nonSerializable)]) | 188 const JsonEncoder([Object toEncodable(Object nonSerializable)]) |
| 185 : this.indent = null, | 189 : this.indent = null, |
| 186 this._toEncodableFunction = toEncodable; | 190 this._toEncodable = toEncodable; |
| 187 | 191 |
| 188 /** | 192 /** |
| 189 * Creates a JSON encoder that creates multi-line JSON. | 193 * Creates a JSON encoder that creates multi-line JSON. |
| 190 * | 194 * |
| 191 * The encoding of elements of lists and maps are indented and put on separate | 195 * The encoding of elements of lists and maps are indented and put on separate |
| 192 * lines. The [indent] string is prepended to these elements, once for each | 196 * lines. The [indent] string is prepended to these elements, once for each |
| 193 * level of indentation. | 197 * level of indentation. |
| 194 * | 198 * |
| 195 * If [indent] is `null`, the output is encoded as a single line. | 199 * If [indent] is `null`, the output is encoded as a single line. |
| 196 * | 200 * |
| 197 * The JSON encoder handles numbers, strings, booleans, null, lists and | 201 * The JSON encoder handles numbers, strings, booleans, null, lists and |
| 198 * maps directly. | 202 * maps directly. |
| 199 * | 203 * |
| 200 * Any other object is attempted converted by [toEncodable] to an | 204 * Any other object is attempted converted by [toEncodable] to an |
| 201 * object that is of one of the convertible types. | 205 * object that is of one of the convertible types. |
| 202 * | 206 * |
| 203 * If [toEncodable] is omitted, it defaults to calling `.toJson()` on | 207 * If [toEncodable] is omitted, it defaults to calling `.toJson()` on |
| 204 * the object. | 208 * the object. |
| 205 */ | 209 */ |
| 206 const JsonEncoder.withIndent(this.indent, | 210 const JsonEncoder.withIndent(this.indent, |
| 207 [Object toEncodable(Object nonSerializable)]) | 211 [Object toEncodable(Object nonSerializable)]) |
| 208 : this._toEncodableFunction = toEncodable; | 212 : this._toEncodable = toEncodable; |
| 209 | 213 |
| 210 /** | 214 /** |
| 211 * Converts [object] to a JSON [String]. | 215 * Converts [object] to a JSON [String]. |
| 212 * | 216 * |
| 213 * Directly serializable values are [num], [String], [bool], and [Null], as | 217 * Directly serializable values are [num], [String], [bool], and [Null], as |
| 214 * well as some [List] and [Map] values. | 218 * well as some [List] and [Map] values. |
| 215 * For [List], the elements must all be serializable. | 219 * For [List], the elements must all be serializable. |
| 216 * For [Map], the keys must be [String] and the values must be serializable. | 220 * For [Map], the keys must be [String] and the values must be serializable. |
| 217 * | 221 * |
| 218 * If a value is any other type is attempted serialized, the conversion | 222 * If a value is any other type is attempted serialized, the conversion |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 229 * other lists or maps, it cannot be serialized and a [JsonCyclicError] is | 233 * other lists or maps, it cannot be serialized and a [JsonCyclicError] is |
| 230 * thrown. | 234 * thrown. |
| 231 * | 235 * |
| 232 * [object] should not change during serialization. | 236 * [object] should not change during serialization. |
| 233 * | 237 * |
| 234 * If an object is serialized more than once, [convert] may cache the text | 238 * If an object is serialized more than once, [convert] may cache the text |
| 235 * for it. In other words, if the content of an object changes after it is | 239 * for it. In other words, if the content of an object changes after it is |
| 236 * first serialized, the new values may not be reflected in the result. | 240 * first serialized, the new values may not be reflected in the result. |
| 237 */ | 241 */ |
| 238 String convert(Object object) => | 242 String convert(Object object) => |
| 239 _JsonStringifier.stringify(object, _toEncodableFunction, indent); | 243 _JsonStringStringifier.stringify(object, _toEncodable, indent); |
| 240 | 244 |
| 241 /** | 245 /** |
| 242 * Starts a chunked conversion. | 246 * Starts a chunked conversion. |
| 243 * | 247 * |
| 244 * The converter works more efficiently if the given [sink] is a | 248 * The converter works more efficiently if the given [sink] is a |
| 245 * [StringConversionSink]. | 249 * [StringConversionSink]. |
| 246 * | 250 * |
| 247 * Returns a chunked-conversion sink that accepts at most one object. It is | 251 * Returns a chunked-conversion sink that accepts at most one object. It is |
| 248 * an error to invoke `add` more than once on the returned sink. | 252 * an error to invoke `add` more than once on the returned sink. |
| 249 */ | 253 */ |
| 250 ChunkedConversionSink<Object> startChunkedConversion(Sink<String> sink) { | 254 ChunkedConversionSink<Object> startChunkedConversion(Sink<String> sink) { |
| 251 if (sink is! StringConversionSink) { | 255 if (sink is! StringConversionSink) { |
| 252 sink = new StringConversionSink.from(sink); | 256 sink = new StringConversionSink.from(sink); |
| 257 } else if (sink is _Utf8EncoderSink) { | |
| 258 return new _JsonUtf8EncoderSink(sink._sink, _toEncodable, | |
| 259 JsonUtf8Encoder._utf8Encode(indent), | |
| 260 _JsonUtf8EncoderSink.defaultBufferSize); | |
| 253 } | 261 } |
| 254 return new _JsonEncoderSink(sink, _toEncodableFunction, indent); | 262 return new _JsonEncoderSink(sink, _toEncodable, indent); |
| 255 } | 263 } |
| 256 | 264 |
| 257 // Override the base-classes bind, to provide a better type. | 265 // Override the base-classes bind, to provide a better type. |
| 258 Stream<String> bind(Stream<Object> stream) => super.bind(stream); | 266 Stream<String> bind(Stream<Object> stream) => super.bind(stream); |
| 267 | |
| 268 Converter<Object, dynamic> fuse(Converter<String, dynamic> other) { | |
| 269 if (other is Utf8Encoder) { | |
| 270 return new JsonUtf8Encoder(indent, _toEncodable); | |
| 271 } | |
| 272 return super.fuse(other); | |
| 273 } | |
| 259 } | 274 } |
| 260 | 275 |
| 261 /** | 276 /** |
| 277 * Encoder that encodes a single object as a UTF-8 encoded JSON string. | |
| 278 * | |
| 279 * This encoder works equivalently to first converting the object to | |
| 280 * a JSON string, and then UTF-8 encoding the string, but without | |
| 281 * creating an intermediate string. | |
| 282 */ | |
| 283 class JsonUtf8Encoder extends Converter<Object, List<int>> { | |
| 284 /** Default buffer size used by the JSON-to-UTF-8 encoder. */ | |
| 285 static const int defaultBufferSize = 256; | |
|
Søren Gjesse
2014/11/04 08:26:31
Isn't this a pretty low default value?
So you are
Lasse Reichstein Nielsen
2014/11/04 10:25:33
It's smallish, but that's good for the cases where
| |
| 286 /** Indentation used in pretty-print mode, `null` if not pretty. */ | |
| 287 final List<int> _indent; | |
| 288 /** Function called with each un-encodable object encountered. */ | |
| 289 final Function _toEncodable; | |
| 290 /** UTF-8 buffer size. */ | |
| 291 final int _bufferSize; | |
| 292 | |
| 293 /** | |
| 294 * Create converter. | |
| 295 * | |
| 296 * If [indent] is non-`null`, the converter attempts to "pretty-print" the | |
| 297 * JSON, and uses `indent` as the indentation. Otherwise the result has no | |
| 298 * whitespace outside of string literals. | |
| 299 * If `indent` contains characters that are not valid JSON whitespace | |
| 300 * characters, the result will not be valid JSON. JSON whitespace characters | |
| 301 * are space (U+0020), tab (U+0009), line feed (U+000a) and carriage return | |
| 302 * (U+000d) (ECMA 404). | |
| 303 * | |
| 304 * The [bufferSize] is the size of the internal buffers used to collect | |
| 305 * UTF-8 code units. | |
| 306 * If using [startChunkedConversion], it will be the size of the chunks. | |
| 307 * | |
| 308 * The JSON encoder handles numbers, strings, booleans, null, lists and | |
| 309 * maps directly. | |
| 310 * | |
| 311 * Any other object is attempted converted by [toEncodable] to an | |
| 312 * object that is of one of the convertible types. | |
| 313 * | |
| 314 * If [toEncodable] is omitted, it defaults to calling `.toJson()` on | |
| 315 * the object. | |
| 316 */ | |
| 317 JsonUtf8Encoder([String indent, | |
| 318 toEncodable(Object object), | |
| 319 int bufferSize = defaultBufferSize]) | |
| 320 : _indent = _utf8Encode(indent), | |
| 321 _toEncodable = toEncodable, | |
| 322 _bufferSize = bufferSize; | |
| 323 | |
| 324 static List<int> _utf8Encode(String string) { | |
| 325 if (string == null) return null; | |
| 326 if (string.isEmpty) return new Uint8List(0); | |
| 327 checkAscii: { | |
| 328 for (int i = 0; i < string.length; i++) { | |
| 329 if (string.codeUnitAt(i) >= 0x80) break checkAscii; | |
| 330 } | |
| 331 return string.codeUnits; | |
| 332 } | |
| 333 return UTF8.encode(string); | |
| 334 } | |
| 335 | |
| 336 /** Convert [object] into UTF-8 encoded JSON. */ | |
| 337 List<int> convert(Object object) { | |
| 338 List<List<int>> bytes = []; | |
| 339 // The `stringify` function always converts into chunks. | |
| 340 // Collect the chunks into the `bytes` list, then combine them afterwards. | |
| 341 void addChunk(Uint8List chunk, int start, int end) { | |
| 342 if (start > 0 || end < chunk.length) { | |
| 343 int length = end - start; | |
| 344 chunk = new Uint8List.view(chunk.buffer, | |
| 345 chunk.offsetInBytes + start, | |
| 346 length); | |
| 347 } | |
| 348 bytes.add(chunk); | |
| 349 } | |
| 350 _JsonUtf8Stringifier.stringify(object, | |
| 351 _indent, | |
| 352 _toEncodable, | |
| 353 _bufferSize, | |
| 354 addChunk); | |
| 355 if (bytes.length == 1) return bytes[0]; | |
| 356 int length = 0; | |
| 357 for (int i = 0; i < bytes.length; i++) { | |
| 358 length += bytes[i].length; | |
| 359 } | |
| 360 Uint8List result = new Uint8List(length); | |
| 361 for (int i = 0, offset = 0; i < bytes.length; i++) { | |
| 362 var byteList = bytes[i]; | |
| 363 int end = offset + byteList.length; | |
| 364 result.setRange(offset, end, byteList); | |
| 365 offset = end; | |
| 366 } | |
| 367 return result; | |
| 368 } | |
| 369 | |
| 370 /** | |
| 371 * Start a chunked conversion. | |
| 372 * | |
| 373 * Only one object can be passed into the returned sink. | |
| 374 * | |
| 375 * The argument [sink] will receive byte lists in sizes depending on the | |
| 376 * `bufferSize` passed to the constructor when creating this encoder. | |
| 377 */ | |
| 378 ChunkedConversionSink<Object> startChunkedConversion(Sink<List<int>> sink) { | |
| 379 ByteConversionSink byteSink; | |
| 380 if (sink is ByteConversionSink) { | |
| 381 byteSink = sink; | |
| 382 } else { | |
| 383 byteSink = new ByteConversionSink.from(sink); | |
| 384 } | |
| 385 return new _JsonUtf8EncoderSink(byteSink, _toEncodable, | |
| 386 _indent, _bufferSize); | |
| 387 } | |
| 388 | |
| 389 // Override the base-classes bind, to provide a better type. | |
| 390 Stream<List<int>> bind(Stream<Object> stream) { | |
| 391 return super.bind(stream); | |
| 392 } | |
| 393 | |
| 394 Converter<Object, dynamic> fuse(Converter<List<int>, dynamic> other) { | |
| 395 return super.fuse(other); | |
| 396 } | |
| 397 } | |
| 398 | |
| 399 /** | |
| 262 * Implements the chunked conversion from object to its JSON representation. | 400 * Implements the chunked conversion from object to its JSON representation. |
| 263 * | 401 * |
| 264 * The sink only accepts one value, but will produce output in a chunked way. | 402 * The sink only accepts one value, but will produce output in a chunked way. |
| 265 */ | 403 */ |
| 266 class _JsonEncoderSink extends ChunkedConversionSink<Object> { | 404 class _JsonEncoderSink extends ChunkedConversionSink<Object> { |
| 267 final String _indent; | 405 final String _indent; |
| 268 final Function _toEncodableFunction; | 406 final Function _toEncodable; |
| 269 final StringConversionSink _sink; | 407 final StringConversionSink _sink; |
| 270 bool _isDone = false; | 408 bool _isDone = false; |
| 271 | 409 |
| 272 _JsonEncoderSink(this._sink, this._toEncodableFunction, this._indent); | 410 _JsonEncoderSink(this._sink, this._toEncodable, this._indent); |
| 273 | 411 |
| 274 /** | 412 /** |
| 275 * Encodes the given object [o]. | 413 * Encodes the given object [o]. |
| 276 * | 414 * |
| 277 * It is an error to invoke this method more than once on any instance. While | 415 * It is an error to invoke this method more than once on any instance. While |
| 278 * this makes the input effectly non-chunked the output will be generated in | 416 * this makes the input effectly non-chunked the output will be generated in |
| 279 * a chunked way. | 417 * a chunked way. |
| 280 */ | 418 */ |
| 281 void add(Object o) { | 419 void add(Object o) { |
| 282 if (_isDone) { | 420 if (_isDone) { |
| 283 throw new StateError("Only one call to add allowed"); | 421 throw new StateError("Only one call to add allowed"); |
| 284 } | 422 } |
| 285 _isDone = true; | 423 _isDone = true; |
| 286 ClosableStringSink stringSink = _sink.asStringSink(); | 424 ClosableStringSink stringSink = _sink.asStringSink(); |
| 287 _JsonStringifier.printOn(o, stringSink, _toEncodableFunction, _indent); | 425 _JsonStringStringifier.printOn(o, stringSink, _toEncodable, _indent); |
| 288 stringSink.close(); | 426 stringSink.close(); |
| 289 } | 427 } |
| 290 | 428 |
| 291 void close() { /* do nothing */ } | 429 void close() { /* do nothing */ } |
| 292 } | 430 } |
| 293 | 431 |
| 294 /** | 432 /** |
| 433 * Sink returned when starting a chunked conversion from object to bytes. | |
| 434 */ | |
| 435 class _JsonUtf8EncoderSink extends ChunkedConversionSink<Object> { | |
| 436 /** The byte sink receiveing the encoded chunks. */ | |
| 437 final ByteConversionSink _sink; | |
| 438 final List<int> _indent; | |
| 439 final Function _toEncodable; | |
| 440 final int _bufferSize; | |
| 441 bool _isDone = false; | |
| 442 _JsonUtf8EncoderSink(this._sink, this._toEncodable, this._indent, | |
| 443 this._bufferSize); | |
| 444 | |
| 445 /** Callback called for each slice of result bytes. */ | |
| 446 void _addChunk(Uint8List chunk, int start, int end) { | |
| 447 _sink.addSlice(chunk, start, end, false); | |
| 448 } | |
| 449 | |
| 450 void add(Object object) { | |
| 451 if (_isDone) { | |
| 452 throw new StateError("Only one call to add allowed"); | |
| 453 } | |
| 454 _isDone = true; | |
| 455 _JsonUtf8Stringifier.stringify(object, _indent, _toEncodable, | |
| 456 _bufferSize, | |
| 457 _addChunk); | |
| 458 _sink.close(); | |
| 459 } | |
| 460 | |
| 461 void close() { | |
| 462 if (!_isDone) { | |
| 463 _isDone = true; | |
| 464 _sink.close(); | |
| 465 } | |
| 466 } | |
| 467 } | |
| 468 | |
| 469 /** | |
| 295 * This class parses JSON strings and builds the corresponding objects. | 470 * This class parses JSON strings and builds the corresponding objects. |
| 296 */ | 471 */ |
| 297 class JsonDecoder extends Converter<String, Object> { | 472 class JsonDecoder extends Converter<String, Object> { |
| 298 final _Reviver _reviver; | 473 final _Reviver _reviver; |
| 299 /** | 474 /** |
| 300 * Constructs a new JsonDecoder. | 475 * Constructs a new JsonDecoder. |
| 301 * | 476 * |
| 302 * The [reviver] may be `null`. | 477 * The [reviver] may be `null`. |
| 303 */ | 478 */ |
| 304 const JsonDecoder([reviver(var key, var value)]) : this._reviver = reviver; | 479 const JsonDecoder([reviver(var key, var value)]) : this._reviver = reviver; |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 333 } | 508 } |
| 334 | 509 |
| 335 // Internal optimized JSON parsing implementation. | 510 // Internal optimized JSON parsing implementation. |
| 336 external _parseJson(String source, reviver(key, value)); | 511 external _parseJson(String source, reviver(key, value)); |
| 337 | 512 |
| 338 | 513 |
| 339 // Implementation of encoder/stringifier. | 514 // Implementation of encoder/stringifier. |
| 340 | 515 |
| 341 Object _defaultToEncodable(object) => object.toJson(); | 516 Object _defaultToEncodable(object) => object.toJson(); |
| 342 | 517 |
| 343 class _JsonStringifier { | 518 /** |
| 519 * JSON encoder that traverses an object structure and writes JSON source. | |
| 520 * | |
| 521 * This is an abstract implementation that doesn't decide on the output | |
| 522 * format, but writes the JSON through abstract methods like [writeString]. | |
| 523 */ | |
| 524 abstract class _JsonStringifier { | |
| 344 // Character code constants. | 525 // Character code constants. |
| 345 static const int BACKSPACE = 0x08; | 526 static const int BACKSPACE = 0x08; |
| 346 static const int TAB = 0x09; | 527 static const int TAB = 0x09; |
| 347 static const int NEWLINE = 0x0a; | 528 static const int NEWLINE = 0x0a; |
| 348 static const int CARRIAGE_RETURN = 0x0d; | 529 static const int CARRIAGE_RETURN = 0x0d; |
| 349 static const int FORM_FEED = 0x0c; | 530 static const int FORM_FEED = 0x0c; |
| 350 static const int QUOTE = 0x22; | 531 static const int QUOTE = 0x22; |
| 351 static const int CHAR_0 = 0x30; | 532 static const int CHAR_0 = 0x30; |
| 352 static const int BACKSLASH = 0x5c; | 533 static const int BACKSLASH = 0x5c; |
| 353 static const int CHAR_b = 0x62; | 534 static const int CHAR_b = 0x62; |
| 354 static const int CHAR_f = 0x66; | 535 static const int CHAR_f = 0x66; |
| 355 static const int CHAR_n = 0x6e; | 536 static const int CHAR_n = 0x6e; |
| 356 static const int CHAR_r = 0x72; | 537 static const int CHAR_r = 0x72; |
| 357 static const int CHAR_t = 0x74; | 538 static const int CHAR_t = 0x74; |
| 358 static const int CHAR_u = 0x75; | 539 static const int CHAR_u = 0x75; |
| 359 | 540 |
| 541 /** List of objects currently being traversed. Used to detect cycles. */ | |
| 542 final List _seen = new List(); | |
| 543 /** Function called for each un-encodable object encountered. */ | |
| 360 final Function _toEncodable; | 544 final Function _toEncodable; |
| 361 final StringSink _sink; | |
| 362 final List _seen; | |
| 363 | 545 |
| 364 factory _JsonStringifier(StringSink sink, Function toEncodable, | 546 _JsonStringifier(Object _toEncodable(Object o)) |
| 365 String indent) { | 547 : _toEncodable = (_toEncodable != null) ? _toEncodable |
| 366 if (indent == null) return new _JsonStringifier._(sink, toEncodable); | 548 : _defaultToEncodable; |
| 367 return new _JsonStringifierPretty(sink, toEncodable, indent); | |
| 368 } | |
| 369 | 549 |
| 370 _JsonStringifier._(this._sink, this._toEncodable) | 550 /** Append a string to the JSON output. */ |
| 371 : this._seen = new List(); | 551 void writeString(String characters); |
| 372 | 552 /** Append part of a string to the JSON output. */ |
| 373 static String stringify(object, toEncodable(object), String indent) { | 553 void writeStringSlice(String characters, int start, int end); |
| 374 if (toEncodable == null) toEncodable = _defaultToEncodable; | 554 /** Append a single character, given by its code point, to the JSON output. */ |
| 375 StringBuffer output = new StringBuffer(); | 555 void writeCharCode(int charCode); |
| 376 printOn(object, output, toEncodable, indent); | 556 /** Write a number to the JSON output. */ |
| 377 return output.toString(); | 557 void writeNumber(num number); |
| 378 } | |
| 379 | |
| 380 static void printOn(object, StringSink output, toEncodable(object), | |
| 381 String indent) { | |
| 382 new _JsonStringifier(output, toEncodable, indent).stringifyValue(object); | |
| 383 } | |
| 384 | |
| 385 static String numberToString(num x) { | |
| 386 return x.toString(); | |
| 387 } | |
| 388 | 558 |
| 389 // ('0' + x) or ('a' + x - 10) | 559 // ('0' + x) or ('a' + x - 10) |
| 390 static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; | 560 static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; |
| 391 | 561 |
| 392 void escape(String s) { | 562 /** |
| 563 * Write, and suitably escape, a string's content as a JSON string literal. | |
| 564 */ | |
| 565 void writeStringContent(String s) { | |
| 393 int offset = 0; | 566 int offset = 0; |
| 394 final int length = s.length; | 567 final int length = s.length; |
| 395 for (int i = 0; i < length; i++) { | 568 for (int i = 0; i < length; i++) { |
| 396 int charCode = s.codeUnitAt(i); | 569 int charCode = s.codeUnitAt(i); |
| 397 if (charCode > BACKSLASH) continue; | 570 if (charCode > BACKSLASH) continue; |
| 398 if (charCode < 32) { | 571 if (charCode < 32) { |
| 399 if (i > offset) _sink.write(s.substring(offset, i)); | 572 if (i > offset) writeStringSlice(s, offset, i); |
| 400 offset = i + 1; | 573 offset = i + 1; |
| 401 _sink.writeCharCode(BACKSLASH); | 574 writeCharCode(BACKSLASH); |
| 402 switch (charCode) { | 575 switch (charCode) { |
| 403 case BACKSPACE: | 576 case BACKSPACE: |
| 404 _sink.writeCharCode(CHAR_b); | 577 writeCharCode(CHAR_b); |
| 405 break; | 578 break; |
| 406 case TAB: | 579 case TAB: |
| 407 _sink.writeCharCode(CHAR_t); | 580 writeCharCode(CHAR_t); |
| 408 break; | 581 break; |
| 409 case NEWLINE: | 582 case NEWLINE: |
| 410 _sink.writeCharCode(CHAR_n); | 583 writeCharCode(CHAR_n); |
| 411 break; | 584 break; |
| 412 case FORM_FEED: | 585 case FORM_FEED: |
| 413 _sink.writeCharCode(CHAR_f); | 586 writeCharCode(CHAR_f); |
| 414 break; | 587 break; |
| 415 case CARRIAGE_RETURN: | 588 case CARRIAGE_RETURN: |
| 416 _sink.writeCharCode(CHAR_r); | 589 writeCharCode(CHAR_r); |
| 417 break; | 590 break; |
| 418 default: | 591 default: |
| 419 _sink.writeCharCode(CHAR_u); | 592 writeCharCode(CHAR_u); |
| 420 _sink.writeCharCode(CHAR_0); | 593 writeCharCode(CHAR_0); |
| 421 _sink.writeCharCode(CHAR_0); | 594 writeCharCode(CHAR_0); |
| 422 _sink.writeCharCode(hexDigit((charCode >> 4) & 0xf)); | 595 writeCharCode(hexDigit((charCode >> 4) & 0xf)); |
| 423 _sink.writeCharCode(hexDigit(charCode & 0xf)); | 596 writeCharCode(hexDigit(charCode & 0xf)); |
| 424 break; | 597 break; |
| 425 } | 598 } |
| 426 } else if (charCode == QUOTE || charCode == BACKSLASH) { | 599 } else if (charCode == QUOTE || charCode == BACKSLASH) { |
| 427 if (i > offset) _sink.write(s.substring(offset, i)); | 600 if (i > offset) writeStringSlice(s, offset, i); |
| 428 offset = i + 1; | 601 offset = i + 1; |
| 429 _sink.writeCharCode(BACKSLASH); | 602 writeCharCode(BACKSLASH); |
| 430 _sink.writeCharCode(charCode); | 603 writeCharCode(charCode); |
| 431 } | 604 } |
| 432 } | 605 } |
| 433 if (offset == 0) { | 606 if (offset == 0) { |
| 434 _sink.write(s); | 607 writeString(s); |
| 435 } else if (offset < length) { | 608 } else if (offset < length) { |
| 436 _sink.write(s.substring(offset, length)); | 609 writeStringSlice(s, offset, length); |
| 437 } | 610 } |
| 438 } | 611 } |
| 439 | 612 |
| 440 void checkCycle(object) { | 613 /** |
| 614 * Check if an encountered object is already being traversed. | |
| 615 * | |
| 616 * Records the object if it isn't already seen. | |
| 617 * Should have a matching call to [_removeSeen] when the object | |
| 618 * is no longer being traversed. | |
| 619 */ | |
| 620 void _checkCycle(object) { | |
| 441 for (int i = 0; i < _seen.length; i++) { | 621 for (int i = 0; i < _seen.length; i++) { |
| 442 if (identical(object, _seen[i])) { | 622 if (identical(object, _seen[i])) { |
| 443 throw new JsonCyclicError(object); | 623 throw new JsonCyclicError(object); |
| 444 } | 624 } |
| 445 } | 625 } |
| 446 _seen.add(object); | 626 _seen.add(object); |
| 447 } | 627 } |
| 448 | 628 |
| 449 void stringifyValue(object) { | 629 /** |
| 630 * Removes object from the list of currently traversed objects. | |
| 631 * | |
| 632 * Should be called in the opposite order of the matching [_checkCycle] | |
| 633 * calls. | |
| 634 */ | |
| 635 void _removeSeen(object) { | |
| 636 assert(!_seen.isEmpty); | |
| 637 assert(identical(_seen.last, object)); | |
| 638 _seen.removeLast(); | |
| 639 } | |
| 640 | |
| 641 /** | |
| 642 * Writes an object. | |
| 643 * | |
| 644 * If the object isn't directly encodable, the [_toEncodable] function | |
| 645 * gets one chance to return a replacement which is encodable. | |
| 646 */ | |
| 647 void writeObject(object) { | |
| 450 // Tries stringifying object directly. If it's not a simple value, List or | 648 // Tries stringifying object directly. If it's not a simple value, List or |
| 451 // Map, call toJson() to get a custom representation and try serializing | 649 // Map, call toJson() to get a custom representation and try serializing |
| 452 // that. | 650 // that. |
| 453 if (!stringifyJsonValue(object)) { | 651 if (writeJsonValue(object)) return; |
| 454 checkCycle(object); | 652 _checkCycle(object); |
| 455 try { | 653 try { |
| 456 var customJson = _toEncodable(object); | 654 var customJson = _toEncodable(object); |
| 457 if (!stringifyJsonValue(customJson)) { | 655 if (!writeJsonValue(customJson)) { |
| 458 throw new JsonUnsupportedObjectError(object); | 656 throw new JsonUnsupportedObjectError(object); |
| 459 } | |
| 460 _removeSeen(object); | |
| 461 } catch (e) { | |
| 462 throw new JsonUnsupportedObjectError(object, cause: e); | |
| 463 } | 657 } |
| 658 _removeSeen(object); | |
| 659 } catch (e) { | |
| 660 throw new JsonUnsupportedObjectError(object, cause: e); | |
| 464 } | 661 } |
| 465 } | 662 } |
| 466 | 663 |
| 467 /** | 664 /** |
| 468 * Serializes a [num], [String], [bool], [Null], [List] or [Map] value. | 665 * Serializes a [num], [String], [bool], [Null], [List] or [Map] value. |
| 469 * | 666 * |
| 470 * Returns true if the value is one of these types, and false if not. | 667 * Returns true if the value is one of these types, and false if not. |
| 471 * If a value is both a [List] and a [Map], it's serialized as a [List]. | 668 * If a value is both a [List] and a [Map], it's serialized as a [List]. |
| 472 */ | 669 */ |
| 473 bool stringifyJsonValue(object) { | 670 bool writeJsonValue(object) { |
| 474 if (object is num) { | 671 if (object is num) { |
| 475 if (!object.isFinite) return false; | 672 if (!object.isFinite) return false; |
| 476 _sink.write(numberToString(object)); | 673 writeNumber(object); |
| 477 return true; | 674 return true; |
| 478 } else if (identical(object, true)) { | 675 } else if (identical(object, true)) { |
| 479 _sink.write('true'); | 676 writeString('true'); |
| 480 return true; | 677 return true; |
| 481 } else if (identical(object, false)) { | 678 } else if (identical(object, false)) { |
| 482 _sink.write('false'); | 679 writeString('false'); |
| 483 return true; | 680 return true; |
| 484 } else if (object == null) { | 681 } else if (object == null) { |
| 485 _sink.write('null'); | 682 writeString('null'); |
| 486 return true; | 683 return true; |
| 487 } else if (object is String) { | 684 } else if (object is String) { |
| 488 _sink.write('"'); | 685 writeString('"'); |
| 489 escape(object); | 686 writeStringContent(object); |
| 490 _sink.write('"'); | 687 writeString('"'); |
| 491 return true; | 688 return true; |
| 492 } else if (object is List) { | 689 } else if (object is List) { |
| 493 checkCycle(object); | 690 _checkCycle(object); |
| 494 List a = object; | 691 writeList(object); |
| 495 _sink.write('['); | |
| 496 if (a.length > 0) { | |
| 497 stringifyValue(a[0]); | |
| 498 for (int i = 1; i < a.length; i++) { | |
| 499 _sink.write(','); | |
| 500 stringifyValue(a[i]); | |
| 501 } | |
| 502 } | |
| 503 _sink.write(']'); | |
| 504 _removeSeen(object); | 692 _removeSeen(object); |
| 505 return true; | 693 return true; |
| 506 } else if (object is Map) { | 694 } else if (object is Map) { |
| 507 checkCycle(object); | 695 _checkCycle(object); |
| 508 Map<String, Object> m = object; | 696 writeMap(object); |
| 509 _sink.write('{'); | |
| 510 String separator = '"'; | |
| 511 m.forEach((String key, value) { | |
| 512 _sink.write(separator); | |
| 513 separator = ',"'; | |
| 514 escape(key); | |
| 515 _sink.write('":'); | |
| 516 stringifyValue(value); | |
| 517 }); | |
| 518 _sink.write('}'); | |
| 519 _removeSeen(object); | 697 _removeSeen(object); |
| 520 return true; | 698 return true; |
| 521 } else { | 699 } else { |
| 522 return false; | 700 return false; |
| 523 } | 701 } |
| 524 } | 702 } |
| 525 | 703 |
| 526 void _removeSeen(object) { | 704 /** Serializes a [List]. */ |
| 527 assert(!_seen.isEmpty); | 705 void writeList(List list) { |
| 528 assert(identical(_seen.last, object)); | 706 writeString('['); |
|
Søren Gjesse
2014/11/04 08:26:31
writeCharCode?
ditto below.
Lasse Reichstein Nielsen
2014/11/04 10:25:33
For an underlying StringBuffer, this is likely to
| |
| 529 _seen.removeLast(); | 707 if (list.length > 0) { |
| 708 writeObject(list[0]); | |
| 709 for (int i = 1; i < list.length; i++) { | |
| 710 writeString(','); | |
| 711 writeObject(list[i]); | |
| 712 } | |
| 713 } | |
| 714 writeString(']'); | |
| 715 } | |
| 716 | |
| 717 /** Serializes a [Map]. */ | |
| 718 void writeMap(Map<String, Object> map) { | |
| 719 writeString('{'); | |
| 720 String separator = '"'; | |
| 721 map.forEach((String key, value) { | |
| 722 writeString(separator); | |
| 723 separator = ',"'; | |
| 724 writeStringContent(key); | |
| 725 writeString('":'); | |
| 726 writeObject(value); | |
| 727 }); | |
| 728 writeString('}'); | |
| 530 } | 729 } |
| 531 } | 730 } |
| 532 | 731 |
| 533 /** | 732 /** |
| 534 * A subclass of [_JsonStringifier] which indents the contents of [List] and | 733 * A modification of [_JsonStringifier] which indents the contents of [List] and |
| 535 * [Map] objects using the specified indent value. | 734 * [Map] objects using the specified indent value. |
| 735 * | |
| 736 * Subclasses should implement [writeIndentation]. | |
| 536 */ | 737 */ |
| 537 class _JsonStringifierPretty extends _JsonStringifier { | 738 abstract class _JsonPrettyPrintMixin implements _JsonStringifier { |
| 739 int _indentLevel = 0; | |
| 740 | |
| 741 /** | |
| 742 * Add [indentLevel] indentations to the JSON output. | |
| 743 */ | |
| 744 void writeIndentation(indentLevel); | |
| 745 | |
| 746 void writeList(List list) { | |
| 747 if (list.isEmpty) { | |
| 748 writeString('[]'); | |
| 749 } else { | |
| 750 writeString('[\n'); | |
| 751 _indentLevel++; | |
| 752 writeIndentation(_indentLevel); | |
| 753 writeObject(list[0]); | |
| 754 for (int i = 1; i < list.length; i++) { | |
| 755 writeString(',\n'); | |
| 756 writeIndentation(_indentLevel); | |
| 757 writeObject(list[i]); | |
| 758 } | |
| 759 writeString('\n'); | |
| 760 _indentLevel--; | |
| 761 writeIndentation(_indentLevel); | |
| 762 writeString(']'); | |
| 763 } | |
| 764 } | |
| 765 | |
| 766 void writeMap(Map map) { | |
| 767 if (map.isEmpty) { | |
| 768 writeString('{}'); | |
| 769 } else { | |
| 770 writeString('{\n'); | |
| 771 _indentLevel++; | |
| 772 bool first = true; | |
| 773 map.forEach((String key, Object value) { | |
| 774 if (!first) { | |
| 775 writeString(",\n"); | |
| 776 } | |
| 777 writeIndentation(_indentLevel); | |
| 778 writeString('"'); | |
| 779 writeStringContent(key); | |
| 780 writeString('": '); | |
| 781 writeObject(value); | |
| 782 first = false; | |
| 783 }); | |
| 784 writeString('\n'); | |
| 785 _indentLevel--; | |
| 786 writeIndentation(_indentLevel); | |
| 787 writeString('}'); | |
| 788 } | |
| 789 } | |
| 790 } | |
| 791 | |
| 792 /** | |
| 793 * A specialziation of [_JsonStringifier] that writes its JSON to a string. | |
| 794 */ | |
| 795 class _JsonStringStringifier extends _JsonStringifier { | |
| 796 final StringSink _sink; | |
| 797 | |
| 798 _JsonStringStringifier(this._sink, _toEncodable) : super(_toEncodable); | |
| 799 | |
| 800 /** | |
| 801 * Convert object to a string. | |
| 802 * | |
| 803 * The [toEncodable] function is used to convert non-encodable objects | |
| 804 * to encodable ones. | |
| 805 * | |
| 806 * If [indent] is not `null`, the resulting JSON will be "pretty-printed" | |
| 807 * with newlines and indentation. The `indent` string is added as indentation | |
| 808 * for each indentation level. It should only contain valid JSON whitespace | |
| 809 * characters (space, tab, carriage return or line feed). | |
| 810 */ | |
| 811 static String stringify(object, toEncodable(object), String indent) { | |
| 812 StringBuffer output = new StringBuffer(); | |
| 813 printOn(object, output, toEncodable, indent); | |
| 814 return output.toString(); | |
| 815 } | |
| 816 | |
| 817 /** | |
| 818 * Convert object to a string, and write the result to the [output] sink. | |
| 819 * | |
| 820 * The result is written piecemally to the sink. | |
| 821 */ | |
| 822 static void printOn(object, StringSink output, toEncodable(object), | |
| 823 String indent) { | |
| 824 var stringifier; | |
| 825 if (indent == null) { | |
| 826 stringifier = new _JsonStringStringifier(output, toEncodable); | |
| 827 } else { | |
| 828 stringifier = | |
| 829 new _JsonStringStringifierPretty(output, toEncodable, indent); | |
| 830 } | |
| 831 stringifier.writeObject(object); | |
| 832 } | |
| 833 | |
| 834 void writeNumber(num number) { | |
| 835 _sink.write(number.toString()); | |
| 836 } | |
| 837 void writeString(String string) { | |
| 838 _sink.write(string); | |
| 839 } | |
| 840 void writeStringSlice(String string, int start, int end) { | |
| 841 _sink.write(string.substring(start, end)); | |
| 842 } | |
| 843 void writeCharCode(int charCode) { | |
| 844 _sink.writeCharCode(charCode); | |
| 845 } | |
| 846 } | |
| 847 | |
| 848 class _JsonStringStringifierPretty extends _JsonStringStringifier | |
| 849 with _JsonPrettyPrintMixin { | |
| 538 final String _indent; | 850 final String _indent; |
| 539 | 851 |
| 540 int _indentLevel = 0; | 852 _JsonStringStringifierPretty(StringSink sink, Function toEncodable, |
| 541 | 853 this._indent) |
| 542 _JsonStringifierPretty(_sink, _toEncodable, this._indent) | 854 : super(sink, toEncodable); |
| 543 : super._(_sink, _toEncodable); | 855 |
| 544 | 856 void writeIndentation(int count) { |
| 545 void _write([String value = '']) { | 857 for (int i = 0; i < count; i++) writeString(_indent); |
| 546 _sink.write(_indent * _indentLevel); | 858 } |
| 547 _sink.write(value); | 859 } |
| 548 } | 860 |
| 549 | 861 class _JsonUtf8Stringifier extends _JsonStringifier { |
|
Søren Gjesse
2014/11/04 08:26:31
Please add a class comment.
Lasse Reichstein Nielsen
2014/11/04 10:25:33
Done.
| |
| 550 /** | 862 final int bufferSize; |
| 551 * Serializes a [num], [String], [bool], [Null], [List] or [Map] value. | 863 final Function addChunk; |
| 552 * | 864 Uint8List buffer; |
| 553 * Returns true if the value is one of these types, and false if not. | 865 int index = 0; |
| 554 * If a value is both a [List] and a [Map], it's serialized as a [List]. | 866 |
| 555 */ | 867 _JsonUtf8Stringifier(toEncodable, int bufferSize, this.addChunk) |
| 556 bool stringifyJsonValue(final object) { | 868 : super(toEncodable), |
| 557 if (object is List) { | 869 this.bufferSize = bufferSize, |
| 558 checkCycle(object); | 870 buffer = new Uint8List(bufferSize); |
| 559 List a = object; | 871 |
| 560 if (a.isEmpty) { | 872 /** |
| 561 _sink.write('[]'); | 873 * Convert [object] to UTF-8 encoded JSON. |
| 874 * | |
| 875 * Calls [addChunk] with slices of UTF-8 code units. | |
| 876 * These will typically have size [bufferSize], but may be shorter. | |
| 877 * The buffers are not reused, so the [addChunk] call may keep and reuse | |
| 878 * the chunks. | |
| 879 * | |
| 880 * If [indent] is non-`null`, the result will be "pretty-printed" with | |
| 881 * extra newlines and indentation, using [indent] as the indentation. | |
| 882 */ | |
| 883 static void stringify(Object object, | |
| 884 List<int> indent, | |
| 885 toEncodableFunction(Object o), | |
| 886 int bufferSize, | |
| 887 void addChunk(Uint8List chunk, int start, int end)) { | |
| 888 _JsonUtf8Stringifier stringifier; | |
| 889 if (indent != null) { | |
| 890 stringifier = new _JsonUtf8StringifierPretty(toEncodableFunction, indent, | |
| 891 bufferSize, addChunk); | |
| 892 } else { | |
| 893 stringifier = new _JsonUtf8Stringifier(toEncodableFunction, | |
| 894 bufferSize, addChunk); | |
| 895 } | |
| 896 stringifier.writeObject(object); | |
| 897 stringifier.flush(); | |
| 898 } | |
| 899 | |
| 900 /** | |
| 901 * Must be called at the end to push the last chunk to the [addChunk] | |
| 902 * callback. | |
| 903 */ | |
| 904 void flush() { | |
| 905 if (index > 0) { | |
| 906 addChunk(buffer, 0, index); | |
| 907 } | |
| 908 buffer = null; | |
| 909 index = 0; | |
| 910 } | |
| 911 | |
| 912 void writeNumber(num number) { | |
| 913 writeAsciiString(number.toString()); | |
| 914 } | |
| 915 | |
| 916 /** Write a string that is known to not have non-ASCII characters. */ | |
| 917 void writeAsciiString(String string) { | |
| 918 // TODO(lrn): Optimize by copying directly into buffer instead of going | |
| 919 // through writeCharCode; | |
| 920 for (int i = 0; i < string.length; i++) { | |
| 921 int char = string.codeUnitAt(i); | |
| 922 assert(char <= 0x7f); | |
| 923 writeByte(char); | |
| 924 } | |
| 925 } | |
| 926 | |
| 927 void writeString(String string) { | |
| 928 writeStringSlice(string, 0, string.length); | |
| 929 } | |
| 930 | |
| 931 void writeStringSlice(String string, int start, int end) { | |
| 932 // TODO(lrn): Optimize by copying directly into buffer instead of going | |
| 933 // through writeCharCode/writeByte. Assumption is the most characters | |
| 934 // in starings are plain ASCII. | |
| 935 for (int i = start; i < end; i++) { | |
| 936 int char = string.codeUnitAt(i); | |
| 937 if (char <= 0x7f) { | |
| 938 writeByte(char); | |
| 562 } else { | 939 } else { |
| 563 _sink.writeln('['); | 940 if ((char & 0xFC00) == 0xD800 && i + 1 < end) { |
| 564 _indentLevel++; | 941 // Lead surrogate. |
| 565 _write(); | 942 int nextChar = string.codeUnitAt(i + 1); |
| 566 stringifyValue(a[0]); | 943 if ((nextChar & 0xFC00) == 0xDC00) { |
| 567 for (int i = 1; i < a.length; i++) { | 944 // Tail surrogate. |
| 568 _sink.writeln(','); | 945 char = 0x10000 + ((char & 0x3ff) << 10) + (nextChar & 0x3ff); |
| 569 _write(); | 946 writeFourByteCharCode(char); |
| 570 stringifyValue(a[i]); | 947 i++; |
| 948 continue; | |
| 949 } | |
| 571 } | 950 } |
| 572 _sink.writeln(); | 951 writeMultiByteCharCode(char); |
| 573 _indentLevel--; | 952 } |
| 574 _write(']'); | 953 } |
| 575 } | 954 } |
| 576 _seen.remove(object); | 955 |
| 577 return true; | 956 void writeCharCode(int charCode) { |
| 578 } else if (object is Map) { | 957 if (charCode <= 0x7f) { |
| 579 checkCycle(object); | 958 writeByte(charCode); |
| 580 Map<String, Object> m = object; | 959 return; |
| 581 if (m.isEmpty) { | 960 } |
| 582 _sink.write('{}'); | 961 writeMultiByteCharCode(charCode); |
| 962 } | |
| 963 | |
| 964 void writeMultiByteCharCode(int charCode) { | |
| 965 if (charCode <= 0x7ff) { | |
| 966 writeByte(0xC0 | (charCode >> 6)); | |
| 967 writeByte(0x80 | (charCode & 0x3f)); | |
| 968 return; | |
| 969 } | |
| 970 if (charCode <= 0xffff) { | |
| 971 writeByte(0xE0 | (charCode >> 12)); | |
| 972 writeByte(0x80 | ((charCode >> 6) & 0x3f)); | |
| 973 writeByte(0x80 | (charCode & 0x3f)); | |
| 974 return; | |
| 975 } | |
| 976 writeFourByteCharCode(charCode); | |
| 977 } | |
| 978 | |
| 979 void writeFourByteCharCode(int charCode) { | |
| 980 assert(charCode <= 0x10ffff); | |
| 981 writeByte(0xF0 | (charCode >> 18)); | |
| 982 writeByte(0x80 | ((charCode >> 12) & 0x3f)); | |
| 983 writeByte(0x80 | ((charCode >> 6) & 0x3f)); | |
| 984 writeByte(0x80 | (charCode & 0x3f)); | |
| 985 } | |
| 986 | |
| 987 void writeByte(int byte) { | |
| 988 assert(byte <= 0xff); | |
| 989 if (index == buffer.length) { | |
| 990 addChunk(buffer, 0, index); | |
| 991 buffer = new Uint8List(bufferSize); | |
| 992 index = 0; | |
| 993 } | |
| 994 buffer[index++] = byte; | |
| 995 } | |
| 996 } | |
| 997 | |
| 998 /** | |
| 999 * Pretty-printing version of [_JsonUtf8Stringifier]. | |
| 1000 */ | |
| 1001 class _JsonUtf8StringifierPretty extends _JsonUtf8Stringifier | |
| 1002 with _JsonPrettyPrintMixin { | |
| 1003 final List<int> indent; | |
| 1004 _JsonUtf8StringifierPretty(toEncodableFunction, this.indent, | |
| 1005 bufferSize, addChunk) | |
| 1006 : super(toEncodableFunction, bufferSize, addChunk); | |
| 1007 | |
| 1008 void writeIndentation(int count) { | |
| 1009 List<int> indent = this.indent; | |
| 1010 int indentLength = indent.length; | |
| 1011 if (indentLength == 1) { | |
| 1012 int char = indent[0]; | |
| 1013 while (count > 0) { | |
| 1014 writeByte(char); | |
| 1015 count -= 1; | |
| 1016 } | |
| 1017 return; | |
| 1018 } | |
| 1019 while (count > 0) { | |
| 1020 count--; | |
| 1021 int end = index + indentLength; | |
| 1022 if (end <= buffer.length) { | |
| 1023 buffer.setRange(index, end, indent); | |
| 1024 index = end; | |
| 583 } else { | 1025 } else { |
| 584 _sink.write('{'); | 1026 for (int i = 0; i < indentLength; i++) { |
| 585 _sink.writeln(); | 1027 writeByte(indent[i]); |
| 586 _indentLevel++; | 1028 } |
| 587 bool first = true; | 1029 } |
| 588 m.forEach((String key, Object value) { | 1030 } |
| 589 if (!first) { | 1031 } |
| 590 _sink.writeln(','); | 1032 } |
| 591 } | |
| 592 _write('"'); | |
| 593 escape(key); | |
| 594 _sink.write('": '); | |
| 595 stringifyValue(value); | |
| 596 first = false; | |
| 597 }); | |
| 598 _sink.writeln(); | |
| 599 _indentLevel--; | |
| 600 _write('}'); | |
| 601 } | |
| 602 _seen.remove(object); | |
| 603 return true; | |
| 604 } | |
| 605 return super.stringifyJsonValue(object); | |
| 606 } | |
| 607 } | |
| OLD | NEW |