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

372 lines
11 KiB
Go

// Copyright 2012-2013 Apcera Inc. All rights reserved.
package termtables
import (
"bytes"
"os"
"runtime"
"strings"
"github.com/goodrain/rainbond/util/termtables/term"
)
// MaxColumns represents the maximum number of columns that are available for
// display without wrapping around the right-hand side of the terminal window.
// At program initialization, the value will be automatically set according
// to available sources of information, including the $COLUMNS environment
// variable and, on Unix, tty information.
var MaxColumns = 80
// Element the interface that can draw a representation of the contents of a
// table cell.
type Element interface {
Render(*renderStyle) string
}
type outputMode int
const (
outputTerminal outputMode = iota
outputMarkdown
outputHTML
)
// Open question: should UTF-8 become an output mode? It does require more
// tracking when resetting, if the locale-enabling had been used.
var outputsEnabled struct {
UTF8 bool
HTML bool
Markdown bool
titleStyle titleStyle
}
var defaultOutputMode outputMode = outputTerminal
// Table represents a terminal table. The Style can be directly accessed
// and manipulated; all other access is via methods.
type Table struct {
Style *TableStyle
elements []Element
headers []interface{}
title interface{}
titleCell *Cell
outputMode outputMode
}
// EnableUTF8 will unconditionally enable using UTF-8 box-drawing characters
// for any tables created after this call, as the default style.
func EnableUTF8() {
outputsEnabled.UTF8 = true
}
// SetModeHTML will control whether or not new tables generated will be in HTML
// mode by default; HTML-or-not takes precedence over options which control how
// a terminal output will be rendered, such as whether or not to use UTF8.
// This affects any tables created after this call.
func SetModeHTML(onoff bool) {
outputsEnabled.HTML = onoff
chooseDefaultOutput()
}
// SetModeMarkdown will control whether or not new tables generated will be
// in Markdown mode by default. HTML-mode takes precedence.
func SetModeMarkdown(onoff bool) {
outputsEnabled.Markdown = onoff
chooseDefaultOutput()
}
// EnableUTF8PerLocale will use current locale character map information to
// determine if UTF-8 is expected and, if so, is equivalent to EnableUTF8.
func EnableUTF8PerLocale() {
locale := getLocale()
if strings.Contains(locale, "UTF-8") {
EnableUTF8()
}
}
// getLocale returns the current locale name.
func getLocale() string {
if runtime.GOOS == "windows" {
// TODO: detect windows locale
return "US-ASCII"
}
return unixLocale()
}
// unixLocale returns the locale by checking the $LC_ALL, $LC_CTYPE, and $LANG
// environment variables. If none of those are set, it returns "US-ASCII".
func unixLocale() string {
for _, env := range []string{"LC_ALL", "LC_CTYPE", "LANG"} {
if locale := os.Getenv(env); locale != "" {
return locale
}
}
return "US-ASCII"
}
// SetHTMLStyleTitle lets an HTML title output mode be chosen.
func SetHTMLStyleTitle(want titleStyle) {
outputsEnabled.titleStyle = want
}
// chooseDefaultOutput sets defaultOutputMode based on priority
// choosing amongst the options which are enabled. Pros: simpler
// encapsulation; cons: setting markdown doesn't disable HTML if
// HTML was previously enabled and was later disabled.
// This seems fairly reasonable.
func chooseDefaultOutput() {
if outputsEnabled.HTML {
defaultOutputMode = outputHTML
} else if outputsEnabled.Markdown {
defaultOutputMode = outputMarkdown
} else {
defaultOutputMode = outputTerminal
}
}
func init() {
// Do not enable UTF-8 per locale by default, breaks tests.
sz, err := term.GetSize()
if err == nil && sz.Columns != 0 {
MaxColumns = sz.Columns
}
}
// CreateTable creates an empty Table using defaults for style.
func CreateTable() *Table {
t := &Table{elements: []Element{}, Style: DefaultStyle}
if outputsEnabled.UTF8 {
t.Style.setUtfBoxStyle()
}
if outputsEnabled.titleStyle != titleStyle(0) {
t.Style.htmlRules.title = outputsEnabled.titleStyle
}
t.outputMode = defaultOutputMode
return t
}
// AddSeparator adds a line to the table content, where the line
// consists of separator characters.
func (t *Table) AddSeparator() {
t.elements = append(t.elements, &Separator{})
}
// AddRow adds the supplied items as cells in one row of the table.
func (t *Table) AddRow(items ...interface{}) *Row {
row := CreateRow(items)
t.elements = append(t.elements, row)
return row
}
// AddTitle supplies a table title, which if present will be rendered as
// one cell across the width of the table, as the first row.
func (t *Table) AddTitle(title interface{}) {
t.title = title
}
// AddHeaders supplies column headers for the table.
func (t *Table) AddHeaders(headers ...interface{}) {
t.headers = append(t.headers, headers...)
}
// SetAlign changes the alignment for elements in a column of the table;
// alignments are stored with each cell, so cells added after a call to
// SetAlign will not pick up the change. Columns are numbered from 1.
func (t *Table) SetAlign(align tableAlignment, column int) {
if column < 0 {
return
}
for i := range t.elements {
row, ok := t.elements[i].(*Row)
if !ok {
continue
}
if column >= len(row.cells) {
continue
}
row.cells[column-1].alignment = &align
}
}
// UTF8Box sets the table style to use UTF-8 box-drawing characters,
// overriding all relevant style elements at the time of the call.
func (t *Table) UTF8Box() {
t.Style.setUtfBoxStyle()
}
// SetModeHTML switches this table to be in HTML when rendered; the
// default depends upon whether the package function SetModeHTML() has been
// called, and with what value. This method forces the feature on for this
// table. Turning off involves choosing a different mode, per-table.
func (t *Table) SetModeHTML() {
t.outputMode = outputHTML
}
// SetModeMarkdown switches this table to be in Markdown mode
func (t *Table) SetModeMarkdown() {
t.outputMode = outputMarkdown
}
// SetModeTerminal switches this table to be in terminal mode.
func (t *Table) SetModeTerminal() {
t.outputMode = outputTerminal
}
// SetHTMLStyleTitle lets an HTML output mode be chosen; we should rework this
// into a more generic and extensible API as we clean up termtables.
func (t *Table) SetHTMLStyleTitle(want titleStyle) {
t.Style.htmlRules.title = want
}
// Render returns a string representation of a fully rendered table, drawn
// out for display, with embedded newlines. If this table is in HTML mode,
// then this is equivalent to RenderHTML().
func (t *Table) Render() string {
// Elements is already populated with row data.
switch t.outputMode {
case outputTerminal:
return t.renderTerminal()
case outputMarkdown:
return t.renderMarkdown()
case outputHTML:
return t.RenderHTML()
default:
panic("unknown output mode set")
}
}
// renderTerminal returns a string representation of a fully rendered table,
// drawn out for display, with embedded newlines.
func (t *Table) renderTerminal() string {
// Use a placeholder rather than adding titles/headers to the tables
// elements or else successive calls will compound them.
tt := t.clone()
// Initial top line.
if !tt.Style.SkipBorder {
if tt.title != nil && tt.headers == nil {
tt.elements = append([]Element{&Separator{where: LINE_SUBTOP}}, tt.elements...)
} else if tt.title == nil && tt.headers == nil {
tt.elements = append([]Element{&Separator{where: LINE_TOP}}, tt.elements...)
} else {
tt.elements = append([]Element{&Separator{where: LINE_INNER}}, tt.elements...)
}
}
// If we have headers, include them.
if tt.headers != nil {
ne := make([]Element, 2)
ne[1] = CreateRow(tt.headers)
if tt.title != nil {
ne[0] = &Separator{where: LINE_SUBTOP}
} else {
ne[0] = &Separator{where: LINE_TOP}
}
tt.elements = append(ne, tt.elements...)
}
// If we have a title, write it.
if tt.title != nil {
// Match changes to this into renderMarkdown too.
tt.titleCell = CreateCell(tt.title, &CellStyle{Alignment: AlignCenter, ColSpan: 999})
ne := []Element{
&StraightSeparator{where: LINE_TOP},
CreateRow([]interface{}{tt.titleCell}),
}
tt.elements = append(ne, tt.elements...)
}
// Create a new table from the
// generate the runtime style. Must include all cells being printed.
style := createRenderStyle(tt)
// Loop over the elements and render them.
b := bytes.NewBuffer(nil)
for _, e := range tt.elements {
b.WriteString(e.Render(style))
b.WriteString("\n")
}
// Add bottom line.
if !style.SkipBorder {
b.WriteString((&Separator{where: LINE_BOTTOM}).Render(style) + "\n")
}
return b.String()
}
// renderMarkdown returns a string representation of a table in Markdown
// markup format using GitHub Flavored Markdown's notation (since tables
// are not in the core Markdown spec).
func (t *Table) renderMarkdown() string {
// We need ASCII drawing characters; we need a line after the header;
// *do* need a header! Do not need to markdown-escape contents of
// tables as markdown is ignored in there. Do need to do _something_
// with a '|' character shown as a member of a table.
t.Style.setAsciiBoxStyle()
firstLines := make([]Element, 0, 2)
if t.headers == nil {
initial := createRenderStyle(t)
if initial.columns > 1 {
row := CreateRow([]interface{}{})
for i := 0; i < initial.columns; i++ {
row.AddCell(CreateCell(i+1, &CellStyle{}))
}
}
}
firstLines = append(firstLines, CreateRow(t.headers))
// This is a dummy line, swapped out below.
firstLines = append(firstLines, firstLines[0])
t.elements = append(firstLines, t.elements...)
// Generate the runtime style.
style := createRenderStyle(t)
// We know that the second line is a dummy, we can replace it.
mdRow := CreateRow([]interface{}{})
for i := 0; i < style.columns; i++ {
mdRow.AddCell(CreateCell(strings.Repeat("-", style.cellWidths[i]), &CellStyle{}))
}
t.elements[1] = mdRow
b := bytes.NewBuffer(nil)
// Comes after style is generated, which must come after all width-affecting
// changes are in.
if t.title != nil {
// Markdown doesn't support titles or column spanning; we _should_
// escape the title, but doing that to handle all possible forms of
// markup would require a heavy dependency, so we punt.
b.WriteString("Table: ")
b.WriteString(strings.TrimSpace(CreateCell(t.title, &CellStyle{}).Render(style)))
b.WriteString("\n\n")
}
// Loop over the elements and render them.
for _, e := range t.elements {
b.WriteString(e.Render(style))
b.WriteString("\n")
}
return b.String()
}
// clone returns a copy of the table with the underlying slices being copied;
// the references to the Elements/cells are left as shallow copies.
func (t *Table) clone() *Table {
tt := &Table{outputMode: t.outputMode, Style: t.Style, title: t.title}
if t.headers != nil {
tt.headers = make([]interface{}, len(t.headers))
copy(tt.headers, t.headers)
}
if t.elements != nil {
tt.elements = make([]Element, len(t.elements))
copy(tt.elements, t.elements)
}
return tt
}