github.com/onsi/gomega@v1.32.0/gmeasure/table/table.go (about) 1 package table 2 3 // This is a temporary package - Table will move to github.com/onsi/consolable once some more dust settles 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 // lets compute the contribution to width from the borders + padding 282 borderWidth := t.TableStyle.Padding 283 if t.TableStyle.VerticalBorders { 284 borderWidth += 1 + t.TableStyle.Padding 285 } 286 totalBorderWidth := borderWidth * (nCol - 1) 287 288 // lets compute the width of each column 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 // ignore rows with fewer columns 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 // do we already fit? 308 if sum(widths)+totalBorderWidth <= t.TableStyle.MaxTableWidth { 309 // yes! we're done 310 return sum(widths) + totalBorderWidth, widths 311 } 312 313 // clamp the widths and minWidths to MaxColWidth 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 // do we fit now? 320 if sum(widths)+totalBorderWidth <= t.TableStyle.MaxTableWidth { 321 // yes! we're done 322 return sum(widths) + totalBorderWidth, widths 323 } 324 325 // hmm... still no... can we possibly squeeze the table in without violating minWidths? 326 if sum(minWidths)+totalBorderWidth >= t.TableStyle.MaxTableWidth { 327 // nope - we're just going to have to exceed MaxTableWidth 328 return sum(minWidths) + totalBorderWidth, minWidths 329 } 330 331 // looks like we don't fit yet, but we should be able to fit without violating minWidths 332 // lets start scaling down 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 // in case we somehow fail to converge 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 }