...

Source file src/github.com/onsi/gomega/gmeasure/table/table.go

Documentation: github.com/onsi/gomega/gmeasure/table

     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  }
   371  

View as plain text