mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 02:24:28 +11:00
Committed everything
This commit is contained in:
@@ -0,0 +1,647 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user