• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jedib0t / go-pretty / 19480081140

18 Nov 2025 08:20PM UTC coverage: 99.952% (+0.05%) from 99.903%
19480081140

push

github

jedib0t
table: escSeqToSpanConverter

106 of 106 new or added lines in 2 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

4133 of 4135 relevant lines covered (99.95%)

1.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.9
/table/render_html.go
1
package table
2

3
import (
4
        "fmt"
5
        "html"
6
        "strings"
7

8
        "github.com/jedib0t/go-pretty/v6/text"
9
)
10

11
const (
12
        // DefaultHTMLCSSClass stores the css-class to use when none-provided via
13
        // SetHTMLCSSClass(cssClass string).
14
        DefaultHTMLCSSClass = "go-pretty-table"
15
)
16

17
// RenderHTML renders the Table in HTML format. Example:
18
//
19
//        <table class="go-pretty-table">
20
//          <thead>
21
//          <tr>
22
//            <th align="right">#</th>
23
//            <th>First Name</th>
24
//            <th>Last Name</th>
25
//            <th align="right">Salary</th>
26
//            <th>&nbsp;</th>
27
//          </tr>
28
//          </thead>
29
//          <tbody>
30
//          <tr>
31
//            <td align="right">1</td>
32
//            <td>Arya</td>
33
//            <td>Stark</td>
34
//            <td align="right">3000</td>
35
//            <td>&nbsp;</td>
36
//          </tr>
37
//          <tr>
38
//            <td align="right">20</td>
39
//            <td>Jon</td>
40
//            <td>Snow</td>
41
//            <td align="right">2000</td>
42
//            <td>You know nothing, Jon Snow!</td>
43
//          </tr>
44
//          <tr>
45
//            <td align="right">300</td>
46
//            <td>Tyrion</td>
47
//            <td>Lannister</td>
48
//            <td align="right">5000</td>
49
//            <td>&nbsp;</td>
50
//          </tr>
51
//          </tbody>
52
//          <tfoot>
53
//          <tr>
54
//            <td align="right">&nbsp;</td>
55
//            <td>&nbsp;</td>
56
//            <td>Total</td>
57
//            <td align="right">10000</td>
58
//            <td>&nbsp;</td>
59
//          </tr>
60
//          </tfoot>
61
//        </table>
62
func (t *Table) RenderHTML() string {
1✔
63
        t.initForRender()
1✔
64

1✔
65
        var out strings.Builder
1✔
66
        if t.numColumns > 0 {
2✔
67
                out.WriteString("<table class=\"")
1✔
68
                if t.htmlCSSClass != "" {
2✔
69
                        out.WriteString(t.htmlCSSClass)
1✔
70
                } else {
2✔
71
                        out.WriteString(t.style.HTML.CSSClass)
1✔
72
                }
1✔
73
                out.WriteString("\">\n")
1✔
74
                t.htmlRenderTitle(&out)
1✔
75
                t.htmlRenderRowsHeader(&out)
1✔
76
                t.htmlRenderRows(&out, t.rows, renderHint{})
1✔
77
                t.htmlRenderRowsFooter(&out)
1✔
78
                t.htmlRenderCaption(&out)
1✔
79
                out.WriteString("</table>")
1✔
80
        }
81
        return t.render(&out)
1✔
82
}
83

84
func (t *Table) htmlGetColStrAndTag(row rowStr, colIdx int, hint renderHint) (string, string) {
1✔
85
        // get the column contents
1✔
86
        var colStr string
1✔
87
        if colIdx < len(row) {
2✔
88
                colStr = row[colIdx]
1✔
89
        }
1✔
90

91
        // header uses "th" instead of "td"
92
        colTagName := "td"
1✔
93
        if hint.isHeaderRow {
2✔
94
                colTagName = "th"
1✔
95
        }
1✔
96

97
        return colStr, colTagName
1✔
98
}
99

100
func (t *Table) htmlRenderCaption(out *strings.Builder) {
1✔
101
        if t.caption != "" {
2✔
102
                out.WriteString("  <caption class=\"caption\" style=\"caption-side: bottom;\">")
1✔
103
                out.WriteString(t.caption)
1✔
104
                out.WriteString("</caption>\n")
1✔
105
        }
1✔
106
}
107

