Skip to content

Commit

Permalink
feat(ansi): add more tests for wcwidth and grapheme width methods
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Oct 22, 2024
1 parent e9757d9 commit 2c97f3a
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 204 deletions.
12 changes: 12 additions & 0 deletions ansi/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ const (
WcWidth Method = iota
GraphemeWidth
)

// String returns the string representation of the Method.
func (m Method) String() string {
switch m {
case WcWidth:
return "WcWidth"
case GraphemeWidth:
return "GraphemeWidth"
default:
return "Unknown"
}
}
91 changes: 52 additions & 39 deletions ansi/truncate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,63 @@ import (

// nolint
var tcases = []struct {
name string
input string
tail string
width int
expect string
name string
input string
tail string
width int
expect string
wcexpect string
}{
{"empty", "", "", 0, ""},
{"equalascii", "one", ".", 3, "one"},
{"equalemoji", "on👋", ".", 3, "on."},
{"equalcontrolemoji", "one\x1b[0m", ".", 3, "one\x1b[0m"},
{"truncate_tail_greater", "foo", "...", 5, "foo"},
{"simple", "foobar", "", 3, "foo"},
{"passthrough", "foobar", "", 10, "foobar"},
{"ascii", "hello", "", 3, "hel"},
{"emoji", "👋", "", 2, "👋"},
{"wideemoji", "🫧", "", 2, "🫧"},
{"controlemoji", "\x1b[31mhello 👋abc\x1b[0m", "", 8, "\x1b[31mhello 👋\x1b[0m"},
{"osc8", "\x1b]8;;https://charm.sh\x1b\\Charmbracelet 🫧\x1b]8;;\x1b\\", "", 5, "\x1b]8;;https://charm.sh\x1b\\Charm\x1b]8;;\x1b\\"},
{"osc8_8bit", "\x9d8;;https://charm.sh\x9cCharmbracelet 🫧\x9d8;;\x9c", "", 5, "\x9d8;;https://charm.sh\x9cCharm\x9d8;;\x9c"},
{"style_tail", "\x1B[38;5;219mHiya!", "…", 3, "\x1B[38;5;219mHi…"},
{"double_style_tail", "\x1B[38;5;219mHiya!\x1B[38;5;219mHello", "…", 7, "\x1B[38;5;219mHiya!\x1B[38;5;219mH…"},
{"noop", "\x1B[7m--", "", 2, "\x1B[7m--"},
{"double_width", "\x1B[38;2;249;38;114m你好\x1B[0m", "", 3, "\x1B[38;2;249;38;114m你\x1B[0m"},
{"double_width_rune", "你", "", 1, ""},
{"double_width_runes", "你好", "", 2, "你"},
{"spaces_only", " ", "…", 2, " …"},
{"longer_tail", "foo", "...", 2, ""},
{"same_tail_width", "foo", "...", 3, "foo"},
{"same_tail_width_control", "\x1b[31mfoo\x1b[0m", "...", 3, "\x1b[31mfoo\x1b[0m"},
{"same_width", "foo", "", 3, "foo"},
{"truncate_with_tail", "foobar", ".", 4, "foo."},
{"style", "I really \x1B[38;2;249;38;114mlove\x1B[0m Go!", "", 8, "I really\x1B[38;2;249;38;114m\x1B[0m"},
{"dcs", "\x1BPq#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~$#2??}}GG}}??}}??-#1!14@\x1B\\foobar", "…", 4, "\x1BPq#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~$#2??}}GG}}??}}??-#1!14@\x1B\\foo…"},
{"emoji_tail", "\x1b[36mHello there!\x1b[m", "😃", 8, "\x1b[36mHello 😃\x1b[m"},
{"unicode", "\x1b[35mClaire‘s Boutique\x1b[0m", "", 8, "\x1b[35mClaire‘s\x1b[0m"},
{"wide_chars", "こんにちは", "…", 7, "こんに…"},
{"style_wide_chars", "\x1b[35mこんにちは\x1b[m", "…", 7, "\x1b[35mこんに…\x1b[m"},
{"osc8_lf", "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\\nสวัสดีสวัสดี\x1b]8;;\x1b\\", "…", 9, "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\\n\x1b]8;;\x1b\\"},
{"empty", "", "", 0, "", ""},
{"equalascii", "one", ".", 3, "one", "one"},
{"equalemoji", "on👋", ".", 3, "on.", "on."},
{"equalcontrolemoji", "one\x1b[0m", ".", 3, "one\x1b[0m", "one\x1b[0m"},
{"truncate_tail_greater", "foo", "...", 5, "foo", "foo"},
{"simple", "foobar", "", 3, "foo", "foo"},
{"passthrough", "foobar", "", 10, "foobar", "foobar"},
{"ascii", "hello", "", 3, "hel", "hel"},
{"emoji", "👋", "", 2, "👋", "👋"},
{"wideemoji", "🫧", "", 2, "🫧", "🫧"},
{"composableemoji1", "👨🏾‍🌾", "", 2, "👨🏾‍🌾", ""},
{"composableemoji2", "👨🏾‍🌾", "", 6, "👨🏾‍🌾", "👨🏾‍🌾"},
{"controlemoji", "\x1b[31mhello 👋abc\x1b[0m", "", 8, "\x1b[31mhello 👋\x1b[0m", "\x1b[31mhello 👋\x1b[0m"},
{"osc8", "\x1b]8;;https://charm.sh\x1b\\Charmbracelet 🫧\x1b]8;;\x1b\\", "", 5, "\x1b]8;;https://charm.sh\x1b\\Charm\x1b]8;;\x1b\\", "\x1b]8;;https://charm.sh\x1b\\Charm\x1b]8;;\x1b\\"},
{"osc8_8bit", "\x9d8;;https://charm.sh\x9cCharmbracelet 🫧\x9d8;;\x9c", "", 5, "\x9d8;;https://charm.sh\x9cCharm\x9d8;;\x9c", "\x9d8;;https://charm.sh\x9cCharm\x9d8;;\x9c"},
{"style_tail", "\x1B[38;5;219mHiya!", "…", 3, "\x1B[38;5;219mHi…", "\x1B[38;5;219mHi…"},
{"double_style_tail", "\x1B[38;5;219mHiya!\x1B[38;5;219mHello", "…", 7, "\x1B[38;5;219mHiya!\x1B[38;5;219mH…", "\x1B[38;5;219mHiya!\x1B[38;5;219mH…"},
{"noop", "\x1B[7m--", "", 2, "\x1B[7m--", "\x1B[7m--"},
{"double_width", "\x1B[38;2;249;38;114m你好\x1B[0m", "", 3, "\x1B[38;2;249;38;114m你\x1B[0m", "\x1B[38;2;249;38;114m你\x1B[0m"},
{"double_width_rune", "你", "", 1, "", ""},
{"double_width_runes", "你好", "", 2, "你", "你"},
{"spaces_only", " ", "…", 2, " …", " …"},
{"longer_tail", "foo", "...", 2, "", ""},
{"same_tail_width", "foo", "...", 3, "foo", "foo"},
{"same_tail_width_control", "\x1b[31mfoo\x1b[0m", "...", 3, "\x1b[31mfoo\x1b[0m", "\x1b[31mfoo\x1b[0m"},
{"same_width", "foo", "", 3, "foo", "foo"},
{"truncate_with_tail", "foobar", ".", 4, "foo.", "foo."},
{"style", "I really \x1B[38;2;249;38;114mlove\x1B[0m Go!", "", 8, "I really\x1B[38;2;249;38;114m\x1B[0m", "I really\x1B[38;2;249;38;114m\x1B[0m"},
{"dcs", "\x1BPq#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~$#2??}}GG}}??}}??-#1!14@\x1B\\foobar", "…", 4, "\x1BPq#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~$#2??}}GG}}??}}??-#1!14@\x1B\\foo…", "\x1BPq#0;2;0;0;0#1;2;100;100;0#2;2;0;100;0#1~~@@vv@@~~@@~~$#2??}}GG}}??}}??-#1!14@\x1B\\foo…"},
{"emoji_tail", "\x1b[36mHello there!\x1b[m", "😃", 8, "\x1b[36mHello 😃\x1b[m", "\x1b[36mHello 😃\x1b[m"},
{"unicode", "\x1b[35mClaire‘s Boutique\x1b[0m", "", 8, "\x1b[35mClaire‘s\x1b[0m", "\x1b[35mClaire‘s\x1b[0m"},
{"wide_chars", "こんにちは", "…", 7, "こんに…", "こんに…"},
{"style_wide_chars", "\x1b[35mこんにちは\x1b[m", "…", 7, "\x1b[35mこんに…\x1b[m", "\x1b[35mこんに…\x1b[m"},
{"osc8_lf", "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\\nสวัสดีสวัสดี\x1b]8;;\x1b\\", "…", 9, "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\\n\x1b]8;;\x1b\\", "สวัสดีสวัสดี\x1b]8;;https://example.com\x1b\\\n\x1b]8;;\x1b\\"},
}

func TestTruncate(t *testing.T) {
func TestGraphemeTruncate(t *testing.T) {
for i, c := range tcases {
t.Run(c.name, func(t *testing.T) {
if result := Truncate(c.input, c.width, c.tail); result != c.expect {
if result := GraphemeWidth.Truncate(c.input, c.width, c.tail); result != c.expect {
t.Errorf("test case %d failed: expected %q, got %q", i+1, c.expect, result)
}
})
}
}

func TestWcTruncate(t *testing.T) {
for i, c := range tcases {
t.Run(c.name, func(t *testing.T) {
if result := WcWidth.Truncate(c.input, c.width, c.tail); result != c.wcexpect {
t.Errorf("test case %d failed: expected %q, got %q", i+1, c.expect, result)
}
})
Expand Down
57 changes: 34 additions & 23 deletions ansi/width_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,30 @@ var cases = []struct {
input string
stripped string
width int
wcwidth int
}{
{"empty", "", "", 0},
{"ascii", "hello", "hello", 5},
{"emoji", "👋", "👋", 2},
{"wideemoji", "🫧", "🫧", 2},
{"combining", "a\u0300", "à", 1},
{"control", "\x1b[31mhello\x1b[0m", "hello", 5},
{"csi8", "\x9b38;5;1mhello\x9bm", "hello", 5},
{"osc", "\x9d2;charmbracelet: ~/Source/bubbletea\x9c", "", 0},
{"controlemoji", "\x1b[31m👋\x1b[0m", "👋", 2},
{"oscwideemoji", "\x1b]2;title👨‍👩‍👦\x07", "", 0},
{"oscwideemoji", "\x1b[31m👨‍👩‍👦\x1b[m", "👨\u200d👩\u200d👦", 2},
{"multiemojicsi", "👨‍👩‍👦\x9b38;5;1mhello\x9bm", "👨‍👩‍👦hello", 7},
{"osc8eastasianlink", "\x9d8;id=1;https://example.com/\x9c打豆豆\x9d8;id=1;\x07", "打豆豆", 6},
{"dcsarabic", "\x1bP?123$pسلام\x1b\\اهلا", "اهلا", 4},
{"newline", "hello\nworld", "hello\nworld", 10},
{"tab", "hello\tworld", "hello\tworld", 10},
{"controlnewline", "\x1b[31mhello\x1b[0m\nworld", "hello\nworld", 10},
{"style", "\x1B[38;2;249;38;114mfoo", "foo", 3},
{"unicode", "\x1b[35m“box”\x1b[0m", "“box”", 5},
{"just_unicode", "Claire’s Boutique", "Claire’s Boutique", 17},
{"unclosed_ansi", "Hey, \x1b[7m\n猴", "Hey, \n猴", 7},
{"double_asian_runes", " 你\x1b[8m好.", " 你好.", 6},
{"empty", "", "", 0, 0},
{"ascii", "hello", "hello", 5, 5},
{"emoji", "👋", "👋", 2, 2},
{"wideemoji", "🫧", "🫧", 2, 2},
{"combining", "a\u0300", "à", 1, 1},
{"control", "\x1b[31mhello\x1b[0m", "hello", 5, 5},
{"csi8", "\x9b38;5;1mhello\x9bm", "hello", 5, 5},
{"osc", "\x9d2;charmbracelet: ~/Source/bubbletea\x9c", "", 0, 0},
{"controlemoji", "\x1b[31m👋\x1b[0m", "👋", 2, 2},
{"oscwideemoji", "\x1b]2;title👨‍👩‍👦\x07", "", 0, 0},
{"oscmultiemoji", "\x1b[31m👨‍👩‍👦\x1b[m", "👨\u200d👩\u200d👦", 2, 6},
{"multiemojicsi", "👨‍👩‍👦\x9b38;5;1mhello\x9bm", "👨‍👩‍👦hello", 7, 11},
{"osc8eastasianlink", "\x9d8;id=1;https://example.com/\x9c打豆豆\x9d8;id=1;\x07", "打豆豆", 6, 6},
{"dcsarabic", "\x1bP?123$pسلام\x1b\\اهلا", "اهلا", 4, 4},
{"newline", "hello\nworld", "hello\nworld", 10, 10},
{"tab", "hello\tworld", "hello\tworld", 10, 10},
{"controlnewline", "\x1b[31mhello\x1b[0m\nworld", "hello\nworld", 10, 10},
{"style", "\x1B[38;2;249;38;114mfoo", "foo", 3, 3},
{"unicode", "\x1b[35m“box”\x1b[0m", "“box”", 5, 5},
{"just_unicode", "Claire’s Boutique", "Claire’s Boutique", 17, 17},
{"unclosed_ansi", "Hey, \x1b[7m\n猴", "Hey, \n猴", 7, 7},
{"double_asian_runes", " 你\x1b[8m好.", " 你好.", 6, 6},
}

func TestStrip(t *testing.T) {
Expand All @@ -44,7 +45,7 @@ func TestStrip(t *testing.T) {
}
}

func TestStringWidth(t *testing.T) {
func TestGraphemeStringWidth(t *testing.T) {
for i, c := range cases {
t.Run(c.name, func(t *testing.T) {
if width := GraphemeWidth.StringWidth(c.input); width != c.width {
Expand All @@ -54,6 +55,16 @@ func TestStringWidth(t *testing.T) {
}
}

func TestWcStringWidth(t *testing.T) {
for i, c := range cases {
t.Run(c.name, func(t *testing.T) {
if width := WcWidth.StringWidth(c.input); width != c.wcwidth {
t.Errorf("test case %d failed: expected %d, got %d", i+1, c.width, width)
}
})
}
}

func BenchmarkStringWidth(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
b.ReportAllocs()
Expand Down
20 changes: 8 additions & 12 deletions ansi/wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func (m Method) Hardwrap(s string, limit int, preserveSpace bool) string {
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
i += len(cluster)

if m == WcWidth {
width = wcwidth.StringWidth(string(cluster))
}
if curWidth+width > limit {
addNewline()
}
Expand All @@ -66,12 +69,7 @@ func (m Method) Hardwrap(s string, limit int, preserveSpace bool) string {
}

buf.Write(cluster)
switch m {
case WcWidth:
curWidth += wcwidth.StringWidth(string(cluster))
case GraphemeWidth:
curWidth += width
}
curWidth += width
pstate = parser.GroundState
continue
}
Expand Down Expand Up @@ -180,6 +178,9 @@ func (m Method) Wordwrap(s string, limit int, breakpoints string) string {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
i += len(cluster)
if m == WcWidth {
width = wcwidth.StringWidth(string(cluster))
}

r, _ := utf8.DecodeRune(cluster)
if r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp {
Expand All @@ -192,12 +193,7 @@ func (m Method) Wordwrap(s string, limit int, breakpoints string) string {
curWidth++
} else {
word.Write(cluster)
switch m {
case WcWidth:
wordLen += wcwidth.StringWidth(string(cluster))
case GraphemeWidth:
wordLen += width
}
wordLen += width
if curWidth+space.Len()+wordLen > limit &&
wordLen < limit {
addNewline()
Expand Down
Loading

0 comments on commit 2c97f3a

Please sign in to comment.