mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 02:24:28 +11:00
648 lines
19 KiB
C#
648 lines
19 KiB
C#
using System;
|
|
using System.Text;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
namespace Ink.Runtime
|
|
{
|
|
/// <summary>
|
|
/// Simple custom JSON serialisation implementation that takes JSON-able System.Collections that
|
|
/// are produced by the ink engine and converts to and from JSON text.
|
|
/// </summary>
|
|
public static class SimpleJson
|
|
{
|
|
public static Dictionary<string, object> TextToDictionary (string text)
|
|
{
|
|
return new Reader (text).ToDictionary ();
|
|
}
|
|
|
|
public static List<object> TextToArray(string text)
|
|
{
|
|
return new Reader(text).ToArray();
|
|
}
|
|
|
|
class Reader
|
|
{
|
|
public Reader (string text)
|
|
{
|
|
_text = text;
|
|
_offset = 0;
|
|
|
|
SkipWhitespace ();
|
|
|
|
_rootObject = ReadObject ();
|
|
}
|
|
|
|
public Dictionary<string, object> ToDictionary ()
|
|
{
|
|
return (Dictionary<string, object>)_rootObject;
|
|
}
|
|
|
|
public List<object> ToArray()
|
|
{
|
|
return (List<object>)_rootObject;
|
|
}
|
|
|
|
bool IsNumberChar (char c)
|
|
{
|
|
return c >= '0' && c <= '9' || c == '.' || c == '-' || c == '+' || c == 'E' || c == 'e';
|
|
}
|
|
|
|
bool IsFirstNumberChar(char c)
|
|
{
|
|
return c >= '0' && c <= '9' || c == '-' || c == '+';
|
|
}
|
|
|
|
object ReadObject ()
|
|
{
|
|
var currentChar = _text [_offset];
|
|
|
|
if( currentChar == '{' )
|
|
return ReadDictionary ();
|
|
|
|
else if (currentChar == '[')
|
|
return ReadArray ();
|
|
|
|
else if (currentChar == '"')
|
|
return ReadString ();
|
|
|
|
else if (IsFirstNumberChar(currentChar))
|
|
return ReadNumber ();
|
|
|
|
else if (TryRead ("true"))
|
|
return true;
|
|
|
|
else if (TryRead ("false"))
|
|
return false;
|
|
|
|
else if (TryRead ("null"))
|
|
return null;
|
|
|
|
throw new System.Exception ("Unhandled object type in JSON: "+_text.Substring (_offset, 30));
|
|
}
|
|
|
|
Dictionary<string, object> ReadDictionary ()
|
|
{
|
|
var dict = new Dictionary<string, object> ();
|
|
|
|
Expect ("{");
|
|
|
|
SkipWhitespace ();
|
|
|
|
// Empty dictionary?
|
|
if (TryRead ("}"))
|
|
return dict;
|
|
|
|
do {
|
|
|
|
SkipWhitespace ();
|
|
|
|
// Key
|
|
var key = ReadString ();
|
|
Expect (key != null, "dictionary key");
|
|
|
|
SkipWhitespace ();
|
|
|
|
// :
|
|
Expect (":");
|
|
|
|
SkipWhitespace ();
|
|
|
|
// Value
|
|
var val = ReadObject ();
|
|
Expect (val != null, "dictionary value");
|
|
|
|
// Add to dictionary
|
|
dict [key] = val;
|
|
|
|
SkipWhitespace ();
|
|
|
|
} while ( TryRead (",") );
|
|
|
|
Expect ("}");
|
|
|
|
return dict;
|
|
}
|
|
|
|
List<object> ReadArray ()
|
|
{
|
|
var list = new List<object> ();
|
|
|
|
Expect ("[");
|
|
|
|
SkipWhitespace ();
|
|
|
|
// Empty list?
|
|
if (TryRead ("]"))
|
|
return list;
|
|
|
|
do {
|
|
|
|
SkipWhitespace ();
|
|
|
|
// Value
|
|
var val = ReadObject ();
|
|
|
|
// Add to array
|
|
list.Add (val);
|
|
|
|
SkipWhitespace ();
|
|
|
|
} while (TryRead (","));
|
|
|
|
Expect ("]");
|
|
|
|
return list;
|
|
}
|
|
|
|
string ReadString ()
|
|
{
|
|
Expect ("\"");
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
for (; _offset < _text.Length; _offset++) {
|
|
var c = _text [_offset];
|
|
|
|
if (c == '\\') {
|
|
// Escaped character
|
|
_offset++;
|
|
if (_offset >= _text.Length) {
|
|
throw new Exception("Unexpected EOF while reading string");
|
|
}
|
|
c = _text[_offset];
|
|
switch (c)
|
|
{
|
|
case '"':
|
|
case '\\':
|
|
case '/': // Yes, JSON allows this to be escaped
|
|
sb.Append(c);
|
|
break;
|
|
case 'n':
|
|
sb.Append('\n');
|
|
break;
|
|
case 't':
|
|
sb.Append('\t');
|
|
break;
|
|
case 'r':
|
|
case 'b':
|
|
case 'f':
|
|
// Ignore other control characters
|
|
break;
|
|
case 'u':
|
|
// 4-digit Unicode
|
|
if (_offset + 4 >=_text.Length) {
|
|
throw new Exception("Unexpected EOF while reading string");
|
|
}
|
|
var digits = _text.Substring(_offset + 1, 4);
|
|
int uchar;
|
|
if (int.TryParse(digits, System.Globalization.NumberStyles.AllowHexSpecifier, System.Globalization.CultureInfo.InvariantCulture, out uchar)) {
|
|
sb.Append((char)uchar);
|
|
_offset += 4;
|
|
} else {
|
|
throw new Exception("Invalid Unicode escape character at offset " + (_offset - 1));
|
|
}
|
|
break;
|
|
default:
|
|
// The escaped character is invalid per json spec
|
|
throw new Exception("Invalid Unicode escape character at offset " + (_offset - 1));
|
|
}
|
|
} else if( c == '"' ) {
|
|
break;
|
|
} else {
|
|
sb.Append(c);
|
|
}
|
|
}
|
|
|
|
Expect ("\"");
|
|
return sb.ToString();
|
|
}
|
|
|
|
object ReadNumber ()
|
|
{
|
|
var startOffset = _offset;
|
|
|
|
bool isFloat = false;
|
|
for (; _offset < _text.Length; _offset++) {
|
|
var c = _text [_offset];
|
|
if (c == '.' || c == 'e' || c == 'E') isFloat = true;
|
|
if (IsNumberChar (c))
|
|
continue;
|
|
else
|
|
break;
|
|
}
|
|
|
|
string numStr = _text.Substring (startOffset, _offset - startOffset);
|
|
|
|
if (isFloat) {
|
|
float f;
|
|
if (float.TryParse (numStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f)) {
|
|
return f;
|
|
}
|
|
} else {
|
|
int i;
|
|
if (int.TryParse (numStr, out i)) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
throw new System.Exception ("Failed to parse number value: "+numStr);
|
|
}
|
|
|
|
bool TryRead (string textToRead)
|
|
{
|
|
if (_offset + textToRead.Length > _text.Length)
|
|
return false;
|
|
|
|
for (int i = 0; i < textToRead.Length; i++) {
|
|
if (textToRead [i] != _text [_offset + i])
|
|
return false;
|
|
}
|
|
|
|
_offset += textToRead.Length;
|
|
|
|
return true;
|
|
}
|
|
|
|
void Expect (string expectedStr)
|
|
{
|
|
if (!TryRead (expectedStr))
|
|
Expect (false, expectedStr);
|
|
}
|
|
|
|
void Expect (bool condition, string message = null)
|
|
{
|
|
if (!condition) {
|
|
if (message == null) {
|
|
message = "Unexpected token";
|
|
} else {
|
|
message = "Expected " + message;
|
|
}
|
|
message += " at offset " + _offset;
|
|
|
|
throw new System.Exception (message);
|
|
}
|
|
}
|
|
|
|
void SkipWhitespace ()
|
|
{
|
|
while (_offset < _text.Length) {
|
|
var c = _text [_offset];
|
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
|
|
_offset++;
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
|
|
string _text;
|
|
int _offset;
|
|
|
|
object _rootObject;
|
|
}
|
|
|
|
|
|
public class Writer
|
|
{
|
|
public Writer()
|
|
{
|
|
_writer = new StringWriter();
|
|
}
|
|
|
|
public Writer(Stream stream)
|
|
{
|
|
_writer = new System.IO.StreamWriter(stream, Encoding.UTF8);
|
|
}
|
|
|
|
public void WriteObject(Action<Writer> inner)
|
|
{
|
|
WriteObjectStart();
|
|
inner(this);
|
|
WriteObjectEnd();
|
|
}
|
|
|
|
public void WriteObjectStart()
|
|
{
|
|
StartNewObject(container: true);
|
|
_stateStack.Push(new StateElement { type = State.Object });
|
|
_writer.Write("{");
|
|
}
|
|
|
|
public void WriteObjectEnd()
|
|
{
|
|
Assert(state == State.Object);
|
|
_writer.Write("}");
|
|
_stateStack.Pop();
|
|
}
|
|
|
|
public void WriteProperty(string name, Action<Writer> inner)
|
|
{
|
|
WriteProperty<string>(name, inner);
|
|
}
|
|
|
|
public void WriteProperty(int id, Action<Writer> inner)
|
|
{
|
|
WriteProperty<int>(id, inner);
|
|
}
|
|
|
|
public void WriteProperty(string name, string content)
|
|
{
|
|
WritePropertyStart(name);
|
|
Write(content);
|
|
WritePropertyEnd();
|
|
}
|
|
|
|
public void WriteProperty(string name, int content)
|
|
{
|
|
WritePropertyStart(name);
|
|
Write(content);
|
|
WritePropertyEnd();
|
|
}
|
|
|
|
public void WriteProperty(string name, bool content)
|
|
{
|
|
WritePropertyStart(name);
|
|
Write(content);
|
|
WritePropertyEnd();
|
|
}
|
|
|
|
public void WritePropertyStart(string name)
|
|
{
|
|
WritePropertyStart<string>(name);
|
|
}
|
|
|
|
public void WritePropertyStart(int id)
|
|
{
|
|
WritePropertyStart<int>(id);
|
|
}
|
|
|
|
public void WritePropertyEnd()
|
|
{
|
|
Assert(state == State.Property);
|
|
Assert(childCount == 1);
|
|
_stateStack.Pop();
|
|
}
|
|
|
|
public void WritePropertyNameStart()
|
|
{
|
|
Assert(state == State.Object);
|
|
|
|
if (childCount > 0)
|
|
_writer.Write(",");
|
|
|
|
_writer.Write("\"");
|
|
|
|
IncrementChildCount();
|
|
|
|
_stateStack.Push(new StateElement { type = State.Property });
|
|
_stateStack.Push(new StateElement { type = State.PropertyName });
|
|
}
|
|
|
|
public void WritePropertyNameEnd()
|
|
{
|
|
Assert(state == State.PropertyName);
|
|
|
|
_writer.Write("\":");
|
|
|
|
// Pop PropertyName, leaving Property state
|
|
_stateStack.Pop();
|
|
}
|
|
|
|
public void WritePropertyNameInner(string str)
|
|
{
|
|
Assert(state == State.PropertyName);
|
|
_writer.Write(str);
|
|
}
|
|
|
|
void WritePropertyStart<T>(T name)
|
|
{
|
|
Assert(state == State.Object);
|
|
|
|
if (childCount > 0)
|
|
_writer.Write(",");
|
|
|
|
_writer.Write("\"");
|
|
_writer.Write(name);
|
|
_writer.Write("\":");
|
|
|
|
IncrementChildCount();
|
|
|
|
_stateStack.Push(new StateElement { type = State.Property });
|
|
}
|
|
|
|
|
|
// allow name to be string or int
|
|
void WriteProperty<T>(T name, Action<Writer> inner)
|
|
{
|
|
WritePropertyStart(name);
|
|
|
|
inner(this);
|
|
|
|
WritePropertyEnd();
|
|
}
|
|
|
|
public void WriteArrayStart()
|
|
{
|
|
StartNewObject(container: true);
|
|
_stateStack.Push(new StateElement { type = State.Array });
|
|
_writer.Write("[");
|
|
}
|
|
|
|
public void WriteArrayEnd()
|
|
{
|
|
Assert(state == State.Array);
|
|
_writer.Write("]");
|
|
_stateStack.Pop();
|
|
}
|
|
|
|
public void Write(int i)
|
|
{
|
|
StartNewObject(container: false);
|
|
_writer.Write(i);
|
|
}
|
|
|
|
public void Write(float f)
|
|
{
|
|
StartNewObject(container: false);
|
|
|
|
// TODO: Find an heap-allocation-free way to do this please!
|
|
// _writer.Write(formatStr, obj (the float)) requires boxing
|
|
// Following implementation seems to work ok but requires creating temporary garbage string.
|
|
string floatStr = f.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
if( floatStr == "Infinity" ) {
|
|
_writer.Write("3.4E+38"); // JSON doesn't support, do our best alternative
|
|
} else if (floatStr == "-Infinity") {
|
|
_writer.Write("-3.4E+38"); // JSON doesn't support, do our best alternative
|
|
} else if ( floatStr == "NaN" ) {
|
|
_writer.Write("0.0"); // JSON doesn't support, not much we can do
|
|
} else {
|
|
_writer.Write(floatStr);
|
|
if (!floatStr.Contains(".") && !floatStr.Contains("E"))
|
|
_writer.Write(".0"); // ensure it gets read back in as a floating point value
|
|
}
|
|
}
|
|
|
|
public void Write(string str, bool escape = true)
|
|
{
|
|
StartNewObject(container: false);
|
|
|
|
_writer.Write("\"");
|
|
if (escape)
|
|
WriteEscapedString(str);
|
|
else
|
|
_writer.Write(str);
|
|
_writer.Write("\"");
|
|
}
|
|
|
|
public void Write(bool b)
|
|
{
|
|
StartNewObject(container: false);
|
|
_writer.Write(b ? "true" : "false");
|
|
}
|
|
|
|
public void WriteNull()
|
|
{
|
|
StartNewObject(container: false);
|
|
_writer.Write("null");
|
|
}
|
|
|
|
public void WriteStringStart()
|
|
{
|
|
StartNewObject(container: false);
|
|
_stateStack.Push(new StateElement { type = State.String });
|
|
_writer.Write("\"");
|
|
}
|
|
|
|
public void WriteStringEnd()
|
|
{
|
|
Assert(state == State.String);
|
|
_writer.Write("\"");
|
|
_stateStack.Pop();
|
|
}
|
|
|
|
public void WriteStringInner(string str, bool escape = true)
|
|
{
|
|
Assert(state == State.String);
|
|
if (escape)
|
|
WriteEscapedString(str);
|
|
else
|
|
_writer.Write(str);
|
|
}
|
|
|
|
void WriteEscapedString(string str)
|
|
{
|
|
foreach (var c in str)
|
|
{
|
|
if (c < ' ')
|
|
{
|
|
// Don't write any control characters except \n and \t
|
|
switch (c)
|
|
{
|
|
case '\n':
|
|
_writer.Write("\\n");
|
|
break;
|
|
case '\t':
|
|
_writer.Write("\\t");
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
switch (c)
|
|
{
|
|
case '\\':
|
|
case '"':
|
|
_writer.Write("\\");
|
|
_writer.Write(c);
|
|
break;
|
|
default:
|
|
_writer.Write(c);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void StartNewObject(bool container)
|
|
{
|
|
|
|
if (container)
|
|
Assert(state == State.None || state == State.Property || state == State.Array);
|
|
else
|
|
Assert(state == State.Property || state == State.Array);
|
|
|
|
if (state == State.Array && childCount > 0)
|
|
_writer.Write(",");
|
|
|
|
if (state == State.Property)
|
|
Assert(childCount == 0);
|
|
|
|
if (state == State.Array || state == State.Property)
|
|
IncrementChildCount();
|
|
}
|
|
|
|
State state
|
|
{
|
|
get
|
|
{
|
|
if (_stateStack.Count > 0) return _stateStack.Peek().type;
|
|
else return State.None;
|
|
}
|
|
}
|
|
|
|
int childCount
|
|
{
|
|
get
|
|
{
|
|
if (_stateStack.Count > 0) return _stateStack.Peek().childCount;
|
|
else return 0;
|
|
}
|
|
}
|
|
|
|
void IncrementChildCount()
|
|
{
|
|
Assert(_stateStack.Count > 0);
|
|
var currEl = _stateStack.Pop();
|
|
currEl.childCount++;
|
|
_stateStack.Push(currEl);
|
|
}
|
|
|
|
// Shouldn't hit this assert outside of initial JSON development,
|
|
// so it's save to make it debug-only.
|
|
[System.Diagnostics.Conditional("DEBUG")]
|
|
void Assert(bool condition)
|
|
{
|
|
if (!condition)
|
|
throw new System.Exception("Assert failed while writing JSON");
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return _writer.ToString();
|
|
}
|
|
|
|
enum State
|
|
{
|
|
None,
|
|
Object,
|
|
Array,
|
|
Property,
|
|
PropertyName,
|
|
String
|
|
};
|
|
|
|
struct StateElement
|
|
{
|
|
public State type;
|
|
public int childCount;
|
|
}
|
|
|
|
Stack<StateElement> _stateStack = new Stack<StateElement>();
|
|
TextWriter _writer;
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|