0be34a9c16
- measure text with grapheme-aware terminal widths - add ansi-aware truncate slice and wrap checks
70 lines
3.8 KiB
C#
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));
|
|
}
|