feat: convert tabs to spaces with Style.TabWidth(int) (#204)

* feat: convert tabs to spaces with Style.TabWidth(int)

By default tabs will be converted to 4 spaces. To disable tab
conversion set Style.TabWidth(NoTabConversion).
This commit is contained in:
Christian Rocha 2023-07-24 11:22:48 -04:00 committed by GitHub
parent 233079e2d9
commit df8b3fa1f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 79 additions and 4 deletions

6
get.go
View File

@ -370,6 +370,12 @@ func (s Style) GetMaxHeight() int {
return s.getAsInt(maxHeightKey)
}
// GetTabWidth returns the style's tab width setting. If no value is set 4 is
// returned which is the implicit default.
func (s Style) GetTabWidth() int {
return s.getAsInt(tabWidthKey)
}
// GetUnderlineSpaces returns whether or not the style is set to underline
// spaces. If not value is set false is returned.
func (s Style) GetUnderlineSpaces() bool {

28
set.go
View File

@ -14,7 +14,14 @@ func (s *Style) set(key propKey, value interface{}) {
switch v := value.(type) {
case int:
// We don't allow negative integers on any of our values, so just keep
// TabWidth is the only property that may have a negative value (and
// that negative value can be no less than -1).
if key == tabWidthKey {
s.rules[key] = v
break
}
// We don't allow negative integers on any of our other values, so just keep
// them at zero or above. We could use uints instead, but the
// conversions are a little tedious, so we're sticking with ints for
// sake of usability.
@ -497,13 +504,30 @@ func (s Style) MaxWidth(n int) Style {
// styles.
//
// Because this in intended to be used at the time of render, this method will
// not mutate the style and instead return a copy.
// not mutate the style and instead returns a copy.
func (s Style) MaxHeight(n int) Style {
o := s.Copy()
o.set(maxHeightKey, n)
return o
}
// NoTabConversion can be passed to [Style.TabWidth] to disable the replacement
// of tabs with spaces at render time.
const NoTabConversion = -1
// TabWidth sets the number of spaces that a tab (/t) should be rendered as.
// When set to 0, tabs will be removed. To disable the replacement of tabs with
// spaces entirely, set this to [NoTabConversion].
//
// By default, tabs will be replaced with 4 spaces.
func (s Style) TabWidth(n int) Style {
if n <= -1 {
n = -1
}
s.set(tabWidthKey, n)
return s
}
// UnderlineSpaces determines whether to underline spaces between words. By
// default, this is true. Spaces can also be underlined without underlining the
// text itself.

View File

@ -10,6 +10,8 @@ import (
"github.com/muesli/termenv"
)
const tabWidthDefault = 4
// Property for a key.
type propKey int
@ -68,6 +70,7 @@ const (
inlineKey
maxWidthKey
maxHeightKey
tabWidthKey
underlineSpacesKey
strikethroughSpacesKey
)
@ -224,7 +227,7 @@ func (s Style) Render(strs ...string) string {
)
if len(s.rules) == 0 {
return str
return s.maybeConvertTabs(str)
}
// Enable support for ANSI on the legacy Windows cmd.exe console. This is a
@ -287,6 +290,9 @@ func (s Style) Render(strs ...string) string {
teSpace = teSpace.CrossOut()
}
// Potentially convert tabs to spaces
str = s.maybeConvertTabs(str)
// Strip newlines in single line mode
if inline {
str = strings.ReplaceAll(str, "\n", "")
@ -397,6 +403,21 @@ func (s Style) Render(strs ...string) string {
return str
}
func (s Style) maybeConvertTabs(str string) string {
tw := tabWidthDefault
if s.isSet(tabWidthKey) {
tw = s.getAsInt(tabWidthKey)
}
switch tw {
case -1:
return str
case 0:
return strings.ReplaceAll(str, "\t", "")
default:
return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw))
}
}
func (s Style) applyMargins(str string, inline bool) string {
var (
topMargin = s.getAsInt(marginTopKey)

View File

@ -181,7 +181,8 @@ func TestStyleCopy(t *testing.T) {
Foreground(Color("#ffffff")).
Background(Color("#111111")).
Margin(1, 1, 1, 1).
Padding(1, 1, 1, 1)
Padding(1, 1, 1, 1).
TabWidth(2)
i := s.Copy()
@ -202,6 +203,7 @@ func TestStyleCopy(t *testing.T) {
requireEqual(t, s.GetPaddingRight(), i.GetPaddingRight())
requireEqual(t, s.GetPaddingTop(), i.GetPaddingTop())
requireEqual(t, s.GetPaddingBottom(), i.GetPaddingBottom())
requireEqual(t, s.GetTabWidth(), i.GetTabWidth())
}
func TestStyleUnset(t *testing.T) {
@ -312,6 +314,12 @@ func TestStyleUnset(t *testing.T) {
requireTrue(t, s.GetBorderLeft())
s.UnsetBorderLeft()
requireFalse(t, s.GetBorderLeft())
// tab width
s = NewStyle().TabWidth(2)
requireEqual(t, s.GetTabWidth(), 2)
s.UnsetTabWidth()
requireNotEqual(t, s.GetTabWidth(), 4)
}
func TestStyleValue(t *testing.T) {
@ -352,7 +360,17 @@ func TestStyleValue(t *testing.T) {
res, formatEscapes(res))
}
}
}
func TestTabConversion(t *testing.T) {
s := NewStyle()
requireEqual(t, "[ ]", s.Render("[\t]"))
s = NewStyle().TabWidth(2)
requireEqual(t, "[ ]", s.Render("[\t]"))
s = NewStyle().TabWidth(0)
requireEqual(t, "[]", s.Render("[\t]"))
s = NewStyle().TabWidth(-1)
requireEqual(t, "[\t]", s.Render("[\t]"))
}
func BenchmarkStyleRender(b *testing.B) {

View File

@ -287,6 +287,12 @@ func (s Style) UnsetMaxHeight() Style {
return s
}
// UnsetMaxHeight removes the max height style rule, if set.
func (s Style) UnsetTabWidth() Style {
delete(s.rules, tabWidthKey)
return s
}
// UnsetUnderlineSpaces removes the value set by UnderlineSpaces.
func (s Style) UnsetUnderlineSpaces() Style {
delete(s.rules, underlineSpacesKey)