using System;
using System.Globalization;
using System.IO;
using System.Text;
namespace Unity.VisualScripting.FullSerializer
{
public static class fsJsonPrinter
{
///
/// Inserts the given number of indents into the builder.
///
private static void InsertSpacing(TextWriter stream, int count)
{
for (var i = 0; i < count; ++i)
{
stream.Write(" ");
}
}
///
/// Escapes a string.
///
private static string EscapeString(string str)
{
// Escaping a string is pretty allocation heavy, so we try hard to
// not do it.
var needsEscape = false;
for (var i = 0; i < str.Length; ++i)
{
var c = str[i];
// unicode code point
var intChar = Convert.ToInt32(c);
if (intChar < 0 || intChar > 127)
{
needsEscape = true;
break;
}
// standard escape character
switch (c)
{
case '"':
case '\\':
case '\a':
case '\b':
case '\f':
case '\n':
case '\r':
case '\t':
case '\0':
needsEscape = true;
break;
}
if (needsEscape)
{
break;
}
}
if (needsEscape == false)
{
return str;
}
var result = new StringBuilder();
for (var i = 0; i < str.Length; ++i)
{
var c = str[i];
// unicode code point
var intChar = Convert.ToInt32(c);
if (intChar < 0 || intChar > 127)
{
result.Append($"\\u{intChar:x4} ".Trim());
continue;
}
// standard escape character
switch (c)
{
case '"':
result.Append("\\\"");
continue;
case '\\':
result.Append(@"\\");
continue;
case '\a':
result.Append(@"\a");
continue;
case '\b':
result.Append(@"\b");
continue;
case '\f':
result.Append(@"\f");
continue;
case '\n':
result.Append(@"\n");
continue;
case '\r':
result.Append(@"\r");
continue;
case '\t':
result.Append(@"\t");
continue;
case '\0':
result.Append(@"\0");
continue;
}
// no escaping needed
result.Append(c);
}
return result.ToString();
}
private static void BuildCompressedString(fsData data, TextWriter stream)
{
switch (data.Type)
{
case fsDataType.Null:
stream.Write("null");
break;
case fsDataType.Boolean:
if (data.AsBool)
{
stream.Write("true");
}
else
{
stream.Write("false");
}
break;
case fsDataType.Double:
// doubles must *always* include a decimal
stream.Write(ConvertDoubleToString(data.AsDouble));
break;
case fsDataType.Int64:
stream.Write(data.AsInt64);
break;
case fsDataType.String:
stream.Write('"');
stream.Write(EscapeString(data.AsString));
stream.Write('"');
break;
case fsDataType.Object:
{
stream.Write('{');
var comma = false;
foreach (var entry in data.AsDictionary)
{
if (comma)
{
stream.Write(',');
}
comma = true;
stream.Write('"');
stream.Write(entry.Key);
stream.Write('"');
stream.Write(":");
BuildCompressedString(entry.Value, stream);
}
stream.Write('}');
break;
}
case fsDataType.Array:
{
stream.Write('[');
var comma = false;
foreach (var entry in data.AsList)
{
if (comma)
{
stream.Write(',');
}
comma = true;
BuildCompressedString(entry, stream);
}
stream.Write(']');
break;
}
}
}
///
/// Formats this data into the given builder.
///
private static void BuildPrettyString(fsData data, TextWriter stream, int depth)
{
switch (data.Type)
{
case fsDataType.Null:
stream.Write("null");
break;
case fsDataType.Boolean:
if (data.AsBool)
{
stream.Write("true");
}
else
{
stream.Write("false");
}
break;
case fsDataType.Double:
stream.Write(ConvertDoubleToString(data.AsDouble));
break;
case fsDataType.Int64:
stream.Write(data.AsInt64);
break;
case fsDataType.String:
stream.Write('"');
stream.Write(EscapeString(data.AsString));
stream.Write('"');
break;
case fsDataType.Object:
{
stream.Write('{');
stream.WriteLine();
var comma = false;
foreach (var entry in data.AsDictionary)
{
if (comma)
{
stream.Write(',');
stream.WriteLine();
}
comma = true;
InsertSpacing(stream, depth + 1);
stream.Write('"');
stream.Write(entry.Key);
stream.Write('"');
stream.Write(": ");
BuildPrettyString(entry.Value, stream, depth + 1);
}
stream.WriteLine();
InsertSpacing(stream, depth);
stream.Write('}');
break;
}
case fsDataType.Array:
// special case for empty lists; we don't put an empty line
// between the brackets
if (data.AsList.Count == 0)
{
stream.Write("[]");
}
else
{
var comma = false;
stream.Write('[');
stream.WriteLine();
foreach (var entry in data.AsList)
{
if (comma)
{
stream.Write(',');
stream.WriteLine();
}
comma = true;
InsertSpacing(stream, depth + 1);
BuildPrettyString(entry, stream, depth + 1);
}
stream.WriteLine();
InsertSpacing(stream, depth);
stream.Write(']');
}
break;
}
}
///
/// Writes the pretty JSON output data to the given stream.
///
/// The data to print.
/// Where to write the printed data.
public static void PrettyJson(fsData data, TextWriter outputStream)
{
BuildPrettyString(data, outputStream, 0);
}
///
/// Returns the data in a pretty printed JSON format.
///
public static string PrettyJson(fsData data)
{
var sb = new StringBuilder();
using (var writer = new StringWriter(sb))
{
BuildPrettyString(data, writer, 0);
return sb.ToString();
}
}
///
/// Writes the compressed JSON output data to the given stream.
///
/// The data to print.
/// Where to write the printed data.
public static void CompressedJson(fsData data, StreamWriter outputStream)
{
BuildCompressedString(data, outputStream);
}
///
/// Returns the data in a relatively compressed JSON format.
///
public static string CompressedJson(fsData data)
{
var sb = new StringBuilder();
using (var writer = new StringWriter(sb))
{
BuildCompressedString(data, writer);
return sb.ToString();
}
}
///
/// Utility method that converts a double to a string.
///
private static string ConvertDoubleToString(double d)
{
if (Double.IsInfinity(d) || Double.IsNaN(d))
{
return d.ToString(CultureInfo.InvariantCulture);
}
var doubledString = d.ToString(CultureInfo.InvariantCulture);
// NOTE/HACK: If we don't serialize with a period or an exponent,
// then the number will be deserialized as an Int64, not a double.
if (doubledString.Contains(".") == false &&
doubledString.Contains("e") == false &&
doubledString.Contains("E") == false)
{
doubledString += ".0";
}
return doubledString;
}
}
}