using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace Terminal.Gui; /// /// Json converter for . Supports Json converter for . Supports A string as /// one of: - unicode char (e.g. "☑") - U+hex format (e.g. "U+2611") - \u format (e.g. "\\u2611") A number - The /// unicode code in decimal /// internal class RuneJsonConverter : JsonConverter { public override Rune Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { switch (reader.TokenType) { case JsonTokenType.String: { string value = reader.GetString (); int first = RuneExtensions.MaxUnicodeCodePoint + 1; int second = RuneExtensions.MaxUnicodeCodePoint + 1; if (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase)) { // Handle encoded single char, surrogate pair, or combining mark + char uint [] codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})") .Select ( match => uint.Parse ( match.Groups [1].Value, NumberStyles.HexNumber ) ) .ToArray (); if (codePoints.Length == 0 || codePoints.Length > 2) { throw new JsonException ($"Invalid Rune: {value}."); } if (codePoints.Length > 0) { first = (int)codePoints [0]; } if (codePoints.Length == 2) { second = (int)codePoints [1]; } } else { // Handle single character, surrogate pair, or combining mark + char if (value.Length == 0 || value.Length > 2) { throw new JsonException ($"Invalid Rune: {value}."); } if (value.Length > 0) { first = value [0]; } if (value.Length == 2) { second = value [1]; } } Rune result; if (second == RuneExtensions.MaxUnicodeCodePoint + 1) { // Single codepoint if (!Rune.TryCreate (first, out result)) { throw new JsonException ($"Invalid Rune: {value}."); } return result; } // Surrogate pair? if (Rune.TryCreate ((char)first, (char)second, out result)) { return result; } if (!Rune.IsValid (second)) { throw new JsonException ($"The second codepoint is not valid: {second} in ({value})"); } var cm = new Rune (second); if (!cm.IsCombiningMark ()) { throw new JsonException ($"The second codepoint is not a combining mark: {cm} in ({value})"); } // not a surrogate pair, so a combining mark + char? string combined = string.Concat ((char)first, (char)second).Normalize (); if (!Rune.IsValid (combined [0])) { throw new JsonException ($"Invalid combined Rune ({value})"); } return new Rune (combined [0]); } case JsonTokenType.Number: { uint num = reader.GetUInt32 (); if (Rune.IsValid (num)) { return new Rune (num); } throw new JsonException ($"Invalid Rune (not a scalar Unicode value): {num}."); } default: throw new JsonException ($"Unexpected token when parsing Rune: {reader.TokenType}."); } } public override void Write (Utf8JsonWriter writer, Rune value, JsonSerializerOptions options) { // HACK: Writes a JSON comment in addition to the glyph to ease debugging. // Technically, JSON comments are not valid, but we use relaxed decoding // (ReadCommentHandling = JsonCommentHandling.Skip) //writer.WriteCommentValue ($"(U+{value.Value:X8})"); //var printable = value.MakePrintable (); //if (printable == Rune.ReplacementChar) { // writer.WriteStringValue (value.ToString ()); //} else { // //writer.WriteRawValue ($"\"{value}\""); //} writer.WriteNumberValue (value.Value); } } #pragma warning restore 1591