0 | 0 |
package gotabulate
|
1 | 1 |
|
2 | |
import "fmt"
|
3 | |
import "bytes"
|
4 | |
import "math"
|
|
2 |
import (
|
|
3 |
"bytes"
|
|
4 |
"fmt"
|
|
5 |
"math"
|
|
6 |
"unicode/utf8"
|
|
7 |
|
|
8 |
"github.com/mattn/go-runewidth"
|
|
9 |
)
|
5 | 10 |
|
6 | 11 |
// Basic Structure of TableFormat
|
7 | 12 |
type TableFormat struct {
|
|
11 | 16 |
LineBottom Line
|
12 | 17 |
HeaderRow Row
|
13 | 18 |
DataRow Row
|
|
19 |
TitleRow Row
|
14 | 20 |
Padding int
|
15 | 21 |
HeaderHide bool
|
16 | 22 |
FitScreen bool
|
|
41 | 47 |
LineBottom: Line{"", "-", " ", ""},
|
42 | 48 |
HeaderRow: Row{"", " ", ""},
|
43 | 49 |
DataRow: Row{"", " ", ""},
|
|
50 |
TitleRow: Row{"", " ", ""},
|
44 | 51 |
Padding: 1,
|
45 | 52 |
},
|
46 | 53 |
"plain": TableFormat{
|
47 | 54 |
HeaderRow: Row{"", " ", ""},
|
48 | 55 |
DataRow: Row{"", " ", ""},
|
|
56 |
TitleRow: Row{"", " ", ""},
|
49 | 57 |
Padding: 1,
|
50 | 58 |
},
|
51 | 59 |
"grid": TableFormat{
|
|
55 | 63 |
LineBottom: Line{"+", "-", "+", "+"},
|
56 | 64 |
HeaderRow: Row{"|", "|", "|"},
|
57 | 65 |
DataRow: Row{"|", "|", "|"},
|
|
66 |
TitleRow: Row{"|", " ", "|"},
|
58 | 67 |
Padding: 1,
|
59 | 68 |
},
|
60 | 69 |
}
|
|
64 | 73 |
|
65 | 74 |
// Main Tabulate structure
|
66 | 75 |
type Tabulate struct {
|
67 | |
Data []*TabulateRow
|
68 | |
Headers []string
|
69 | |
FloatFormat byte
|
70 | |
TableFormat TableFormat
|
71 | |
Align string
|
72 | |
EmptyVar string
|
73 | |
HideLines []string
|
74 | |
MaxSize int
|
75 | |
WrapStrings bool
|
|
76 |
Data []*TabulateRow
|
|
77 |
Title string
|
|
78 |
TitleAlign string
|
|
79 |
Headers []string
|
|
80 |
FloatFormat byte
|
|
81 |
TableFormat TableFormat
|
|
82 |
Align string
|
|
83 |
EmptyVar string
|
|
84 |
HideLines []string
|
|
85 |
MaxSize int
|
|
86 |
WrapStrings bool
|
|
87 |
WrapDelimiter rune
|
|
88 |
SplitConcat string
|
|
89 |
DenseMode bool
|
76 | 90 |
}
|
77 | 91 |
|
78 | 92 |
// Represents normalized tabulate Row
|
79 | 93 |
type TabulateRow struct {
|
80 | 94 |
Elements []string
|
81 | 95 |
Continuos bool
|
82 | |
Extra bool
|
|
96 |
}
|
|
97 |
|
|
98 |
type writeBuffer struct {
|
|
99 |
Buffer bytes.Buffer
|
|
100 |
}
|
|
101 |
|
|
102 |
func createBuffer() *writeBuffer {
|
|
103 |
return &writeBuffer{}
|
|
104 |
}
|
|
105 |
|
|
106 |
func (b *writeBuffer) Write(str string, count int) *writeBuffer {
|
|
107 |
for i := 0; i < count; i++ {
|
|
108 |
b.Buffer.WriteString(str)
|
|
109 |
}
|
|
110 |
return b
|
|
111 |
}
|
|
112 |
func (b *writeBuffer) String() string {
|
|
113 |
return b.Buffer.String()
|
83 | 114 |
}
|
84 | 115 |
|
85 | 116 |
// Add padding to each cell
|
|
89 | 120 |
}
|
90 | 121 |
padded := make([]string, len(arr))
|
91 | 122 |
for index, el := range arr {
|
92 | |
var buffer bytes.Buffer
|
93 | |
// Pad left
|
94 | |
for i := 0; i < padding; i++ {
|
95 | |
buffer.WriteString(" ")
|
96 | |
}
|
97 | |
|
98 | |
buffer.WriteString(el)
|
99 | |
|
100 | |
// Pad Right
|
101 | |
for i := 0; i < padding; i++ {
|
102 | |
buffer.WriteString(" ")
|
103 | |
}
|
104 | |
|
105 | |
padded[index] = buffer.String()
|
|
123 |
b := createBuffer()
|
|
124 |
b.Write(" ", padding)
|
|
125 |
b.Write(el, 1)
|
|
126 |
b.Write(" ", padding)
|
|
127 |
padded[index] = b.String()
|
106 | 128 |
}
|
107 | 129 |
return padded
|
108 | 130 |
}
|
109 | 131 |
|
110 | 132 |
// Align right (Add padding left)
|
111 | 133 |
func (t *Tabulate) padLeft(width int, str string) string {
|
112 | |
var buffer bytes.Buffer
|
113 | |
// Pad left
|
114 | |
padding := width - len(str)
|
115 | |
for i := 0; i < padding; i++ {
|
116 | |
buffer.WriteString(" ")
|
117 | |
}
|
118 | |
buffer.WriteString(str)
|
119 | |
return buffer.String()
|
|
134 |
b := createBuffer()
|
|
135 |
b.Write(" ", (width - runewidth.StringWidth(str)))
|
|
136 |
b.Write(str, 1)
|
|
137 |
return b.String()
|
120 | 138 |
}
|
121 | 139 |
|
122 | 140 |
// Align Left (Add padding right)
|
123 | 141 |
func (t *Tabulate) padRight(width int, str string) string {
|
124 | |
var buffer bytes.Buffer
|
125 | |
padding := width - len(str)
|
126 | |
|
127 | |
buffer.WriteString(str)
|
128 | |
|
129 | |
// Add Padding right
|
130 | |
for i := 0; i < padding; i++ {
|
131 | |
buffer.WriteString(" ")
|
132 | |
}
|
133 | |
return buffer.String()
|
|
142 |
b := createBuffer()
|
|
143 |
b.Write(str, 1)
|
|
144 |
b.Write(" ", (width - runewidth.StringWidth(str)))
|
|
145 |
return b.String()
|
134 | 146 |
}
|
135 | 147 |
|
136 | 148 |
// Center the element in the cell
|
137 | 149 |
func (t *Tabulate) padCenter(width int, str string) string {
|
138 | |
var buffer bytes.Buffer
|
139 | |
length := len(str)
|
140 | |
|
141 | |
padding := int(math.Ceil(float64((width - length)) / 2.0))
|
142 | |
|
143 | |
// Add padding left
|
144 | |
for i := 0; i < padding; i++ {
|
145 | |
buffer.WriteString(" ")
|
146 | |
}
|
147 | |
|
148 | |
// Write string
|
149 | |
buffer.WriteString(str)
|
150 | |
|
151 | |
// Calculate how much space is left
|
152 | |
current := (width - len(buffer.String()))
|
153 | |
|
154 | |
// Add padding right
|
155 | |
for i := 0; i < current; i++ {
|
156 | |
buffer.WriteString(" ")
|
157 | |
}
|
158 | |
return buffer.String()
|
|
150 |
b := createBuffer()
|
|
151 |
padding := int(math.Ceil(float64((width - runewidth.StringWidth(str))) / 2.0))
|
|
152 |
b.Write(" ", padding)
|
|
153 |
b.Write(str, 1)
|
|
154 |
b.Write(" ", (width - runewidth.StringWidth(b.String())))
|
|
155 |
|
|
156 |
return b.String()
|
159 | 157 |
}
|
160 | 158 |
|
161 | 159 |
// Build Line based on padded_widths from t.GetWidths()
|
|
163 | 161 |
cells := make([]string, len(padded_widths))
|
164 | 162 |
|
165 | 163 |
for i, _ := range cells {
|
166 | |
var buffer bytes.Buffer
|
167 | |
for j := 0; j < padding[i]+MIN_PADDING; j++ {
|
168 | |
buffer.WriteString(l.hline)
|
169 | |
}
|
170 | |
cells[i] = buffer.String()
|
171 | |
}
|
|
164 |
b := createBuffer()
|
|
165 |
b.Write(l.hline, padding[i]+MIN_PADDING)
|
|
166 |
cells[i] = b.String()
|
|
167 |
}
|
|
168 |
|
172 | 169 |
var buffer bytes.Buffer
|
173 | |
|
174 | |
// Print begin
|
175 | 170 |
buffer.WriteString(l.begin)
|
176 | 171 |
|
177 | 172 |
// Print contents
|
178 | 173 |
for i := 0; i < len(cells); i++ {
|
|
174 |
buffer.WriteString(cells[i])
|
179 | 175 |
if i != len(cells)-1 {
|
180 | |
buffer.WriteString(cells[i] + l.sep)
|
181 | |
} else {
|
182 | |
buffer.WriteString(cells[i])
|
183 | |
}
|
184 | |
}
|
185 | |
|
186 | |
// Print end
|
|
176 |
buffer.WriteString(l.sep)
|
|
177 |
}
|
|
178 |
}
|
|
179 |
|
187 | 180 |
buffer.WriteString(l.end)
|
188 | 181 |
return buffer.String()
|
189 | 182 |
}
|
|
197 | 190 |
// Print contents
|
198 | 191 |
for i := 0; i < len(padded_widths); i++ {
|
199 | 192 |
output := ""
|
200 | |
if len(elements) > i {
|
|
193 |
if len(elements) <= i || (len(elements) > i && elements[i] == " nil ") {
|
|
194 |
output = padFunc(padded_widths[i], t.EmptyVar)
|
|
195 |
} else if len(elements) > i {
|
201 | 196 |
output = padFunc(padded_widths[i], elements[i])
|
202 | |
} else {
|
203 | |
output = padFunc(padded_widths[i], t.EmptyVar)
|
204 | 197 |
}
|
205 | 198 |
buffer.WriteString(output)
|
206 | 199 |
if i != len(padded_widths)-1 {
|
207 | 200 |
buffer.WriteString(d.sep)
|
208 | 201 |
}
|
209 | 202 |
}
|
210 | |
// Print end
|
|
203 |
|
211 | 204 |
buffer.WriteString(d.end)
|
212 | |
|
213 | 205 |
return buffer.String()
|
|
206 |
}
|
|
207 |
|
|
208 |
//SetWrapDelimiter assigns the character ina string that the rednderer
|
|
209 |
//will attempt to split strings on when a cell must be wrapped
|
|
210 |
func (t *Tabulate) SetWrapDelimiter(r rune) {
|
|
211 |
t.WrapDelimiter = r
|
|
212 |
}
|
|
213 |
|
|
214 |
//SetSplitConcat assigns the character that will be used when a WrapDelimiter is
|
|
215 |
//set but the renderer cannot abide by the desired split. This may happen when
|
|
216 |
//the WrapDelimiter is a space ' ' but a single word is longer than the width of a cell
|
|
217 |
func (t *Tabulate) SetSplitConcat(r string) {
|
|
218 |
t.SplitConcat = r
|
214 | 219 |
}
|
215 | 220 |
|
216 | 221 |
// Render the data table
|
|
218 | 223 |
var lines []string
|
219 | 224 |
|
220 | 225 |
// If headers are set use them, otherwise pop the first row
|
221 | |
if len(t.Headers) < 1 {
|
|
226 |
if len(t.Headers) < 1 && len(t.Data) > 1 {
|
222 | 227 |
t.Headers, t.Data = t.Data[0].Elements, t.Data[1:]
|
223 | 228 |
}
|
224 | 229 |
|
|
235 | 240 |
|
236 | 241 |
// Check if Data is present
|
237 | 242 |
if len(t.Data) < 1 {
|
238 | |
panic("No Data specified")
|
|
243 |
return ""
|
239 | 244 |
}
|
240 | 245 |
|
241 | 246 |
if len(t.Headers) < len(t.Data[0].Elements) {
|
|
255 | 260 |
padded_widths[i] = cols[i] + MIN_PADDING*t.TableFormat.Padding
|
256 | 261 |
}
|
257 | 262 |
|
|
263 |
// Calculate total width of the table
|
|
264 |
totalWidth := len(t.TableFormat.DataRow.sep) * (len(cols) - 1) // Include all but the final separator
|
|
265 |
for _, w := range padded_widths {
|
|
266 |
totalWidth += w
|
|
267 |
}
|
|
268 |
|
258 | 269 |
// Start appending lines
|
|
270 |
if len(t.Title) > 0 {
|
|
271 |
if !inSlice("aboveTitle", t.HideLines) {
|
|
272 |
lines = append(lines, t.buildLine(padded_widths, cols, t.TableFormat.LineTop))
|
|
273 |
}
|
|
274 |
savedAlign := t.Align
|
|
275 |
if len(t.TitleAlign) > 0 {
|
|
276 |
t.SetAlign(t.TitleAlign) // Temporary replace alignment with the title alignment
|
|
277 |
}
|
|
278 |
lines = append(lines, t.buildRow([]string{t.Title}, []int{totalWidth}, nil, t.TableFormat.TitleRow))
|
|
279 |
t.SetAlign(savedAlign)
|
|
280 |
}
|
259 | 281 |
|
260 | 282 |
// Append top line if not hidden
|
261 | 283 |
if !inSlice("top", t.HideLines) {
|
|
273 | 295 |
// Add Data Rows
|
274 | 296 |
for index, element := range t.Data {
|
275 | 297 |
lines = append(lines, t.buildRow(t.padRow(element.Elements, t.TableFormat.Padding), padded_widths, cols, t.TableFormat.DataRow))
|
276 | |
if index < len(t.Data)-1 {
|
277 | |
if element.Continuos != true {
|
|
298 |
if !t.DenseMode && index < len(t.Data)-1 {
|
|
299 |
if element.Continuos != true && !inSlice("betweenLine", t.HideLines) {
|
278 | 300 |
lines = append(lines, t.buildLine(padded_widths, cols, t.TableFormat.LineBetweenRows))
|
279 | 301 |
}
|
280 | 302 |
}
|
|
298 | 320 |
widths := make([]int, len(headers))
|
299 | 321 |
current_max := len(t.EmptyVar)
|
300 | 322 |
for i := 0; i < len(headers); i++ {
|
301 | |
current_max = len(headers[i])
|
|
323 |
current_max = runewidth.StringWidth(headers[i])
|
302 | 324 |
for _, item := range data {
|
303 | 325 |
if len(item.Elements) > i && len(widths) > i {
|
304 | 326 |
element := item.Elements[i]
|
305 | |
if len(element) > current_max {
|
306 | |
widths[i] = len(element)
|
307 | |
current_max = len(element)
|
|
327 |
strLength := runewidth.StringWidth(element)
|
|
328 |
if strLength > current_max {
|
|
329 |
widths[i] = strLength
|
|
330 |
current_max = strLength
|
308 | 331 |
} else {
|
309 | 332 |
widths[i] = current_max
|
310 | 333 |
}
|
|
313 | 336 |
}
|
314 | 337 |
|
315 | 338 |
return widths
|
|
339 |
}
|
|
340 |
|
|
341 |
// SetTitle sets the title of the table can also accept a second string to define an alignment for the title
|
|
342 |
func (t *Tabulate) SetTitle(title ...string) *Tabulate {
|
|
343 |
|
|
344 |
t.Title = title[0]
|
|
345 |
if len(title) > 1 {
|
|
346 |
t.TitleAlign = title[1]
|
|
347 |
}
|
|
348 |
|
|
349 |
return t
|
316 | 350 |
}
|
317 | 351 |
|
318 | 352 |
// Set Headers of the table
|
|
354 | 388 |
// Can be:
|
355 | 389 |
// top - Top line of the table,
|
356 | 390 |
// belowheader - Line below the header,
|
357 | |
// bottom - Bottom line of the table
|
|
391 |
// bottomLine - Bottom line of the table
|
|
392 |
// betweenLine - Between line of the table
|
358 | 393 |
func (t *Tabulate) SetHideLines(hide []string) {
|
359 | 394 |
t.HideLines = hide
|
360 | 395 |
}
|
|
370 | 405 |
t.MaxSize = max
|
371 | 406 |
}
|
372 | 407 |
|
|
408 |
// Sets dense mode
|
|
409 |
// Under dense mode, no space line between rows
|
|
410 |
func (t *Tabulate) SetDenseMode() {
|
|
411 |
t.DenseMode = true
|
|
412 |
}
|
|
413 |
|
|
414 |
func (t *Tabulate) splitElement(e string) (bool, string) {
|
|
415 |
//check if we are not attempting to smartly wrap
|
|
416 |
if t.WrapDelimiter == 0 {
|
|
417 |
if t.SplitConcat == "" {
|
|
418 |
return false, runewidth.Truncate(e, t.MaxSize, "")
|
|
419 |
} else {
|
|
420 |
return false, runewidth.Truncate(e, t.MaxSize, t.SplitConcat)
|
|
421 |
}
|
|
422 |
}
|
|
423 |
|
|
424 |
//we are attempting to wrap
|
|
425 |
//grab the current width
|
|
426 |
var i int
|
|
427 |
for i = t.MaxSize; i > 1; i-- {
|
|
428 |
//loop through our proposed truncation size looking for one that ends on
|
|
429 |
//our requested delimiter
|
|
430 |
x := runewidth.Truncate(e, i, "")
|
|
431 |
//check if the NEXT string is a
|
|
432 |
//delimiter, if it IS, then we truncate and tell the caller to shrink
|
|
433 |
r, _ := utf8.DecodeRuneInString(e[i:])
|
|
434 |
if r == 0 || r == 1 {
|
|
435 |
//decode failed, take the truncation as is
|
|
436 |
return false, x
|
|
437 |
}
|
|
438 |
if r == t.WrapDelimiter {
|
|
439 |
return true, x //inform the caller that they can remove the next rune
|
|
440 |
}
|
|
441 |
}
|
|
442 |
//didn't find a good length, truncate at will
|
|
443 |
if t.SplitConcat != "" {
|
|
444 |
return false, runewidth.Truncate(e, t.MaxSize, t.SplitConcat)
|
|
445 |
}
|
|
446 |
return false, runewidth.Truncate(e, t.MaxSize, "")
|
|
447 |
}
|
|
448 |
|
373 | 449 |
// If string size is larger than t.MaxSize, then split it to multiple cells (downwards)
|
374 | 450 |
func (t *Tabulate) wrapCellData() []*TabulateRow {
|
375 | 451 |
var arr []*TabulateRow
|
|
452 |
var cleanSplit bool
|
|
453 |
var addr int
|
|
454 |
if len(t.Data) == 0 {
|
|
455 |
return arr
|
|
456 |
}
|
376 | 457 |
next := t.Data[0]
|
377 | 458 |
for index := 0; index <= len(t.Data); index++ {
|
378 | 459 |
elements := next.Elements
|
379 | 460 |
new_elements := make([]string, len(elements))
|
380 | 461 |
|
381 | 462 |
for i, e := range elements {
|
382 | |
if len(e) > t.MaxSize {
|
383 | |
new_elements[i] = e[t.MaxSize:]
|
384 | |
elements[i] = e[:t.MaxSize]
|
|
463 |
if runewidth.StringWidth(e) > t.MaxSize {
|
|
464 |
elements[i] = runewidth.Truncate(e, t.MaxSize, "")
|
|
465 |
cleanSplit, elements[i] = t.splitElement(e)
|
|
466 |
if cleanSplit {
|
|
467 |
//remove the next rune
|
|
468 |
r, w := utf8.DecodeRuneInString(e[len(elements[i]):])
|
|
469 |
if r != 0 && r != 1 {
|
|
470 |
addr = w
|
|
471 |
}
|
|
472 |
} else {
|
|
473 |
addr = 0
|
|
474 |
}
|
|
475 |
new_elements[i] = e[len(elements[i])+addr:]
|
385 | 476 |
next.Continuos = true
|
386 | 477 |
}
|
387 | 478 |
}
|
388 | 479 |
|
389 | 480 |
if next.Continuos {
|
390 | 481 |
arr = append(arr, next)
|
391 | |
next = &TabulateRow{Elements: new_elements, Extra: true}
|
|
482 |
next = &TabulateRow{Elements: new_elements}
|
392 | 483 |
index--
|
393 | |
} else if next.Extra && index+1 < len(t.Data) {
|
394 | |
arr = append(arr, next)
|
395 | |
next = t.Data[index+1]
|
396 | 484 |
} else if index+1 < len(t.Data) {
|
397 | 485 |
arr = append(arr, next)
|
398 | 486 |
next = t.Data[index+1]
|