github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/utils/outputwriter/markdowntable.go (about)

     1  package outputwriter
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  )
     7  
     8  const (
     9  	tableRowFirstColumnSeparator = "| :---------------------: |"
    10  	tableRowColumnSeparator      = " :-----------------------------------: |"
    11  	cellFirstCellPlaceholder     = "| %s                |"
    12  	cellCellPlaceholder          = " %s                  |"
    13  	cellDefaultValue             = "-"
    14  
    15  	// (Default value for columns) If more than one value exists in a cell, the values will be separated by the delimiter.
    16  	SeparatorDelimited MarkdownColumnType = "single"
    17  	// If more than one value exists in a cell, for each value a new row will be created.
    18  	// The first row will contain the other columns values, and the rest of the rows will contain the values of the multi value column only.
    19  	// Only works if exists up to one MultiRowColumn in the table.
    20  	MultiRowColumn MarkdownColumnType = "multi"
    21  )
    22  
    23  // Create a markdown table using the provided columns and rows, and construct a markdown string with the table's content.
    24  // Each cell in the table can contain no values (represented as column default value), single or multiple values (separated by the table delimiter).
    25  type MarkdownTableBuilder struct {
    26  	columns   []*MarkdownColumn
    27  	delimiter string
    28  	rows      [][]CellData
    29  }
    30  
    31  type MarkdownColumnType string
    32  
    33  type MarkdownColumn struct {
    34  	Name         string
    35  	ColumnType   MarkdownColumnType
    36  	DefaultValue string
    37  }
    38  
    39  // CellData represents the data of a cell in the markdown table. Each cell can contain multiple values.
    40  type CellData []string
    41  
    42  func NewCellData(values ...string) CellData {
    43  	if len(values) == 0 {
    44  		// In markdown table, empty cell = cell with no values = cell with one empty value
    45  		return CellData{""}
    46  	}
    47  	return values
    48  }
    49  
    50  // Create a markdown table builder with the provided columns.
    51  func NewMarkdownTable(columns ...string) *MarkdownTableBuilder {
    52  	columnsInfo := []*MarkdownColumn{}
    53  	for _, column := range columns {
    54  		columnsInfo = append(columnsInfo, &MarkdownColumn{Name: column, ColumnType: SeparatorDelimited, DefaultValue: cellDefaultValue})
    55  	}
    56  	return &MarkdownTableBuilder{columns: columnsInfo, delimiter: simpleSeparator}
    57  }
    58  
    59  // Set the delimiter that will be used to separate multiple values in a cell.
    60  func (t *MarkdownTableBuilder) SetDelimiter(delimiter string) *MarkdownTableBuilder {
    61  	t.delimiter = delimiter
    62  	return t
    63  }
    64  
    65  // Get the column information output controller by the provided name.
    66  func (t *MarkdownTableBuilder) GetColumnInfo(name string) *MarkdownColumn {
    67  	for _, column := range t.columns {
    68  		if column.Name == name {
    69  			return column
    70  		}
    71  	}
    72  	return nil
    73  }
    74  
    75  // Add a row to the markdown table, each value will be added to the corresponding column.
    76  // Use to add row with single value columns only.
    77  func (t *MarkdownTableBuilder) AddRow(values ...string) *MarkdownTableBuilder {
    78  	if len(values) == 0 {
    79  		return t
    80  	}
    81  	cellData := []CellData{}
    82  	for _, value := range values {
    83  		cellData = append(cellData, NewCellData(value))
    84  	}
    85  	return t.AddRowWithCellData(cellData...)
    86  }
    87  
    88  // Add a row to the markdown table, each value will be added to the corresponding column.
    89  // Use to add row with multiple value columns.
    90  func (t *MarkdownTableBuilder) AddRowWithCellData(values ...CellData) *MarkdownTableBuilder {
    91  	if len(values) == 0 {
    92  		return t
    93  	}
    94  	nColumns := len(t.columns)
    95  	row := make([]CellData, nColumns)
    96  
    97  	for c := 0; c < nColumns; c++ {
    98  		if c < len(values) {
    99  			row[c] = values[c]
   100  		} else {
   101  			row[c] = NewCellData()
   102  		}
   103  	}
   104  
   105  	t.rows = append(t.rows, row)
   106  	return t
   107  }
   108  
   109  func (t *MarkdownTableBuilder) Build() string {
   110  	if len(t.columns) == 0 {
   111  		return ""
   112  	}
   113  	var tableBuilder strings.Builder
   114  	// Header
   115  	for c, column := range t.columns {
   116  		if c == 0 {
   117  			tableBuilder.WriteString(fmt.Sprintf(cellFirstCellPlaceholder, column.Name))
   118  		} else {
   119  			tableBuilder.WriteString(fmt.Sprintf(cellCellPlaceholder, column.Name))
   120  		}
   121  	}
   122  	tableBuilder.WriteString("\n")
   123  	// Separator
   124  	for c := range t.columns {
   125  		if c == 0 {
   126  			tableBuilder.WriteString(tableRowFirstColumnSeparator)
   127  		} else {
   128  			tableBuilder.WriteString(tableRowColumnSeparator)
   129  		}
   130  	}
   131  	// Content
   132  	for _, row := range t.rows {
   133  		tableBuilder.WriteString(t.getRowContent(row))
   134  	}
   135  	return tableBuilder.String()
   136  }
   137  
   138  func (t *MarkdownTableBuilder) getRowContent(row []CellData) string {
   139  	if columnIndex, multiValueColumn := t.getMultiValueColumn(); multiValueColumn != nil && len(row[columnIndex]) > 1 {
   140  		return t.getMultiValueRowsContent(row, columnIndex)
   141  	}
   142  	return t.getSeparatorDelimitedRowContent(row)
   143  }
   144  
   145  func (t *MarkdownTableBuilder) getMultiValueColumn() (int, *MarkdownColumn) {
   146  	for i, column := range t.columns {
   147  		if column.ColumnType == MultiRowColumn {
   148  			return i, column
   149  		}
   150  	}
   151  	return -1, nil
   152  }
   153  
   154  func (t *MarkdownTableBuilder) getMultiValueRowsContent(row []CellData, multiValueColumnIndex int) string {
   155  	var rowBuilder strings.Builder
   156  	firstRow := true
   157  	for _, value := range row[multiValueColumnIndex] {
   158  		// Add row for each value in the multi values column
   159  		if len(value) == 0 {
   160  			continue
   161  		}
   162  		content := []string{}
   163  		for column, cell := range row {
   164  			if column == multiValueColumnIndex {
   165  				// Multi values column separated by different rows, add the specific value for this row
   166  				content = append(content, value)
   167  			} else {
   168  				if firstRow {
   169  					// First row contains the other columns values as well
   170  					content = append(content, t.getCellContent(cell, t.columns[column].DefaultValue))
   171  				} else {
   172  					// Rest of the rows contains only the multi values column value
   173  					content = append(content, " ")
   174  				}
   175  			}
   176  		}
   177  		firstRow = false
   178  		rowBuilder.WriteString(buildRowContent(content...))
   179  	}
   180  	return rowBuilder.String()
   181  }
   182  
   183  func (t *MarkdownTableBuilder) getSeparatorDelimitedRowContent(row []CellData) string {
   184  	content := []string{}
   185  	for column, columnInfo := range t.columns {
   186  		content = append(content, t.getCellContent(row[column], columnInfo.DefaultValue))
   187  	}
   188  	return buildRowContent(content...)
   189  }
   190  
   191  func buildRowContent(content ...string) string {
   192  	if len(content) == 0 {
   193  		return ""
   194  	}
   195  	var rowBuilder strings.Builder
   196  	rowBuilder.WriteString("\n")
   197  	for c, cell := range content {
   198  		if c == 0 {
   199  			rowBuilder.WriteString(fmt.Sprintf("| %s |", cell))
   200  		} else {
   201  			rowBuilder.WriteString(fmt.Sprintf(" %s |", cell))
   202  		}
   203  	}
   204  	return rowBuilder.String()
   205  }
   206  
   207  func (t *MarkdownTableBuilder) getCellContent(data CellData, defaultValue string) string {
   208  	if len(data) == 0 {
   209  		return defaultValue
   210  	}
   211  	var cellBuilder strings.Builder
   212  	for _, value := range data {
   213  		value = strings.TrimSpace(value)
   214  		if value == "" {
   215  			continue
   216  		}
   217  		cellBuilder.WriteString(fmt.Sprintf("%s%s", value, t.delimiter))
   218  	}
   219  	value := strings.TrimSuffix(cellBuilder.String(), t.delimiter)
   220  	if value == "" {
   221  		return defaultValue
   222  	}
   223  	return value
   224  }