Files
ttui/test/TinyTUI.TextChecks/Program.cs
T
chuan 0be34a9c16 feat: improve ansi text utilities
- measure text with grapheme-aware terminal widths

- add ansi-aware truncate slice and wrap checks
2026-06-04 02:10:46 +08:00

70 lines
3.8 KiB
C#

using TinyTUI.Text;
var measurer = new TerminalTextMeasurer();
AssertEqual(5, measurer.GetWidth("\t\e[31m界\e[0m"), "tab and ansi width");
AssertEqual(2, measurer.GetWidth("🇨"), "single regional indicator width");
AssertEqual(2, measurer.GetWidth("🇨🇳"), "regional indicator pair width");
AssertEqual(2, measurer.GetWidth("👨‍💻"), "zwj emoji width");
AssertEqual(2, measurer.GetWidth("⚡️"), "variation selector emoji width");
var strictTabSlice = measurer.Slice("out 192M\t.pi/skill-tests/results-ha", 0, 10, strict: true);
AssertEqual("out 192M", strictTabSlice.Text, "strict tab slice text");
AssertEqual(8, strictTabSlice.Width, "strict tab slice width");
AssertEqual(measurer.GetWidth(strictTabSlice.Text), strictTabSlice.Width, "strict tab slice measured width");
var afterTabSlice = measurer.Slice("out 192M\t.pi/skill-tests/results-ha", 13, 10, strict: true);
AssertEqual("i/skill-te", afterTabSlice.Text, "slice after tab text");
AssertEqual(measurer.GetWidth(afterTabSlice.Text), afterTabSlice.Width, "slice after tab measured width");
var ansiText = $"\e[31m{"hello ".Repeat(100)}\e[0m";
var truncated = measurer.Truncate(ansiText, 20, "…");
AssertTrue(measurer.GetWidth(truncated) <= 20, "ansi truncate width");
AssertTrue(truncated.Contains("\e[31m", StringComparison.Ordinal), "ansi truncate keeps style prefix");
AssertTrue(truncated.EndsWith("\e[0m…\e[0m", StringComparison.Ordinal), "ansi truncate brackets ellipsis");
var wideEllipsis = measurer.Truncate("abcdef", 2, "🙂");
AssertEqual("\e[0m🙂\e[0m", wideEllipsis, "wide ellipsis clipping");
AssertEqual(string.Empty, measurer.Truncate("abcdef", 1, "🙂"), "wide ellipsis too narrow");
var plainWrapped = measurer.Wrap("hello world this is a test", 10);
AssertTrue(plainWrapped.Count > 1, "plain wrap line count");
AssertTrue(plainWrapped.All(line => measurer.GetWidth(line) <= 10), "plain wrap width");
var redWrapped = measurer.Wrap($"\e[31mhello world this is red\e[0m", 10);
AssertTrue(redWrapped.Skip(1).All(line => line.StartsWith("\e[31m", StringComparison.Ordinal)), "ansi wrap restores red");
AssertTrue(redWrapped.Take(redWrapped.Count - 1).All(line => !line.EndsWith("\e[0m", StringComparison.Ordinal)), "ansi wrap avoids full reset before final");
var underlinedUrl = measurer.Wrap($"read this thread \e[4mhttps://example.com/very/long/path/that/will/definitely/wrap\e[24m", 40);
AssertEqual("read this thread", underlinedUrl[0], "underline wrap keeps prefix unstyled");
AssertTrue(underlinedUrl[1].StartsWith("\e[4m", StringComparison.Ordinal), "underline wrap starts style on url line");
AssertTrue(underlinedUrl.Take(underlinedUrl.Count - 1).Any(line => line.EndsWith("\e[24m", StringComparison.Ordinal)), "underline wrap closes line");
var hyperlink = "\e]8;;https://example.com\a0123456789\e]8;;\a";
var hyperlinkWrapped = measurer.Wrap(hyperlink, 6);
AssertTrue(hyperlinkWrapped.Count > 1, "osc8 wrap line count");
AssertTrue(hyperlinkWrapped.All(line => line.Contains("\e]8;;https://example.com\a", StringComparison.Ordinal)), "osc8 wrap reopens hyperlink with bel");
AssertTrue(hyperlinkWrapped.Take(hyperlinkWrapped.Count - 1).All(line => line.EndsWith("\e]8;;\a", StringComparison.Ordinal)), "osc8 wrap closes hyperlink with bel");
Console.WriteLine("TinyTUI text checks passed");
static void AssertEqual<T>(T expected, T actual, string name)
{
if (!EqualityComparer<T>.Default.Equals(expected, actual))
throw new InvalidOperationException($"{name}: expected '{expected}', actual '{actual}'");
}
static void AssertTrue(bool condition, string name)
{
if (!condition)
throw new InvalidOperationException($"{name}: assertion failed");
}
internal static class StringExtensions
{
/// <summary>
/// 重复字符串用于构造长文本测试输入
/// </summary>
public static string Repeat(this string value, int count) => string.Concat(Enumerable.Repeat(value, count));
}