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 }