Skip to content

Instantly share code, notes, and snippets.

@take4blue
Created May 16, 2022 21:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save take4blue/016608a932184297de6eaf03058fbb8e to your computer and use it in GitHub Desktop.
Save take4blue/016608a932184297de6eaf03058fbb8e to your computer and use it in GitHub Desktop.
A slightly improved version of the Dart standard Json Encoder.
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