Repository: Evertras/bubble-table
Pull request #175 (2024-05-15 23:15:13)

add `filterFunc` to model
I think the `FilterFunc` stuff is pretty solid, but I think I need to spend some more time working on the way that the column sizes are calculated. There's a bunch of unnecessary copying of the `m.columns` slice.

trevorstarick: I'm using both features in tandem locally, but if it makes more sense to have them as two PR then I'll split them

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 17429af..cabbf7a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -12,7 +12,7 @@ jobs:
     name: Build and test
     strategy:
       matrix:
-        go-version: [~1.13, ^1]
+        go-version: [~1.18, ^1]
         os: [ubuntu-latest]
     runs-on: ${{ matrix.os }}
     steps:
@@ -32,4 +32,3 @@ jobs:
 
       - name: Test
         run: go test -race ./table
-
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 4377ca8..f67d6df 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -15,7 +15,7 @@ jobs:
       - name: Install Go
         uses: actions/setup-go@v4
         with:
-          go-version: "1.17.6"
+          go-version: "1.18.10"
 
       - name: Check out code
         uses: actions/checkout@v4
@@ -35,4 +35,3 @@ jobs:
         env:
           COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         run: goveralls -coverprofile=covprofile -service=github
-
diff --git a/README.md b/README.md
index 8d2f03b..a9e3467 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,9 @@ determining overrides.  The default base style is a basic right-alignment.
 [See the main feature example](./examples/features) to see styles and
 how they override each other.
 
+Styles can also be applied via a style function which can be used to apply
+zebra striping, data-specific formatting, etc.
+
 Can be focused to highlight a row and navigate with up/down (and j/k).  These
 keys can be customized with a KeyMap.
 
diff --git a/examples/updates/main.go b/examples/updates/main.go
index 207a9ac..608b5da 100644
--- a/examples/updates/main.go
+++ b/examples/updates/main.go
@@ -31,9 +31,30 @@ type Model struct {
 	data []*SomeData
 }
 
+func rowStyleFunc(input table.RowStyleFuncInput) lipgloss.Style {
+	calculatedStyle := lipgloss.NewStyle()
+
+	switch input.Row.Data[columnKeyStatus] {
+	case "Critical":
+		calculatedStyle = styleCritical.Copy()
+	case "Stable":
+		calculatedStyle = styleStable.Copy()
+	case "Good":
+		calculatedStyle = styleGood.Copy()
+	}
+
+	if input.Index%2 == 0 {
+		calculatedStyle = calculatedStyle.Background(lipgloss.Color("#222"))
+	} else {
+		calculatedStyle = calculatedStyle.Background(lipgloss.Color("#444"))
+	}
+
+	return calculatedStyle
+}
+
 func NewModel() Model {
 	return Model{
-		table:       table.New(generateColumns(0)),
+		table:       table.New(generateColumns(0)).WithRowStyleFunc(rowStyleFunc),
 		updateDelay: time.Second,
 	}
 }
@@ -152,18 +173,6 @@ func generateRowsFromData(data []*SomeData) []table.Row {
 			columnKeyStatus: entry.Status,
 		})
 
-		// Highlight different statuses
-		switch entry.Status {
-		case "Critical":
-			row = row.WithStyle(styleCritical)
-
-		case "Stable":
-			row = row.WithStyle(styleStable)
-
-		case "Good":
-			row = row.WithStyle(styleGood)
-		}
-
 		rows = append(rows, row)
 	}
 
diff --git a/go.mod b/go.mod
index 50a674b..ae7f683 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/evertras/bubble-table
 
