// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Runtime.InteropServices; using System.Text; namespace Microsoft.ReactNative.Managed { // JSValueObject is based on Dictionary and can be used to initialize Object value in JSValue. // It is possible to write: JSValueObject{{"X", 4}, {"Y", 5}} or JSValueObject{["X"] = 4, ["Y"] = 5} and assign it to JSValue. public class JSValueObject : Dictionary, IEquatable { // Default constructor public JSValueObject() { } // Create JSValueObject as a shallow copy of 'other'. public static JSValueObject CopyFrom(IReadOnlyDictionary other) { JSValueObject obj = new JSValueObject(); foreach (var keyValue in other) { obj.Add(keyValue.Key, keyValue.Value); } return obj; } // Return true if two JSValueObject interfaces are strictly equal to each other. // Both objects must have the same set of equal properties. // Property values must be equal. public static bool Equals(IReadOnlyDictionary left, IReadOnlyDictionary right) { if (left == right) { return true; } else if (left == null) { return false; } else if (right == null) { return false; } else if (left.Count != right.Count) { return false; } foreach (var entry in left) { if (!right.TryGetValue(entry.Key, out var value) || !value.Equals(entry.Value)) { return false; } } return true; } // Return true if this JSValueObject is strictly equal to 'other'. public bool Equals(JSValueObject other) { return Equals(this as IReadOnlyDictionary, other as IReadOnlyDictionary); } // Return true if this JSValueObject is strictly equal to 'other'. public bool Equals(IReadOnlyDictionary other) { return Equals(this as IReadOnlyDictionary, other); } // Return true if this JSValueObject is strictly equal to 'obj'. public override bool Equals(object obj) { return Equals(this as IReadOnlyDictionary, obj as IReadOnlyDictionary); } // True if Equals(left, right) public static bool operator ==(JSValueObject left, IReadOnlyDictionary right) { return Equals(left, right); } // True if !Equals(left, right) public static bool operator !=(JSValueObject left, IReadOnlyDictionary right) { return !Equals(left, right); } // True if Equals(left, right) public static bool operator ==(IReadOnlyDictionary left, JSValueObject right) { return Equals(left, right); } // True if !Equals(left, right) public static bool operator !=(IReadOnlyDictionary left, JSValueObject right) { return !Equals(left, right); } // Return true if this JSValueObject is strictly equal to other JSValueObject // after their property values are converted to the same type. // See JSValue::JSEquals for details about the conversion. public static bool JSEquals(IReadOnlyDictionary left, IReadOnlyDictionary right) { if (left == right) { return true; } else if (left == null) { return false; } else if (right == null) { return false; } else if (left.Count != right.Count) { return false; } foreach (var keyValue in left) { if (!right.TryGetValue(keyValue.Key, out var rightValue) || !keyValue.Value.JSEquals(rightValue)) { return false; } } return true; } // Calculation of the JSValueObject hash code is quite expensive. // It is not implemented. public override int GetHashCode() { throw new NotImplementedException("JSValueObject currently does not support hash codes"); } // Create JSValueObject from IJSValueReader. public static JSValueObject ReadFrom(IJSValueReader reader) => JSValue.ReadObjectFrom(reader); // Write this JSValueObject to IJSValueWriter. public void WriteTo(IJSValueWriter writer) => JSValue.WriteObject(writer, this); } // JSValueObject is based on List and can be used to initialize Array value in JSValue. // It is possible to write: JSValueArray{"X", 42, JSValue.Null, true} and assign it to JSValue. public class JSValueArray : List, IEquatable { // Default constructor public JSValueArray() { } public static JSValueArray CopyFrom(IReadOnlyList other) { JSValueArray arr = new JSValueArray(); foreach (var item in other) { arr.Add(item); } return arr; } // Return true if two JSValuearray interfaces are strictly equal to each other. // Both arrays must have the same set of items. Items must be equal. public static bool Equals(IReadOnlyList left, IReadOnlyList right) { if (left == right) { return true; } else if (left == null) { return false; } else if (right == null) { return false; } else if (left.Count != right.Count) { return false; } for (int i = 0; i < left.Count; ++i) { if (!left[i].Equals(right[i])) { return false; } } return true; } // Return true if this JSValueArray is strictly equal to 'other'. public bool Equals(JSValueArray other) { return Equals(this as IReadOnlyList, other as IReadOnlyList); } // Return true if this JSValueArray is strictly equal to 'other'. public bool Equals(IReadOnlyList other) { return Equals(this as IReadOnlyList, other); } // Return true if this JSValueArray is strictly equal to 'obj'. public override bool Equals(object obj) { return Equals(this as IReadOnlyList, obj as IReadOnlyList); } // True if Equals(left, right) public static bool operator ==(JSValueArray left, IReadOnlyList right) { return Equals(left, right); } // True if !Equals(left, right) public static bool operator !=(JSValueArray left, IReadOnlyList right) { return !Equals(left, right); } // True if Equals(left, right) public static bool operator ==(IReadOnlyList left, JSValueArray right) { return Equals(left, right); } // True if !Equals(left, right) public static bool operator !=(IReadOnlyList left, JSValueArray right) { return !Equals(left, right); } // Return true if this JSValueArray is strictly equal to other JSValueArray // after their property values are converted to the same type. // See JSValue::JSEquals for details about the conversion. public static bool JSEquals(IReadOnlyList left, IReadOnlyList right) { if (left == right) { return true; } else if (left == null) { return false; } else if (right == null) { return false; } else if (left.Count != right.Count) { return false; } for (int i = 0; i < left.Count; ++i) { if (!left[i].JSEquals(right[i])) { return false; } } return true; } // Calculation of the JSValueArray hash code is quite expensive. // It is not implemented. public override int GetHashCode() { throw new NotImplementedException("JSValueArray currently does not support hash codes"); } // Create JSValueArray from IJSValueReader. public static JSValueArray ReadFrom(IJSValueReader reader) => JSValue.ReadArrayFrom(reader); // Write this JSValueArray to IJSValueWriter. public void WriteTo(IJSValueWriter writer) => JSValue.WriteArray(writer, this); } // JSValue represents an immutable JavaScript value passed as a parameter. // It is created to simplify working with IJSValueReader in complex situations. // For example, when we need to serialize a TypeScript discriminating union and // we need to read 'kind' property before we process other properties. // // JSValue is designed to minimize number of memory allocations: // - It is a struct that avoids extra memory allocations when it is stored in a container. // - It avoids boxing simple values by using a union type to store them. // The JSValue is an immutable and is safe to be used from multiple threads. // It does not implement GetHashCode() and must not be used as a key in a dictionary. public struct JSValue : IEquatable { public static readonly JSValue Null = default; public static readonly JSValue EmptyObject = new JSValueObject(); public static readonly JSValue EmptyArray = new JSValueArray(); public static readonly JSValue EmptyString = ""; // Used by AsBoolean() conversion from a string to a Boolean. private static readonly HashSet StringToBoolean = new HashSet(StringComparer.OrdinalIgnoreCase) { "true", "1", "yes", "y", "on" }; // The Void type could be used instead of void in generic types. public struct Void { }; private readonly SimpleValue m_simpleValue; private readonly object m_objValue; // Create an Object JSValue. public JSValue(IReadOnlyDictionary value) { Type = JSValueType.Object; m_simpleValue = new SimpleValue(); m_objValue = value ?? EmptyObject.ObjectValue; } // Create an Array JSValue. public JSValue(IReadOnlyList value) { Type = JSValueType.Array; m_simpleValue = new SimpleValue(); m_objValue = value ?? EmptyArray.ArrayValue; } // Create a String JSValue. public JSValue(string value) { Type = JSValueType.String; m_simpleValue = new SimpleValue(); m_objValue = value ?? ""; } // Create a Boolean JSValue. public JSValue(bool value) { Type = JSValueType.Boolean; m_simpleValue = new SimpleValue { BooleanValue = value }; m_objValue = null; } // Create an Int64 JSValue. public JSValue(long value) { Type = JSValueType.Int64; m_simpleValue = new SimpleValue { Int64Value = value }; m_objValue = null; } // Create a Double JSValue. public JSValue(double value) { Type = JSValueType.Double; m_simpleValue = new SimpleValue { DoubleValue = value }; m_objValue = null; } public static implicit operator JSValue(ReadOnlyDictionary value) => new JSValue(value); public static implicit operator JSValue(Dictionary value) => new JSValue(value); public static implicit operator JSValue(JSValueObject value) => new JSValue(value); public static implicit operator JSValue(ReadOnlyCollection value) => new JSValue(value); public static implicit operator JSValue(List value) => new JSValue(value); public static implicit operator JSValue(JSValueArray value) => new JSValue(value); public static implicit operator JSValue(string value) => new JSValue(value); public static implicit operator JSValue(bool value) => new JSValue(value); public static implicit operator JSValue(sbyte value) => new JSValue(value); public static implicit operator JSValue(short value) => new JSValue(value); public static implicit operator JSValue(int value) => new JSValue(value); public static implicit operator JSValue(long value) => new JSValue(value); public static implicit operator JSValue(byte value) => new JSValue(value); public static implicit operator JSValue(ushort value) => new JSValue(value); public static implicit operator JSValue(uint value) => new JSValue(value); public static implicit operator JSValue(ulong value) => new JSValue((long)value); public static implicit operator JSValue(float value) => new JSValue(value); public static implicit operator JSValue(double value) => new JSValue(value); // Get JSValue type. public JSValueType Type { get; } // Return true if JSValue type is Null. public bool IsNull => Type == JSValueType.Null; // Return true and Object value if JSValue type is Object, or false and JSValue.Null otherwise. public bool TryGetObject(out IReadOnlyDictionary value) { value = (Type == JSValueType.Object) ? (IReadOnlyDictionary)m_objValue : null; return value != null; } // Return true and Array value if JSValue type is Array, or false and JSValue.Null otherwise. public bool TryGetArray(out IReadOnlyList value) { value = (Type == JSValueType.Array) ? (IReadOnlyList)m_objValue : null; return value != null; } // Return true and String value if JSValue type is String, or false and JSValue.Null otherwise. public bool TryGetString(out string value) { value = (Type == JSValueType.String) ? (string)m_objValue : null; return value != null; } // Return true and Boolean value if JSValue type is Boolean, or false and JSValue.Null otherwise. public bool TryGetBoolean(out bool value) { if (Type == JSValueType.Boolean) { value = m_simpleValue.BooleanValue; return true; } value = false; return false; } // Return true and Int64 value if JSValue type is Int64, or false and JSValue.Null otherwise. public bool TryGetInt64(out long value) { if (Type == JSValueType.Int64) { value = m_simpleValue.Int64Value; return true; } value = 0; return false; } // Return true and Double value if JSValue type is Double, or false and JSValue.Null otherwise. public bool TryGetDouble(out double value) { if (Type == JSValueType.Double) { value = m_simpleValue.DoubleValue; return true; } value = 0; return false; } // Return Object representation of JSValue. It is JSValue.EmptyObject if type is not Object. public IReadOnlyDictionary AsObject() => Type == JSValueType.Object ? ObjectValue : EmptyObject.ObjectValue; // Return Array representation of JSValue. It is JSValue.EmptyArray if type is not Array. public IReadOnlyList AsArray() => Type == JSValueType.Array ? ArrayValue : EmptyArray.ArrayValue; // Return a String representation of JSValue. // Null is "null". // Object and Array are empty strings "". // Boolean is "true" or "false". // Int64 is converted to string using integer representation. // Double uses AsJSString() conversion which uses "NaN", "Infinity", and "-Infinity" for special values. public string AsString() { switch (Type) { case JSValueType.Null: return JSConverter.NullString; case JSValueType.String: return StringValue; case JSValueType.Boolean: return JSConverter.ToJSString(BooleanValue); case JSValueType.Int64: return Int64Value.ToString(); case JSValueType.Double: return JSConverter.ToJSString(DoubleValue); default: return ""; } } // Return a Boolean representation of JSValue. // Object and Array are true if they are not empty. // String is true if it case-insensitively matches "true", "1", "yes", "y", or "on" strings. // Int64 or Double are false if they are zero or NAN. public bool AsBoolean() { switch (Type) { case JSValueType.Object: return ObjectValue.Count != 0; case JSValueType.Array: return ArrayValue.Count != 0; case JSValueType.String: return StringToBoolean.Contains(StringValue); case JSValueType.Boolean: return BooleanValue; case JSValueType.Int64: return Int64Value != 0; case JSValueType.Double: return !double.IsNaN(DoubleValue) && DoubleValue != 0; default: return false; } } // Return an Int8 representation of JSValue. It is the same as (Int8)AsInt64(). public sbyte AsInt8() => (sbyte)AsInt64(); // Return an Int16 representation of JSValue. It is the same as (Int16)AsInt64(). public short AsInt16() => (short)AsInt64(); // Return an Int32 representation of JSValue. It is the same as (Int32)AsInt64(). public int AsInt32() => (int)AsInt64(); // Return an Int64 representation of JSValue. // String is converted to double first before converting to Int64. // Boolean is converted to 0 or 1. // Null, Object, and Array are 0. public long AsInt64() { switch (Type) { case JSValueType.String: return JSConverter.ToInt64(JSConverter.ToJSNumber(StringValue)); case JSValueType.Boolean: return BooleanValue ? 1 : 0; case JSValueType.Int64: return Int64Value; case JSValueType.Double: return JSConverter.ToInt64(DoubleValue); default: return 0; } } // Return an Uint8 representation of JSValue. It is the same as (UInt8)AsInt64(). public byte AsUInt8() => (byte)AsInt64(); // Return an UInt16 representation of JSValue. It is the same as (UInt16)AsInt64(). public ushort AsUInt16() => (ushort)AsInt64(); // Return an UInt32 representation of JSValue. It is the same as (UInt32)AsInt64(). public uint AsUInt32() => (uint)AsInt64(); // Return an UInt64 representation of JSValue. It is the same as (UInt64)AsInt64(). public ulong AsUInt64() => (ulong)AsInt64(); // Return a Single representation of JSValue. It is the same as (Single)AsDouble(). public float AsSingle() => (float)AsDouble(); // Return a double representation of JSValue. // Boolean is converted to 0.0 or 1.0. // Null, Object, and Array are 0. public double AsDouble() { switch (Type) { case JSValueType.String: return JSConverter.ToJSNumber(StringValue); case JSValueType.Boolean: return BooleanValue ? 1.0 : 0.0; case JSValueType.Int64: return Int64Value; case JSValueType.Double: return DoubleValue; default: return 0; } } // Cast JSValue to String using AsString() call. public static explicit operator string(JSValue value) => value.AsString(); // Cast JSValue to Boolean using AsBoolean() call. public static explicit operator bool(JSValue value) => value.AsBoolean(); // Cast JSValue to Int8 using AsInt8() call. public static explicit operator sbyte(JSValue value) => value.AsInt8(); // Cast JSValue to Int16 using AsInt16() call. public static explicit operator short(JSValue value) => value.AsInt16(); // Cast JSValue to Int32 using AsInt32() call. public static explicit operator int(JSValue value) => value.AsInt32(); // Cast JSValue to Int64 using AsInt64() call. public static explicit operator long(JSValue value) => value.AsInt64(); // Cast JSValue to UInt8 using AsUInt8() call. public static explicit operator byte(JSValue value) => value.AsUInt8(); // Cast JSValue to UInt16 using AsUInt16() call. public static explicit operator ushort(JSValue value) => value.AsUInt16(); // Cast JSValue to UInt32 using AsUInt32() call. public static explicit operator uint(JSValue value) => value.AsUInt32(); // Cast JSValue to UInt64 using AsUInt64() call. public static explicit operator ulong(JSValue value) => value.AsUInt64(); // Cast JSValue to Single using AsSingle() call. public static explicit operator float(JSValue value) => value.AsSingle(); // Cast JSValue to Double using AsDouble() call. public static explicit operator double(JSValue value) => value.AsDouble(); // Return a String representation of JSValue. It is equivalent to JavaScript String(value) result. public string AsJSString() { StringBuilder WriteValue(StringBuilder sb, JSValue node) { switch (node.Type) { case JSValueType.Null: return sb.Append(JSConverter.NullString); case JSValueType.Object: return sb.Append(JSConverter.ObjectString); case JSValueType.Array: { bool start = true; foreach (var item in node.ArrayValue) { if (start) { start = false; } else { sb.Append(','); } WriteValue(sb, item); } return sb; } case JSValueType.String: return sb.Append(node.StringValue); case JSValueType.Boolean: return sb.Append(JSConverter.ToJSString(node.BooleanValue)); case JSValueType.Int64: return sb.Append(node.Int64Value); case JSValueType.Double: return sb.Append(JSConverter.ToJSString(node.DoubleValue)); default: return sb; } } switch (Type) { case JSValueType.Null: return JSConverter.NullString; case JSValueType.Object: return JSConverter.ObjectString; case JSValueType.Array: { StringBuilder sb = new StringBuilder(); WriteValue(sb, this); return sb.ToString(); } case JSValueType.String: return StringValue; case JSValueType.Boolean: return JSConverter.ToJSString(BooleanValue); case JSValueType.Int64: return Int64Value.ToString(); case JSValueType.Double: return JSConverter.ToJSString(DoubleValue); default: return ""; } } // Return a Boolean representation of JSValue. It is equivalent to JavaScript Boolean(value) result. public bool AsJSBoolean() { switch (Type) { case JSValueType.Object: case JSValueType.Array: return true; case JSValueType.String: return !string.IsNullOrEmpty(StringValue); case JSValueType.Boolean: return BooleanValue; case JSValueType.Int64: return Int64Value != 0; case JSValueType.Double: return !double.IsNaN(DoubleValue) && DoubleValue != 0; default: return false; } } // Return a Double representation of JSValue. It is equivalent to JavaScript Number(value) result. public double AsJSNumber() { switch (Type) { case JSValueType.Object: case JSValueType.Array: case JSValueType.String: return JSConverter.ToJSNumber(AsJSString()); case JSValueType.Boolean: return BooleanValue ? 1 : 0; case JSValueType.Int64: return Int64Value; case JSValueType.Double: return DoubleValue; default: return 0; } } // Convert JSValue to a readable string that can be used for logging and debugging. public override string ToString() { switch (Type) { case JSValueType.Null: return JSConverter.NullString; case JSValueType.Object: { var sb = new StringBuilder(); sb.Append("{"); bool start = true; foreach (var prop in ObjectValue) { if (start) { start = false; } else { sb.Append(", "); } sb.Append(prop.Key); sb.Append(": "); sb.Append(prop.Value.ToString()); } sb.Append("}"); return sb.ToString(); } case JSValueType.Array: { var sb = new StringBuilder(); sb.Append("["); bool start = true; foreach (var item in ArrayValue) { if (start) { start = false; } else { sb.Append(", "); } sb.Append(item.ToString()); } sb.Append("]"); return sb.ToString(); } case JSValueType.String: return "\"" + StringValue + "\""; case JSValueType.Boolean: return JSConverter.ToJSString(BooleanValue); case JSValueType.Int64: return Int64Value.ToString(); case JSValueType.Double: return JSConverter.ToJSString(DoubleValue); default: return ""; } } // Return value T that is created from JSValue using a ReadValue extension function. // Default T is constructed by using default constructor. public T To() => new JSValueTreeReader(this).ReadValue(); // Create JSValue from a type that has WriteValue extension method defined to write to IJSValueWriter. public JSValue From(T value) { var writer = new JSValueTreeWriter(); writer.WriteValue(value); return writer.TakeValue(); } // Return property count if JSValue is Object, or 0 otherwise. public int PropertyCount => Type == JSValueType.Object ? ObjectValue.Count : 0; // Try to get an object property value if JSValue type is Object and the property is found. // It returns false and outputs JSValue.Null otherwise. public bool TryGetObjectProperty(string propertyName, out JSValue value) { if (TryGetObject(out var objectValue) && objectValue.TryGetValue(propertyName, out value)) { return true; } value = Null; return false; } // Get an object property value if JSValue type is Object and the property is found, // or JSValue.Null otherwise. public JSValue GetObjectProperty(string propertyName) { TryGetObjectProperty(propertyName, out JSValue result); return result; } // Get an object property value if JSValue type is Object and the property is found, // or JSValue.Null otherwise. public JSValue this[string propertyName] => GetObjectProperty(propertyName); // Return item count if JSValue is Array, or 0 otherwise. public int ItemCount => Type == JSValueType.Array ? ArrayValue.Count : 0; // Try to get an array item if JSValue type is Array and the index is in bounds. // It returns false and outputs JSValue.Null otherwise. public bool TryGetArrayItem(int index, out JSValue value) { if (index >= 0 && TryGetArray(out var arrayValue) && index < arrayValue.Count) { value = arrayValue[index]; return true; } value = Null; return false; } // Get an array item if JSValue type is Array and the index is in bounds, // or JSValue.Null otherwise. public JSValue GetArrayItem(int index) { TryGetArrayItem(index, out JSValue result); return result; } // Get an array item if JSValue type is Array and the index is in bounds, // or JSValue.Null otherwise. public JSValue this[int index] => GetArrayItem(index); // Return true if this JSValue is strictly equal to JSValue. // Compared values must have the same type and value. // // The behavior is similar to JavaScript === operator except for Object and Array where // this functions does a deep structured comparison instead of pointer equality. public bool Equals(JSValue other) { if (Type != other.Type) { return false; } switch (Type) { case JSValueType.Null: return true; case JSValueType.Object: return JSValueObject.Equals(ObjectValue, other.ObjectValue); case JSValueType.Array: return JSValueArray.Equals(ArrayValue, other.ArrayValue); case JSValueType.String: return StringValue == other.StringValue; case JSValueType.Boolean: return BooleanValue == other.BooleanValue; case JSValueType.Int64: return Int64Value == other.Int64Value; case JSValueType.Double: return DoubleValue == other.DoubleValue; default: return false; } } // Return true if obj is a JSValue it is strictly equal to this JSValue. public override bool Equals(object obj) { return (obj is JSValue) && Equals((JSValue)obj); } // True if left.Equals(right) public static bool operator ==(JSValue left, JSValue right) { return left.Equals(right); } //! True if !left.Equals(right) public static bool operator !=(JSValue left, JSValue right) { return !left.Equals(right); } // Return true if this JSValue is strictly equal to JSValue after they are converted to the same type. // // Null is not converted to any other type before comparison. // Object and Array types are converted first to a String type using AsString() before comparing // with other types, and then we apply the same rules as for the String type. // String is converted to Double before comparing with Boolean, Int64, or Double. // Boolean is converted to 1.0 and +0.0 when comparing with String, Int64, or Double. // Int64 is converted to Double before comparing with other types. // // The behavior is similar to JavaScript == operator except for Object and Array for which // this functions does a deep structured comparison using JSEquals instead // of pointer equality. public bool JSEquals(JSValue other) { if (Type == other.Type) { switch (Type) { case JSValueType.Object: return JSValueObject.JSEquals(ObjectValue, other.ObjectValue); case JSValueType.Array: return JSValueArray.JSEquals(ArrayValue, other.ArrayValue); default: return Equals(other); } } else if (Type == JSValueType.Null || other.Type == JSValueType.Null) { return false; } // If one of the types Boolean, Int64, or Double, then compare as Numbers, // otherwise compare as strings. JSValueType greatestType = Type > other.Type ? Type : other.Type; if (greatestType >= JSValueType.Boolean) { return AsJSNumber() == other.AsJSNumber(); } else { return AsJSString() == other.AsJSString(); } } // Calculation of the JSValue hash code is quite expensive. // It is not implemented. public override int GetHashCode() { throw new NotImplementedException("JSValue currently does not support hash codes"); } // Create JSValue from IJSValueReader. public static JSValue ReadFrom(IJSValueReader reader) { if (reader is IJSValueTreeReader treeReader) { return treeReader.Current; } return InternalReadFrom(reader); } // Create JSValueObject from IJSValueReader. public static JSValueObject ReadObjectFrom(IJSValueReader reader) { if (reader.ValueType == JSValueType.Object) { if (reader is IJSValueTreeReader treeReader) { return JSValueObject.CopyFrom(treeReader.Current.ObjectValue); } return InternalReadObjectFrom(reader); } return new JSValueObject(); } // Create JSValueArray from IJSValueReader. public static JSValueArray ReadArrayFrom(IJSValueReader reader) { if (reader.ValueType == JSValueType.Array) { if (reader is IJSValueTreeReader treeReader) { return JSValueArray.CopyFrom(treeReader.Current.ArrayValue); } return InternalReadArrayFrom(reader); } return new JSValueArray(); } // Write this JSValue to IJSValueWriter. public void WriteTo(IJSValueWriter writer) { switch (Type) { case JSValueType.Null: writer.WriteNull(); break; case JSValueType.Object: WriteObject(writer, ObjectValue); break; case JSValueType.Array: WriteArray(writer, ArrayValue); break; case JSValueType.String: writer.WriteString(StringValue); break; case JSValueType.Boolean: writer.WriteBoolean(BooleanValue); break; case JSValueType.Int64: writer.WriteInt64(Int64Value); break; case JSValueType.Double: writer.WriteDouble(DoubleValue); break; } } // Write JSValueObject key-value pairs to IJSValueWriter. public static void WriteObject(IJSValueWriter writer, IEnumerable> value) { writer.WriteObjectBegin(); foreach (var keyValue in value) { writer.WritePropertyName(keyValue.Key); keyValue.Value.WriteTo(writer); } writer.WriteObjectEnd(); } // Write JSValueArray items to IJSValueWriter. public static void WriteArray(IJSValueWriter writer, IEnumerable value) { writer.WriteArrayBegin(); foreach (var item in value) { item.WriteTo(writer); } writer.WriteArrayEnd(); } private static JSValue InternalReadFrom(IJSValueReader reader) { switch (reader.ValueType) { case JSValueType.Null: return Null; case JSValueType.Object: return new JSValue(InternalReadObjectFrom(reader)); case JSValueType.Array: return new JSValue(InternalReadArrayFrom(reader)); case JSValueType.String: return new JSValue(reader.GetString()); case JSValueType.Boolean: return new JSValue(reader.GetBoolean()); case JSValueType.Int64: return new JSValue(reader.GetInt64()); case JSValueType.Double: return new JSValue(reader.GetDouble()); default: throw new ReactException("Unexpected JSValueType"); } } private static JSValueObject InternalReadObjectFrom(IJSValueReader reader) { var jsObject = new JSValueObject(); while (reader.GetNextObjectProperty(out string propertyName)) { jsObject.Add(propertyName, InternalReadFrom(reader)); } return jsObject; } private static JSValueArray InternalReadArrayFrom(IJSValueReader reader) { var jsArray = new JSValueArray(); while (reader.GetNextArrayItem()) { jsArray.Add(InternalReadFrom(reader)); } return jsArray; } #region Obsolete members [Obsolete("Use TryGetObject or AsObject")] public IReadOnlyDictionary Object => (IReadOnlyDictionary)m_objValue; [Obsolete("Use TryGetArray or AsArray")] public IReadOnlyList Array => (IReadOnlyList)m_objValue; [Obsolete("Use TryGetString, AsString, or AsJSString")] public string String => (string)m_objValue; [Obsolete("Use TryGetBoolean, AsBoolean, or AsJSBoolean")] public bool Boolean => m_simpleValue.BooleanValue; [Obsolete("Use TryGetInt64 or AsInt64")] public long Int64 => m_simpleValue.Int64Value; [Obsolete("Use TryGetDouble, AsDouble, or AsJSNumber")] public double Double => m_simpleValue.DoubleValue; [Obsolete("Use ReadObjectFrom")] public static Dictionary ReadObjectPropertiesFrom(IJSValueReader reader) { return ReadObjectFrom(reader); } [Obsolete("Use ReadArrayFrom")] public static List ReadArrayItemsFrom(IJSValueReader reader) { return ReadArrayFrom(reader); } #endregion private IReadOnlyDictionary ObjectValue => (IReadOnlyDictionary)m_objValue; private IReadOnlyList ArrayValue => (IReadOnlyList)m_objValue; private string StringValue => (string)m_objValue; private bool BooleanValue => m_simpleValue.BooleanValue; private long Int64Value => m_simpleValue.Int64Value; private double DoubleValue => m_simpleValue.DoubleValue; // This is a 'union' type that may store in the same location bool, long, or double. [StructLayout(LayoutKind.Explicit)] private struct SimpleValue { [FieldOffset(0)] public bool BooleanValue; [FieldOffset(0)] public long Int64Value; [FieldOffset(0)] public double DoubleValue; } private static class JSConverter { public static readonly string NullString = "null"; public static readonly string ObjectString = "[object Object]"; public static string ToJSString(bool value) { return value ? "true" : "false"; } public static string ToJSString(double value) { if (double.IsNaN(value)) { return "NaN"; } else if (double.IsPositiveInfinity(value)) { return "Infinity"; } else if (double.IsNegativeInfinity(value)) { return "-Infinity"; } else { return value.ToString(); } } public static double ToJSNumber(string value) { var trimmed = value.Trim(); if (trimmed.Length == 0) { return 0; } // NumberStyles.Float does not allow numbers to have thousand's commas insider. return double.TryParse(trimmed, NumberStyles.Float, CultureInfo.InvariantCulture, out double doubleValue) ? doubleValue : double.NaN; } public static long ToInt64(double value) => (long.MinValue <= value && value <= long.MaxValue) ? unchecked((long)value) : 0; } } }