forked from goodrain/rainbond
-
Notifications
You must be signed in to change notification settings - Fork 2
/
table.go
371 lines (321 loc) · 10.8 KB
/
table.go
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
// Copyright 2012-2013 Apcera Inc. All rights reserved.
package termtables
import (
"bytes"
"os"
"runtime"
"strings"
"github.com/wutong-paas/wutong/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
}