1 package table
2
3
4
5 import (
6 "reflect"
7 "strings"
8 "unicode/utf8"
9 )
10
11 type AlignType uint
12
13 const (
14 AlignTypeLeft AlignType = iota
15 AlignTypeCenter
16 AlignTypeRight
17 )
18
19 type Divider string
20
21 type Row struct {
22 Cells []Cell
23 Divider string
24 Style string
25 }
26
27 func R(args ...interface{}) *Row {
28 r := &Row{
29 Divider: "-",
30 }
31 for _, arg := range args {
32 switch reflect.TypeOf(arg) {
33 case reflect.TypeOf(Divider("")):
34 r.Divider = string(arg.(Divider))
35 case reflect.TypeOf(r.Style):
36 r.Style = arg.(string)
37 case reflect.TypeOf(Cell{}):
38 r.Cells = append(r.Cells, arg.(Cell))
39 }
40 }
41 return r
42 }
43
44 func (r *Row) AppendCell(cells ...Cell) *Row {
45 r.Cells = append(r.Cells, cells...)
46 return r
47 }
48
49 func (r *Row) Render(widths []int, totalWidth int, tableStyle TableStyle, isLastRow bool) string {
50 out := ""
51 if len(r.Cells) == 1 {
52 out += strings.Join(r.Cells[0].render(totalWidth, r.Style, tableStyle), "\n") + "\n"
53 } else {
54 if len(r.Cells) != len(widths) {
55 panic("row vs width mismatch")
56 }
57 renderedCells := make([][]string, len(r.Cells))
58 maxHeight := 0
59 for colIdx, cell := range r.Cells {
60 renderedCells[colIdx] = cell.render(widths[colIdx], r.Style, tableStyle)
61 if len(renderedCells[colIdx]) > maxHeight {
62 maxHeight = len(renderedCells[colIdx])
63 }
64 }
65 for colIdx := range r.Cells {
66 for len(renderedCells[colIdx]) < maxHeight {
67 renderedCells[colIdx] = append(renderedCells[colIdx], strings.Repeat(" ", widths[colIdx]))
68 }
69 }
70 border := strings.Repeat(" ", tableStyle.Padding)
71 if tableStyle.VerticalBorders {
72 border += "|" + border
73 }
74 for lineIdx := 0; lineIdx < maxHeight; lineIdx++ {
75 for colIdx := range r.Cells {
76 out += renderedCells[colIdx][lineIdx]
77 if colIdx < len(r.Cells)-1 {
78 out += border
79 }
80 }
81 out += "\n"
82 }
83 }
84 if tableStyle.HorizontalBorders && !isLastRow && r.Divider != "" {
85 out += strings.Repeat(string(r.Divider), totalWidth) + "\n"
86 }
87
88 return out
89 }
90
91 type Cell struct {
92 Contents []string
93 Style string
94 Align AlignType
95 }
96
97 func C(contents string, args ...interface{}) Cell {
98 c := Cell{
99 Contents: strings.Split(contents, "\n"),
100 }
101 for _, arg := range args {
102 switch reflect.TypeOf(arg) {
103 case reflect.TypeOf(c.Style):
104 c.Style = arg.(string)
105 case reflect.TypeOf(c.Align):
106 c.Align = arg.(AlignType)
107 }
108 }
109 return c
110 }
111
112 func (c Cell) Width() (int, int) {
113 w, minW := 0, 0
114 for _, line := range c.Contents {
115 lineWidth := utf8.RuneCountInString(line)
116 if lineWidth > w {
117 w = lineWidth
118 }
119 for _, word := range strings.Split(line, " ") {
120 wordWidth := utf8.RuneCountInString(word)
121 if wordWidth > minW {
122 minW = wordWidth
123 }
124 }
125 }
126 return w, minW
127 }
128
129 func (c Cell) alignLine(line string, width int) string {
130 lineWidth := utf8.RuneCountInString(line)
131 if lineWidth == width {
132 return line
133 }
134 if lineWidth < width {
135 gap := width - lineWidth
136 switch c.Align {
137 case AlignTypeLeft:
138 return line + strings.Repeat(" ", gap)
139 case AlignTypeRight:
140 return strings.Repeat(" ", gap) + line
141 case AlignTypeCenter:
142 leftGap := gap / 2
143 rightGap := gap - leftGap
144 return strings.Repeat(" ", leftGap) + line + strings.Repeat(" ", rightGap)
145 }
146 }
147 return line
148 }
149
150 func (c Cell) splitWordToWidth(word string, width int) []string {
151 out := []string{}
152 n, subWord := 0, ""
153 for _, c := range word {
154 subWord += string(c)
155 n += 1
156 if n == width-1 {
157 out = append(out, subWord+"-")
158 n, subWord = 0, ""
159 }
160 }
161 return out
162 }
163
164 func (c Cell) splitToWidth(line string, width int) []string {
165 lineWidth := utf8.RuneCountInString(line)
166 if lineWidth <= width {
167 return []string{line}
168 }
169
170 outLines := []string{}
171 words := strings.Split(line, " ")
172 outWords := []string{words[0]}
173 length := utf8.RuneCountInString(words[0])
174 if length > width {
175 splitWord := c.splitWordToWidth(words[0], width)
176 lastIdx := len(splitWord) - 1
177 outLines = append(outLines, splitWord[:lastIdx]...)
178 outWords = []string{splitWord[lastIdx]}
179 length = utf8.RuneCountInString(splitWord[lastIdx])
180 }
181
182 for _, word := range words[1:] {
183 wordLength := utf8.RuneCountInString(word)
184 if length+wordLength+1 <= width {
185 length += wordLength + 1
186 outWords = append(outWords, word)
187 continue
188 }
189 outLines = append(outLines, strings.Join(outWords, " "))
190
191 outWords = []string{word}
192 length = wordLength
193 if length > width {
194 splitWord := c.splitWordToWidth(word, width)
195 lastIdx := len(splitWord) - 1
196 outLines = append(outLines, splitWord[:lastIdx]...)
197 outWords = []string{splitWord[lastIdx]}
198 length = utf8.RuneCountInString(splitWord[lastIdx])
199 }
200 }
201 if len(outWords) > 0 {
202 outLines = append(outLines, strings.Join(outWords, " "))
203 }
204
205 return outLines
206 }
207
208 func (c Cell) render(width int, style string, tableStyle TableStyle) []string {
209 out := []string{}
210 for _, line := range c.Contents {
211 out = append(out, c.splitToWidth(line, width)...)
212 }
213 for idx := range out {
214 out[idx] = c.alignLine(out[idx], width)
215 }
216
217 if tableStyle.EnableTextStyling {
218 style = style + c.Style
219 if style != "" {
220 for idx := range out {
221 out[idx] = style + out[idx] + "{{/}}"
222 }
223 }
224 }
225
226 return out
227 }
228
229 type TableStyle struct {
230 Padding int
231 VerticalBorders bool
232 HorizontalBorders bool
233 MaxTableWidth int
234 MaxColWidth int
235 EnableTextStyling bool
236 }
237
238 var DefaultTableStyle = TableStyle{
239 Padding: 1,
240 VerticalBorders: true,
241 HorizontalBorders: true,
242 MaxTableWidth: 120,
243 MaxColWidth: 40,
244 EnableTextStyling: true,
245 }
246
247 type Table struct {
248 Rows []*Row
249
250 TableStyle TableStyle
251 }
252
253 func NewTable() *Table {
254 return &Table{
255 TableStyle: DefaultTableStyle,
256 }
257 }
258
259 func (t *Table) AppendRow(row *Row) *Table {
260 t.Rows = append(t.Rows, row)
261 return t
262 }
263
264 func (t *Table) Render() string {
265 out := ""
266 totalWidth, widths := t.computeWidths()
267 for rowIdx, row := range t.Rows {
268 out += row.Render(widths, totalWidth, t.TableStyle, rowIdx == len(t.Rows)-1)
269 }
270 return out
271 }
272
273 func (t *Table) computeWidths() (int, []int) {
274 nCol := 0
275 for _, row := range t.Rows {
276 if len(row.Cells) > nCol {
277 nCol = len(row.Cells)
278 }
279 }
280
281
282 borderWidth := t.TableStyle.Padding
283 if t.TableStyle.VerticalBorders {
284 borderWidth += 1 + t.TableStyle.Padding
285 }
286 totalBorderWidth := borderWidth * (nCol - 1)
287
288
289 widths := make([]int, nCol)
290 minWidths := make([]int, nCol)
291 for colIdx := range widths {
292 for _, row := range t.Rows {
293 if colIdx >= len(row.Cells) {
294
295 continue
296 }
297 w, minWid := row.Cells[colIdx].Width()
298 if w > widths[colIdx] {
299 widths[colIdx] = w
300 }
301 if minWid > minWidths[colIdx] {
302 minWidths[colIdx] = minWid
303 }
304 }
305 }
306
307
308 if sum(widths)+totalBorderWidth <= t.TableStyle.MaxTableWidth {
309
310 return sum(widths) + totalBorderWidth, widths
311 }
312
313
314 for colIdx := range widths {
315 widths[colIdx] = min(widths[colIdx], t.TableStyle.MaxColWidth)
316 minWidths[colIdx] = min(minWidths[colIdx], t.TableStyle.MaxColWidth)
317 }
318
319
320 if sum(widths)+totalBorderWidth <= t.TableStyle.MaxTableWidth {
321
322 return sum(widths) + totalBorderWidth, widths
323 }
324
325
326 if sum(minWidths)+totalBorderWidth >= t.TableStyle.MaxTableWidth {
327
328 return sum(minWidths) + totalBorderWidth, minWidths
329 }
330
331
332
333 n := 0
334 for sum(widths)+totalBorderWidth > t.TableStyle.MaxTableWidth {
335 budget := t.TableStyle.MaxTableWidth - totalBorderWidth
336 baseline := sum(widths)
337
338 for colIdx := range widths {
339 widths[colIdx] = max((widths[colIdx]*budget)/baseline, minWidths[colIdx])
340 }
341 n += 1
342 if n > 100 {
343 break
344 }
345 }
346
347 return sum(widths) + totalBorderWidth, widths
348 }
349
350 func sum(s []int) int {
351 out := 0
352 for _, v := range s {
353 out += v
354 }
355 return out
356 }
357
358 func min(a int, b int) int {
359 if a < b {
360 return a
361 }
362 return b
363 }
364
365 func max(a int, b int) int {
366 if a > b {
367 return a
368 }
369 return b
370 }
371
View as plain text