StringExtensions.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. using System.Buffers;
  2. namespace Terminal.Gui.Text;
  3. /// <summary>Extensions to <see cref="string"/> to support TUI text manipulation.</summary>
  4. public static class StringExtensions
  5. {
  6. /// <summary>Unpacks the last UTF-8 encoding in the string.</summary>
  7. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  8. /// <param name="str">The string to decode.</param>
  9. /// <param name="end">Index in string to stop at; if -1, use the buffer length.</param>
  10. /// <returns></returns>
  11. public static (Rune rune, int size) DecodeLastRune (this string str, int end = -1)
  12. {
  13. Rune rune = str.EnumerateRunes ().ToArray () [end == -1 ? ^1 : end];
  14. byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
  15. OperationStatus operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed);
  16. if (operationStatus == OperationStatus.Done)
  17. {
  18. return (rune, bytesConsumed);
  19. }
  20. return (Rune.ReplacementChar, 1);
  21. }
  22. /// <summary>Unpacks the first UTF-8 encoding in the string and returns the rune and its width in bytes.</summary>
  23. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  24. /// <param name="str">The string to decode.</param>
  25. /// <param name="start">Starting offset.</param>
  26. /// <param name="count">Number of bytes in the buffer, or -1 to make it the length of the buffer.</param>
  27. /// <returns></returns>
  28. public static (Rune Rune, int Size) DecodeRune (this string str, int start = 0, int count = -1)
  29. {
  30. Rune rune = str.EnumerateRunes ().ToArray () [start];
  31. byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
  32. if (count == -1)
  33. {
  34. count = bytes.Length;
  35. }
  36. OperationStatus operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed);
  37. if (operationStatus == OperationStatus.Done && bytesConsumed >= count)
  38. {
  39. return (rune, bytesConsumed);
  40. }
  41. return (Rune.ReplacementChar, 1);
  42. }
  43. /// <summary>Gets the number of columns the string occupies in the terminal.</summary>
  44. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  45. /// <param name="str">The string to measure.</param>
  46. /// <param name="ignoreLessThanZero">Indicates whether to ignore values ​​less than zero, such as control keys.</param>
  47. /// <returns></returns>
  48. public static int GetColumns (this string str, bool ignoreLessThanZero = true)
  49. {
  50. if (string.IsNullOrEmpty (str))
  51. {
  52. return 0;
  53. }
  54. var total = 0;
  55. foreach (string grapheme in GraphemeHelper.GetGraphemes (str))
  56. {
  57. // Get the maximum rune width within this grapheme cluster
  58. int clusterWidth = grapheme.EnumerateRunes ()
  59. .Sum (r =>
  60. {
  61. int w = r.GetColumns ();
  62. return ignoreLessThanZero && w < 0 ? 0 : w;
  63. });
  64. // Clamp to realistic max display width
  65. if (clusterWidth > 2)
  66. {
  67. clusterWidth = 2;
  68. }
  69. total += clusterWidth;
  70. }
  71. return total;
  72. }
  73. /// <summary>Gets the number of runes in the string.</summary>
  74. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  75. /// <param name="str">The string to count.</param>
  76. /// <returns></returns>
  77. public static int GetRuneCount (this string str) { return str.EnumerateRunes ().Count (); }
  78. /// <summary>
  79. /// Determines if this <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> is composed entirely of ASCII
  80. /// digits.
  81. /// </summary>
  82. /// <param name="stringSpan">A <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to check.</param>
  83. /// <returns>
  84. /// A <see langword="bool"/> indicating if all elements of the <see cref="ReadOnlySpan{T}"/> are ASCII digits (
  85. /// <see langword="true"/>) or not (<see langword="false"/>
  86. /// </returns>
  87. public static bool IsAllAsciiDigits (this ReadOnlySpan<char> stringSpan) { return !stringSpan.IsEmpty && stringSpan.ToString ().All (char.IsAsciiDigit); }
  88. /// <summary>
  89. /// Determines if this <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> is composed entirely of ASCII
  90. /// digits.
  91. /// </summary>
  92. /// <param name="stringSpan">A <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to check.</param>
  93. /// <returns>
  94. /// A <see langword="bool"/> indicating if all elements of the <see cref="ReadOnlySpan{T}"/> are ASCII digits (
  95. /// <see langword="true"/>) or not (<see langword="false"/>
  96. /// </returns>
  97. public static bool IsAllAsciiHexDigits (this ReadOnlySpan<char> stringSpan) { return !stringSpan.IsEmpty && stringSpan.ToString ().All (char.IsAsciiHexDigit); }
  98. /// <summary>Repeats the string <paramref name="n"/> times.</summary>
  99. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  100. /// <param name="str">The text to repeat.</param>
  101. /// <param name="n">Number of times to repeat the text.</param>
  102. /// <returns>The text repeated if <paramref name="n"/> is greater than zero, otherwise <see langword="null"/>.</returns>
  103. public static string? Repeat (this string str, int n)
  104. {
  105. if (n <= 0)
  106. {
  107. return null;
  108. }
  109. if (string.IsNullOrEmpty (str) || n == 1)
  110. {
  111. return str;
  112. }
  113. return new StringBuilder (str.Length * n)
  114. .Insert (0, str, n)
  115. .ToString ();
  116. }
  117. /// <summary>Converts the string into a <see cref="List{Rune}"/>.</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 List<Rune> ToRuneList (this string str) { return str.EnumerateRunes ().ToList (); }
  122. /// <summary>Converts the string into a <see cref="Rune"/> array.</summary>
  123. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  124. /// <param name="str">The string to convert.</param>
  125. /// <returns></returns>
  126. public static Rune [] ToRunes (this string str) { return str.EnumerateRunes ().ToArray (); }
  127. /// <summary>Converts a <see cref="Rune"/> generic collection into a string.</summary>
  128. /// <param name="runes">The enumerable rune to convert.</param>
  129. /// <returns></returns>
  130. public static string ToString (IEnumerable<Rune> runes)
  131. {
  132. const int maxCharsPerRune = 2;
  133. const int maxStackallocTextBufferSize = 1048; // ~2 kB
  134. // If rune count is easily available use stackalloc buffer or alternatively rented array.
  135. if (runes.TryGetNonEnumeratedCount (out int count))
  136. {
  137. if (count == 0)
  138. {
  139. return string.Empty;
  140. }
  141. char[]? rentedBufferArray = null;
  142. try
  143. {
  144. int maxRequiredTextBufferSize = count * maxCharsPerRune;
  145. Span<char> textBuffer = maxRequiredTextBufferSize <= maxStackallocTextBufferSize
  146. ? stackalloc char[maxRequiredTextBufferSize]
  147. : (rentedBufferArray = ArrayPool<char>.Shared.Rent(maxRequiredTextBufferSize));
  148. Span<char> remainingBuffer = textBuffer;
  149. foreach (Rune rune in runes)
  150. {
  151. int charsWritten = rune.EncodeToUtf16 (remainingBuffer);
  152. remainingBuffer = remainingBuffer [charsWritten..];
  153. }
  154. ReadOnlySpan<char> text = textBuffer[..^remainingBuffer.Length];
  155. return text.ToString ();
  156. }
  157. finally
  158. {
  159. if (rentedBufferArray != null)
  160. {
  161. ArrayPool<char>.Shared.Return (rentedBufferArray);
  162. }
  163. }
  164. }
  165. // Fallback to StringBuilder append.
  166. StringBuilder stringBuilder = new();
  167. Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
  168. foreach (Rune rune in runes)
  169. {
  170. int charsWritten = rune.EncodeToUtf16 (runeBuffer);
  171. ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
  172. stringBuilder.Append (runeChars);
  173. }
  174. return stringBuilder.ToString ();
  175. }
  176. /// <summary>Converts a byte generic collection into a string in the provided encoding (default is UTF8)</summary>
  177. /// <param name="bytes">The enumerable byte to convert.</param>
  178. /// <param name="encoding">The encoding to be used.</param>
  179. /// <returns></returns>
  180. public static string ToString (IEnumerable<byte> bytes, Encoding? encoding = null)
  181. {
  182. encoding ??= Encoding.UTF8;
  183. return encoding.GetString (bytes.ToArray ());
  184. }
  185. /// <summary>Converts a <see cref="string"/> generic collection into a string.</summary>
  186. /// <param name="strings">The enumerable string to convert.</param>
  187. /// <returns></returns>
  188. public static string ToString (IEnumerable<string> strings) { return string.Concat (strings); }
  189. /// <summary>Converts the string into a <see cref="List{String}"/>.</summary>
  190. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  191. /// <param name="str">The string to convert.</param>
  192. /// <returns></returns>
  193. public static List<string> ToStringList (this string str)
  194. {
  195. List<string> strings = [];
  196. foreach (string grapheme in GraphemeHelper.GetGraphemes (str))
  197. {
  198. strings.Add (grapheme);
  199. }
  200. return strings;
  201. }
  202. /// <summary>Reports whether a string is a surrogate code point.</summary>
  203. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  204. /// <param name="str">The string to probe.</param>
  205. /// <returns><see langword="true"/> if the string is a surrogate code point; <see langword="false"/> otherwise.</returns>
  206. public static bool IsSurrogatePair (this string str)
  207. {
  208. if (str.Length != 2)
  209. {
  210. return false;
  211. }
  212. Rune rune = Rune.GetRuneAt (str, 0);
  213. return rune.IsSurrogatePair ();
  214. }
  215. /// <summary>
  216. /// Ensures the text is not a control character and can be displayed by translating characters below 0x20 to
  217. /// equivalent, printable, Unicode chars.
  218. /// </summary>
  219. /// <remarks>This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.</remarks>
  220. /// <param name="str">The text.</param>
  221. /// <returns></returns>
  222. public static string MakePrintable (this string str)
  223. {
  224. if (str.Length > 1)
  225. {
  226. return str;
  227. }
  228. char ch = str [0];
  229. return char.IsControl (ch) ? new ((char)(ch + 0x2400), 1) : str;
  230. }
  231. }