Upstream charmbracelet/lipgloss state of the touched files before 1a8841c478
This commit is contained in:
commit
080e6dc737
2 files changed
+128
No files matched your search
@@ -0,0 +1,76 @@
|
||||
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; {
|
||||
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.
|
||||
runeWidth := ansi.StringWidth(string(r[j]))
|
||||
if runeWidth < 1 {
|
||||
runeWidth = 1
|
||||
}
|
||||
i += runeWidth
|
||||
j++
|
||||
if j >= len(r) {
|
||||
j = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Fill any extra gaps white 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package lipgloss
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWhitespaceRenderWithTab(t *testing.T) {
|
||||
// This test verifies that rendering whitespace with tab characters
|
||||
// doesn't cause an infinite loop (issue #108)
|
||||
done := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
ws := newWhitespace(WithWhitespaceChars("\t"))
|
||||
_ = ws.render(10)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Success - render completed
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("whitespace.render() with tab character caused infinite loop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitespaceRenderWithZeroWidthChar(t *testing.T) {
|
||||
// Test with zero-width joiner (another zero-width character)
|
||||
done := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
ws := newWhitespace(WithWhitespaceChars("\u200d")) // zero-width joiner
|
||||
_ = ws.render(5)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Success
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("whitespace.render() with zero-width character caused infinite loop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitespaceRenderNormal(t *testing.T) {
|
||||
// Verify normal behavior still works
|
||||
ws := newWhitespace(WithWhitespaceChars("*"))
|
||||
result := ws.render(5)
|
||||
if len(result) != 5 {
|
||||
t.Errorf("expected 5 characters, got %d", len(result))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user