Rainbond/util/termtables/cell.go
2020-09-06 11:11:11 +08:00

169 lines
4.6 KiB
Go

// Copyright 2012 Apcera Inc. All rights reserved.
package termtables
import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
"unicode/utf8"
runewidth "github.com/mattn/go-runewidth"
)
var (
// Must match SGR escape sequence, which is "CSI Pm m", where the Control
// Sequence Introducer (CSI) is "ESC ["; where Pm is "A multiple numeric
// parameter composed of any number of single numeric parameters, separated
// by ; character(s). Individual values for the parameters are listed with
// Ps" and where Ps is A single (usually optional) numeric parameter,
// composed of one of [sic] more digits."
//
// In practice, the end sequence is usually given as \e[0m but reading that
// definition, it's clear that the 0 is optional and some testing confirms
// that it is certainly optional with MacOS Terminal 2.3, so we need to
// support the string \e[m as a terminator too.
colorFilter = regexp.MustCompile(`\033\[(?:\d+(?:;\d+)*)?m`)
)
// A Cell denotes one cell of a table; it spans one row and a variable number
// of columns. A given Cell can only be used at one place in a table; the act
// of adding the Cell to the table mutates it with position information, so
// do not create one "const" Cell to add it multiple times.
type Cell struct {
column int
formattedValue string
alignment *tableAlignment
colSpan int
}
// CreateCell returns a Cell where the content is the supplied value, with the
// optional supplied style (which may be given as nil). The style can include
// a non-zero ColSpan to cause the cell to become column-spanning. Changing
// the style afterwards will not adjust the column-spanning state of the cell
// itself.
func CreateCell(v interface{}, style *CellStyle) *Cell {
return createCell(0, v, style)
}
func createCell(column int, v interface{}, style *CellStyle) *Cell {
cell := &Cell{column: column, formattedValue: renderValue(v), colSpan: 1}
if style != nil {
cell.alignment = &style.Alignment
if style.ColSpan != 0 {
cell.colSpan = style.ColSpan
}
}
return cell
}
// Width returns the width of the content of the cell, measured in runes as best
// as possible considering sophisticated Unicode.
func (c *Cell) Width() int {
return runewidth.StringWidth(filterColorCodes(c.formattedValue))
}
// Filter out terminal bold/color sequences in a string.
// This supports only basic bold/color escape sequences.
func filterColorCodes(s string) string {
return colorFilter.ReplaceAllString(s, "")
}
// Render returns a string representing the content of the cell, together with
// padding (to the widths specified) and handling any alignment.
func (c *Cell) Render(style *renderStyle) (buffer string) {
// if no alignment is set, import the table's default
if c.alignment == nil {
c.alignment = &style.Alignment
}
// left padding
buffer += strings.Repeat(" ", style.PaddingLeft)
// append the main value and handle alignment
buffer += c.alignCell(style)
// right padding
buffer += strings.Repeat(" ", style.PaddingRight)
// this handles escaping for, eg, Markdown, where we don't care about the
// alignment quite as much
if style.replaceContent != nil {
buffer = style.replaceContent(buffer)
}
return buffer
}
func (c *Cell) alignCell(style *renderStyle) string {
buffer := ""
width := style.CellWidth(c.column)
if c.colSpan > 1 {
for i := 1; i < c.colSpan; i++ {
w := style.CellWidth(c.column + i)
if w == 0 {
break
}
width += style.PaddingLeft + w + style.PaddingRight + utf8.RuneCountInString(style.BorderY)
}
}
switch *c.alignment {
default:
buffer += c.formattedValue
if l := width - c.Width(); l > 0 {
buffer += strings.Repeat(" ", l)
}
case AlignLeft:
buffer += c.formattedValue
if l := width - c.Width(); l > 0 {
buffer += strings.Repeat(" ", l)
}
case AlignRight:
if l := width - c.Width(); l > 0 {
buffer += strings.Repeat(" ", l)
}
buffer += c.formattedValue
case AlignCenter:
left, right := 0, 0
if l := width - c.Width(); l > 0 {
lf := float64(l)
left = int(math.Floor(lf / 2))
right = int(math.Ceil(lf / 2))
}
buffer += strings.Repeat(" ", left)
buffer += c.formattedValue
buffer += strings.Repeat(" ", right)
}
return buffer
}
// Format the raw value as a string depending on the type
func renderValue(v interface{}) string {
switch vv := v.(type) {
case string:
return vv
case bool:
return strconv.FormatBool(vv)
case int:
return strconv.Itoa(vv)
case int64:
return strconv.FormatInt(vv, 10)
case uint64:
return strconv.FormatUint(vv, 10)
case float64:
return strconv.FormatFloat(vv, 'f', 2, 64)
case fmt.Stringer:
return vv.String()
}
return fmt.Sprintf("%v", v)
}