RuneJsonConverter.cs 5.4 KB

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