RuneJsonConverter.cs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. using System.Globalization;
  2. using System.Text.Json;
  3. using System.Text.Json.Serialization;
  4. using System.Text.RegularExpressions;
  5. namespace Terminal.Gui;
  6. /// <summary>
  7. /// Json converter for <see cref="Rune"/>.
  8. /// <para>
  9. /// If the Rune is printable, it will be serialized as the glyph; otherwise the \u format (e.g. "\\u2611") is used.
  10. /// </para>
  11. /// <para>
  12. /// Supports deserializing as one of:
  13. /// - unicode glyph in a string (e.g. "☑")
  14. /// - U+hex format in a string (e.g. "U+2611")
  15. /// - \u format in a string (e.g. "\\u2611")
  16. /// - A decimal number (e.g. 97 for "a")
  17. /// </para>
  18. /// </summary>
  19. internal class RuneJsonConverter : JsonConverter<Rune>
  20. {
  21. public override Rune Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  22. {
  23. switch (reader.TokenType)
  24. {
  25. case JsonTokenType.String:
  26. {
  27. string value = reader.GetString ();
  28. int first = RuneExtensions.MaxUnicodeCodePoint + 1;
  29. int second = RuneExtensions.MaxUnicodeCodePoint + 1;
  30. if (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase)
  31. || value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase))
  32. {
  33. // Handle encoded single char, surrogate pair, or combining mark + char
  34. uint [] codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})")
  35. .Select (
  36. match => uint.Parse (
  37. match.Groups [1].Value,
  38. NumberStyles.HexNumber
  39. )
  40. )
  41. .ToArray ();
  42. if (codePoints.Length == 0 || codePoints.Length > 2)
  43. {
  44. throw new JsonException ($"Invalid Rune: {value}.");
  45. }
  46. if (codePoints.Length > 0)
  47. {
  48. first = (int)codePoints [0];
  49. }
  50. if (codePoints.Length == 2)
  51. {
  52. second = (int)codePoints [1];
  53. }
  54. }
  55. else
  56. {
  57. // Handle single character, surrogate pair, or combining mark + char
  58. if (value.Length == 0 || value.Length > 2)
  59. {
  60. throw new JsonException ($"Invalid Rune: {value}.");
  61. }
  62. if (value.Length > 0)
  63. {
  64. first = value [0];
  65. }
  66. if (value.Length == 2)
  67. {
  68. second = value [1];
  69. }
  70. }
  71. Rune result;
  72. if (second == RuneExtensions.MaxUnicodeCodePoint + 1)
  73. {
  74. // Single codepoint
  75. if (!Rune.TryCreate (first, out result))
  76. {
  77. throw new JsonException ($"Invalid Rune: {value}.");
  78. }
  79. return result;
  80. }
  81. // Surrogate pair?
  82. if (Rune.TryCreate ((char)first, (char)second, out result))
  83. {
  84. return result;
  85. }
  86. if (!Rune.IsValid (second))
  87. {
  88. throw new JsonException ($"The second codepoint is not valid: {second} in ({value})");
  89. }
  90. var cm = new Rune (second);
  91. if (!cm.IsCombiningMark ())
  92. {
  93. throw new JsonException ($"The second codepoint is not a combining mark: {cm} in ({value})");
  94. }
  95. // not a surrogate pair, so a combining mark + char?
  96. string combined = string.Concat ((char)first, (char)second).Normalize ();
  97. if (!Rune.IsValid (combined [0]))
  98. {
  99. throw new JsonException ($"Invalid combined Rune ({value})");
  100. }
  101. return new (combined [0]);
  102. }
  103. case JsonTokenType.Number:
  104. {
  105. uint num = reader.GetUInt32 ();
  106. if (Rune.IsValid (num))
  107. {
  108. return new (num);
  109. }
  110. throw new JsonException ($"Invalid Rune (not a scalar Unicode value): {num}.");
  111. }
  112. default:
  113. throw new JsonException ($"Unexpected token when parsing Rune: {reader.TokenType}.");
  114. }
  115. }
  116. public override void Write (Utf8JsonWriter writer, Rune value, JsonSerializerOptions options)
  117. {
  118. Rune printable = value.MakePrintable ();
  119. if (printable == Rune.ReplacementChar)
  120. {
  121. // Write as /u string
  122. writer.WriteRawValue ($"\"{value}\"");
  123. }
  124. else
  125. {
  126. // Write as the actual glyph
  127. writer.WriteStringValue (value.ToString ());
  128. }
  129. }
  130. }
  131. #pragma warning restore 1591