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.
82 lines
1.8 KiB
Go
82 lines
1.8 KiB
Go
package lipgloss
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/x/ansi"
|
|
)
|
|
|
|
// whitespace is a whitespace renderer.
|
|
type whitespace struct {
|
|
chars string
|
|
style Style
|
|
}
|
|
|
|
// newWhitespace creates a new whitespace renderer.
|
|
func newWhitespace(opts ...WhitespaceOption) *whitespace {
|
|
w := &whitespace{}
|
|
for _, opt := range opts {
|
|
opt(w)
|
|
}
|
|
return w
|
|
}
|
|
|
|
// Render whitespaces.
|
|
func (w whitespace) render(width int) string {
|
|
if w.chars == "" {
|
|
w.chars = " "
|
|
}
|
|
|
|
r := []rune(w.chars)
|
|
j := 0
|
|
b := strings.Builder{}
|
|
|
|
// Cycle through runes and print them into the whitespace.
|
|
for i := 0; i < width; {
|
|
// 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) {
|
|
j = 0
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
b.WriteString(strings.Repeat(" ", short))
|
|
}
|
|
|
|
return w.style.Render(b.String())
|
|
}
|
|
|
|
// WhitespaceOption sets a styling rule for rendering whitespace.
|
|
type WhitespaceOption func(*whitespace)
|
|
|
|
// WithWhitespaceStyle sets the style for the whitespace.
|
|
func WithWhitespaceStyle(s Style) WhitespaceOption {
|
|
return func(w *whitespace) {
|
|
w.style = s
|
|
}
|
|
}
|
|
|
|
// WithWhitespaceChars sets the characters to be rendered in the whitespace.
|
|
func WithWhitespaceChars(s string) WhitespaceOption {
|
|
return func(w *whitespace) {
|
|
w.chars = s
|
|
}
|
|
}
|