-go 1.17
+go 1.18
 
 require (
 	github.com/charmbracelet/bubbles v0.11.0
diff --git a/table/filter.go b/table/filter.go
index 0ba233b..26e580c 100644
--- a/table/filter.go
+++ b/table/filter.go
@@ -14,8 +14,14 @@ func (m Model) getFilteredRows(rows []Row) []Row {
 	filteredRows := make([]Row, 0)
 
 	for _, row := range rows {
-		if isRowMatched(m.columns, row, filterInputValue) {
-			filteredRows = append(filteredRows, row)
+		if m.filterFunc != nil {
+			if m.filterFunc(row, filterInputValue) {
+				filteredRows = append(filteredRows, row)
+			}
+		} else {
+			if isRowMatched(m.columns, row, filterInputValue) {
+				filteredRows = append(filteredRows, row)
+			}
 		}
 	}
 
diff --git a/table/footer.go b/table/footer.go
index ba657a1..c68709c 100644
--- a/table/footer.go
+++ b/table/footer.go
@@ -19,7 +19,7 @@ func (m Model) renderFooter(width int, includeTop bool) string {
 	styleFooter := m.baseStyle.Copy().Inherit(m.border.styleFooter).Width(width - borderAdjustment)
 
 	if includeTop {
-		styleFooter.BorderTop(true)
+		styleFooter = styleFooter.BorderTop(true)
 	}
 
 	if m.staticFooter != "" {
diff --git a/table/keys.go b/table/keys.go
index bc7d119..fe2eccc 100644
--- a/table/keys.go
+++ b/table/keys.go
@@ -30,44 +30,91 @@ type KeyMap struct {
 	ScrollLeft key.Binding
 }
 
-// DefaultKeyMap returns a set of sensible defaults for controlling a focused table.
+// DefaultKeyMap returns a set of sensible defaults for controlling a focused table with help text.
 func DefaultKeyMap() KeyMap {
 	return KeyMap{
 		RowDown: key.NewBinding(
 			key.WithKeys("down", "j"),
+			key.WithHelp("↓/j", "move down"),
 		),
 		RowUp: key.NewBinding(
 			key.WithKeys("up", "k"),
+			key.WithHelp("↑/k", "move up"),
 		),
 		RowSelectToggle: key.NewBinding(
 			key.WithKeys(" ", "enter"),
+			key.WithHelp("<space>/enter", "select row"),
 		),
 		PageDown: key.NewBinding(
 			key.WithKeys("right", "l", "pgdown"),
+			key.WithHelp("→/h/page down", "next page"),
 		),
 		PageUp: key.NewBinding(
 			key.WithKeys("left", "h", "pgup"),
+			key.WithHelp("←/h/page up", "previous page"),
 		),
 		PageFirst: key.NewBinding(
 			key.WithKeys("home", "g"),
+			key.WithHelp("home/g", "first page"),
 		),
 		PageLast: key.NewBinding(
 			key.WithKeys("end", "G"),
+			key.WithHelp("end/G", "last page"),
 		),
 		Filter: key.NewBinding(
 			key.WithKeys("/"),
+			key.WithHelp("/", "filter"),
 		),
 		FilterBlur: key.NewBinding(
 			key.WithKeys("enter", "esc"),
+			key.WithHelp("enter/esc", "unfocus"),
 		),
 		FilterClear: key.NewBinding(
 			key.WithKeys("esc"),
+			key.WithHelp("esc", "clear filter"),
 		),
 		ScrollRight: key.NewBinding(
 			key.WithKeys("shift+right"),
+			key.WithHelp("shift+→", "scroll right"),
 		),
 		ScrollLeft: key.NewBinding(
 			key.WithKeys("shift+left"),
+			key.WithHelp("shift+←", "scroll left"),
 		),
 	}
 }
+
+// FullHelp returns a multi row view of all the helpkeys that are defined. Needed to fullfil the 'help.Model' interface.
+// Also appends all user defined extra keys to the help.
+func (m Model) FullHelp() [][]key.Binding {
+	keyBinds := [][]key.Binding{
+		{m.keyMap.RowDown, m.keyMap.RowUp, m.keyMap.RowSelectToggle},
+		{m.keyMap.PageDown, m.keyMap.PageUp, m.keyMap.PageFirst, m.keyMap.PageLast},
+		{m.keyMap.Filter, m.keyMap.FilterBlur, m.keyMap.FilterClear, m.keyMap.ScrollRight, m.keyMap.ScrollLeft},
+	}
+	if m.additionalFullHelpKeys != nil {
+		keyBinds = append(keyBinds, m.additionalFullHelpKeys())
+	}
+
+	return keyBinds
+}
+
+// ShortHelp just returns a single row of help views. Needed to fullfil the 'help.Model' interface.
+// Also appends all user defined extra keys to the help.
+func (m Model) ShortHelp() []key.Binding {
+	keyBinds := []key.Binding{
+		m.keyMap.RowDown,
+		m.keyMap.RowUp,
+		m.keyMap.RowSelectToggle,
+		m.keyMap.PageDown,
+		m.keyMap.PageUp,
+		m.keyMap.Filter,
+		m.keyMap.FilterBlur,
+		m.keyMap.FilterClear,
+	}
+	if m.additionalShortHelpKeys != nil {
+		keyBinds = append(keyBinds, m.additionalShortHelpKeys()...)
+	}
+
+	return keyBinds
+}
diff --git a/table/model.go b/table/model.go
index b1934f8..0081134 100644
--- a/table/model.go
+++ b/table/model.go
@@ -1,6 +1,7 @@
 package table
 
 import (
+	"github.com/charmbracelet/bubbles/key"
 	"github.com/charmbracelet/bubbles/textinput"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
@@ -28,8 +29,21 @@ type Model struct {
 	missingDataIndicator interface{}
 
 	// Interaction
-	focused        bool
-	keyMap         KeyMap
+	focused bool
+	keyMap  KeyMap
+
+	// Taken from: 'Bubbles/List'
+	// Additional key mappings for the short and full help views. This allows
+	// you to add additional key mappings to the help menu without
+	// re-implementing the help component. Of course, you can also disable the
+	// list's help component and implement a new one if you need more
+	// flexibility.
+	// You have to supply a keybinding like this:
+	// key.NewBinding( key.WithKeys("shift+left"), key.WithHelp("shift+←", "scroll left"))
+	// It needs both 'WithKeys' and 'WithHelp'
+	additionalShortHelpKeys func() []key.Binding
+	additionalFullHelpKeys  func() []key.Binding
+
 	selectableRows bool
 	rowCursorIndex int
 
@@ -40,6 +54,7 @@ type Model struct {
 	baseStyle      lipgloss.Style
 	highlightStyle lipgloss.Style
 	headerStyle    lipgloss.Style
+	rowStyleFunc   func(RowStyleFuncInput) lipgloss.Style
 	border         Border
 	selectedText   string
 	unselectedText string
@@ -63,6 +78,7 @@ type Model struct {
 	// Filter
 	filtered        bool
 	filterTextInput textinput.Model
+	filterFunc      func(Row, string) bool
 
 	// For flex columns
 	targetTotalWidth int
@@ -110,6 +126,7 @@ func New(columns []Column) Model {
 		unselectedText: "[ ]",
 
 		filterTextInput: filterInput,
+		filterFunc:      nil,
 		baseStyle:       lipgloss.NewStyle().Align(lipgloss.Right),
 
 		paginationWrapping: true,
diff --git a/table/options.go b/table/options.go
index 0bf43ad..66351ff 100644
--- a/table/options.go
+++ b/table/options.go
@@ -1,10 +1,39 @@
 package table
 
 import (
+	"github.com/charmbracelet/bubbles/key"
 	"github.com/charmbracelet/bubbles/textinput"
 	"github.com/charmbracelet/lipgloss"
 )
 
+// RowStyleFuncInput is the input to the style function that can
+// be applied to each row.  This is useful for things like zebra
+// striping or other data-based styles.
+//
+// Note that we use a struct here to allow for future expansion
+// while keeping backwards compatibility.
+type RowStyleFuncInput struct {
+	// Index is the index of the row, starting at 0.
+	Index int
+
+	// Row is the full row data.
+	Row Row
+
+	// IsHighlighted is true if the row is currently highlighted.
+	IsHighlighted bool
+}
+
+// WithRowStyleFunc sets a function that can be used to apply a style to each row
+// based on the row data.  This is useful for things like zebra striping or other
+// data-based styles.  It can be safely set to nil to remove it later.
+// This style is applied after the base style and before individual row styles.
+// This will override any HighlightStyle settings.
+func (m Model) WithRowStyleFunc(f func(RowStyleFuncInput) lipgloss.Style) Model {
+	m.rowStyleFunc = f
+
+	return m
+}
+
 // WithHighlightedRow sets the highlighted row to the given index.
 func (m Model) WithHighlightedRow(index int) Model {
 	m.rowCursorIndex = index
@@ -112,7 +141,8 @@ func (m Model) SelectedRows() []Row {
 }
 
 // HighlightStyle sets a custom style to use when the row is being highlighted
-// by the cursor.
+// by the cursor.  This should not be used with WithRowStyleFunc.  Instead, use
+// the IsHighlighted field in the style function.
 func (m Model) HighlightStyle(style lipgloss.Style) Model {
 	m.highlightStyle = style
 
@@ -273,7 +303,6 @@ func (m Model) WithCurrentPage(currentPage int) Model {
 	if m.pageSize == 0 || currentPage == m.CurrentPage() {
 		return m
 	}
-
 	if currentPage < 1 {
 		currentPage = 1
 	} else {
@@ -283,7 +312,6 @@ func (m Model) WithCurrentPage(currentPage int) Model {
 			currentPage = maxPages
 		}
 	}
-
 	m.currentPage = currentPage - 1
 	m.rowCursorIndex = m.currentPage * m.pageSize
 
@@ -336,6 +364,16 @@ func (m Model) WithFilterInputValue(value string) Model {
 	return m
 }
 
+// WithFilterFunc adds a filter function to the model. If the function returns
+// true, the row will be included in the filtered results. If the function
+// is nil, the function won't be used. The filter input is passed as the second
+// argument to the function.
+func (m Model) WithFilterFunc(shouldInclude func(row Row, filterInput string) bool) Model {
+	m.filterFunc = shouldInclude
+
+	return m
+}
+
 // WithFooterVisibility sets the visibility of the footer.
 func (m Model) WithFooterVisibility(visibility bool) Model {
 	m.footerVisible = visibility
@@ -436,3 +474,21 @@ func (m Model) WithMultiline(multiline bool) Model {
 
 	return m
 }
+
+// WithAdditionalShortHelpKeys enables you to add more keybindings to the 'short help' view.
+func (m Model) WithAdditionalShortHelpKeys(keys []key.Binding) Model {
+	m.additionalShortHelpKeys = func() []key.Binding {
+		return keys
+	}
+
+	return m
+}
+
+// WithAdditionalFullHelpKeys enables you to add more keybindings to the 'full help' view.
+func (m Model) WithAdditionalFullHelpKeys(keys []key.Binding) Model {
+	m.additionalFullHelpKeys = func() []key.Binding {
+		return keys
+	}
+
+	return m
+}
diff --git a/table/overflow.go b/table/overflow.go
index 1438290..19c5b0a 100644
--- a/table/overflow.go
+++ b/table/overflow.go
@@ -6,11 +6,7 @@ const columnKeyOverflowRight = "___overflow_r___"
 const columnKeyOverflowLeft = "___overflow_l__"
 
 func genOverflowStyle(base lipgloss.Style, width int) lipgloss.Style {
-	style := lipgloss.NewStyle().Width(width).Align(lipgloss.Right)
-
-	style.Inherit(base)
-
-	return style
+	return base.Width(width).Align(lipgloss.Right)
 }
 
 func genOverflowColumnRight(width int) Column {
diff --git a/table/row.go b/table/row.go
index ab3dd61..2ba4519 100644
--- a/table/row.go
+++ b/table/row.go
@@ -115,7 +115,15 @@ func (m Model) renderRow(rowIndex int, last bool) string {
 
 	rowStyle := row.Style.Copy()
 
-	if m.focused && highlighted {
+	if m.rowStyleFunc != nil {
+		styleResult := m.rowStyleFunc(RowStyleFuncInput{
+			Index:         rowIndex,
+			Row:           row,
+			IsHighlighted: m.focused && highlighted,
+		})
+
+		rowStyle = rowStyle.Inherit(styleResult)
+	} else if m.focused && highlighted {
 		rowStyle = rowStyle.Inherit(m.highlightStyle)
 	}
 


Test patch:
diff --git a/table/keys_test.go b/table/keys_test.go
new file mode 100644
index 0000000..8831562
--- /dev/null
+++ b/table/keys_test.go
@@ -0,0 +1,89 @@
+package table
+
+import (
+	"testing"
+
+	"github.com/charmbracelet/bubbles/help"
+	"github.com/charmbracelet/bubbles/key"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestKeyMapShortHelp(t *testing.T) {
+	columns := []Column{NewColumn("c1", "Column1", 10)}
+	model := New(columns)
+	km := DefaultKeyMap()
+	model.WithKeyMap(km)
+	assert.Nil(t, model.additionalShortHelpKeys)
+	assert.Equal(t, model.ShortHelp(), []key.Binding{
+		model.keyMap.RowDown,
+		model.keyMap.RowUp,
+		model.keyMap.RowSelectToggle,
+		model.keyMap.PageDown,
+		model.keyMap.PageUp,
+		model.keyMap.Filter,
+		model.keyMap.FilterBlur,
+		model.keyMap.FilterClear},
+	)
+	// Testing if the 'adding of keys' works too.
+	keys := []key.Binding{key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))}
+	model = model.WithAdditionalShortHelpKeys(keys)
+	assert.NotNil(t, model.additionalShortHelpKeys)
+	assert.Equal(t, model.ShortHelp(), []key.Binding{
+		model.keyMap.RowDown,
+		model.keyMap.RowUp,
+		model.keyMap.RowSelectToggle,
+		model.keyMap.PageDown,
+		model.keyMap.PageUp,
+		model.keyMap.Filter,
+		model.keyMap.FilterBlur,
+		model.keyMap.FilterClear,
+		key.NewBinding(
+			key.WithKeys("t"),
+			key.WithHelp("t",
+				"Testing additional keybinds"),
+		),
+	})
+}
+
+func TestKeyMapFullHelp(t *testing.T) {
+	columns := []Column{NewColumn("c1", "Column1", 10)}
+	model := New(columns)
+	km := DefaultKeyMap()
+	model.WithKeyMap(km)
+	assert.Nil(t, model.additionalFullHelpKeys)
+	assert.Equal(t,
+		model.FullHelp(),
+		[][]key.Binding{
+			{model.keyMap.RowDown, model.keyMap.RowUp, model.keyMap.RowSelectToggle},
+			{model.keyMap.PageDown, model.keyMap.PageUp, model.keyMap.PageFirst, model.keyMap.PageLast},
+			{
+				model.keyMap.Filter,
+				model.keyMap.FilterBlur,
+				model.keyMap.FilterClear,
+				model.keyMap.ScrollRight,
+				model.keyMap.ScrollLeft,
+			},
+		},
+	)
+	// Testing if the 'adding of keys' works too.
+	keys := []key.Binding{key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))}
+	model = model.WithAdditionalFullHelpKeys(keys)
+	assert.NotNil(t, model.additionalFullHelpKeys)
+	assert.Equal(t,
+		model.FullHelp(),
+		[][]key.Binding{
+			{model.keyMap.RowDown, model.keyMap.RowUp, model.keyMap.RowSelectToggle},
+			{model.keyMap.PageDown, model.keyMap.PageUp, model.keyMap.PageFirst, model.keyMap.PageLast},
+			{model.keyMap.Filter, model.keyMap.FilterBlur,
+				model.keyMap.FilterClear,
+				model.keyMap.ScrollRight,
+				model.keyMap.ScrollLeft},
+			{key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "Testing additional keybinds"))}},
+	)
+}
+
+// Testing if Model actually implements the 'help.KeyMap' interface.
+func TestKeyMapInterface(t *testing.T) {
+	model := New(nil)
+	assert.Implements(t, (*help.KeyMap)(nil), model)
+}
diff --git a/table/view_test.go b/table/view_test.go
index 07014b1..b4b8cf8 100644
--- a/table/view_test.go
+++ b/table/view_test.go
@@ -798,6 +798,110 @@ func TestSimple3x3StyleOverridesAsBaseColumnRowCell(t *testing.T) {
 	assert.Equal(t, expectedTable, rendered)
 }
 
+func TestStyleFuncAppliesAfterBaseStyleAndColStylesAndBeforeRowStyle(t *testing.T) {
+	styleFunc := func(input RowStyleFuncInput) lipgloss.Style {
+		if input.Index%2 == 0 {
+			return lipgloss.NewStyle().Align(lipgloss.Left)
+		}
+
+		return lipgloss.NewStyle()
+	}
+
+	model := New([]Column{
+		NewColumn("1", "1", 6),
+		// This column style should be overridden by the style func
+		NewColumn("2", "2", 6).WithStyle(lipgloss.NewStyle().Align(lipgloss.Right)),
+		NewColumn("3", "3", 6),
+	}).
+		WithBaseStyle(lipgloss.NewStyle().Align(lipgloss.Center)).
+		WithRowStyleFunc(styleFunc)
+
+	rows := []Row{}
+
+	for rowIndex := 1; rowIndex <= 5; rowIndex++ {
+		rowData := RowData{}
+
+		for columnIndex := 1; columnIndex <= 3; columnIndex++ {
+			id := fmt.Sprintf("%d", columnIndex)
+
+			rowData[id] = fmt.Sprintf("%d,%d", columnIndex, rowIndex)
+		}
+
+		rows = append(rows, NewRow(rowData))
+	}
+
+	rows[0] = rows[0].WithStyle(lipgloss.NewStyle().Align(lipgloss.Right))
+
+	model = model.WithRows(rows)
+
+	const expectedTable = `┏━━━━━━┳━━━━━━┳━━━━━━┓
+┃  1   ┃     2┃  3   ┃
+┣━━━━━━╋━━━━━━╋━━━━━━┫
+┃   1,1┃   2,1┃   3,1┃
+┃ 1,2  ┃   2,2┃ 3,2  ┃
+┃1,3   ┃2,3   ┃3,3   ┃
+┃ 1,4  ┃   2,4┃ 3,4  ┃
+┃1,5   ┃2,5   ┃3,5   ┃
+┗━━━━━━┻━━━━━━┻━━━━━━┛`
+
+	rendered := model.View()
+
+	assert.Equal(t, expectedTable, rendered)
+}
+
+func TestStyleFuncAppliesHighlighted(t *testing.T) {
+	styleFunc := func(input RowStyleFuncInput) lipgloss.Style {
+		if input.IsHighlighted {
+			return lipgloss.NewStyle().Align(lipgloss.Center)
+		}
+
+		if input.Index%2 == 0 {
+			return lipgloss.NewStyle().Align(lipgloss.Right)
+		}
+
+		return lipgloss.NewStyle().Align(lipgloss.Left)
+	}
+
+	model := New([]Column{
+		NewColumn("1", "1", 6),
+		NewColumn("2", "2", 6),
+		NewColumn("3", "3", 6),
+	}).
+		WithRowStyleFunc(styleFunc).
+		Focused(true)
+
+	rows := []Row{}
+
+	for rowIndex := 1; rowIndex <= 5; rowIndex++ {
+		rowData := RowData{}
+
+		for columnIndex := 1; columnIndex <= 3; columnIndex++ {
+			id := fmt.Sprintf("%d", columnIndex)
+
+			rowData[id] = fmt.Sprintf("%d,%d", columnIndex, rowIndex)
+		}
+
+		rows = append(rows, NewRow(rowData))
+	}
+
+	model = model.WithRows(rows).
+		WithHighlightedRow(2)
+
+	const expectedTable = `┏━━━━━━┳━━━━━━┳━━━━━━┓
+┃     1┃     2┃     3┃
+┣━━━━━━╋━━━━━━╋━━━━━━┫
+┃   1,1┃   2,1┃   3,1┃
+┃1,2   ┃2,2   ┃3,2   ┃
+┃ 1,3  ┃ 2,3  ┃ 3,3  ┃
+┃1,4   ┃2,4   ┃3,4   ┃
+┃   1,5┃   2,5┃   3,5┃
+┗━━━━━━┻━━━━━━┻━━━━━━┛`
+
+	rendered := model.View()
+
+	assert.Equal(t, expectedTable, rendered)
+}
+
 // This is a long test due to typing and multiple big table strings, that's okay
 //
 //nolint:funlen