diff --git a/spine-csharp/src/Json.cs b/spine-csharp/src/Json.cs index 89b3c8c12..85dc9cd91 100644 --- a/spine-csharp/src/Json.cs +++ b/spine-csharp/src/Json.cs @@ -1,533 +1,17 @@ -/* - * Copyright (c) 2012 Calvin Rien - * - * Based on the JSON parser by Patrick van Bergen - * http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html - * - * Simplified it so that it doesn't throw exceptions - * and can be used in Unity iPhone with maximum code stripping. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -using System; -using System.Collections; -using System.Collections.Generic; using System.IO; -using System.Text; -using System.Globalization; namespace Spine { - // Example usage: - // - // using UnityEngine; - // using System.Collections; - // using System.Collections.Generic; - // using MiniJSON; - // - // public class MiniJSONTest : MonoBehaviour { - // void Start () { - // var jsonString = "{ \"array\": [1.44,2,3], " + - // "\"object\": {\"key1\":\"value1\", \"key2\":256}, " + - // "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " + - // "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " + - // "\"int\": 65536, " + - // "\"float\": 3.1415926, " + - // "\"bool\": true, " + - // "\"null\": null }"; - // - // var dict = Json.Deserialize(jsonString) as Dictionary; - // - // Debug.Log("deserialized: " + dict.GetType()); - // Debug.Log("dict['array'][0]: " + ((List) dict["array"])[0]); - // Debug.Log("dict['string']: " + (string) dict["string"]); - // Debug.Log("dict['float']: " + (float) dict["float"]); - // Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs - // Debug.Log("dict['unicode']: " + (string) dict["unicode"]); - // - // var str = Json.Serialize(dict); - // - // Debug.Log("serialized: " + str); - // } - // } - - /// - /// This class encodes and decodes JSON strings. - /// Spec. details, see http://www.json.org/ - /// - /// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary. - /// All numbers are parsed to floats. - /// public static class Json { - /// - /// Parses the string json into a value - /// - /// A JSON string. - /// An List<object>, a Dictionary<string, object>, a float, an integer,a string, null, true, or false - public static object Deserialize (TextReader json) { - if (json == null) { - return null; - } - return Parser.Parse(json); + + static readonly SharpJson.JsonDecoder parser; + + static Json () { + parser = new SharpJson.JsonDecoder(); + parser.parseNumbersAsFloat = true; } - sealed class Parser : IDisposable { - const string WHITE_SPACE = " \t\n\r"; - const string WORD_BREAK = " \t\n\r{}[],:\""; - - enum TOKEN { - NONE, - CURLY_OPEN, - CURLY_CLOSE, - SQUARED_OPEN, - SQUARED_CLOSE, - COLON, - COMMA, - STRING, - NUMBER, - TRUE, - FALSE, - NULL - }; - - TextReader json; - - Parser (TextReader reader) { - json = reader; - } - - public static object Parse (TextReader reader) { - using (var instance = new Parser(reader)) { - return instance.ParseValue(); - } - } - - public void Dispose () { - json.Dispose(); - json = null; - } - - Dictionary ParseObject () { - Dictionary table = new Dictionary(); - - // ditch opening brace - json.Read(); - - // { - while (true) { - switch (NextToken) { - case TOKEN.NONE: - return null; - case TOKEN.COMMA: - continue; - case TOKEN.CURLY_CLOSE: - return table; - default: - // name - string name = ParseString(); - if (name == null) { - return null; - } - - // : - if (NextToken != TOKEN.COLON) { - return null; - } - // ditch the colon - json.Read(); - - // value - table[name] = ParseValue(); - break; - } - } - } - - List ParseArray () { - List array = new List(); - - // ditch opening bracket - json.Read(); - - // [ - var parsing = true; - while (parsing) { - TOKEN nextToken = NextToken; - - switch (nextToken) { - case TOKEN.NONE: - return null; - case TOKEN.COMMA: - continue; - case TOKEN.SQUARED_CLOSE: - parsing = false; - break; - default: - object value = ParseByToken(nextToken); - - array.Add(value); - break; - } - } - - return array; - } - - object ParseValue () { - TOKEN nextToken = NextToken; - return ParseByToken(nextToken); - } - - object ParseByToken (TOKEN token) { - switch (token) { - case TOKEN.STRING: - return ParseString(); - case TOKEN.NUMBER: - return ParseNumber(); - case TOKEN.CURLY_OPEN: - return ParseObject(); - case TOKEN.SQUARED_OPEN: - return ParseArray(); - case TOKEN.TRUE: - return true; - case TOKEN.FALSE: - return false; - case TOKEN.NULL: - return null; - default: - return null; - } - } - - string ParseString () { - StringBuilder s = new StringBuilder(); - char c; - - // ditch opening quote - json.Read(); - - bool parsing = true; - while (parsing) { - - if (json.Peek() == -1) { - parsing = false; - break; - } - - c = NextChar; - switch (c) { - case '"': - parsing = false; - break; - case '\\': - if (json.Peek() == -1) { - parsing = false; - break; - } - - c = NextChar; - switch (c) { - case '"': - case '\\': - case '/': - s.Append(c); - break; - case 'b': - s.Append('\b'); - break; - case 'f': - s.Append('\f'); - break; - case 'n': - s.Append('\n'); - break; - case 'r': - s.Append('\r'); - break; - case 't': - s.Append('\t'); - break; - case 'u': - var hex = new StringBuilder(); - - for (int i = 0; i < 4; i++) { - hex.Append(NextChar); - } - - s.Append((char)Convert.ToInt32(hex.ToString(), 16)); - break; - } - break; - default: - s.Append(c); - break; - } - } - - return s.ToString(); - } - - object ParseNumber () { - string number = NextWord; - float parsedFloat; - float.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedFloat); - return parsedFloat; - } - - void EatWhitespace () { - while (WHITE_SPACE.IndexOf(PeekChar) != -1) { - json.Read(); - - if (json.Peek() == -1) { - break; - } - } - } - - char PeekChar { - get { - return Convert.ToChar(json.Peek()); - } - } - - char NextChar { - get { - return Convert.ToChar(json.Read()); - } - } - - string NextWord { - get { - StringBuilder word = new StringBuilder(); - - while (WORD_BREAK.IndexOf(PeekChar) == -1) { - word.Append(NextChar); - - if (json.Peek() == -1) { - break; - } - } - - return word.ToString(); - } - } - - TOKEN NextToken { - get { - EatWhitespace(); - - if (json.Peek() == -1) { - return TOKEN.NONE; - } - - char c = PeekChar; - switch (c) { - case '{': - return TOKEN.CURLY_OPEN; - case '}': - json.Read(); - return TOKEN.CURLY_CLOSE; - case '[': - return TOKEN.SQUARED_OPEN; - case ']': - json.Read(); - return TOKEN.SQUARED_CLOSE; - case ',': - json.Read(); - return TOKEN.COMMA; - case '"': - return TOKEN.STRING; - case ':': - return TOKEN.COLON; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '-': - return TOKEN.NUMBER; - } - - string word = NextWord; - - switch (word) { - case "false": - return TOKEN.FALSE; - case "true": - return TOKEN.TRUE; - case "null": - return TOKEN.NULL; - } - - return TOKEN.NONE; - } - } - } - - /// - /// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string - /// - /// A Dictionary<string, object> / List<object> - /// A JSON encoded string, or null if object 'json' is not serializable - public static string Serialize (object obj) { - return Serializer.Serialize(obj); - } - - sealed class Serializer { - StringBuilder builder; - - Serializer () { - builder = new StringBuilder(); - } - - public static string Serialize (object obj) { - var instance = new Serializer(); - - instance.SerializeValue(obj); - - return instance.builder.ToString(); - } - - void SerializeValue (object value) { - IList asList; - IDictionary asDict; - string asStr; - - if (value == null) { - builder.Append("null"); - } else if ((asStr = value as string) != null) { - SerializeString(asStr); - } else if (value is bool) { - builder.Append(value.ToString().ToLower()); - } else if ((asList = value as IList) != null) { - SerializeArray(asList); - } else if ((asDict = value as IDictionary) != null) { - SerializeObject(asDict); - } else if (value is char) { - SerializeString(value.ToString()); - } else { - SerializeOther(value); - } - } - - void SerializeObject (IDictionary obj) { - bool first = true; - - builder.Append('{'); - - foreach (object e in obj.Keys) { - if (!first) { - builder.Append(','); - } - - SerializeString(e.ToString()); - builder.Append(':'); - - SerializeValue(obj[e]); - - first = false; - } - - builder.Append('}'); - } - - void SerializeArray (IList anArray) { - builder.Append('['); - - bool first = true; - - foreach (object obj in anArray) { - if (!first) { - builder.Append(','); - } - - SerializeValue(obj); - - first = false; - } - - builder.Append(']'); - } - - void SerializeString (string str) { - builder.Append('\"'); - - char[] charArray = str.ToCharArray(); - foreach (var c in charArray) { - switch (c) { - case '"': - builder.Append("\\\""); - break; - case '\\': - builder.Append("\\\\"); - break; - case '\b': - builder.Append("\\b"); - break; - case '\f': - builder.Append("\\f"); - break; - case '\n': - builder.Append("\\n"); - break; - case '\r': - builder.Append("\\r"); - break; - case '\t': - builder.Append("\\t"); - break; - default: - int codepoint = Convert.ToInt32(c); - if ((codepoint >= 32) && (codepoint <= 126)) { - builder.Append(c); - } else { - builder.Append("\\u" + Convert.ToString(codepoint, 16).PadLeft(4, '0')); - } - break; - } - } - - builder.Append('\"'); - } - - void SerializeOther (object value) { - if (value is float - || value is int - || value is uint - || value is long - || value is float - || value is sbyte - || value is byte - || value is short - || value is ushort - || value is ulong - || value is decimal) { - builder.Append(value.ToString()); - } else { - SerializeString(value.ToString()); - } - } + public static object Deserialize (TextReader text) { + return parser.Decode(text.ReadToEnd()); } } } \ No newline at end of file diff --git a/spine-csharp/src/SharpJson.cs b/spine-csharp/src/SharpJson.cs new file mode 100644 index 000000000..4c9a1d09c --- /dev/null +++ b/spine-csharp/src/SharpJson.cs @@ -0,0 +1,495 @@ +/** + * + * Copyright (c) 2016 Adriano Tinoco d'Oliveira Rezende + * + * Based on the JSON parser by Patrick van Bergen + * http://techblog.procurios.nl/k/news/view/14605/14863/how-do-i-write-my-own-parser-(for-json).html + * + * Changes made: + * + * - Optimized parser speed (deserialize roughly near 3x faster than original) + * - Added support to handle lexer/parser error messages with line numbers + * - Added more fine grained control over type conversions during the parsing + * - Refactory API (Separate Lexer code from Parser code and the Encoder from Decoder) + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +using System; +using System.Text; +using System.Collections; +using System.Globalization; +using System.Collections.Generic; + +namespace SharpJson +{ + class Lexer + { + public enum Token { + None, + Null, + True, + False, + Colon, + Comma, + String, + Number, + CurlyOpen, + CurlyClose, + SquaredOpen, + SquaredClose, + }; + + public bool hasError { + get { + return !success; + } + } + + public int lineNumber { + get; + private set; + } + + public bool parseNumbersAsFloat { + get; + set; + } + + char[] json; + int index = 0; + bool success = true; + char[] stringBuffer = new char[4096]; + + public Lexer(string text) + { + Reset(); + + json = text.ToCharArray(); + parseNumbersAsFloat = false; + } + + public void Reset() + { + index = 0; + lineNumber = 1; + success = true; + } + + public string ParseString() + { + int idx = 0; + StringBuilder builder = null; + + SkipWhiteSpaces(); + + // " + char c = json[index++]; + + bool failed = false; + bool complete = false; + + while (!complete && !failed) { + if (index == json.Length) + break; + + c = json[index++]; + if (c == '"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.Length) + break; + + c = json[index++]; + + switch (c) { + case '"': + stringBuffer[idx++] = '"'; + break; + case '\\': + stringBuffer[idx++] = '\\'; + break; + case '/': + stringBuffer[idx++] = '/'; + break; + case 'b': + stringBuffer[idx++] = '\b'; + break; + case'f': + stringBuffer[idx++] = '\f'; + break; + case 'n': + stringBuffer[idx++] = '\n'; + break; + case 'r': + stringBuffer[idx++] = '\r'; + break; + case 't': + stringBuffer[idx++] = '\t'; + break; + case 'u': + int remainingLength = json.Length - index; + if (remainingLength >= 4) { + var hex = new string(json, index, 4); + + // XXX: handle UTF + stringBuffer[idx++] = (char) Convert.ToInt32(hex, 16); + + // skip 4 chars + index += 4; + } else { + failed = true; + } + break; + } + } else { + stringBuffer[idx++] = c; + } + + if (idx >= stringBuffer.Length) { + if (builder == null) + builder = new StringBuilder(); + + builder.Append(stringBuffer, 0, idx); + idx = 0; + } + } + + if (!complete) { + success = false; + return null; + } + + if (builder != null) + return builder.ToString (); + else + return new string (stringBuffer, 0, idx); + } + + string GetNumberString() + { + SkipWhiteSpaces(); + + int lastIndex = GetLastIndexOfNumber(index); + int charLength = (lastIndex - index) + 1; + + var result = new string (json, index, charLength); + + index = lastIndex + 1; + + return result; + } + + public float ParseFloatNumber() + { + float number; + var str = GetNumberString (); + + if (!float.TryParse (str, NumberStyles.Float, CultureInfo.InvariantCulture, out number)) + return 0; + + return number; + } + + public double ParseDoubleNumber() + { + double number; + var str = GetNumberString (); + + if (!double.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out number)) + return 0; + + return number; + } + + int GetLastIndexOfNumber(int index) + { + int lastIndex; + + for (lastIndex = index; lastIndex < json.Length; lastIndex++) { + char ch = json[lastIndex]; + + if ((ch < '0' || ch > '9') && ch != '+' && ch != '-' + && ch != '.' && ch != 'e' && ch != 'E') + break; + } + + return lastIndex - 1; + } + + void SkipWhiteSpaces() + { + for (; index < json.Length; index++) { + char ch = json[index]; + + if (ch == '\n') + lineNumber++; + + if (!char.IsWhiteSpace(json[index])) + break; + } + } + + public Token LookAhead() + { + SkipWhiteSpaces(); + + int savedIndex = index; + return NextToken(json, ref savedIndex); + } + + public Token NextToken() + { + SkipWhiteSpaces(); + return NextToken(json, ref index); + } + + static Token NextToken(char[] json, ref int index) + { + if (index == json.Length) + return Token.None; + + char c = json[index++]; + + switch (c) { + case '{': + return Token.CurlyOpen; + case '}': + return Token.CurlyClose; + case '[': + return Token.SquaredOpen; + case ']': + return Token.SquaredClose; + case ',': + return Token.Comma; + case '"': + return Token.String; + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '-': + return Token.Number; + case ':': + return Token.Colon; + } + + index--; + + int remainingLength = json.Length - index; + + // false + if (remainingLength >= 5) { + if (json[index] == 'f' && + json[index + 1] == 'a' && + json[index + 2] == 'l' && + json[index + 3] == 's' && + json[index + 4] == 'e') { + index += 5; + return Token.False; + } + } + + // true + if (remainingLength >= 4) { + if (json[index] == 't' && + json[index + 1] == 'r' && + json[index + 2] == 'u' && + json[index + 3] == 'e') { + index += 4; + return Token.True; + } + } + + // null + if (remainingLength >= 4) { + if (json[index] == 'n' && + json[index + 1] == 'u' && + json[index + 2] == 'l' && + json[index + 3] == 'l') { + index += 4; + return Token.Null; + } + } + + return Token.None; + } + } + + public class JsonDecoder + { + public string errorMessage { + get; + private set; + } + + public bool parseNumbersAsFloat { + get; + set; + } + + Lexer lexer; + + public JsonDecoder() + { + errorMessage = null; + parseNumbersAsFloat = false; + } + + public object Decode(string text) + { + errorMessage = null; + + lexer = new Lexer(text); + lexer.parseNumbersAsFloat = parseNumbersAsFloat; + + return ParseValue(); + } + + public static object DecodeText(string text) + { + var builder = new JsonDecoder(); + return builder.Decode(text); + } + + IDictionary ParseObject() + { + var table = new Dictionary(); + + // { + lexer.NextToken(); + + while (true) { + var token = lexer.LookAhead(); + + switch (token) { + case Lexer.Token.None: + TriggerError("Invalid token"); + return null; + case Lexer.Token.Comma: + lexer.NextToken(); + break; + case Lexer.Token.CurlyClose: + lexer.NextToken(); + return table; + default: + // name + string name = EvalLexer(lexer.ParseString()); + + if (errorMessage != null) + return null; + + // : + token = lexer.NextToken(); + + if (token != Lexer.Token.Colon) { + TriggerError("Invalid token; expected ':'"); + return null; + } + + // value + object value = ParseValue(); + + if (errorMessage != null) + return null; + + table[name] = value; + break; + } + } + + //return null; // Unreachable code + } + + IList ParseArray() + { + var array = new List(); + + // [ + lexer.NextToken(); + + while (true) { + var token = lexer.LookAhead(); + + switch (token) { + case Lexer.Token.None: + TriggerError("Invalid token"); + return null; + case Lexer.Token.Comma: + lexer.NextToken(); + break; + case Lexer.Token.SquaredClose: + lexer.NextToken(); + return array; + default: + object value = ParseValue(); + + if (errorMessage != null) + return null; + + array.Add(value); + break; + } + } + + //return null; // Unreachable code + } + + object ParseValue() + { + switch (lexer.LookAhead()) { + case Lexer.Token.String: + return EvalLexer(lexer.ParseString()); + case Lexer.Token.Number: + if (parseNumbersAsFloat) + return EvalLexer(lexer.ParseFloatNumber()); + else + return EvalLexer(lexer.ParseDoubleNumber()); + case Lexer.Token.CurlyOpen: + return ParseObject(); + case Lexer.Token.SquaredOpen: + return ParseArray(); + case Lexer.Token.True: + lexer.NextToken(); + return true; + case Lexer.Token.False: + lexer.NextToken(); + return false; + case Lexer.Token.Null: + lexer.NextToken(); + return null; + case Lexer.Token.None: + break; + } + + TriggerError("Unable to parse value"); + return null; + } + + void TriggerError(string message) + { + errorMessage = string.Format("Error: '{0}' at line {1}", + message, lexer.lineNumber); + } + + T EvalLexer(T value) + { + if (lexer.hasError) + TriggerError("Lexical error ocurred"); + + return value; + } + } +}