diff --git a/src/AddIns/Uno.UI.Lottie/System.Json/JavaScriptReader.cs b/src/AddIns/Uno.UI.Lottie/System.Json/JavaScriptReader.cs new file mode 100644 index 000000000000..1ae531bc9d81 --- /dev/null +++ b/src/AddIns/Uno.UI.Lottie/System.Json/JavaScriptReader.cs @@ -0,0 +1,436 @@ +// Licensed to the .NET Foundation under one or more agreements. +// See the LICENSE file in the project root for more information. +// Export from https://github.com/dotnet/corefx/commit/f398b6f7c3d08d8e437939cbd9ef29cb3beda1db +#nullable disable + +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace System.Runtime.Serialization.Json +{ + internal class JavaScriptReader + { + private readonly TextReader _r; + private int _line = 1, _column = 0; + private int _peek; + private bool _has_peek; + private bool _prev_lf; + + public JavaScriptReader(TextReader reader) + { + Debug.Assert(reader != null); + + _r = reader; + } + + public object Read() + { + object v = ReadCore(); + SkipSpaces(); + if (ReadChar() >= 0) + { + throw JsonError("ArgumentException_ExtraCharacters"); + } + return v; + } + + private object ReadCore() + { + SkipSpaces(); + int c = PeekChar(); + if (c < 0) + { + throw JsonError("ArgumentException_IncompleteInput"); + } + + switch (c) + { + case '[': + ReadChar(); + var list = new List(); + SkipSpaces(); + if (PeekChar() == ']') + { + ReadChar(); + return list; + } + + while (true) + { + list.Add(ReadCore()); + SkipSpaces(); + c = PeekChar(); + if (c != ',') + break; + ReadChar(); + continue; + } + + if (ReadChar() != ']') + { + throw JsonError("ArgumentException_ArrayMustEndWithBracket"); + } + + return list.ToArray(); + + case '{': + ReadChar(); + var obj = new Dictionary(); + SkipSpaces(); + if (PeekChar() == '}') + { + ReadChar(); + return obj; + } + + while (true) + { + SkipSpaces(); + if (PeekChar() == '}') + { + ReadChar(); + break; + } + string name = ReadStringLiteral(); + SkipSpaces(); + Expect(':'); + SkipSpaces(); + obj[name] = ReadCore(); // it does not reject duplicate names. + SkipSpaces(); + c = ReadChar(); + if (c == ',') + { + continue; + } + if (c == '}') + { + break; + } + } + return obj.ToArray(); + + case 't': + Expect("true"); + return true; + + case 'f': + Expect("false"); + return false; + + case 'n': + Expect("null"); + return null; + + case '"': + return ReadStringLiteral(); + + default: + if ('0' <= c && c <= '9' || c == '-') + { + return ReadNumericLiteral(); + } + throw JsonError("ArgumentException_UnexpectedCharacter"); + } + } + + private int PeekChar() + { + if (!_has_peek) + { + _peek = _r.Read(); + _has_peek = true; + } + return _peek; + } + + private int ReadChar() + { + int v = _has_peek ? _peek : _r.Read(); + + _has_peek = false; + + if (_prev_lf) + { + _line++; + _column = 0; + _prev_lf = false; + } + + if (v == '\n') + { + _prev_lf = true; + } + + _column++; + + return v; + } + + private void SkipSpaces() + { + while (true) + { + switch (PeekChar()) + { + case ' ': + case '\t': + case '\r': + case '\n': + ReadChar(); + continue; + + default: + return; + } + } + } + + // It could return either int, long, ulong, decimal or double, depending on the parsed value. + private object ReadNumericLiteral() + { + var sb = new StringBuilder(); + + if (PeekChar() == '-') + { + sb.Append((char)ReadChar()); + } + + int c; + int x = 0; + bool zeroStart = PeekChar() == '0'; + for (; ; x++) + { + c = PeekChar(); + if (c < '0' || '9' < c) + { + break; + } + + sb.Append((char)ReadChar()); + if (zeroStart && x == 1) + { + throw JsonError("ArgumentException_LeadingZeros"); + } + } + + if (x == 0) // Reached e.g. for "- " + { + throw JsonError("ArgumentException_NoDigitFound"); + } + + // fraction + bool hasFrac = false; + int fdigits = 0; + if (PeekChar() == '.') + { + hasFrac = true; + sb.Append((char)ReadChar()); + if (PeekChar() < 0) + { + throw JsonError("ArgumentException_ExtraDot"); + } + + while (true) + { + c = PeekChar(); + if (c < '0' || '9' < c) + { + break; + } + + sb.Append((char)ReadChar()); + fdigits++; + } + if (fdigits == 0) + { + throw JsonError("ArgumentException_ExtraDot"); + } + } + + c = PeekChar(); + if (c != 'e' && c != 'E') + { + if (!hasFrac) + { + int valueInt; + if (int.TryParse(sb.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out valueInt)) + { + return valueInt; + } + + long valueLong; + if (long.TryParse(sb.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out valueLong)) + { + return valueLong; + } + + ulong valueUlong; + if (ulong.TryParse(sb.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out valueUlong)) + { + return valueUlong; + } + } + + decimal valueDecimal; + if (decimal.TryParse(sb.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out valueDecimal) && valueDecimal != 0) + { + return valueDecimal; + } + } + else + { + // exponent + sb.Append((char)ReadChar()); + if (PeekChar() < 0) + { + throw JsonError("ArgumentException_IncompleteExponent"); + } + + c = PeekChar(); + if (c == '-') + { + sb.Append((char)ReadChar()); + } + else if (c == '+') + { + sb.Append((char)ReadChar()); + } + + if (PeekChar() < 0) + { + throw JsonError("ArgumentException_IncompleteExponent"); + } + + while (true) + { + c = PeekChar(); + if (c < '0' || '9' < c) + { + break; + } + + sb.Append((char)ReadChar()); + } + } + + return double.Parse(sb.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture); + } + + private readonly StringBuilder _vb = new StringBuilder(); + + private string ReadStringLiteral() + { + if (PeekChar() != '"') + { + throw JsonError("ArgumentException_InvalidLiteralFormat"); + } + + ReadChar(); + _vb.Length = 0; + while (true) + { + int c = ReadChar(); + if (c < 0) + { + throw JsonError("ArgumentException_StringNotClosed"); + } + + if (c == '"') + { + return _vb.ToString(); + } + else if (c != '\\') + { + _vb.Append((char)c); + continue; + } + + // escaped expression + c = ReadChar(); + if (c < 0) + { + throw JsonError("ArgumentException_IncompleteEscapeSequence"); + } + switch (c) + { + case '"': + case '\\': + case '/': + _vb.Append((char)c); + break; + case 'b': + _vb.Append('\x8'); + break; + case 'f': + _vb.Append('\f'); + break; + case 'n': + _vb.Append('\n'); + break; + case 'r': + _vb.Append('\r'); + break; + case 't': + _vb.Append('\t'); + break; + case 'u': + ushort cp = 0; + for (int i = 0; i < 4; i++) + { + cp <<= 4; + if ((c = ReadChar()) < 0) + { + throw JsonError("ArgumentException_IncompleteEscapeLiteral"); + } + + if ('0' <= c && c <= '9') + { + cp += (ushort)(c - '0'); + } + if ('A' <= c && c <= 'F') + { + cp += (ushort)(c - 'A' + 10); + } + if ('a' <= c && c <= 'f') + { + cp += (ushort)(c - 'a' + 10); + } + } + _vb.Append((char)cp); + break; + default: + throw JsonError("ArgumentException_UnexpectedEscapeCharacter"); + } + } + } + + private void Expect(char expected) + { + int c; + if ((c = ReadChar()) != expected) + { + throw JsonError("ArgumentException_ExpectedXButGotY"); + } + } + + private void Expect(string expected) + { + for (int i = 0; i < expected.Length; i++) + { + if (ReadChar() != expected[i]) + { + throw JsonError("ArgumentException_ExpectedXDiferedAtY"); + } + } + } + + private Exception JsonError(string msg) + { + return new ArgumentException("ArgumentException_MessageAt"); + } + } +} diff --git a/src/AddIns/Uno.UI.Lottie/System.Json/JsonArray.cs b/src/AddIns/Uno.UI.Lottie/System.Json/JsonArray.cs new file mode 100644 index 000000000000..6b2d228ec275 --- /dev/null +++ b/src/AddIns/Uno.UI.Lottie/System.Json/JsonArray.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// See the LICENSE file in the project root for more information. +// Export from https://github.com/dotnet/corefx/commit/f398b6f7c3d08d8e437939cbd9ef29cb3beda1db +#nullable disable + +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace System.Json +{ + public class JsonArray : JsonValue, IList + { + private readonly List _list; + + public JsonArray(params JsonValue[] items) + { + _list = new List(); + AddRange(items); + } + + public JsonArray(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + _list = new List(items); + } + + public override int Count => _list.Count; + + public bool IsReadOnly => false; + + public override sealed JsonValue this[int index] + { + get { return _list[index]; } + set { _list[index] = value; } + } + + public override JsonType JsonType => JsonType.Array; + + public void Add(JsonValue item) + { + _list.Add(item); + } + + public void AddRange(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + _list.AddRange(items); + } + + public void AddRange(params JsonValue[] items) + { + if (items != null) + { + _list.AddRange(items); + } + } + + public void Clear() => _list.Clear(); + + public bool Contains(JsonValue item) => _list.Contains(item); + + public void CopyTo(JsonValue[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + public int IndexOf(JsonValue item) => _list.IndexOf(item); + + public void Insert(int index, JsonValue item) => _list.Insert(index, item); + + public bool Remove(JsonValue item) => _list.Remove(item); + + public void RemoveAt(int index) => _list.RemoveAt(index); + + public override void Save(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + stream.WriteByte((byte)'['); + + for (int i = 0; i < _list.Count; i++) + { + JsonValue v = _list[i]; + if (v != null) + { + v.Save(stream); + } + else + { + stream.WriteByte((byte)'n'); + stream.WriteByte((byte)'u'); + stream.WriteByte((byte)'l'); + stream.WriteByte((byte)'l'); + } + + if (i < Count - 1) + { + stream.WriteByte((byte)','); + stream.WriteByte((byte)' '); + } + } + + stream.WriteByte((byte)']'); + } + + IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); + } +} diff --git a/src/AddIns/Uno.UI.Lottie/System.Json/JsonObject.cs b/src/AddIns/Uno.UI.Lottie/System.Json/JsonObject.cs new file mode 100644 index 000000000000..1b94ab14a9db --- /dev/null +++ b/src/AddIns/Uno.UI.Lottie/System.Json/JsonObject.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// See the LICENSE file in the project root for more information. +// Export from https://github.com/dotnet/corefx/commit/f398b6f7c3d08d8e437939cbd9ef29cb3beda1db +#nullable disable + +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using JsonPair = System.Collections.Generic.KeyValuePair; +using JsonPairEnumerable = System.Collections.Generic.IEnumerable>; + +namespace System.Json +{ + public class JsonObject : JsonValue, IDictionary, ICollection + { + // Use SortedDictionary to make result of ToString() deterministic + private readonly SortedDictionary _map; + + public JsonObject(params JsonPair[] items) + { + _map = new SortedDictionary(StringComparer.Ordinal); + + if (items != null) + { + AddRange(items); + } + } + + public JsonObject(JsonPairEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + _map = new SortedDictionary(StringComparer.Ordinal); + AddRange(items); + } + + public override int Count => _map.Count; + + public IEnumerator GetEnumerator() => _map.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _map.GetEnumerator(); + + public override sealed JsonValue this[string key] + { + get { return _map[key]; } + set { _map[key] = value; } + } + + public override JsonType JsonType => JsonType.Object; + + public ICollection Keys => _map.Keys; + + public ICollection Values => _map.Values; + + public void Add(string key, JsonValue value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + _map.Add(key, value); + } + + public void Add(JsonPair pair) => Add(pair.Key, pair.Value); + + public void AddRange(JsonPairEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + foreach (var pair in items) + { + _map.Add(pair.Key, pair.Value); + } + } + + public void AddRange(params JsonPair[] items) => AddRange((JsonPairEnumerable)items); + + public void Clear() => _map.Clear(); + + bool ICollection.Contains(JsonPair item) => (_map as ICollection).Contains(item); + + bool ICollection.Remove(JsonPair item) => (_map as ICollection).Remove(item); + + public override bool ContainsKey(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _map.ContainsKey(key); + } + + public void CopyTo(JsonPair[] array, int arrayIndex) => (_map as ICollection).CopyTo(array, arrayIndex); + + public bool Remove(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return _map.Remove(key); + } + + bool ICollection.IsReadOnly => false; + + public override void Save(Stream stream) + { + base.Save(stream); + } + + public bool TryGetValue(string key, out JsonValue value) => _map.TryGetValue(key, out value); + } +} diff --git a/src/AddIns/Uno.UI.Lottie/System.Json/JsonPrimitive.cs b/src/AddIns/Uno.UI.Lottie/System.Json/JsonPrimitive.cs new file mode 100644 index 000000000000..6d8f1a93542c --- /dev/null +++ b/src/AddIns/Uno.UI.Lottie/System.Json/JsonPrimitive.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// See the LICENSE file in the project root for more information. +// Export from https://github.com/dotnet/corefx/commit/f398b6f7c3d08d8e437939cbd9ef29cb3beda1db +#nullable disable + +using System.Globalization; +using System.IO; +using System.Text; + +namespace System.Json +{ + public class JsonPrimitive : JsonValue + { + private static readonly byte[] s_trueBytes = Encoding.UTF8.GetBytes("true"); + private static readonly byte[] s_falseBytes = Encoding.UTF8.GetBytes("false"); + private readonly object _value; + + public JsonPrimitive(bool value) + { + _value = value; + } + + public JsonPrimitive(byte value) + { + _value = value; + } + + public JsonPrimitive(char value) + { + _value = value; + } + + public JsonPrimitive(decimal value) + { + _value = value; + } + + public JsonPrimitive(double value) + { + _value = value; + } + + public JsonPrimitive(float value) + { + _value = value; + } + + public JsonPrimitive(int value) + { + _value = value; + } + + public JsonPrimitive(long value) + { + _value = value; + } + + //[CLSCompliant(false)] + public JsonPrimitive(sbyte value) + { + _value = value; + } + + public JsonPrimitive(short value) + { + _value = value; + } + + public JsonPrimitive(string value) + { + _value = value; + } + + public JsonPrimitive(DateTime value) + { + _value = value; + } + + //[CLSCompliant(false)] + public JsonPrimitive(uint value) + { + _value = value; + } + + //[CLSCompliant(false)] + public JsonPrimitive(ulong value) + { + _value = value; + } + + //[CLSCompliant(false)] + public JsonPrimitive(ushort value) + { + _value = value; + } + + public JsonPrimitive(DateTimeOffset value) + { + _value = value; + } + + public JsonPrimitive(Guid value) + { + _value = value; + } + + public JsonPrimitive(TimeSpan value) + { + _value = value; + } + + public JsonPrimitive(Uri value) + { + _value = value; + } + + internal object Value => _value; + + public override JsonType JsonType => + _value == null || _value.GetType() == typeof(char) || _value.GetType() == typeof(string) || _value.GetType() == typeof(DateTime) || _value.GetType() == typeof(object) ? JsonType.String : // DateTimeOffset || Guid || TimeSpan || Uri + _value.GetType() == typeof(bool) ? JsonType.Boolean : + JsonType.Number; + + public override void Save(Stream stream) + { + switch (JsonType) + { + case JsonType.Boolean: + byte[] boolBytes = (bool)_value ? s_trueBytes : s_falseBytes; + stream.Write(boolBytes, 0, boolBytes.Length); + break; + + case JsonType.String: + stream.WriteByte((byte)'\"'); + byte[] bytes = Encoding.UTF8.GetBytes(EscapeString(_value.ToString())); + stream.Write(bytes, 0, bytes.Length); + stream.WriteByte((byte)'\"'); + break; + + default: + bytes = Encoding.UTF8.GetBytes(GetFormattedString()); + stream.Write(bytes, 0, bytes.Length); + break; + } + } + + internal string GetFormattedString() + { + switch (JsonType) + { + case JsonType.String: + if (_value is string || _value == null) + { + return (string)_value; + } + if (_value is char) + { + return _value.ToString(); + } + throw new NotImplementedException("NotImplemented_GetFormattedString"); + + case JsonType.Number: + string s = _value is float || _value is double ? + ((IFormattable)_value).ToString("R", CultureInfo.InvariantCulture) : // Use "round-trip" format + ((IFormattable)_value).ToString("G", CultureInfo.InvariantCulture); + return s == "NaN" || s == "Infinity" || s == "-Infinity" ? + "\"" + s + "\"" : + s; + + default: + throw new InvalidOperationException(); + } + } + } +} diff --git a/src/AddIns/Uno.UI.Lottie/System.Json/JsonType.cs b/src/AddIns/Uno.UI.Lottie/System.Json/JsonType.cs new file mode 100644 index 000000000000..cb5b4481d6a0 --- /dev/null +++ b/src/AddIns/Uno.UI.Lottie/System.Json/JsonType.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// See the LICENSE file in the project root for more information. +// Export from https://github.com/dotnet/corefx/commit/f398b6f7c3d08d8e437939cbd9ef29cb3beda1db +#nullable disable + +namespace System.Json +{ + public enum JsonType + { + String, + Number, + Object, + Array, + Boolean, + } +} diff --git a/src/AddIns/Uno.UI.Lottie/System.Json/JsonValue.cs b/src/AddIns/Uno.UI.Lottie/System.Json/JsonValue.cs new file mode 100644 index 000000000000..9db6bb8eb641 --- /dev/null +++ b/src/AddIns/Uno.UI.Lottie/System.Json/JsonValue.cs @@ -0,0 +1,536 @@ +// Licensed to the .NET Foundation under one or more agreements. +// See the LICENSE file in the project root for more information. +// Export from https://github.com/dotnet/corefx/commit/f398b6f7c3d08d8e437939cbd9ef29cb3beda1db +#nullable disable + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; + +using JsonPair = System.Collections.Generic.KeyValuePair; + +namespace System.Json +{ + public abstract class JsonValue : IEnumerable + { + private static readonly UTF8Encoding s_encoding = new UTF8Encoding(false, true); + + public static JsonValue Load(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + return Load(new StreamReader(stream, true)); + } + + public static JsonValue Load(TextReader textReader) + { + if (textReader == null) + { + throw new ArgumentNullException(nameof(textReader)); + } + + return ToJsonValue(new JavaScriptReader(textReader).Read()); + } + + private static IEnumerable> ToJsonPairEnumerable(IEnumerable> kvpc) + { + foreach (KeyValuePair kvp in kvpc) + { + yield return new KeyValuePair(kvp.Key, ToJsonValue(kvp.Value)); + } + } + + private static IEnumerable ToJsonValueEnumerable(IEnumerable arr) + { + foreach (object obj in arr) + { + yield return ToJsonValue(obj); + } + } + + private static JsonValue ToJsonValue(object ret) + { + if (ret == null) + { + return null; + } + + var kvpc = ret as IEnumerable>; + if (kvpc != null) + { + return new JsonObject(ToJsonPairEnumerable(kvpc)); + } + + var arr = ret as IEnumerable; + if (arr != null) + { + return new JsonArray(ToJsonValueEnumerable(arr)); + } + + if (ret is bool) return new JsonPrimitive((bool)ret); + if (ret is decimal) return new JsonPrimitive((decimal)ret); + if (ret is double) return new JsonPrimitive((double)ret); + if (ret is int) return new JsonPrimitive((int)ret); + if (ret is long) return new JsonPrimitive((long)ret); + if (ret is string) return new JsonPrimitive((string)ret); + + Debug.Assert(ret is ulong); + return new JsonPrimitive((ulong)ret); + } + + public static JsonValue Parse(string jsonString) + { + if (jsonString == null) + { + throw new ArgumentNullException(nameof(jsonString)); + } + + return Load(new StringReader(jsonString)); + } + + public virtual int Count + { + get { throw new InvalidOperationException(); } + } + + public abstract JsonType JsonType { get; } + + public virtual JsonValue this[int index] + { + get { throw new InvalidOperationException(); } + set { throw new InvalidOperationException(); } + } + + public virtual JsonValue this[string key] + { + get { throw new InvalidOperationException(); } + set { throw new InvalidOperationException(); } + } + + public virtual bool ContainsKey(string key) + { + throw new InvalidOperationException(); + } + + public virtual void Save(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + using (StreamWriter writer = new StreamWriter(stream, s_encoding, 1024, true)) + { + Save(writer); + } + } + + public virtual void Save(TextWriter textWriter) + { + if (textWriter == null) + { + throw new ArgumentNullException(nameof(textWriter)); + } + + SaveInternal(textWriter); + } + + private void SaveInternal(TextWriter w) + { + switch (JsonType) + { + case JsonType.Object: + w.Write('{'); + bool following = false; + foreach (JsonPair pair in ((JsonObject)this)) + { + if (following) + { + w.Write(", "); + } + + w.Write('\"'); + w.Write(EscapeString(pair.Key)); + w.Write("\": "); + if (pair.Value == null) + { + w.Write("null"); + } + else + { + pair.Value.SaveInternal(w); + } + + following = true; + } + w.Write('}'); + break; + + case JsonType.Array: + w.Write('['); + following = false; + foreach (JsonValue v in ((JsonArray)this)) + { + if (following) + { + w.Write(", "); + } + + if (v != null) + { + v.SaveInternal(w); + } + else + { + w.Write("null"); + } + + following = true; + } + w.Write(']'); + break; + + case JsonType.Boolean: + w.Write(this ? "true" : "false"); + break; + + case JsonType.String: + w.Write('"'); + w.Write(EscapeString(((JsonPrimitive)this).GetFormattedString())); + w.Write('"'); + break; + + default: + w.Write(((JsonPrimitive)this).GetFormattedString()); + break; + } + } + + public override string ToString() + { + var sw = new StringWriter(); + Save(sw); + return sw.ToString(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new InvalidOperationException(); + } + + // Characters which have to be escaped: + // - Required by JSON Spec: Control characters, '"' and '\\' + // - Broken surrogates to make sure the JSON string is valid Unicode + // (and can be encoded as UTF8) + // - JSON does not require U+2028 and U+2029 to be escaped, but + // JavaScript does require this: + // http://stackoverflow.com/questions/2965293/javascript-parse-error-on-u2028-unicode-character/9168133#9168133 + // - '/' also does not have to be escaped, but escaping it when + // preceeded by a '<' avoids problems with JSON in HTML