RuneJsonConverter.cs 3.8 KB

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