108
func (t *Table) htmlRenderColumn(out *strings.Builder, colStr string) {
1✔
109
        // convertEscSequencesToSpans already escapes text content, so skip
1✔
110
        // EscapeText if ConvertColorsToSpans is true
1✔
111
        if t.style.HTML.ConvertColorsToSpans {
2✔
112
                colStr = convertEscSequencesToSpans(colStr)
1✔
113
        } else if t.style.HTML.EscapeText {
2✔
UNCOV
114
                colStr = html.EscapeString(colStr)
×
UNCOV
115
        }
×
116
        if t.style.HTML.Newline != "\n" {
2✔
117
                colStr = strings.ReplaceAll(colStr, "\n", t.style.HTML.Newline)
1✔
118
        }
1✔
119
        out.WriteString(colStr)
1✔
120
}
121

122
func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, colIdx int, hint renderHint, alignOverride text.Align) {
1✔
123
        // determine the HTML "align"/"valign" property values
1✔
124
        align := alignOverride.HTMLProperty()
1✔
125
        vAlign := t.getVAlign(colIdx, hint).HTMLProperty()
1✔
126
        // determine the HTML "class" property values for the colors
1✔
127
        class := t.getColumnColors(colIdx, hint).HTMLProperty()
1✔
128

1✔
129
        if align != "" {
2✔
130
                out.WriteRune(' ')
1✔
131
                out.WriteString(align)
1✔
132
        }
1✔
133
        if class != "" {
2✔
134
                out.WriteRune(' ')
1✔
135
                out.WriteString(class)
1✔
136
        }
1✔
137
        if vAlign != "" {
2✔
138
                out.WriteRune(' ')
1✔
139
                out.WriteString(vAlign)
1✔
140
        }
1✔
141
}
142

143
func (t *Table) htmlRenderColumnAutoIndex(out *strings.Builder, hint renderHint) {
1✔
144
        if hint.isHeaderRow {
2✔
145
                out.WriteString("    <th>")
1✔
146
                out.WriteString(t.style.HTML.EmptyColumn)
1✔
147
                out.WriteString("</th>\n")
1✔
148
        } else if hint.isFooterRow {
3✔
149
                out.WriteString("    <td>")
1✔
150
                out.WriteString(t.style.HTML.EmptyColumn)
1✔
151
                out.WriteString("</td>\n")
1✔
152
        } else {
2✔
153
                out.WriteString("    <td align=\"right\">")
1✔
154
                out.WriteString(fmt.Sprint(hint.rowNumber))
1✔
155
                out.WriteString("</td>\n")
1✔
156
        }
1✔
157
}
158

159
func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) {
1✔
160
        out.WriteString("  <tr>\n")
1✔
161
        for colIdx := 0; colIdx < t.numColumns; colIdx++ {
2✔
162
                // auto-index column
1✔
163
                if colIdx == 0 && t.autoIndex {
2✔
164
                        t.htmlRenderColumnAutoIndex(out, hint)
1✔
165
                }
1✔
166
                // auto-merged columns should be skipped
167
                if t.shouldMergeCellsVerticallyAbove(colIdx, hint) {
2✔
168
                        continue
1✔
169
                }
170

171
                align := t.getAlign(colIdx, hint)
1✔
172
                rowConfig := t.getRowConfig(hint)
1✔
173
                extraColumnsRendered := 0
1✔
174
                if rowConfig.AutoMerge && !hint.isSeparatorRow {
2✔
175
                        // get the real row to consider all lines in each column instead of just
1✔
176
                        // looking at the current "line"
1✔
177
                        rowUnwrapped := t.getRow(hint.rowNumber-1, hint)
1✔
178
                        for idx := colIdx + 1; idx < len(rowUnwrapped); idx++ {
2✔
179
                                if rowUnwrapped[colIdx] != rowUnwrapped[idx] {
2✔
180
                                        break
1✔
181
                                }
182
                                align = rowConfig.getAutoMergeAlign()
1✔
183
                                extraColumnsRendered++
1✔
184
                        }
185
                }
186

187
                colStr, colTagName := t.htmlGetColStrAndTag(row, colIdx, hint)
1✔
188
                // write the row
1✔
189
                out.WriteString("    <")
1✔
190
                out.WriteString(colTagName)
1✔
191
                t.htmlRenderColumnAttributes(out, colIdx, hint, align)
1✔
192
                if extraColumnsRendered > 0 {
2✔
193
                        out.WriteString(" colspan=")
1✔
194
                        out.WriteString(fmt.Sprint(extraColumnsRendered + 1))
1✔
195
                } else if rowSpan := t.shouldMergeCellsVerticallyBelow(colIdx, hint); rowSpan > 1 {
3✔
196
                        out.WriteString(" rowspan=")
1✔
197
                        out.WriteString(fmt.Sprint(rowSpan))
1✔
198
                }
1✔
199
                out.WriteString(">")
1✔
200
                if len(colStr) == 0 {
2✔
201
                        out.WriteString(t.style.HTML.EmptyColumn)
1✔
202
                } else {
2✔
203
                        t.htmlRenderColumn(out, colStr)
1✔
204
                }
1✔
205
                out.WriteString("</")
1✔
206
                out.WriteString(colTagName)
1✔
207
                out.WriteString(">\n")
1✔
208
                colIdx += extraColumnsRendered
1✔
209
        }
210
        out.WriteString("  </tr>\n")
1✔
211
}
212

