blob: a4aeb6a96035e4a4a48668a9f2fb8b18a3bccd86 [file] [log] [blame]
// 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]);
}
}
}
}
}