table.go

  1package ansi
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"io"
  7
  8	"github.com/charmbracelet/lipgloss/v2"
  9	"github.com/charmbracelet/lipgloss/v2/table"
 10	astext "github.com/yuin/goldmark/extension/ast"
 11)
 12
 13// A TableElement is used to render tables.
 14type TableElement struct {
 15	lipgloss *table.Table
 16	table    *astext.Table
 17	header   []string
 18	row      []string
 19	source   []byte
 20
 21	tableImages []tableLink
 22	tableLinks  []tableLink
 23}
 24
 25// A TableRowElement is used to render a single row in a table.
 26type TableRowElement struct{}
 27
 28// A TableHeadElement is used to render a table's head element.
 29type TableHeadElement struct{}
 30
 31// A TableCellElement is used to render a single cell in a row.
 32type TableCellElement struct {
 33	Children []ElementRenderer
 34	Head     bool
 35}
 36
 37// Render renders a TableElement.
 38func (e *TableElement) Render(w io.Writer, ctx RenderContext) error {
 39	bs := ctx.blockStack
 40
 41	var indentation uint
 42	var margin uint
 43	rules := ctx.options.Styles.Table
 44	if rules.Indent != nil {
 45		indentation = *rules.Indent
 46	}
 47	if rules.Margin != nil {
 48		margin = *rules.Margin
 49	}
 50
 51	iw := NewIndentWriter(w, int(indentation+margin), func(_ io.Writer) { //nolint:gosec
 52		_, _ = renderText(w, bs.Current().Style.StylePrimitive, " ")
 53	})
 54
 55	style := bs.With(rules.StylePrimitive)
 56
 57	_, _ = renderText(iw, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
 58	_, _ = renderText(iw, style, rules.Prefix)
 59	width := int(ctx.blockStack.Width(ctx)) //nolint: gosec
 60
 61	wrap := true
 62	if ctx.options.TableWrap != nil {
 63		wrap = *ctx.options.TableWrap
 64	}
 65	ctx.table.lipgloss = table.New().Width(width).Wrap(wrap)
 66
 67	if err := e.collectLinksAndImages(ctx); err != nil {
 68		return err
 69	}
 70
 71	return nil
 72}
 73
 74func (e *TableElement) setStyles(ctx RenderContext) {
 75	docRules := ctx.options.Styles.Document
 76	if docRules.BackgroundColor != nil {
 77		baseStyle := lipgloss.NewStyle().Background(lipgloss.Color(*docRules.BackgroundColor))
 78		ctx.table.lipgloss.BaseStyle(baseStyle)
 79	}
 80
 81	ctx.table.lipgloss = ctx.table.lipgloss.StyleFunc(func(_, col int) lipgloss.Style {
 82		st := lipgloss.NewStyle().Inline(false)
 83		// Default Styles
 84		st = st.Margin(0, 1)
 85
 86		// Override with custom styles
 87		if m := ctx.options.Styles.Table.Margin; m != nil {
 88			st = st.Padding(0, int(*m)) //nolint: gosec
 89		}
 90		switch e.table.Alignments[col] {
 91		case astext.AlignLeft:
 92			st = st.Align(lipgloss.Left).PaddingRight(0)
 93		case astext.AlignCenter:
 94			st = st.Align(lipgloss.Center)
 95		case astext.AlignRight:
 96			st = st.Align(lipgloss.Right).PaddingLeft(0)
 97		case astext.AlignNone:
 98			// do nothing
 99		}
100
101		return st
102	})
103}
104
105func (e *TableElement) setBorders(ctx RenderContext) {
106	rules := ctx.options.Styles.Table
107	border := lipgloss.NormalBorder()
108
109	if rules.RowSeparator != nil && rules.ColumnSeparator != nil {
110		border = lipgloss.Border{
111			Top:    *rules.RowSeparator,
112			Bottom: *rules.RowSeparator,
113			Left:   *rules.ColumnSeparator,
114			Right:  *rules.ColumnSeparator,
115			Middle: *rules.CenterSeparator,
116		}
117	}
118	ctx.table.lipgloss.Border(border)
119	ctx.table.lipgloss.BorderTop(false)
120	ctx.table.lipgloss.BorderLeft(false)
121	ctx.table.lipgloss.BorderRight(false)
122	ctx.table.lipgloss.BorderBottom(false)
123}
124
125// Finish finishes rendering a TableElement.
126func (e *TableElement) Finish(_ io.Writer, ctx RenderContext) error {
127	defer func() {
128		ctx.table.lipgloss = nil
129		ctx.table.tableImages = nil
130		ctx.table.tableLinks = nil
131	}()
132
133	rules := ctx.options.Styles.Table
134
135	e.setStyles(ctx)
136	e.setBorders(ctx)
137
138	ow := ctx.blockStack.Current().Block
139	if _, err := ow.WriteString(ctx.table.lipgloss.String()); err != nil {
140		return fmt.Errorf("glamour: error writing to buffer: %w", err)
141	}
142
143	_, _ = renderText(ow, ctx.blockStack.With(rules.StylePrimitive), rules.Suffix)
144	_, _ = renderText(ow, ctx.blockStack.Current().Style.StylePrimitive, rules.BlockSuffix)
145
146	e.printTableLinks(ctx)
147
148	ctx.table.lipgloss = nil
149	return nil
150}
151
152// Finish finishes rendering a TableRowElement.
153func (e *TableRowElement) Finish(_ io.Writer, ctx RenderContext) error {
154	if ctx.table.lipgloss == nil {
155		return nil
156	}
157
158	ctx.table.lipgloss.Row(ctx.table.row...)
159	ctx.table.row = []string{}
160	return nil
161}
162
163// Finish finishes rendering a TableHeadElement.
164func (e *TableHeadElement) Finish(_ io.Writer, ctx RenderContext) error {
165	if ctx.table.lipgloss == nil {
166		return nil
167	}
168
169	ctx.table.lipgloss.Headers(ctx.table.header...)
170	ctx.table.header = []string{}
171	return nil
172}
173
174// Render renders a TableCellElement.
175func (e *TableCellElement) Render(_ io.Writer, ctx RenderContext) error {
176	var b bytes.Buffer
177	style := ctx.options.Styles.Table.StylePrimitive
178	for _, child := range e.Children {
179		if r, ok := child.(StyleOverriderElementRenderer); ok {
180			if err := r.StyleOverrideRender(&b, ctx, style); err != nil {
181				return fmt.Errorf("glamour: error rendering with style: %w", err)
182			}
183		} else {
184			var bb bytes.Buffer
185			if err := child.Render(&bb, ctx); err != nil {
186				return fmt.Errorf("glamour: error rendering: %w", err)
187			}
188			el := &BaseElement{
189				Token: bb.String(),
190				Style: style,
191			}
192			if err := el.Render(&b, ctx); err != nil {
193				return err
194			}
195		}
196	}
197
198	if e.Head {
199		ctx.table.header = append(ctx.table.header, b.String())
200	} else {
201		ctx.table.row = append(ctx.table.row, b.String())
202	}
203
204	return nil
205}