1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
|
// Copyright 2012-2013 Apcera Inc. All rights reserved.
package termtables
import (
"bytes"
"os"
"regexp"
"runtime"
"strings"
"github.com/scylladb/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()
}
var utfRe = regexp.MustCompile(`utf\-8|utf8|UTF\-8|UTF8`)
// 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 utfRe.MatchString(locale) {
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, columns ...int) {
for i := range t.elements {
row, ok := t.elements[i].(*Row)
if !ok {
continue
}
for _, column := range columns {
if column < 0 || 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
}
|