From 080e6dc7379714f64450b81e88bc82e4d22c9640 Mon Sep 17 00:00:00 2001 From: upstream Date: Wed, 2 Sep 2026 09:44:11 +0200 Subject: [PATCH] Upstream charmbracelet/lipgloss state of the touched files before 1a8841c478 --- whitespace.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++ whitespace_test.go | 52 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 whitespace.go create mode 100644 whitespace_test.go diff --git a/whitespace.go b/whitespace.go new file mode 100644 index 0000000..e353a07 --- /dev/null +++ b/whitespace.go @@ -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 + } +} diff --git a/whitespace_test.go b/whitespace_test.go new file mode 100644 index 0000000..fdc1d09 --- /dev/null +++ b/whitespace_test.go @@ -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)) + } +}