fix(whitespace): prevent wide runes from overflowing render width
whitespace.render checked the loop bound only before writing a rune, then advanced by the rune width. A rune wider than the remaining space was still written, overshooting the requested width by up to runeWidth-1 cells, and this made the trailing space-fill unreachable. Stop before a wide rune would overflow and let the existing space-fill pad the remaining cells, so the result is always exactly the requested width. This also fixes overflow surfaced through Place/PlaceHorizontal with WithWhitespaceChars for multi-cell whitespace characters.
This commit is contained in:
1 parent
080e6dc737
commit
4e196d82d8
2 files changed
+34
-5
No files matched your search
+10
-5
@@ -33,14 +33,19 @@ func (w whitespace) render(width int) string {
|
||||
|
||||
// Cycle through runes and print them into the whitespace.
|
||||
for i := 0; i < width; {
|
||||
b.WriteRune(r[j])
|
||||
// Measure the width of the rune we just wrote, ensuring we always
|
||||
// make progress to avoid infinite loops with zero-width characters
|
||||
// like tabs.
|
||||
// Measure the width of the current rune, ensuring we always make
|
||||
// progress to avoid infinite loops with zero-width characters like
|
||||
// tabs.
|
||||
runeWidth := ansi.StringWidth(string(r[j]))
|
||||
if runeWidth < 1 {
|
||||
runeWidth = 1
|
||||
}
|
||||
// Stop before a wide rune would overflow the requested width; the
|
||||
// remaining cells are filled with spaces below.
|
||||
if i+runeWidth > width {
|
||||
break
|
||||
}
|
||||
b.WriteRune(r[j])
|
||||
i += runeWidth
|
||||
j++
|
||||
if j >= len(r) {
|
||||
@@ -48,7 +53,7 @@ func (w whitespace) render(width int) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Fill any extra gaps white spaces. This might be necessary if any runes
|
||||
// Fill any extra gaps with spaces. This might be necessary if any runes
|
||||
// are more than one cell wide, which could leave a one-rune gap.
|
||||
short := width - ansi.StringWidth(b.String())
|
||||
if short > 0 {
|
||||
|
||||
@@ -3,6 +3,8 @@ package lipgloss
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
func TestWhitespaceRenderWithTab(t *testing.T) {
|
||||
@@ -50,3 +52,25 @@ func TestWhitespaceRenderNormal(t *testing.T) {
|
||||
t.Errorf("expected 5 characters, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitespaceRenderWideRune(t *testing.T) {
|
||||
// A whitespace char that is more than one cell wide must never render
|
||||
// wider than the requested width; the trailing cells are padded with
|
||||
// spaces instead of overflowing.
|
||||
ws := newWhitespace(WithWhitespaceChars("東")) // width 2
|
||||
for _, width := range []int{0, 1, 2, 3, 4, 5} {
|
||||
got := ansi.StringWidth(ws.render(width))
|
||||
if got != width {
|
||||
t.Errorf("render(%d): expected rendered width %d, got %d", width, width, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceHorizontalWideWhitespace(t *testing.T) {
|
||||
// Placing content in a box with a wide whitespace char must keep the
|
||||
// box exactly the requested width.
|
||||
got := PlaceHorizontal(5, Left, "ab", WithWhitespaceChars("東"))
|
||||
if w := ansi.StringWidth(got); w != 5 {
|
||||
t.Errorf("PlaceHorizontal width: expected 5, got %d (%q)", w, got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user