| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| part of dart.convert; |
| |
| /// Error thrown by JSON serialization if an object cannot be serialized. |
| /// |
| /// The [unsupportedObject] field holds that object that failed to be serialized. |
| /// |
| /// If an object isn't directly serializable, the serializer calls the `toJson` |
| /// method on the object. If that call fails, the error will be stored in the |
| /// [cause] field. If the call returns an object that isn't directly |
| /// serializable, the [cause] is null. |
| class JsonUnsupportedObjectError extends Error { |
| /// The object that could not be serialized. |
| final Object? unsupportedObject; |
| |
| /// The exception thrown when trying to convert the object. |
| final Object? cause; |
| |
| /// The partial result of the conversion, up until the error happened. |
| /// |
| /// May be null. |
| final String? partialResult; |
| |
| JsonUnsupportedObjectError(this.unsupportedObject, |
| {this.cause, this.partialResult}); |
| |
| String toString() { |
| var safeString = Error.safeToString(unsupportedObject); |
| String prefix; |
| if (cause != null) { |
| prefix = "Converting object to an encodable object failed:"; |
| } else { |
| prefix = "Converting object did not return an encodable object:"; |
| } |
| return "$prefix $safeString"; |
| } |
| } |
| |
| /// Reports that an object could not be stringified due to cyclic references. |
| /// |
| /// An object that references itself cannot be serialized by |
| /// [JsonCodec.encode]/[JsonEncoder.convert]. |
| /// When the cycle is detected, a [JsonCyclicError] is thrown. |
| class JsonCyclicError extends JsonUnsupportedObjectError { |
| /// The first object that was detected as part of a cycle. |
| JsonCyclicError(Object? object) : super(object); |
| String toString() => "Cyclic error in JSON stringify"; |
| } |
| |
| /// An instance of the default implementation of the [JsonCodec]. |
| /// |
| /// This instance provides a convenient access to the most common JSON |
| /// use cases. |
| /// |
| /// Examples: |
| /// ```dart |
| /// var encoded = json.encode([1, 2, { "a": null }]); |
| /// var decoded = json.decode('["foo", { "bar": 499 }]'); |
| /// ``` |
| /// The top-level [jsonEncode] and [jsonDecode] functions may be used instead if |
| /// a local variable shadows the [json] constant. |
| const JsonCodec json = JsonCodec(); |
| |
| /// Converts [object] to a JSON string. |
| /// |
| /// If value contains objects that are not directly encodable to a JSON |
| /// string (a value that is not a number, boolean, string, null, list or a map |
| /// with string keys), the [toEncodable] function is used to convert it to an |
| /// object that must be directly encodable. |
| /// |
| /// If [toEncodable] is omitted, it defaults to a function that returns the |
| /// result of calling `.toJson()` on the unencodable object. |
| /// |
| /// Shorthand for `json.encode`. Useful if a local variable shadows the global |
| /// [json] constant. |
| String jsonEncode(Object? object, |
| {Object? toEncodable(Object? nonEncodable)?}) => |
| json.encode(object, toEncodable: toEncodable); |
| |
| /// Parses the string and returns the resulting Json object. |
| /// |
| /// The optional [reviver] function is called once for each object or list |
| /// property that has been parsed during decoding. The `key` argument is either |
| /// the integer list index for a list property, the string map key for object |
| /// properties, or `null` for the final result. |
| /// |
| /// The default [reviver] (when not provided) is the identity function. |
| /// |
| /// Shorthand for `json.decode`. Useful if a local variable shadows the global |
| /// [json] constant. |
| dynamic jsonDecode(String source, |
| {Object? reviver(Object? key, Object? value)?}) => |
| json.decode(source, reviver: reviver); |
| |
| /// A [JsonCodec] encodes JSON objects to strings and decodes strings to |
| /// JSON objects. |
| /// |
| /// Examples: |
| /// ```dart |
| /// var encoded = json.encode([1, 2, { "a": null }]); |
| /// var decoded = json.decode('["foo", { "bar": 499 }]'); |
| /// ``` |
| class JsonCodec extends Codec<Object?, String> { |
| final Object? Function(Object? key, Object? value)? _reviver; |
| final Object? Function(dynamic)? _toEncodable; |
| |
| /// Creates a `JsonCodec` with the given reviver and encoding function. |
| /// |
| /// The [reviver] function is called during decoding. It is invoked once for |
| /// each object or list property that has been parsed. |
| /// The `key` argument is either the integer list index for a list property, |
| /// the string map key for object properties, or `null` for the final result. |
| /// |
| /// If [reviver] is omitted, it defaults to returning the value argument. |
| /// |
| /// The [toEncodable] function is used during encoding. It is invoked for |
| /// values that are not directly encodable to a string (a value that is not a |
| /// number, boolean, string, null, list or a map with string keys). The |
| /// function must return an object that is directly encodable. The elements of |
| /// a returned list and values of a returned map do not need to be directly |
| /// encodable, and if they aren't, `toEncodable` will be used on them as well. |
| /// Please notice that it is possible to cause an infinite recursive regress |
| /// in this way, by effectively creating an infinite data structure through |
| /// repeated call to `toEncodable`. |
| /// |
| /// If [toEncodable] is omitted, it defaults to a function that returns the |
| /// result of calling `.toJson()` on the unencodable object. |
| const JsonCodec( |
| {Object? reviver(Object? key, Object? value)?, |
| Object? toEncodable(dynamic object)?}) |
| : _reviver = reviver, |
| _toEncodable = toEncodable; |
| |
| /// Creates a `JsonCodec` with the given reviver. |
| /// |
| /// The [reviver] function is called once for each object or list property |
| /// that has been parsed during decoding. The `key` argument is either the |
| /// integer list index for a list property, the string map key for object |
| /// properties, or `null` for the final result. |
| JsonCodec.withReviver(dynamic reviver(Object? key, Object? value)) |
| : this(reviver: reviver); |
| |
| /// Parses the string and returns the resulting Json object. |
| /// |
| /// The optional [reviver] function is called once for each object or list |
| /// property that has been parsed during decoding. The `key` argument is either |
| /// the integer list index for a list property, the string map key for object |
| /// properties, or `null` for the final result. |
| /// |
| /// The default [reviver] (when not provided) is the identity function. |
| dynamic decode(String source, |
| {Object? reviver(Object? key, Object? value)?}) { |
| reviver ??= _reviver; |
| if (reviver == null) return decoder.convert(source); |
| return JsonDecoder(reviver).convert(source); |
| } |
| |
| /// Converts [value] to a JSON string. |
| /// |
| /// If value contains objects that are not directly encodable to a JSON |
| /// string (a value that is not a number, boolean, string, null, list or a map |
| /// with string keys), the [toEncodable] function is used to convert it to an |
| /// object that must be directly encodable. |
| /// |
| /// If [toEncodable] is omitted, it defaults to a function that returns the |
| /// result of calling `.toJson()` on the unencodable object. |
| String encode(Object? value, {Object? toEncodable(dynamic object)?}) { |
| toEncodable ??= _toEncodable; |
| if (toEncodable == null) return encoder.convert(value); |
| return JsonEncoder(toEncodable).convert(value); |
| } |
| |
| JsonEncoder get encoder { |
| if (_toEncodable == null) return const JsonEncoder(); |
| return JsonEncoder(_toEncodable); |
| } |
| |
| JsonDecoder get decoder { |
| if (_reviver == null) return const JsonDecoder(); |
| return JsonDecoder(_reviver); |
| } |
| } |
| |
| /// This class converts JSON objects to strings. |
| class JsonEncoder extends Converter<Object?, String> { |
| /// The string used for indention. |
| /// |
| /// When generating multi-line output, this string is inserted once at the |
| /// beginning of each indented line for each level of indentation. |
| /// |
| /// If `null`, the output is encoded as a single line. |
| final String? indent; |
| |
| /// Function called on non-encodable objects to return a replacement |
| /// encodable object that will be encoded in the orignal's place. |
| final Object? Function(dynamic)? _toEncodable; |
| |
| /// Creates a JSON encoder. |
| /// |
| /// The JSON encoder handles numbers, strings, booleans, null, lists and |
| /// maps with string keys directly. |
| /// |
| /// Any other object is attempted converted by [toEncodable] to an |
| /// object that is of one of the convertible types. |
| /// |
| /// If [toEncodable] is omitted, it defaults to calling `.toJson()` on |
| /// the object. |
| const JsonEncoder([Object? toEncodable(dynamic object)?]) |
| : indent = null, |
| _toEncodable = toEncodable; |
| |
| /// Creates a JSON encoder that creates multi-line JSON. |
| /// |
| /// The encoding of elements of lists and maps are indented and put on separate |
| /// lines. The [indent] string is prepended to these elements, once for each |
| /// level of indentation. |
| /// |
| /// If [indent] is `null`, the output is encoded as a single line. |
| /// |
| /// The JSON encoder handles numbers, strings, booleans, null, lists and |
| /// maps with string keys directly. |
| /// |
| /// Any other object is attempted converted by [toEncodable] to an |
| /// object that is of one of the convertible types. |
| /// |
| /// If [toEncodable] is omitted, it defaults to calling `.toJson()` on |
| /// the object. |
| const JsonEncoder.withIndent(this.indent, |
| [Object? toEncodable(dynamic object)?]) |
| : _toEncodable = toEncodable; |
| |
| /// Converts [object] to a JSON [String]. |
| /// |
| /// Directly serializable values are [num], [String], [bool], and [Null], as |
| /// well as some [List] and [Map] values. For [List], the elements must all be |
| /// serializable. For [Map], the keys must be [String] and the values must be |
| /// serializable. |
| /// |
| /// If a value of any other type is attempted to be serialized, the |
| /// `toEncodable` function provided in the constructor is called with the value |
| /// as argument. The result, which must be a directly serializable value, is |
| /// serialized instead of the original value. |
| /// |
| /// If the conversion throws, or returns a value that is not directly |
| /// serializable, a [JsonUnsupportedObjectError] exception is thrown. |
| /// If the call throws, the error is caught and stored in the |
| /// [JsonUnsupportedObjectError]'s `cause` field. |
| /// |
| /// If a [List] or [Map] contains a reference to itself, directly or through |
| /// other lists or maps, it cannot be serialized and a [JsonCyclicError] is |
| /// thrown. |
| /// |
| /// [object] should not change during serialization. |
| /// |
| /// If an object is serialized more than once, [convert] may cache the text |
| /// for it. In other words, if the content of an object changes after it is |
| /// first serialized, the new values may not be reflected in the result. |
| String convert(Object? object) => |
| _JsonStringStringifier.stringify(object, _toEncodable, indent); |
| |
| /// Starts a chunked conversion. |
| /// |
| /// The converter works more efficiently if the given [sink] is a |
| /// [StringConversionSink]. |
| /// |
| /// Returns a chunked-conversion sink that accepts at most one object. It is |
| /// an error to invoke `add` more than once on the returned sink. |
| ChunkedConversionSink<Object?> startChunkedConversion(Sink<String> sink) { |
| if (sink is _Utf8EncoderSink) { |
| return _JsonUtf8EncoderSink( |
| sink._sink, |
| _toEncodable, |
| JsonUtf8Encoder._utf8Encode(indent), |
| JsonUtf8Encoder._defaultBufferSize); |
| } |
| return _JsonEncoderSink( |
| sink is StringConversionSink ? sink : StringConversionSink.from(sink), |
| _toEncodable, |
| indent); |
| } |
| |
| // Override the base class's bind, to provide a better type. |
| Stream<String> bind(Stream<Object?> stream) => super.bind(stream); |
| |
| Converter<Object?, T> fuse<T>(Converter<String, T> other) { |
| if (other is Utf8Encoder) { |
| // The instance check guarantees that `T` is (a subtype of) List<int>, |
| // but the static type system doesn't know that, and so we cast. |
| return JsonUtf8Encoder(indent, _toEncodable) as Converter<Object?, T>; |
| } |
| return super.fuse<T>(other); |
| } |
| } |
| |
| /// Encoder that encodes a single object as a UTF-8 encoded JSON string. |
| /// |
| /// This encoder works equivalently to first converting the object to |
| /// a JSON string, and then UTF-8 encoding the string, but without |
| /// creating an intermediate string. |
| class JsonUtf8Encoder extends Converter<Object?, List<int>> { |
| /// Default buffer size used by the JSON-to-UTF-8 encoder. |
| static const int _defaultBufferSize = 256; |
| @deprecated |
| static const int DEFAULT_BUFFER_SIZE = _defaultBufferSize; |
| |
| /// Indentation used in pretty-print mode, `null` if not pretty. |
| final List<int>? _indent; |
| |
| /// Function called with each un-encodable object encountered. |
| final Object? Function(dynamic)? _toEncodable; |
| |
| /// UTF-8 buffer size. |
| final int _bufferSize; |
| |
| /// Create converter. |
| /// |
| /// If [indent] is non-`null`, the converter attempts to "pretty-print" the |
| /// JSON, and uses `indent` as the indentation. Otherwise the result has no |
| /// whitespace outside of string literals. |
| /// If `indent` contains characters that are not valid JSON whitespace |
| /// characters, the result will not be valid JSON. JSON whitespace characters |
| /// are space (U+0020), tab (U+0009), line feed (U+000a) and carriage return |
| /// (U+000d) ([ECMA |
| /// 404](http://www.ecma-international.org/publications/standards/Ecma-404.htm)). |
| /// |
| /// The [bufferSize] is the size of the internal buffers used to collect |
| /// UTF-8 code units. |
| /// If using [startChunkedConversion], it will be the size of the chunks. |
| /// |
| /// The JSON encoder handles numbers, strings, booleans, null, lists and maps |
| /// directly. |
| /// |
| /// Any other object is attempted converted by [toEncodable] to an object that |
| /// is of one of the convertible types. |
| /// |
| /// If [toEncodable] is omitted, it defaults to calling `.toJson()` on the |
| /// object. |
| JsonUtf8Encoder( |
| [String? indent, dynamic toEncodable(dynamic object)?, int? bufferSize]) |
| : _indent = _utf8Encode(indent), |
| _toEncodable = toEncodable, |
| _bufferSize = bufferSize ?? _defaultBufferSize; |
| |
| static List<int>? _utf8Encode(String? string) { |
| if (string == null) return null; |
| if (string.isEmpty) return Uint8List(0); |
| checkAscii: |
| { |
| for (var i = 0; i < string.length; i++) { |
| if (string.codeUnitAt(i) >= 0x80) break checkAscii; |
| } |
| return string.codeUnits; |
| } |
| return utf8.encode(string); |
| } |
| |
| /// Convert [object] into UTF-8 encoded JSON. |
| List<int> convert(Object? object) { |
| var bytes = <List<int>>[]; |
| // The `stringify` function always converts into chunks. |
| // Collect the chunks into the `bytes` list, then combine them afterwards. |
| void addChunk(Uint8List chunk, int start, int end) { |
| if (start > 0 || end < chunk.length) { |
| var length = end - start; |
| chunk = |
| Uint8List.view(chunk.buffer, chunk.offsetInBytes + start, length); |
| } |
| bytes.add(chunk); |
| } |
| |
| _JsonUtf8Stringifier.stringify( |
| object, _indent, _toEncodable, _bufferSize, addChunk); |
| if (bytes.length == 1) return bytes[0]; |
| var length = 0; |
| for (var i = 0; i < bytes.length; i++) { |
| length += bytes[i].length; |
| } |
| var result = Uint8List(length); |
| for (var i = 0, offset = 0; i < bytes.length; i++) { |
| var byteList = bytes[i]; |
| int end = offset + byteList.length; |
| result.setRange(offset, end, byteList); |
| offset = end; |
| } |
| return result; |
| } |
| |
| /// Start a chunked conversion. |
| /// |
| /// Only one object can be passed into the returned sink. |
| /// |
| /// The argument [sink] will receive byte lists in sizes depending on the |
| /// `bufferSize` passed to the constructor when creating this encoder. |
| ChunkedConversionSink<Object?> startChunkedConversion(Sink<List<int>> sink) { |
| ByteConversionSink byteSink; |
| if (sink is ByteConversionSink) { |
| byteSink = sink; |
| } else { |
| byteSink = ByteConversionSink.from(sink); |
| } |
| return _JsonUtf8EncoderSink(byteSink, _toEncodable, _indent, _bufferSize); |
| } |
| |
| // Override the base class's bind, to provide a better type. |
| Stream<List<int>> bind(Stream<Object?> stream) { |
| return super.bind(stream); |
| } |
| } |
| |
| /// Implements the chunked conversion from object to its JSON representation. |
| /// |
| /// The sink only accepts one value, but will produce output in a chunked way. |
| class _JsonEncoderSink extends ChunkedConversionSink<Object?> { |
| final String? _indent; |
| final Object? Function(dynamic)? _toEncodable; |
| final StringConversionSink _sink; |
| bool _isDone = false; |
| |
| _JsonEncoderSink(this._sink, this._toEncodable, this._indent); |
| |
| /// Encodes the given object [o]. |
| /// |
| /// It is an error to invoke this method more than once on any instance. While |
| /// this makes the input effectively non-chunked the output will be generated |
| /// in a chunked way. |
| void add(Object? o) { |
| if (_isDone) { |
| throw StateError("Only one call to add allowed"); |
| } |
| _isDone = true; |
| var stringSink = _sink.asStringSink(); |
| _JsonStringStringifier.printOn(o, stringSink, _toEncodable, _indent); |
| stringSink.close(); |
| } |
| |
| void close() {/* do nothing */} |
| } |
| |
| /// Sink returned when starting a chunked conversion from object to bytes. |
| class _JsonUtf8EncoderSink extends ChunkedConversionSink<Object?> { |
| /// The byte sink receiving the encoded chunks. |
| final ByteConversionSink _sink; |
| final List<int>? _indent; |
| final Object? Function(dynamic)? _toEncodable; |
| final int _bufferSize; |
| bool _isDone = false; |
| _JsonUtf8EncoderSink( |
| this._sink, this._toEncodable, this._indent, this._bufferSize); |
| |
| /// Callback called for each slice of result bytes. |
| void _addChunk(Uint8List chunk, int start, int end) { |
| _sink.addSlice(chunk, start, end, false); |
| } |
| |
| void add(Object? object) { |
| if (_isDone) { |
| throw StateError("Only one call to add allowed"); |
| } |
| _isDone = true; |
| _JsonUtf8Stringifier.stringify( |
| object, _indent, _toEncodable, _bufferSize, _addChunk); |
| _sink.close(); |
| } |
| |
| void close() { |
| if (!_isDone) { |
| _isDone = true; |
| _sink.close(); |
| } |
| } |
| } |
| |
| /// This class parses JSON strings and builds the corresponding objects. |
| /// |
| /// A JSON input must be the JSON encoding of a single JSON value, |
| /// which can be a list or map containing other values. |
| /// |
| /// When used as a [StreamTransformer], the input stream may emit |
| /// multiple strings. The concatenation of all of these strings must |
| /// be a valid JSON encoding of a single JSON value. |
| class JsonDecoder extends Converter<String, Object?> { |
| final Object? Function(Object? key, Object? value)? _reviver; |
| |
| /// Constructs a new JsonDecoder. |
| /// |
| /// The [reviver] may be `null`. |
| const JsonDecoder([Object? reviver(Object? key, Object? value)?]) |
| : _reviver = reviver; |
| |
| /// Converts the given JSON-string [input] to its corresponding object. |
| /// |
| /// Parsed JSON values are of the types [num], [String], [bool], [Null], |
| /// [List]s of parsed JSON values or [Map]s from [String] to parsed JSON |
| /// values. |
| /// |
| /// If `this` was initialized with a reviver, then the parsing operation |
| /// invokes the reviver on every object or list property that has been parsed. |
| /// The arguments are the property name ([String]) or list index ([int]), and |
| /// the value is the parsed value. The return value of the reviver is used as |
| /// the value of that property instead the parsed value. |
| /// |
| /// Throws [FormatException] if the input is not valid JSON text. |
| dynamic convert(String input) => _parseJson(input, _reviver); |
| |
| /// Starts a conversion from a chunked JSON string to its corresponding object. |
| /// |
| /// The output [sink] receives exactly one decoded element through `add`. |
| external StringConversionSink startChunkedConversion(Sink<Object?> sink); |
| |
| // Override the base class's bind, to provide a better type. |
| Stream<Object?> bind(Stream<String> stream) => super.bind(stream); |
| } |
| |
| // Internal optimized JSON parsing implementation. |
| external dynamic _parseJson(String source, reviver(key, value)?); |
| |
| // Implementation of encoder/stringifier. |
| |
| dynamic _defaultToEncodable(dynamic object) => object.toJson(); |
| |
| /// JSON encoder that traverses an object structure and writes JSON source. |
| /// |
| /// This is an abstract implementation that doesn't decide on the output |
| /// format, but writes the JSON through abstract methods like [writeString]. |
| abstract class _JsonStringifier { |
| // Character code constants. |
| static const int backspace = 0x08; |
| static const int tab = 0x09; |
| static const int newline = 0x0a; |
| static const int carriageReturn = 0x0d; |
| static const int formFeed = 0x0c; |
| static const int quote = 0x22; |
| static const int char_0 = 0x30; |
| static const int backslash = 0x5c; |
| static const int char_b = 0x62; |
| static const int char_d = 0x64; |
| static const int char_f = 0x66; |
| static const int char_n = 0x6e; |
| static const int char_r = 0x72; |
| static const int char_t = 0x74; |
| static const int char_u = 0x75; |
| static const int surrogateMin = 0xd800; |
| static const int surrogateMask = 0xfc00; |
| static const int surrogateLead = 0xd800; |
| static const int surrogateTrail = 0xdc00; |
| |
| /// List of objects currently being traversed. Used to detect cycles. |
| final List _seen = []; |
| |
| /// Function called for each un-encodable object encountered. |
| final Function(dynamic) _toEncodable; |
| |
| _JsonStringifier(dynamic toEncodable(dynamic o)?) |
| : _toEncodable = toEncodable ?? _defaultToEncodable; |
| |
| String? get _partialResult; |
| |
| /// Append a string to the JSON output. |
| void writeString(String characters); |
| |
| /// Append part of a string to the JSON output. |
| void writeStringSlice(String characters, int start, int end); |
| |
| /// Append a single character, given by its code point, to the JSON output. |
| void writeCharCode(int charCode); |
| |
| /// Write a number to the JSON output. |
| void writeNumber(num number); |
| |
| // ('0' + x) or ('a' + x - 10) |
| static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; |
| |
| /// Write, and suitably escape, a string's content as a JSON string literal. |
| void writeStringContent(String s) { |
| var offset = 0; |
| final length = s.length; |
| for (var i = 0; i < length; i++) { |
| var charCode = s.codeUnitAt(i); |
| if (charCode > backslash) { |
| if (charCode >= surrogateMin) { |
| // Possible surrogate. Check if it is unpaired. |
| if (((charCode & surrogateMask) == surrogateLead && |
| !(i + 1 < length && |
| (s.codeUnitAt(i + 1) & surrogateMask) == |
| surrogateTrail)) || |
| ((charCode & surrogateMask) == surrogateTrail && |
| !(i - 1 >= 0 && |
| (s.codeUnitAt(i - 1) & surrogateMask) == |
| surrogateLead))) { |
| // Lone surrogate. |
| if (i > offset) writeStringSlice(s, offset, i); |
| offset = i + 1; |
| writeCharCode(backslash); |
| writeCharCode(char_u); |
| writeCharCode(char_d); |
| writeCharCode(hexDigit((charCode >> 8) & 0xf)); |
| writeCharCode(hexDigit((charCode >> 4) & 0xf)); |
| writeCharCode(hexDigit(charCode & 0xf)); |
| } |
| } |
| continue; |
| } |
| if (charCode < 32) { |
| if (i > offset) writeStringSlice(s, offset, i); |
| offset = i + 1; |
| writeCharCode(backslash); |
| switch (charCode) { |
| case backspace: |
| writeCharCode(char_b); |
| break; |
| case tab: |
| writeCharCode(char_t); |
| break; |
| case newline: |
| writeCharCode(char_n); |
| break; |
| case formFeed: |
| writeCharCode(char_f); |
| break; |
| case carriageReturn: |
| writeCharCode(char_r); |
| break; |
| default: |
| writeCharCode(char_u); |
| writeCharCode(char_0); |
| writeCharCode(char_0); |
| writeCharCode(hexDigit((charCode >> 4) & 0xf)); |
| writeCharCode(hexDigit(charCode & 0xf)); |
| break; |
| } |
| } else if (charCode == quote || charCode == backslash) { |
| if (i > offset) writeStringSlice(s, offset, i); |
| offset = i + 1; |
| writeCharCode(backslash); |
| writeCharCode(charCode); |
| } |
| } |
| if (offset == 0) { |
| writeString(s); |
| } else if (offset < length) { |
| writeStringSlice(s, offset, length); |
| } |
| } |
| |
| /// Check if an encountered object is already being traversed. |
| /// |
| /// Records the object if it isn't already seen. Should have a matching call to |
| /// [_removeSeen] when the object is no longer being traversed. |
| void _checkCycle(Object? object) { |
| for (var i = 0; i < _seen.length; i++) { |
| if (identical(object, _seen[i])) { |
| throw JsonCyclicError(object); |
| } |
| } |
| _seen.add(object); |
| } |
| |
| /// Remove [object] from the list of currently traversed objects. |
| /// |
| /// Should be called in the opposite order of the matching [_checkCycle] |
| /// calls. |
| void _removeSeen(Object? object) { |
| assert(_seen.isNotEmpty); |
| assert(identical(_seen.last, object)); |
| _seen.removeLast(); |
| } |
| |
| /// Write an object. |
| /// |
| /// If [object] isn't directly encodable, the [_toEncodable] function gets one |
| /// chance to return a replacement which is encodable. |
| void writeObject(Object? object) { |
| // Tries stringifying object directly. If it's not a simple value, List or |
| // Map, call toJson() to get a custom representation and try serializing |
| // that. |
| if (writeJsonValue(object)) return; |
| _checkCycle(object); |
| try { |
| var customJson = _toEncodable(object); |
| if (!writeJsonValue(customJson)) { |
| throw JsonUnsupportedObjectError(object, partialResult: _partialResult); |
| } |
| _removeSeen(object); |
| } catch (e) { |
| throw JsonUnsupportedObjectError(object, |
| cause: e, partialResult: _partialResult); |
| } |
| } |
| |
| /// Serialize a [num], [String], [bool], [Null], [List] or [Map] value. |
| /// |
| /// Returns true if the value is one of these types, and false if not. |
| /// If a value is both a [List] and a [Map], it's serialized as a [List]. |
| bool writeJsonValue(Object? object) { |
| if (object is num) { |
| if (!object.isFinite) return false; |
| writeNumber(object); |
| return true; |
| } else if (identical(object, true)) { |
| writeString('true'); |
| return true; |
| } else if (identical(object, false)) { |
| writeString('false'); |
| return true; |
| } else if (object == null) { |
| writeString('null'); |
| return true; |
| } else if (object is String) { |
| writeString('"'); |
| writeStringContent(object); |
| writeString('"'); |
| return true; |
| } else if (object is List) { |
| _checkCycle(object); |
| writeList(object); |
| _removeSeen(object); |
| return true; |
| } else if (object is Map) { |
| _checkCycle(object); |
| // writeMap can fail if keys are not all strings. |
| var success = writeMap(object); |
| _removeSeen(object); |
| return success; |
| } else { |
| return false; |
| } |
| } |
| |
| /// Serialize a [List]. |
| void writeList(List<Object?> list) { |
| writeString('['); |
| if (list.isNotEmpty) { |
| writeObject(list[0]); |
| for (var i = 1; i < list.length; i++) { |
| writeString(','); |
| writeObject(list[i]); |
| } |
| } |
| writeString(']'); |
| } |
| |
| /// Serialize a [Map]. |
| bool writeMap(Map<Object?, Object?> map) { |
| if (map.isEmpty) { |
| writeString("{}"); |
| return true; |
| } |
| var keyValueList = List<Object?>.filled(map.length * 2, null); |
| var i = 0; |
| var allStringKeys = true; |
| map.forEach((key, value) { |
| if (key is! String) { |
| allStringKeys = false; |
| } |
| keyValueList[i++] = key; |
| keyValueList[i++] = value; |
| }); |
| if (!allStringKeys) return false; |
| writeString('{'); |
| var separator = '"'; |
| for (var i = 0; i < keyValueList.length; i += 2) { |
| writeString(separator); |
| separator = ',"'; |
| writeStringContent(keyValueList[i] as String); |
| writeString('":'); |
| writeObject(keyValueList[i + 1]); |
| } |
| writeString('}'); |
| return true; |
| } |
| } |
| |
| /// A modification of [_JsonStringifier] which indents the contents of [List] and |
| /// [Map] objects using the specified indent value. |
| /// |
| /// Subclasses should implement [writeIndentation]. |
| abstract class _JsonPrettyPrintMixin implements _JsonStringifier { |
| int _indentLevel = 0; |
| |
| /// Add [indentLevel] indentations to the JSON output. |
| void writeIndentation(int indentLevel); |
| |
| void writeList(List<Object?> list) { |
| if (list.isEmpty) { |
| writeString('[]'); |
| } else { |
| writeString('[\n'); |
| _indentLevel++; |
| writeIndentation(_indentLevel); |
| writeObject(list[0]); |
| for (var i = 1; i < list.length; i++) { |
| writeString(',\n'); |
| writeIndentation(_indentLevel); |
| writeObject(list[i]); |
| } |
| writeString('\n'); |
| _indentLevel--; |
| writeIndentation(_indentLevel); |
| writeString(']'); |
| } |
| } |
| |
| bool writeMap(Map<Object?, Object?> map) { |
| if (map.isEmpty) { |
| writeString("{}"); |
| return true; |
| } |
| var keyValueList = List<Object?>.filled(map.length * 2, null); |
| var i = 0; |
| var allStringKeys = true; |
| map.forEach((key, value) { |
| if (key is! String) { |
| allStringKeys = false; |
| } |
| keyValueList[i++] = key; |
| keyValueList[i++] = value; |
| }); |
| if (!allStringKeys) return false; |
| writeString('{\n'); |
| _indentLevel++; |
| var separator = ""; |
| for (var i = 0; i < keyValueList.length; i += 2) { |
| writeString(separator); |
| separator = ",\n"; |
| writeIndentation(_indentLevel); |
| writeString('"'); |
| writeStringContent(keyValueList[i] as String); |
| writeString('": '); |
| writeObject(keyValueList[i + 1]); |
| } |
| writeString('\n'); |
| _indentLevel--; |
| writeIndentation(_indentLevel); |
| writeString('}'); |
| return true; |
| } |
| } |
| |
| /// A specialization of [_JsonStringifier] that writes its JSON to a string. |
| class _JsonStringStringifier extends _JsonStringifier { |
| final StringSink _sink; |
| |
| _JsonStringStringifier( |
| this._sink, dynamic Function(dynamic object)? _toEncodable) |
| : super(_toEncodable); |
| |
| /// Convert object to a string. |
| /// |
| /// The [toEncodable] function is used to convert non-encodable objects |
| /// to encodable ones. |
| /// |
| /// If [indent] is not `null`, the resulting JSON will be "pretty-printed" |
| /// with newlines and indentation. The `indent` string is added as indentation |
| /// for each indentation level. It should only contain valid JSON whitespace |
| /// characters (space, tab, carriage return or line feed). |
| static String stringify( |
| Object? object, dynamic toEncodable(dynamic object)?, String? indent) { |
| var output = StringBuffer(); |
| printOn(object, output, toEncodable, indent); |
| return output.toString(); |
| } |
| |
| /// Convert object to a string, and write the result to the [output] sink. |
| /// |
| /// The result is written piecemally to the sink. |
| static void printOn(Object? object, StringSink output, |
| dynamic toEncodable(dynamic o)?, String? indent) { |
| _JsonStringifier stringifier; |
| if (indent == null) { |
| stringifier = _JsonStringStringifier(output, toEncodable); |
| } else { |
| stringifier = _JsonStringStringifierPretty(output, toEncodable, indent); |
| } |
| stringifier.writeObject(object); |
| } |
| |
| String? get _partialResult => _sink is StringBuffer ? _sink.toString() : null; |
| |
| void writeNumber(num number) { |
| _sink.write(number.toString()); |
| } |
| |
| void writeString(String string) { |
| _sink.write(string); |
| } |
| |
| void writeStringSlice(String string, int start, int end) { |
| _sink.write(string.substring(start, end)); |
| } |
| |
| void writeCharCode(int charCode) { |
| _sink.writeCharCode(charCode); |
| } |
| } |
| |
| class _JsonStringStringifierPretty extends _JsonStringStringifier |
| with _JsonPrettyPrintMixin { |
| final String _indent; |
| |
| _JsonStringStringifierPretty( |
| StringSink sink, dynamic toEncodable(dynamic o)?, this._indent) |
| : super(sink, toEncodable); |
| |
| void writeIndentation(int count) { |
| for (var i = 0; i < count; i++) writeString(_indent); |
| } |
| } |
| |
| /// Specialization of [_JsonStringifier] that writes the JSON as UTF-8. |
| /// |
| /// The JSON text is UTF-8 encoded and written to [Uint8List] buffers. |
| /// The buffers are then passed back to a user provided callback method. |
| class _JsonUtf8Stringifier extends _JsonStringifier { |
| final int bufferSize; |
| final void Function(Uint8List list, int start, int end) addChunk; |
| Uint8List buffer; |
| int index = 0; |
| |
| _JsonUtf8Stringifier( |
| dynamic toEncodable(dynamic o)?, this.bufferSize, this.addChunk) |
| : buffer = Uint8List(bufferSize), |
| super(toEncodable); |
| |
| /// Convert [object] to UTF-8 encoded JSON. |
| /// |
| /// Calls [addChunk] with slices of UTF-8 code units. |
| /// These will typically have size [bufferSize], but may be shorter. |
| /// The buffers are not reused, so the [addChunk] call may keep and reuse the |
| /// chunks. |
| /// |
| /// If [indent] is non-`null`, the result will be "pretty-printed" with extra |
| /// newlines and indentation, using [indent] as the indentation. |
| static void stringify( |
| Object? object, |
| List<int>? indent, |
| dynamic toEncodable(dynamic o)?, |
| int bufferSize, |
| void addChunk(Uint8List chunk, int start, int end)) { |
| _JsonUtf8Stringifier stringifier; |
| if (indent != null) { |
| stringifier = |
| _JsonUtf8StringifierPretty(toEncodable, indent, bufferSize, addChunk); |
| } else { |
| stringifier = _JsonUtf8Stringifier(toEncodable, bufferSize, addChunk); |
| } |
| stringifier.writeObject(object); |
| stringifier.flush(); |
| } |
| |
| /// Must be called at the end to push the last chunk to the [addChunk] |
| /// callback. |
| void flush() { |
| if (index > 0) { |
| addChunk(buffer, 0, index); |
| } |
| buffer = Uint8List(0); |
| index = 0; |
| } |
| |
| String? get _partialResult => null; |
| |
| void writeNumber(num number) { |
| writeAsciiString(number.toString()); |
| } |
| |
| /// Write a string that is known to not have non-ASCII characters. |
| void writeAsciiString(String string) { |
| // TODO(lrn): Optimize by copying directly into buffer instead of going |
| // through writeCharCode; |
| for (var i = 0; i < string.length; i++) { |
| var char = string.codeUnitAt(i); |
| assert(char <= 0x7f); |
| writeByte(char); |
| } |
| } |
| |
| void writeString(String string) { |
| writeStringSlice(string, 0, string.length); |
| } |
| |
| void writeStringSlice(String string, int start, int end) { |
| // TODO(lrn): Optimize by copying directly into buffer instead of going |
| // through writeCharCode/writeByte. Assumption is the most characters |
| // in strings are plain ASCII. |
| for (var i = start; i < end; i++) { |
| var char = string.codeUnitAt(i); |
| if (char <= 0x7f) { |
| writeByte(char); |
| } else { |
| if ((char & 0xF800) == 0xD800) { |
| // Surrogate. |
| if (char < 0xDC00 && i + 1 < end) { |
| // Lead surrogate. |
| var nextChar = string.codeUnitAt(i + 1); |
| if ((nextChar & 0xFC00) == 0xDC00) { |
| // Tail surrogate. |
| char = 0x10000 + ((char & 0x3ff) << 10) + (nextChar & 0x3ff); |
| writeFourByteCharCode(char); |
| i++; |
| continue; |
| } |
| } |
| // Unpaired surrogate. |
| writeMultiByteCharCode(unicodeReplacementCharacterRune); |
| continue; |
| } |
| writeMultiByteCharCode(char); |
| } |
| } |
| } |
| |
| void writeCharCode(int charCode) { |
| if (charCode <= 0x7f) { |
| writeByte(charCode); |
| return; |
| } |
| writeMultiByteCharCode(charCode); |
| } |
| |
| void writeMultiByteCharCode(int charCode) { |
| if (charCode <= 0x7ff) { |
| writeByte(0xC0 | (charCode >> 6)); |
| writeByte(0x80 | (charCode & 0x3f)); |
| return; |
| } |
| if (charCode <= 0xffff) { |
| writeByte(0xE0 | (charCode >> 12)); |
| writeByte(0x80 | ((charCode >> 6) & 0x3f)); |
| writeByte(0x80 | (charCode & 0x3f)); |
| return; |
| } |
| writeFourByteCharCode(charCode); |
| } |
| |
| void writeFourByteCharCode(int charCode) { |
| assert(charCode <= 0x10ffff); |
| writeByte(0xF0 | (charCode >> 18)); |
| writeByte(0x80 | ((charCode >> 12) & 0x3f)); |
| writeByte(0x80 | ((charCode >> 6) & 0x3f)); |
| writeByte(0x80 | (charCode & 0x3f)); |
| } |
| |
| void writeByte(int byte) { |
| assert(byte <= 0xff); |
| if (index == buffer.length) { |
| addChunk(buffer, 0, index); |
| buffer = Uint8List(bufferSize); |
| index = 0; |
| } |
| buffer[index++] = byte; |
| } |
| } |
| |
| /// Pretty-printing version of [_JsonUtf8Stringifier]. |
| class _JsonUtf8StringifierPretty extends _JsonUtf8Stringifier |
| with _JsonPrettyPrintMixin { |
| final List<int> indent; |
| _JsonUtf8StringifierPretty(dynamic toEncodable(dynamic o)?, this.indent, |
| int bufferSize, void addChunk(Uint8List buffer, int start, int end)) |
| : super(toEncodable, bufferSize, addChunk); |
| |
| void writeIndentation(int count) { |
| var indent = this.indent; |
| var indentLength = indent.length; |
| if (indentLength == 1) { |
| var char = indent[0]; |
| while (count > 0) { |
| writeByte(char); |
| count -= 1; |
| } |
| return; |
| } |
| while (count > 0) { |
| count--; |
| var end = index + indentLength; |
| if (end <= buffer.length) { |
| buffer.setRange(index, end, indent); |
| index = end; |
| } else { |
| for (var i = 0; i < indentLength; i++) { |
| writeByte(indent[i]); |
| } |
| } |
| } |
| } |
| } |