Created
May 16, 2022 21:19
-
-
Save take4blue/016608a932184297de6eaf03058fbb8e to your computer and use it in GitHub Desktop.
A slightly improved version of the Dart standard Json Encoder.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
class JsonEncoder extends Converter<Object?, String> { | |
JsonEncoder({String Function(Object? input)? convert}) | |
: _convert = convert ?? JsonStringStringifier.stringify; | |
final String Function(Object? input) _convert; | |
@override | |
String convert(Object? input) { | |
return _convert(input); | |
} | |
} | |
// 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. | |
// copy from dart:convert | |
/// 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 bool Function(dynamic, List<Object?>) _toEncodable; | |
/// Object converter for Map<String, dynamic> object. | |
final Object? Function(String, dynamic) _keyConverter; | |
/// call object.toJson() | |
static bool _defaultToEncodable(dynamic object, List<Object?> output) => | |
false; | |
/// not convert | |
static Object? _defaultKeyConverter(String key, dynamic object) => object; | |
JsonStringifier( | |
{bool Function(dynamic, List<Object?>)? toEncodable, | |
Object? Function(String, dynamic)? keyConverter}) | |
: _toEncodable = toEncodable ?? _defaultToEncodable, | |
_keyConverter = keyConverter ?? _defaultKeyConverter; | |
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. | |
List<Object?> output = List.filled(1, null, growable: false); | |
final result = _toEncodable(object, output); | |
if (!result) { | |
if (writeJsonValue(object)) return; | |
} | |
_checkCycle(object); | |
try { | |
if (!result || !writeJsonValue(output[0])) { | |
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) { | |
keyValueList[i++] = key; | |
if (key is! String) { | |
allStringKeys = false; | |
keyValueList[i++] = value; | |
} else { | |
keyValueList[i++] = _keyConverter(key, 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 specialization of [JsonStringifier] that writes its JSON to a string. | |
class JsonStringStringifier extends JsonStringifier { | |
final StringSink _sink; | |
JsonStringStringifier( | |
{StringSink? sink, | |
bool Function(dynamic, List<Object?>)? toEncodable, | |
Object? Function(String, dynamic)? keyConverter}) | |
: _sink = sink ?? StringBuffer(), | |
super(toEncodable: toEncodable, keyConverter: keyConverter); | |
static String stringify(Object? object) { | |
var output = StringBuffer(); | |
JsonStringStringifier(sink: output).writeObject(object); | |
return output.toString(); | |
} | |
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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment