diff --git a/LibMatrix.sln b/LibMatrix.sln
index c068216..3294f77 100644
--- a/LibMatrix.sln
+++ b/LibMatrix.sln
@@ -5,22 +5,14 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix.csproj", "{2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExampleBots", "ExampleBots", "{840309F0-435B-43A7-8471-8C2BE643889D}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{A6345ECE-4C5E-400F-9130-886E343BF314}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.MxApiExtensions", "LibMatrix.MxApiExtensions\LibMatrix.MxApiExtensions.csproj", "{32D9616B-91BB-4B43-97C6-2C3840C12EA6}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.ExampleBot", "ExampleBots\LibMatrix.ExampleBot\LibMatrix.ExampleBot.csproj", "{1B1B2197-61FB-416F-B6C8-845F2E5A0442}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModerationBot", "ExampleBots\ModerationBot\ModerationBot.csproj", "{8F0A820E-F6AE-45A2-970E-7A3759693919}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralContactBotPoC", "ExampleBots\PluralContactBotPoC\PluralContactBotPoC.csproj", "{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BFE16D8E-EFC5-49F6-9854-DB001309B3B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{345934FF-CA81-4A4B-B137-9F198102C65F}"
@@ -37,6 +29,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "Lib
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "Tests\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{711D1579-9228-47BA-9CB3-C237F7E8F403}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{87270607-F95C-4EED-AE69-57666863EFDF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{5EE6E02C-A63F-4864-91F3-7375B4C50189}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLib.Tests", "ArcaneLibs\ArcaneLib.Tests\ArcaneLib.Tests.csproj", "{E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.E2eeTestKit", "Utilities\LibMatrix.E2eeTestKit\LibMatrix.E2eeTestKit.csproj", "{A1066F8F-CE8A-4F60-9EFB-553C9E794435}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -54,14 +64,6 @@ Global
{32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Release|Any CPU.Build.0 = Release|Any CPU
- {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Release|Any CPU.Build.0 = Release|Any CPU
- {8F0A820E-F6AE-45A2-970E-7A3759693919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8F0A820E-F6AE-45A2-970E-7A3759693919}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8F0A820E-F6AE-45A2-970E-7A3759693919}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8F0A820E-F6AE-45A2-970E-7A3759693919}.Release|Any CPU.Build.0 = Release|Any CPU
{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -70,10 +72,6 @@ Global
{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.Build.0 = Release|Any CPU
- {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Release|Any CPU.Build.0 = Release|Any CPU
{345934FF-CA81-4A4B-B137-9F198102C65F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{345934FF-CA81-4A4B-B137-9F198102C65F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{345934FF-CA81-4A4B-B137-9F198102C65F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -98,17 +96,59 @@ Global
{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {711D1579-9228-47BA-9CB3-C237F7E8F403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {711D1579-9228-47BA-9CB3-C237F7E8F403}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {711D1579-9228-47BA-9CB3-C237F7E8F403}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {711D1579-9228-47BA-9CB3-C237F7E8F403}.Release|Any CPU.Build.0 = Release|Any CPU
+ {87270607-F95C-4EED-AE69-57666863EFDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {87270607-F95C-4EED-AE69-57666863EFDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {87270607-F95C-4EED-AE69-57666863EFDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {87270607-F95C-4EED-AE69-57666863EFDF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5EE6E02C-A63F-4864-91F3-7375B4C50189}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5EE6E02C-A63F-4864-91F3-7375B4C50189}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5EE6E02C-A63F-4864-91F3-7375B4C50189}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5EE6E02C-A63F-4864-91F3-7375B4C50189}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1066F8F-CE8A-4F60-9EFB-553C9E794435}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1066F8F-CE8A-4F60-9EFB-553C9E794435}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1066F8F-CE8A-4F60-9EFB-553C9E794435}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1066F8F-CE8A-4F60-9EFB-553C9E794435}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {1B1B2197-61FB-416F-B6C8-845F2E5A0442} = {840309F0-435B-43A7-8471-8C2BE643889D}
- {8F0A820E-F6AE-45A2-970E-7A3759693919} = {840309F0-435B-43A7-8471-8C2BE643889D}
{35DF9A1A-D988-4225-AFA3-06BB8EDEB559} = {A6345ECE-4C5E-400F-9130-886E343BF314}
{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7} = {A6345ECE-4C5E-400F-9130-886E343BF314}
- {FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B} = {840309F0-435B-43A7-8471-8C2BE643889D}
{345934FF-CA81-4A4B-B137-9F198102C65F} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4}
{0B9B34D1-9362-45A9-9C21-816FD6959110} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4}
{4D9B5227-48DC-4A30-9263-AFB51DC01ABB} = {A6345ECE-4C5E-400F-9130-886E343BF314}
{13A797D1-7E13-4789-A167-8628B1641AC0} = {01A126FE-9D50-40F2-817B-E55F4065EA76}
{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4}
+ {ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C} = {01A126FE-9D50-40F2-817B-E55F4065EA76}
+ {F94D35FF-E90E-4B86-B26E-A8E46EB54BDD} = {01A126FE-9D50-40F2-817B-E55F4065EA76}
+ {711D1579-9228-47BA-9CB3-C237F7E8F403} = {01A126FE-9D50-40F2-817B-E55F4065EA76}
+ {87270607-F95C-4EED-AE69-57666863EFDF} = {01A126FE-9D50-40F2-817B-E55F4065EA76}
+ {5EE6E02C-A63F-4864-91F3-7375B4C50189} = {01A126FE-9D50-40F2-817B-E55F4065EA76}
+ {7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF} = {01A126FE-9D50-40F2-817B-E55F4065EA76}
+ {E6BF8A7E-92F6-4761-A793-1EC709F7E2BF} = {01A126FE-9D50-40F2-817B-E55F4065EA76}
+ {6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5} = {A6345ECE-4C5E-400F-9130-886E343BF314}
+ {A1066F8F-CE8A-4F60-9EFB-553C9E794435} = {A6345ECE-4C5E-400F-9130-886E343BF314}
EndGlobalSection
EndGlobal
diff --git a/LibMatrix/Extensions/CanonicalJsonSerializer.cs b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
new file mode 100644
index 0000000..a6fbcf4
--- /dev/null
+++ b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
@@ -0,0 +1,91 @@
+using System.Collections.Frozen;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using System.Text.Unicode;
+using ArcaneLibs.Extensions;
+
+namespace LibMatrix.Extensions;
+
+public static class CanonicalJsonSerializer {
+ // TODO: Alphabetise dictionaries
+ private static JsonSerializerOptions _options => new() {
+ WriteIndented = false,
+ Encoder = UnicodeJsonEncoder.Singleton,
+ };
+
+ private static readonly FrozenSet<PropertyInfo> JsonSerializerOptionsProperties = typeof(JsonSerializerOptions)
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(x => x.SetMethod != null && x.GetMethod != null)
+ .ToFrozenSet();
+
+ private static JsonSerializerOptions MergeOptions(JsonSerializerOptions? inputOptions) {
+ var newOptions = _options;
+ if (inputOptions == null)
+ return newOptions;
+
+ foreach (var property in JsonSerializerOptionsProperties) {
+ if(property.Name == nameof(JsonSerializerOptions.Encoder))
+ continue;
+ if (property.Name == nameof(JsonSerializerOptions.WriteIndented))
+ continue;
+
+ var value = property.GetValue(inputOptions);
+ // if (value == null)
+ // continue;
+ property.SetValue(newOptions, value);
+ }
+
+ return newOptions;
+ }
+
+#region STJ API
+
+ public static String Serialize<TValue>(TValue value, JsonSerializerOptions? options = null) {
+ var newOptions = MergeOptions(options);
+
+ return System.Text.Json.JsonSerializer.SerializeToNode(value, options) // We want to allow passing custom converters for eg. double/float -> string here...
+ .SortProperties()!
+ .CanonicalizeNumbers()!
+ .ToJsonString(newOptions);
+
+
+ // System.Text.Json.JsonSerializer.SerializeToNode(System.Text.Json.JsonSerializer.Deserialize<dynamic>("{\n \"a\": -0,\n \"b\": 1e10\n}")).ToJsonString();
+
+ }
+
+ public static String Serialize(object value, Type inputType, JsonSerializerOptions? options = null) => JsonSerializer.Serialize(value, inputType, _options);
+ // public static String Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => JsonSerializer.Serialize(value, jsonTypeInfo, _options);
+ // public static String Serialize(Object value, JsonTypeInfo jsonTypeInfo)
+
+#endregion
+
+ private static partial class JsonExtensions {
+ public static Action<JsonTypeInfo> AlphabetizeProperties(Type type) {
+ return typeInfo => {
+ if (typeInfo.Kind != JsonTypeInfoKind.Object || !type.IsAssignableFrom(typeInfo.Type))
+ return;
+ AlphabetizeProperties()(typeInfo);
+ };
+ }
+
+ public static Action<JsonTypeInfo> AlphabetizeProperties() {
+ return static typeInfo => {
+ if (typeInfo.Kind == JsonTypeInfoKind.Dictionary) { }
+
+ if (typeInfo.Kind != JsonTypeInfoKind.Object)
+ return;
+ var properties = typeInfo.Properties.OrderBy(p => p.Name, StringComparer.Ordinal).ToList();
+ typeInfo.Properties.Clear();
+ for (int i = 0; i < properties.Count; i++) {
+ properties[i].Order = i;
+ typeInfo.Properties.Add(properties[i]);
+ }
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix/Extensions/UnicodeJsonEncoder.cs b/LibMatrix/Extensions/UnicodeJsonEncoder.cs
new file mode 100644
index 0000000..ae58263
--- /dev/null
+++ b/LibMatrix/Extensions/UnicodeJsonEncoder.cs
@@ -0,0 +1,173 @@
+// LibMatrix: File sourced from https://github.com/dotnet/runtime/pull/87147/files under the MIT license.
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text;
+using System.Text.Encodings.Web;
+
+namespace LibMatrix.Extensions;
+
+internal sealed class UnicodeJsonEncoder : JavaScriptEncoder
+{
+ internal static readonly UnicodeJsonEncoder Singleton = new UnicodeJsonEncoder();
+
+ private readonly bool _preferHexEscape;
+ private readonly bool _preferUppercase;
+
+ public UnicodeJsonEncoder()
+ : this(preferHexEscape: false, preferUppercase: false)
+ {
+ }
+
+ public UnicodeJsonEncoder(bool preferHexEscape, bool preferUppercase)
+ {
+ _preferHexEscape = preferHexEscape;
+ _preferUppercase = preferUppercase;
+ }
+
+ public override int MaxOutputCharactersPerInputCharacter => 6; // "\uXXXX" for a single char ("\uXXXX\uYYYY" [12 chars] for supplementary scalar value)
+
+ public override unsafe int FindFirstCharacterToEncode(char* text, int textLength)
+ {
+ for (int index = 0; index < textLength; ++index)
+ {
+ char value = text[index];
+
+ if (NeedsEncoding(value))
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ public override unsafe bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten)
+ {
+ bool encode = WillEncode(unicodeScalar);
+
+ if (!encode)
+ {
+ Span<char> span = new Span<char>(buffer, bufferLength);
+ int spanWritten;
+ bool succeeded = new Rune(unicodeScalar).TryEncodeToUtf16(span, out spanWritten);
+ numberOfCharactersWritten = spanWritten;
+ return succeeded;
+ }
+
+ if (!_preferHexEscape && unicodeScalar <= char.MaxValue && HasTwoCharacterEscape((char)unicodeScalar))
+ {
+ if (bufferLength < 2)
+ {
+ numberOfCharactersWritten = 0;
+ return false;
+ }
+
+ buffer[0] = '\\';
+ buffer[1] = GetTwoCharacterEscapeSuffix((char)unicodeScalar);
+ numberOfCharactersWritten = 2;
+ return true;
+ }
+ else
+ {
+ if (bufferLength < 6)
+ {
+ numberOfCharactersWritten = 0;
+ return false;
+ }
+
+ buffer[0] = '\\';
+ buffer[1] = 'u';
+ buffer[2] = '0';
+ buffer[3] = '0';
+ buffer[4] = ToHexDigit((unicodeScalar & 0xf0) >> 4, _preferUppercase);
+ buffer[5] = ToHexDigit(unicodeScalar & 0xf, _preferUppercase);
+ numberOfCharactersWritten = 6;
+ return true;
+ }
+ }
+
+ public override bool WillEncode(int unicodeScalar)
+ {
+ if (unicodeScalar > char.MaxValue)
+ {
+ return false;
+ }
+
+ return NeedsEncoding((char)unicodeScalar);
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc8259#section-7
+ private static bool NeedsEncoding(char value)
+ {
+ if (value == '"' || value == '\\')
+ {
+ return true;
+ }
+
+ return value <= '\u001f';
+ }
+
+ private static bool HasTwoCharacterEscape(char value)
+ {
+ // RFC 8259, Section 7, "char = " BNF
+ switch (value)
+ {
+ case '"':
+ case '\\':
+ case '/':
+ case '\b':
+ case '\f':
+ case '\n':
+ case '\r':
+ case '\t':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static char GetTwoCharacterEscapeSuffix(char value)
+ {
+ // RFC 8259, Section 7, "char = " BNF
+ switch (value)
+ {
+ case '"':
+ return '"';
+ case '\\':
+ return '\\';
+ case '/':
+ return '/';
+ case '\b':
+ return 'b';
+ case '\f':
+ return 'f';
+ case '\n':
+ return 'n';
+ case '\r':
+ return 'r';
+ case '\t':
+ return 't';
+ default:
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+ }
+
+ private static char ToHexDigit(int value, bool uppercase)
+ {
+ if (value > 0xf)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ if (value < 10)
+ {
+ return (char)(value + '0');
+ }
+ else
+ {
+ return (char)(value - 0xa + (uppercase ? 'A' : 'a'));
+ }
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index d0511ea..e037672 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -8,6 +8,7 @@
<Optimize>true</Optimize>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- Required for UnicodeJsonEncoder... -->
</PropertyGroup>
<ItemGroup>
|