StringExtensions.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #nullable enable
  2. using System.Buffers;
  3. using System.Globalization;
  4. namespace Terminal.Gui.Text;
  5. /// <summary>Extensions to <see cref="string"/> to support TUI text manipulation.</summary>
  6. public static class StringExtensions
  7. {
  8. /// <summary>Unpacks the last UTF-8 encoding in the string.</summary>
  9. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  10. /// <param name="str">The string to decode.</param>
  11. /// <param name="end">Index in string to stop at; if -1, use the buffer length.</param>
  12. /// <returns></returns>
  13. public static (Rune rune, int size) DecodeLastRune (this string str, int end = -1)
  14. {
  15. Rune rune = str.EnumerateRunes ().ToArray () [end == -1 ? ^1 : end];
  16. byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
  17. OperationStatus operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed);
  18. if (operationStatus == OperationStatus.Done)
  19. {
  20. return (rune, bytesConsumed);
  21. }
  22. return (Rune.ReplacementChar, 1);
  23. }
  24. /// <summary>Unpacks the first UTF-8 encoding in the string and returns the rune and its width in bytes.</summary>
  25. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  26. /// <param name="str">The string to decode.</param>
  27. /// <param name="start">Starting offset.</param>
  28. /// <param name="count">Number of bytes in the buffer, or -1 to make it the length of the buffer.</param>
  29. /// <returns></returns>
  30. public static (Rune Rune, int Size) DecodeRune (this string str, int start = 0, int count = -1)
  31. {
  32. Rune rune = str.EnumerateRunes ().ToArray () [start];
  33. byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
  34. if (count == -1)
  35. {
  36. count = bytes.Length;
  37. }
  38. OperationStatus operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed);
  39. if (operationStatus == OperationStatus.Done && bytesConsumed >= count)
  40. {
  41. return (rune, bytesConsumed);
  42. }
  43. return (Rune.ReplacementChar, 1);
  44. }
  45. /// <summary>Gets the number of columns the string occupies in the terminal.</summary>
  46. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  47. /// <param name="str">The string to measure.</param>
  48. /// <returns></returns>
  49. public static int GetColumns (this string str)
  50. {
  51. if (string.IsNullOrEmpty (str))
  52. {
  53. return 0;
  54. }
  55. var total = 0;
  56. TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (str);
  57. while (enumerator.MoveNext ())
  58. {
  59. string element = enumerator.GetTextElement ();
  60. // Get the maximum rune width within this grapheme cluster
  61. int width = element
  62. .EnumerateRunes ()
  63. .Max (r => Math.Max (r.GetColumns (), 0));
  64. total += width;
  65. }
  66. return total;
  67. }
  68. /// <summary>Gets the number of runes in the string.</summary>
  69. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  70. /// <param name="str">The string to count.</param>
  71. /// <returns></returns>
  72. public static int GetRuneCount (this string str) { return str.EnumerateRunes ().Count (); }
  73. /// <summary>
  74. /// Determines if this <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> is composed entirely of ASCII
  75. /// digits.
  76. /// </summary>
  77. /// <param name="stringSpan">A <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to check.</param>
  78. /// <returns>
  79. /// A <see langword="bool"/> indicating if all elements of the <see cref="ReadOnlySpan{T}"/> are ASCII digits (
  80. /// <see langword="true"/>) or not (<see langword="false"/>
  81. /// </returns>
  82. public static bool IsAllAsciiDigits (this ReadOnlySpan<char> stringSpan) { return stringSpan.ToString ().All (char.IsAsciiDigit); }
  83. /// <summary>
  84. /// Determines if this <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> is composed entirely of ASCII
  85. /// digits.
  86. /// </summary>
  87. /// <param name="stringSpan">A <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to check.</param>
  88. /// <returns>
  89. /// A <see langword="bool"/> indicating if all elements of the <see cref="ReadOnlySpan{T}"/> are ASCII digits (
  90. /// <see langword="true"/>) or not (<see langword="false"/>
  91. /// </returns>
  92. public static bool IsAllAsciiHexDigits (this ReadOnlySpan<char> stringSpan) { return stringSpan.ToString ().All (char.IsAsciiHexDigit); }
  93. /// <summary>Repeats the string <paramref name="n"/> times.</summary>
  94. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  95. /// <param name="str">The text to repeat.</param>
  96. /// <param name="n">Number of times to repeat the text.</param>
  97. /// <returns>The text repeated if <paramref name="n"/> is greater than zero, otherwise <see langword="null"/>.</returns>
  98. public static string? Repeat (this string str, int n)
  99. {
  100. if (n <= 0)
  101. {
  102. return null;
  103. }
  104. if (string.IsNullOrEmpty (str) || n == 1)
  105. {
  106. return str;
  107. }
  108. return new StringBuilder (str.Length * n)
  109. .Insert (0, str, n)
  110. .ToString ();
  111. }
  112. /// <summary>Converts the string into a <see cref="List{Rune}"/>.</summary>
  113. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  114. /// <param name="str">The string to convert.</param>
  115. /// <returns></returns>
  116. public static List<Rune> ToRuneList (this string str) { return str.EnumerateRunes ().ToList (); }
  117. /// <summary>Converts the string into a <see cref="Rune"/> array.</summary>
  118. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  119. /// <param name="str">The string to convert.</param>
  120. /// <returns></returns>
  121. public static Rune [] ToRunes (this string str) { return str.EnumerateRunes ().ToArray (); }
  122. /// <summary>Converts a <see cref="Rune"/> generic collection into a string.</summary>
  123. /// <param name="runes">The enumerable rune to convert.</param>
  124. /// <returns></returns>
  125. public static string ToString (IEnumerable<Rune> runes)
  126. {
  127. const int maxCharsPerRune = 2;
  128. const int maxStackallocTextBufferSize = 1048; // ~2 kB
  129. // If rune count is easily available use stackalloc buffer or alternatively rented array.
  130. if (runes.TryGetNonEnumeratedCount (out int count))
  131. {
  132. if (count == 0)
  133. {
  134. return string.Empty;
  135. }
  136. char[]? rentedBufferArray = null;
  137. try
  138. {
  139. int maxRequiredTextBufferSize = count * maxCharsPerRune;
  140. Span<char> textBuffer = maxRequiredTextBufferSize <= maxStackallocTextBufferSize
  141. ? stackalloc char[maxRequiredTextBufferSize]
  142. : (rentedBufferArray = ArrayPool<char>.Shared.Rent(maxRequiredTextBufferSize));
  143. Span<char> remainingBuffer = textBuffer;
  144. foreach (Rune rune in runes)
  145. {
  146. int charsWritten = rune.EncodeToUtf16 (remainingBuffer);
  147. remainingBuffer = remainingBuffer [charsWritten..];
  148. }
  149. ReadOnlySpan<char> text = textBuffer[..^remainingBuffer.Length];
  150. return text.ToString ();
  151. }
  152. finally
  153. {
  154. if (rentedBufferArray != null)
  155. {
  156. ArrayPool<char>.Shared.Return (rentedBufferArray);
  157. }
  158. }
  159. }
  160. // Fallback to StringBuilder append.
  161. StringBuilder stringBuilder = new();
  162. Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
  163. foreach (Rune rune in runes)
  164. {
  165. int charsWritten = rune.EncodeToUtf16 (runeBuffer);
  166. ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
  167. stringBuilder.Append (runeChars);
  168. }
  169. return stringBuilder.ToString ();
  170. }
  171. /// <summary>Converts a byte generic collection into a string in the provided encoding (default is UTF8)</summary>
  172. /// <param name="bytes">The enumerable byte to convert.</param>
  173. /// <param name="encoding">The encoding to be used.</param>
  174. /// <returns></returns>
  175. public static string ToString (IEnumerable<byte> bytes, Encoding? encoding = null)
  176. {
  177. encoding ??= Encoding.UTF8;
  178. return encoding.GetString (bytes.ToArray ());
  179. }
  180. }