213
func (t *Table) htmlRenderRows(out *strings.Builder, rows []rowStr, hint renderHint) {
1✔
214
        if len(rows) > 0 {
2✔
215
                // determine that tag to use based on the type of the row
1✔
216
                rowsTag := "tbody"
1✔
217
                if hint.isHeaderRow {
2✔
218
                        rowsTag = "thead"
1✔
219
                } else if hint.isFooterRow {
3✔
220
                        rowsTag = "tfoot"
1✔
221
                }
1✔
222

223
                var renderedTagOpen, shouldRenderTagClose bool
1✔
224
                for idx, row := range rows {
2✔
225
                        hint.rowNumber = idx + 1
1✔
226
                        if len(row) > 0 {
2✔
227
                                if !renderedTagOpen {
2✔
228
                                        out.WriteString("  <")
1✔
229
                                        out.WriteString(rowsTag)
1✔
230
                                        out.WriteString(">\n")
1✔
231
                                        renderedTagOpen = true
1✔
232
                                }
1✔
233
                                t.htmlRenderRow(out, row, hint)
1✔
234
                                shouldRenderTagClose = true
1✔
235
                        }
236
                        t.firstRowOfPage = false
1✔
237
                }
238
                if shouldRenderTagClose {
2✔
239
                        out.WriteString("  </")
1✔
240
                        out.WriteString(rowsTag)
1✔
241
                        out.WriteString(">\n")
1✔
242
                }
1✔
243
        }
244
}
245

246
func (t *Table) htmlRenderRowsFooter(out *strings.Builder) {
1✔
247
        if len(t.rowsFooter) > 0 {
2✔
248
                t.htmlRenderRows(out, t.rowsFooter, renderHint{isFooterRow: true})
1✔
249
        }
1✔
250
}
251

252
func (t *Table) htmlRenderRowsHeader(out *strings.Builder) {
1✔
253
        if len(t.rowsHeader) > 0 {
2✔
254
                t.htmlRenderRows(out, t.rowsHeader, renderHint{isHeaderRow: true})
1✔
255
        } else if t.autoIndex {
3✔
256
                hint := renderHint{isAutoIndexRow: true, isHeaderRow: true}
1✔
257
                t.htmlRenderRows(out, []rowStr{t.getAutoIndexColumnIDs()}, hint)
1✔
258
        }
1✔
259
}
260

261
func (t *Table) htmlRenderTitle(out *strings.Builder) {
1✔
262
        if t.title != "" {
2✔
263
                align := t.style.Title.Align.HTMLProperty()
1✔
264
                colors := t.style.Title.Colors.HTMLProperty()
1✔
265
                title := t.style.Title.Format.Apply(t.title)
1✔
266

1✔
267
                out.WriteString("  <caption class=\"title\"")
1✔
268
                if align != "" {
2✔
269
                        out.WriteRune(' ')
1✔
270
                        out.WriteString(align)
1✔
271
                }
1✔
272
                if colors != "" {
2✔
273
                        out.WriteRune(' ')
1✔
274
                        out.WriteString(colors)
1✔
275
                }
1✔
276
                out.WriteRune('>')
1✔
277
                out.WriteString(title)
1✔
278
                out.WriteString("</caption>\n")
1✔
279
        }
280
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc