From d354842a40db7ef94c68b35b3570f419d0b85e4a Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 27 Nov 2023 14:33:38 -0500 Subject: [PATCH] feat: Style.Transform for altering strings at render time (#232) * feat: Style.Transform for altering strings at render time * feat: add `UnsetTransform` --------- Co-authored-by: Maas Lalani --- get.go | 17 +++++++++++++++++ set.go | 12 ++++++++++++ style.go | 8 ++++++++ style_test.go | 37 +++++++++++++++++++++++++++++++++++++ unset.go | 6 ++++++ 5 files changed, 80 insertions(+) diff --git a/get.go b/get.go index d2623c4..9be3d64 100644 --- a/get.go +++ b/get.go @@ -408,6 +408,12 @@ func (s Style) GetFrameSize() (x, y int) { return s.GetHorizontalFrameSize(), s.GetVerticalFrameSize() } +// GetTransform returns the transform set on the style. If no transform is set +// nil is returned. +func (s Style) GetTransform() func(string) string { + return s.getAsTransform(transformKey) +} + // Returns whether or not the given property is set. func (s Style) isSet(k propKey) bool { _, exists := s.rules[k] @@ -469,6 +475,17 @@ func (s Style) getBorderStyle() Border { return noBorder } +func (s Style) getAsTransform(k propKey) func(string) string { + v, ok := s.rules[k] + if !ok { + return nil + } + if fn, ok := v.(func(string) string); ok { + return fn + } + return nil +} + // Split a string into lines, additionally returning the size of the widest // line. func getLines(s string) (lines []string, widest int) { diff --git a/set.go b/set.go index 5483f2d..5846b59 100644 --- a/set.go +++ b/set.go @@ -544,6 +544,18 @@ func (s Style) StrikethroughSpaces(v bool) Style { return s } +// Transform applies a given function to a string at render time, allowing for +// the string being rendered to be manipuated. +// +// Example: +// +// s := NewStyle().Transform(strings.ToUpper) +// fmt.Println(s.Render("raow!") // "RAOW!" +func (s Style) Transform(fn func(string) string) Style { + s.set(transformKey, fn) + return s +} + // Renderer sets the renderer for the style. This is useful for changing the // renderer for a style that is being used in a different context. func (s Style) Renderer(r *Renderer) Style { diff --git a/style.go b/style.go index ee50bcf..72be9ef 100644 --- a/style.go +++ b/style.go @@ -73,6 +73,8 @@ const ( tabWidthKey underlineSpacesKey strikethroughSpacesKey + + transformKey ) // A set of properties. @@ -225,6 +227,8 @@ func (s Style) Render(strs ...string) string { // Do we need to style spaces separately? useSpaceStyler = underlineSpaces || strikethroughSpaces + + transform = s.getAsTransform(transformKey) ) if len(s.rules) == 0 { @@ -401,6 +405,10 @@ func (s Style) Render(strs ...string) string { str = strings.Join(lines[:min(maxHeight, len(lines))], "\n") } + if transform != nil { + return transform(str) + } + return str } diff --git a/style_test.go b/style_test.go index 9cb2a9a..a5148db 100644 --- a/style_test.go +++ b/style_test.go @@ -3,6 +3,7 @@ package lipgloss import ( "io" "reflect" + "strings" "testing" "github.com/muesli/termenv" @@ -374,6 +375,42 @@ func TestTabConversion(t *testing.T) { requireEqual(t, "[\t]", s.Render("[\t]")) } +func TestStringTransform(t *testing.T) { + for i, tc := range []struct { + input string + fn func(string) string + expected string + }{ + { + "raow", + strings.ToUpper, + "RAOW", + }, + { + "The quick brown 狐 jumped over the lazy 犬", + func(s string) string { + n := 0 + rune := make([]rune, len(s)) + for _, r := range s { + rune[n] = r + n++ + } + rune = rune[0:n] + for i := 0; i < n/2; i++ { + rune[i], rune[n-1-i] = rune[n-1-i], rune[i] + } + return string(rune) + }, + "犬 yzal eht revo depmuj 狐 nworb kciuq ehT", + }, + } { + res := NewStyle().Transform(tc.fn).Render(tc.input) + if res != tc.expected { + t.Errorf("Test #%d:\nExpected: %q\nGot: %q", i+1, tc.expected, res) + } + } +} + func BenchmarkStyleRender(b *testing.B) { s := NewStyle(). Bold(true). diff --git a/unset.go b/unset.go index 770734c..5387bcf 100644 --- a/unset.go +++ b/unset.go @@ -305,6 +305,12 @@ func (s Style) UnsetStrikethroughSpaces() Style { return s } +// UnsetTransform removes the value set by Transform. +func (s Style) UnsetTransform() Style { + delete(s.rules, transformKey) + return s +} + // UnsetString sets the underlying string value to the empty string. func (s Style) UnsetString() Style { s.value = ""