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