From d726498ddd0bb15a4b35d12759ced316266bfa1e Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Thu, 28 Mar 2024 10:21:30 -0400 Subject: [PATCH] fix: combining both conditional and unconditional wrapping Fixes: https://github.com/muesli/reflow/issues/43 --- style.go | 3 +-- wrap.go | 46 ++++++++++++++++++++++++++++++++++++++++++++ wrap_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 wrap.go create mode 100644 wrap_test.go diff --git a/style.go b/style.go index 43d1144a..752726a9 100644 --- a/style.go +++ b/style.go @@ -308,8 +308,7 @@ func (s Style) Render(strs ...string) string { // Word wrap if !inline && width > 0 { wrapAt := width - leftPadding - rightPadding - str = ansi.Wordwrap(str, wrapAt, "") - str = ansi.Wrap(str, wrapAt, false) // force-wrap long strings + str = wrap(str, wrapAt) } // Render core text diff --git a/wrap.go b/wrap.go new file mode 100644 index 00000000..e0572bbd --- /dev/null +++ b/wrap.go @@ -0,0 +1,46 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" +) + +// wrap wraps a string to a given width. It will break the string at spaces +// and hyphens, and will remove any leading or trailing whitespace from the +// wrapped lines. +func wrap(str string, width int) string { + wrapped := ansi.Wordwrap(str, width, "") + lines := strings.Split(wrapped, "\n") + for i := 0; i < len(lines); i++ { + line := lines[i] + linew := ansi.StringWidth(line) + if linew <= width { + continue + } + + wline := ansi.Wordwrap(line, width, "") + wlines := strings.Split(wline, "\n") + for j := 0; j < len(wlines); j++ { + if ansi.StringWidth(wlines[j]) > width { + wline = ansi.Wrap(line, width, false) + wlines = strings.Split(wline, "\n") + break + } + } + + if len(wlines) > 0 { + lines[i] = wlines[0] + } + + if len(wlines) > 1 && i+1 < len(lines) { + endsWithHyphen := strings.HasSuffix(wlines[1], "-") + if endsWithHyphen { + lines[i+1] = wlines[1] + lines[i+1] + } else { + lines[i+1] = wlines[1] + " " + lines[i+1] + } + } + } + return strings.Join(lines, "\n") +} diff --git a/wrap_test.go b/wrap_test.go new file mode 100644 index 00000000..d7efd7e1 --- /dev/null +++ b/wrap_test.go @@ -0,0 +1,54 @@ +package lipgloss + +import ( + "testing" +) + +var cases = []struct { + name string + input string + expected string + width int +}{ + { + name: "simple", + input: "I really \x1B[38;2;249;38;114mlove\x1B[0m Go!", + expected: "I really\n\x1B[38;2;249;38;114mlove\x1B[0m Go!", + width: 8, + }, + { + name: "passthrough", + input: "hello world", + expected: "hello world", + width: 11, + }, + { + name: "longer", + input: "the quick brown foxxxxxxxxxxxxxxxx jumped over the lazy dog.", + expected: "the quick brown\nfoxxxxxxxxxxxxxx\nxx jumped over\nthe lazy dog.", + width: 16, + }, + { + name: "long input", + input: "Rotated keys for a-good-offensive-cheat-code-incorporated/animal-like-law-on-the-rocks.", + expected: "Rotated keys for a-good-offensive-cheat-code-incorporated/animal-like-law-on\n-the-rocks.", + width: 76, + }, + { + name: "long input2", + input: "Rotated keys for a-good-offensive-cheat-code-incorporated/crypto-line-operating-system.", + expected: "Rotated keys for a-good-offensive-cheat-code-incorporated/crypto-line-operat\ning-system.", + width: 76, + }, +} + +func TestWrapWordwrap(t *testing.T) { + for i, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + output := wrap(tc.input, tc.width) + if output != tc.expected { + t.Errorf("case %d, expected %q, got %q", i+1, tc.expected, output) + } + }) + } +}